Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding Secondary IP Utilization calculation #982

Merged
merged 8 commits into from
Dec 7, 2022
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
from google.cloud import monitoring_v3, asset_v1
from google.protobuf import field_mask_pb2
from googleapiclient import discovery
from metrics import ilb_fwrules, firewall_policies, instances, networks, metrics, limits, peerings, routes, subnets, vpc_firewalls
from metrics import ilb_fwrules, firewall_policies, instances, networks, metrics, limits, peerings, routes, subnets, vpc_firewalls, secondarys

CF_VERSION = os.environ.get("CF_VERSION")

Expand Down Expand Up @@ -158,6 +158,9 @@ def main(event, context=None):
# IP utilization subnet level metrics
subnets.get_subnets(config, metrics_dict)

# IP utilization secondary range metrics
secondarys.get_secondaries(config, metrics_dict)

# Asset inventory queries
gce_instance_dict = instances.get_gce_instance_dict(config)
l4_forwarding_rules_dict = ilb_fwrules.get_forwarding_rules_dict(config, "L4")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,16 @@ metrics_per_subnet:
limit:
name: number_of_max_ip
description: Number of available IP addresses in the subnet.
ip_usage_per_secondaryRange:
usage:
name: number_of_sr_ip_used
description: Number of used IP addresses in the secondary range.
utilization:
name: ip_addresses_per_sr_utilization
description: Percentage of IP used in the secondary range.
limit:
name: number_of_max_sr_ip
description: Number of available IP addresses in the secondary range.
metrics_per_network:
instance_per_network:
usage:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,9 @@ def create_metrics(monitoring_project, config):
monitoring_project, config)
# Parse limits for network and peering group metrics
# Subnet level metrics have a different limit: the subnet IP range size
if sub_metric_key == "limit" and metric_name != "ip_usage_per_subnet":
if sub_metric_key == "limit" and (
metric_name != "ip_usage_per_subnet" and
metric_name != "ip_usage_per_secondaryRange"):
limits_dict_for_metric = {}
if "values" in sub_metric:
for network_link, limit_value in sub_metric["values"].items():
Expand Down Expand Up @@ -262,4 +264,4 @@ def customize_quota_view(quota_results):
for val in result.points:
quotaViewJson.update({'value': val.value.int64_value})
quotaViewList.append(quotaViewJson)
return quotaViewList
return quotaViewList
Original file line number Diff line number Diff line change
@@ -0,0 +1,273 @@
#
# Copyright 2022 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#

import time

from . import metrics
from google.protobuf import field_mask_pb2
from google.protobuf.json_format import MessageToDict
import ipaddress


def get_all_secondaryRange(config):
'''
Returns a dictionary with secondary range informations
Parameters:
config (dict): The dict containing config like clients and limits
Returns:
secondary_dict (dictionary of String: dictionary): Key is the project_id,
value is a nested dictionary with subnet_name/secondary_range_name as the key.
'''
secondary_dict = {}
read_mask = field_mask_pb2.FieldMask()
read_mask.FromJsonString('name,versionedResources')

response = config["clients"]["asset_client"].search_all_resources(
request={
"scope": f"organizations/{config['organization']}",
"asset_types": ['compute.googleapis.com/Subnetwork'],
"read_mask": read_mask,
"page_size": config["page_size"],
})

for asset in response:
for versioned in asset.versioned_resources:
secondaryRange_name = ""
secondaryCidrBlock = ""
subnet_name = ""
network_name = ""
subnet_region = ""
project_id = ""

# Check first if the subnet has any secondary ranges to begin with
if versioned.resource.get('secondaryIpRanges'):
for field_name, field_value in versioned.resource.items():
if field_name == 'name':
subnet_name = field_value
elif field_name == 'network':
# Network self link format:
# "https://www.googleapis.com/compute/v1/projects/<PROJECT_ID>/global/networks/<NETWORK_NAME>"
project_id = field_value.split('/')[6]
network_name = field_value.split('/')[-1]
elif field_name == 'region':
subnet_region = field_value.split('/')[-1]
elif field_name == 'secondaryIpRanges':
for item in field_value:
# Each subnet can have multiple secondary ranges
secondaryRange_name = item['rangeName']
secondaryCidrBlock = item['ipCidrRange']
net = ipaddress.ip_network(secondaryCidrBlock)
total_ip_addresses = int(net.num_addresses)

if project_id not in secondary_dict:
secondary_dict[project_id] = {}
secondary_dict[project_id][
f"{subnet_name}/{secondaryRange_name}"] = {
'name': secondaryRange_name,
'region': subnet_region,
'subnetName': subnet_name,
'ip_cidr_range': secondaryCidrBlock,
'total_ip_addresses': total_ip_addresses,
'used_ip_addresses': 0,
'network_name': network_name
}
return secondary_dict


def compute_GKE_secondaryIP_utilization(config, read_mask, all_secondary_dict):
'''
Counts the IP Addresses used by GKE (Pods and Services)
Parameters:
config (dict): The dict containing config like clients and limits
read_mask (FieldMask): read_mask to get additional metadata from Cloud Asset Inventory
all_secondary_dict (dict): Dict containing the secondary IP Range information for each subnets in the GCP organization
Returns:
all_secondary_dict (dict): Same dict but populated with GKE IP utilization information
'''
cluster_secondary_dict = {}
node_secondary_dict = {}

# Creating cluster dict
# Cluster dict has subnet information
response_cluster = config["clients"]["asset_client"].list_assets(
request={
"parent": f"organizations/{config['organization']}",
"asset_types": ['container.googleapis.com/Cluster'],
"content_type": 'RESOURCE',
"page_size": config["page_size"],
})

for asset in response_cluster:
cluster_project = asset.resource.data['selfLink'].split('/')[5]
cluster_parent = "/".join(asset.resource.data['selfLink'].split('/')[5:10])
cluster_subnetwork = asset.resource.data['subnetwork']
cluster_service_rangeName = asset.resource.data['ipAllocationPolicy'][
'servicesSecondaryRangeName']

cluster_secondary_dict[f"{cluster_parent}/Service"] = {
"project": cluster_project,
"subnet": cluster_subnetwork,
"secondaryRange_name": cluster_service_rangeName,
'used_ip_addresses': 0,
}

for node_pool in asset.resource.data['nodePools']:
nodepool_name = node_pool['name']
node_IPrange = node_pool['networkConfig']['podRange']
cluster_secondary_dict[f"{cluster_parent}/{nodepool_name}"] = {
"project": cluster_project,
"subnet": cluster_subnetwork,
"secondaryRange_name": node_IPrange,
'used_ip_addresses': 0,
}

# Creating node dict
# Node dict allows 1:1 mapping of pod IP utilization, and which secondary Range it is using
response_node = config["clients"]["asset_client"].search_all_resources(
request={
"scope": f"organizations/{config['organization']}",
"asset_types": ['k8s.io/Node'],
"read_mask": read_mask,
"page_size": config["page_size"],
})

for asset in response_node:
# Node name link format:
# "//container.googleapis.com/projects/<PROJECT_ID>/<zones/region>/<LOCATION>/clusters/<CLUSTER_NAME>/k8s/nodes/<NODE_NAME>"
node_parent = "/".join(asset.name.split('/')[4:9])
node_name = asset.name.split('/')[-1]
node_full_name = f"{node_parent}/{node_name}"

for versioned in asset.versioned_resources:
node_secondary_dict[node_full_name] = {
'node_parent':
node_parent,
'this_node_pool':
versioned.resource['metadata']['labels']
['cloud.google.com/gke-nodepool'],
'used_IP':
0
}

# Counting IP addresses used by pods in GKE
response_pods = config["clients"]["asset_client"].search_all_resources(
request={
"scope": f"organizations/{config['organization']}",
"asset_types": ['k8s.io/Pod'],
"read_mask": read_mask,
"page_size": config["page_size"],
})

for asset in response_pods:
pod_parent = "/".join(asset.name.split('/')[4:9])
brianhmj marked this conversation as resolved.
Show resolved Hide resolved

for versioned in asset.versioned_resources:
cur_PodIP = versioned.resource['status']['podIP']
cur_HostIP = versioned.resource['status']['hostIP']
host_node_name = versioned.resource['spec']['nodeName']
pod_full_path = f"{pod_parent}/{host_node_name}"

# A check to make sure pod is not using node IP
if cur_PodIP != cur_HostIP:
node_secondary_dict[pod_full_path]['used_IP'] += 1
brianhmj marked this conversation as resolved.
Show resolved Hide resolved

# Counting IP addresses used by Service in GKE
response_service = config["clients"]["asset_client"].search_all_resources(
request={
"scope": f"organizations/{config['organization']}",
"asset_types": ['k8s.io/Service'],
"read_mask": read_mask,
"page_size": config["page_size"],
})

for asset in response_service:
service_parent = "/".join(asset.name.split('/')[4:9])
service_fullpath = f"{service_parent}/Service"
cluster_secondary_dict[service_fullpath]['used_ip_addresses'] += 1

for item in node_secondary_dict.values():
itemKey = f"{item['node_parent']}/{item['this_node_pool']}"
cluster_secondary_dict[itemKey]['used_ip_addresses'] += item['used_IP']
brianhmj marked this conversation as resolved.
Show resolved Hide resolved

for item in cluster_secondary_dict.values():
itemKey = f"{item['subnet']}/{item['secondaryRange_name']}"
all_secondary_dict[item['project']][itemKey]['used_ip_addresses'] += item[
'used_ip_addresses']


def compute_secondary_utilization(config, all_secondary_dict):
'''
Counts resources (GKE, GCE) using IPs in secondary ranges.
Parameters:
config (dict): Dict containing config like clients and limits
all_secondary_dict (dict): Dict containing the secondary IP Range information for each subnets in the GCP organization
Returns:
None
'''
read_mask = field_mask_pb2.FieldMask()
brianhmj marked this conversation as resolved.
Show resolved Hide resolved
read_mask.FromJsonString('name,versionedResources')
brianhmj marked this conversation as resolved.
Show resolved Hide resolved

compute_GKE_secondaryIP_utilization(config, read_mask, all_secondary_dict)
# TODO: Other Secondary IP like GCE VM using alias IPs


def get_secondaries(config, metrics_dict):
'''
Writes all secondary rang IP address usage metrics to custom metrics.
Parameters:
config (dict): The dict containing config like clients and limits
Returns:
None
'''

secondaryRange_dict = get_all_secondaryRange(config)
# Updates all_subnets_dict with the IP utilization info
compute_secondary_utilization(config, secondaryRange_dict)

timestamp = time.time()
for project_id in config["monitored_projects"]:
if project_id not in secondaryRange_dict:
continue
for secondary_dict in secondaryRange_dict[project_id].values():
ip_utilization = 0
if secondary_dict['used_ip_addresses'] > 0:
ip_utilization = secondary_dict['used_ip_addresses'] / secondary_dict[
'total_ip_addresses']

# Building unique identifier with subnet region/name
subnet_id = f"{secondary_dict['region']}/{secondary_dict['name']}"
metric_labels = {
'project': project_id,
'network_name': secondary_dict['network_name'],
'subnet_id': subnet_id
brianhmj marked this conversation as resolved.
Show resolved Hide resolved
}
metrics.append_data_to_series_buffer(
config, metrics_dict["metrics_per_subnet"]
["ip_usage_per_secondaryRange"]["usage"]["name"],
secondary_dict['used_ip_addresses'], metric_labels,
timestamp=timestamp)
metrics.append_data_to_series_buffer(
config, metrics_dict["metrics_per_subnet"]
["ip_usage_per_secondaryRange"]["limit"]["name"],
secondary_dict['total_ip_addresses'], metric_labels,
timestamp=timestamp)
metrics.append_data_to_series_buffer(
config, metrics_dict["metrics_per_subnet"]
["ip_usage_per_secondaryRange"]["utilization"]["name"],
ip_utilization, metric_labels, timestamp=timestamp)

print("Buffered metrics for secondary ip utilization for VPCs in project",
project_id)
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,8 @@ def compute_subnet_utilization_ilbs(config, read_mask, all_subnets_dict):
for versioned in asset.versioned_resources:
for field_name, field_value in versioned.resource.items():
if 'loadBalancingScheme' in field_name and field_value in [
'INTERNAL', 'INTERNAL_MANAGED']:
'INTERNAL', 'INTERNAL_MANAGED'
]:
brianhmj marked this conversation as resolved.
Show resolved Hide resolved
internal = True
# We want to count only accepted PSC endpoint Forwarding Rule
# If the PSC endpoint Forwarding Rule is pending, we will count it in the reserved IP addresses
Expand Down