diff --git a/blueprints/cloud-operations/network-dashboard/README.md b/blueprints/cloud-operations/network-dashboard/README.md index 1e035d18..b300089b 100644 --- a/blueprints/cloud-operations/network-dashboard/README.md +++ b/blueprints/cloud-operations/network-dashboard/README.md @@ -3,7 +3,7 @@ This repository provides an end-to-end solution to gather some GCP Networking quotas and limits (that cannot be seen in the GCP console today) and display them in a dashboard. The goal is to allow for better visibility of these limits, facilitating capacity planning and avoiding hitting these limits. -Here is an blueprint of dashboard you can get with this solution: +Here is an example of dashboard you can get with this solution: @@ -15,10 +15,11 @@ Here you see utilization (usage compared to the limit) for a specific metric (nu Clone this repository, then go through the following steps to create resources: - Create a terraform.tfvars file with the following content: - - organization_id = "[YOUR-ORG-ID]" - - billing_account = "[YOUR-BILLING-ACCOUNT]" + - organization_id = "" + - billing_account = "" - monitoring_project_id = "project-0" # Monitoring project where the dashboard will be created and the solution deployed - monitored_projects_list = ["project-1", "project2"] # Projects to be monitored by the solution + - monitored_folders_list = ["folder_id"] # Folders to be monitored by the solution - `terraform init` - `terraform apply` @@ -43,18 +44,17 @@ The Cloud Function currently tracks usage, limit and utilization of: - internal forwarding rules for internal L7 load balancers per VPC peering group - Dynamic routes per VPC - Dynamic routes per VPC peering group +- IP utilization per subnet (% of IP addresses used in a subnet) It writes this values to custom metrics in Cloud Monitoring and creates a dashboard to visualize the current utilization of these metrics in Cloud Monitoring. Note that metrics are created in the cloud-function/metrics.yaml file. -You can also edit default limits for a specific network in that file. See the blueprint for `vpc_peering_per_network`. +You can also edit default limits for a specific network in that file. See the example for `vpc_peering_per_network`. ## Next steps and ideas In a future release, we could support: - Static routes per VPC / per VPC peering group -- Dynamic routes per VPC peering group - Google managed VPCs that are peered with PSA (such as Cloud SQL or Memorystore) -- Subnet IP ranges utilization -If you are interested in this and/or would like to contribute, please contact legranda@google.com. +If you are interested in this and/or would like to contribute, please contact legranda@google.com. \ No newline at end of file diff --git a/blueprints/cloud-operations/network-dashboard/cloud-function/main.py b/blueprints/cloud-operations/network-dashboard/cloud-function/main.py index ecb618ad..9cfea04a 100644 --- a/blueprints/cloud-operations/network-dashboard/cloud-function/main.py +++ b/blueprints/cloud-operations/network-dashboard/cloud-function/main.py @@ -14,23 +14,63 @@ # limitations under the License. # -from code import interact from distutils.command.config import config import os -from pickletools import int4 import time from google.cloud import monitoring_v3, asset_v1 from google.protobuf import field_mask_pb2 from googleapiclient import discovery -from metrics import ilb_fwrules, instances, networks, metrics, limits, peerings, routes +from metrics import ilb_fwrules, instances, networks, metrics, limits, peerings, routes, subnets + + +def get_monitored_projects_list(config): + ''' + Gets the projects to be monitored from the MONITORED_FOLDERS_LIST environment variable. + + Parameters: + config (dict): The dict containing config like clients and limits + Returns: + monitored_projects (List of strings): Full list of projects to be monitored + ''' + monitored_projects = config["monitored_projects"] + monitored_folders = os.environ.get("MONITORED_FOLDERS_LIST").split(",") + + # Handling empty monitored folders list + if monitored_folders == ['']: + monitored_folders = [] + + # Gets all projects under each monitored folder (and even in sub folders) + for folder in monitored_folders: + read_mask = field_mask_pb2.FieldMask() + read_mask.FromJsonString('name,versionedResources') + + response = config["clients"]["asset_client"].search_all_resources( + request={ + "scope": f"folders/{folder}", + "asset_types": ["cloudresourcemanager.googleapis.com/Project"], + "read_mask": read_mask + }) + + for resource in response: + for versioned in resource.versioned_resources: + for field_name, field_value in versioned.resource.items(): + if field_name == "projectId": + project_id = field_value + # Avoid duplicate + if project_id not in monitored_projects: + monitored_projects.append(project_id) + + print("List of projects to be monitored:") + print(monitored_projects) + + return monitored_projects def monitoring_interval(): ''' Creates the monitoring interval of 24 hours - Returns: - monitoring_v3.TimeInterval: Moinitoring time interval of 24h + monitoring_v3.TimeInterval: Monitoring time interval of 24h ''' now = time.time() seconds = int(now) @@ -78,20 +118,25 @@ config = { "discovery_client": discovery.build('compute', 'v1'), "asset_client": asset_v1.AssetServiceClient(), "monitoring_client": monitoring_v3.MetricServiceClient() - } + }, } def main(event, context): ''' Cloud Function Entry point, called by the scheduler. - Parameters: event: Not used for now (Pubsub trigger) context: Not used for now (Pubsub trigger) Returns: 'Function executed successfully' ''' + # Handling empty monitored projects list + if config["monitored_projects"] == ['']: + config["monitored_projects"] = [] + + # Gets projects and folders to be monitored + config["monitored_projects"] = get_monitored_projects_list(config) # Keep the monitoring interval up2date during each run config["monitoring_interval"] = monitoring_interval() @@ -99,6 +144,9 @@ def main(event, context): metrics_dict, limits_dict = metrics.create_metrics( config["monitoring_project_link"]) + # IP utilization subnet level metrics + subnets.get_subnets(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") @@ -143,7 +191,7 @@ def main(event, context): routes.get_dynamic_routes_ppg( config, metrics_dict["metrics_per_peering_group"] ["dynamic_routes_per_peering_group"], dynamic_routes_dict, - limits_dict['number_of_subnet_IP_ranges_ppg_limit']) + limits_dict['dynamic_routes_per_peering_group_limit']) return 'Function executed successfully' diff --git a/blueprints/cloud-operations/network-dashboard/cloud-function/metrics.yaml b/blueprints/cloud-operations/network-dashboard/cloud-function/metrics.yaml index 2e5621d7..05f5369c 100644 --- a/blueprints/cloud-operations/network-dashboard/cloud-function/metrics.yaml +++ b/blueprints/cloud-operations/network-dashboard/cloud-function/metrics.yaml @@ -14,6 +14,17 @@ # limitations under the License. # --- +metrics_per_subnet: + ip_usage_per_subnet: + usage: + name: number_of_ip_used + description: Number of used IP addresses in the subnet. + utilization: + name: ip_addresses_per_subnet_utilization + description: Percentage of IP used in the subnet. + limit: + name: number_of_max_ip + description: Number of available IP addresses in the subnet. metrics_per_network: instance_per_network: usage: @@ -60,7 +71,7 @@ metrics_per_network: name: internal_forwarding_rules_l4_limit description: Number of Internal Forwarding Rules for Internal L4 Load Balancers - limit. values: - default_value: 300 + default_value: 500 utilization: name: internal_forwarding_rules_l4_utilization description: Number of Internal Forwarding Rules for Internal L4 Load Balancers - utilization. @@ -97,7 +108,7 @@ metrics_per_peering_group: name: internal_forwarding_rules_l4_ppg_limit description: Number of Internal Forwarding Rules for Internal L4 Load Balancers per VPC peering group - effective limit. values: - default_value: 300 + default_value: 500 utilization: name: internal_forwarding_rules_l4_ppg_utilization description: Number of Internal Forwarding Rules for Internal L4 Load Balancers per VPC peering group - utilization. @@ -148,4 +159,4 @@ metrics_per_peering_group: default_value: 300 utilization: name: dynamic_routes_per_peering_group_utilization - description: Number of Dynamic routes per peering group - utilization. + description: Number of Dynamic routes per peering group - utilization. \ No newline at end of file diff --git a/blueprints/cloud-operations/network-dashboard/cloud-function/metrics/metrics.py b/blueprints/cloud-operations/network-dashboard/cloud-function/metrics/metrics.py index 14183f15..4194a13a 100644 --- a/blueprints/cloud-operations/network-dashboard/cloud-function/metrics/metrics.py +++ b/blueprints/cloud-operations/network-dashboard/cloud-function/metrics/metrics.py @@ -24,7 +24,6 @@ from . import peerings, limits, networks def create_metrics(monitoring_project): ''' Creates all Cloud Monitoring custom metrics based on the metric.yaml file - Parameters: monitoring_project (string): the project where the metrics are written to Returns: @@ -42,15 +41,16 @@ def create_metrics(monitoring_project): metrics_dict = yaml.safe_load(stream) for metric_list in metrics_dict.values(): - for metric in metric_list.values(): + for metric_name, metric in metric_list.items(): for sub_metric_key, sub_metric in metric.items(): metric_link = f"custom.googleapis.com/{sub_metric['name']}" # If the metric doesn't exist yet, then we create it if metric_link not in existing_metrics: create_metric(sub_metric["name"], sub_metric["description"], monitoring_project) - # Parse limits (both default values and network specific ones) - if sub_metric_key == "limit": + # 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": limits_dict_for_metric = {} for network_link, limit_value in sub_metric["values"].items(): limits_dict_for_metric[network_link] = limit_value @@ -64,7 +64,6 @@ def create_metrics(monitoring_project): def create_metric(metric_name, description, monitoring_project): ''' Creates a Cloud Monitoring metric based on the parameter given if the metric is not already existing - Parameters: metric_name (string): Name of the metric to be created description (string): Description of the metric to be created @@ -85,16 +84,16 @@ def create_metric(metric_name, description, monitoring_project): def write_data_to_metric(config, monitored_project_id, value, metric_name, - network_name): + network_name, subnet_id=None): ''' Writes data to Cloud Monitoring custom metrics. - Parameters: config (dict): The dict containing config like clients and limits monitored_project_id: ID of the project where the resource lives (will be added as a label) value (int): Value for the data point of the metric. metric_name (string): Name of the metric network_name (string): Name of the network (will be added as a label) + subnet_id (string): Identifier of the Subnet (region/name of the subnet) Returns: usage (int): Current usage for that network. limit (int): Current usage for that network. @@ -106,6 +105,8 @@ def write_data_to_metric(config, monitored_project_id, value, metric_name, series.resource.type = "global" series.metric.labels["network_name"] = network_name series.metric.labels["project"] = monitored_project_id + if subnet_id: + series.metric.labels["subnet_id"] = subnet_id now = time.time() seconds = int(now) @@ -129,13 +130,13 @@ def write_data_to_metric(config, monitored_project_id, value, metric_name, client.create_time_series(name=config["monitoring_project_link"], time_series=[series]) except Exception as e: + print("Error while writing data point for metric", metric_name) print(e) def get_pgg_data(config, metric_dict, usage_dict, limit_metric, limit_dict): ''' This function gets the usage, limit and utilization per VPC peering group for a specific metric for all projects to be monitored. - Parameters: config (dict): The dict containing config like clients and limits metric_dict (dictionary of string: string): Dictionary with the metric names and description, that will be used to populate the metrics @@ -221,7 +222,6 @@ def get_pgg_data(config, metric_dict, usage_dict, limit_metric, limit_dict): def customize_quota_view(quota_results): ''' Customize the quota output for an easier parsable output. - Parameters: quota_results (string): Input from get_quota_current_usage or get_quota_current_limit. Contains the Current usage or limit for all networks in that project. Returns: @@ -235,4 +235,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 \ No newline at end of file diff --git a/blueprints/cloud-operations/network-dashboard/cloud-function/metrics/subnets.py b/blueprints/cloud-operations/network-dashboard/cloud-function/metrics/subnets.py new file mode 100644 index 00000000..8b173dc8 --- /dev/null +++ b/blueprints/cloud-operations/network-dashboard/cloud-function/metrics/subnets.py @@ -0,0 +1,253 @@ +# +# 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. +# + +from . import metrics +from google.protobuf import field_mask_pb2 +from google.protobuf.json_format import MessageToDict +import ipaddress + + +def get_all_subnets(config): + ''' + Returns a dictionary with subnet level informations (such as IP utilization) + + Parameters: + config (dict): The dict containing config like clients and limits + Returns: + subnet_dict (dictionary of String: dictionary): Key is the project_id, value is a nested dictionary with subnet_region/subnet_name as the key. + ''' + subnet_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, + }) + + for asset in response: + for versioned in asset.versioned_resources: + subnet_name = "" + network_name = "" + project_id = "" + ip_cidr_range = "" + subnet_region = "" + + 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//global/networks/" + project_id = field_value.split('/')[6] + network_name = field_value.split('/')[-1] + elif field_name == 'ipCidrRange': + ip_cidr_range = field_value + elif field_name == 'region': + subnet_region = field_value.split('/')[-1] + + net = ipaddress.ip_network(ip_cidr_range) + # Note that 4 IP addresses are reserved by GCP in all subnets + # Source: https://cloud.google.com/vpc/docs/subnets#reserved_ip_addresses_in_every_subnet + total_ip_addresses = int(net.num_addresses) - 4 + + if project_id not in subnet_dict: + subnet_dict[project_id] = {} + subnet_dict[project_id][f"{subnet_region}/{subnet_name}"] = { + 'name': subnet_name, + 'region': subnet_region, + 'ip_cidr_range': ip_cidr_range, + 'total_ip_addresses': total_ip_addresses, + 'used_ip_addresses': 0, + 'network_name': network_name + } + + return subnet_dict + + +def compute_subnet_utilization(config, all_subnets_dict): + ''' + Counts resources (VMs, ILBs, reserved IPs) using private IPs in the different subnets. + Parameters: + config (dict): Dict containing config like clients and limits + all_subnets_dict (dict): Dict containing the information for each subnets in the GCP organization + + Returns: + None + ''' + read_mask = field_mask_pb2.FieldMask() + read_mask.FromJsonString('name,versionedResources') + response_vm = config["clients"]["asset_client"].search_all_resources( + request={ + "scope": f"organizations/{config['organization']}", + "asset_types": ["compute.googleapis.com/Instance"], + "read_mask": read_mask, + }) + + # Counting IP addresses for GCE instances (VMs) + for asset in response_vm: + for versioned in asset.versioned_resources: + for field_name, field_value in versioned.resource.items(): + if field_name == 'networkInterfaces': + response_dict = MessageToDict(list(field_value._pb)[0]) + # Subnet self link: + # https://www.googleapis.com/compute/v1/projects//regions//subnetworks/ + subnet_region = response_dict['subnetwork'].split('/')[-3] + subnet_name = response_dict['subnetwork'].split('/')[-1] + # Network self link: + # https://www.googleapis.com/compute/v1/projects//global/networks/ + project_id = response_dict['network'].split('/')[6] + network_name = response_dict['network'].split('/')[-1] + + all_subnets_dict[project_id][f"{subnet_region}/{subnet_name}"][ + 'used_ip_addresses'] += 1 + + response_ilb = config["clients"]["asset_client"].search_all_resources( + request={ + "scope": f"organizations/{config['organization']}", + "asset_types": ["compute.googleapis.com/ForwardingRule"], + "read_mask": read_mask, + }) + + # Counting IP addresses for GCE Internal Load Balancers + for asset in response_ilb: + internal = False + psc = False + project_id = '' + subnet_name = '' + subnet_region = '' + address = '' + for versioned in asset.versioned_resources: + for field_name, field_value in versioned.resource.items(): + if 'loadBalancingScheme' in field_name and field_value == 'INTERNAL': + 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 + elif field_name == 'pscConnectionStatus' and field_value == 'ACCEPTED': + psc = True + elif field_name == 'IPAddress': + address = field_value + elif field_name == 'network': + project_id = field_value.split('/')[6] + elif 'subnetwork' in field_name: + subnet_name = field_value.split('/')[-1] + subnet_region = field_value.split('/')[-3] + + if internal: + all_subnets_dict[project_id][f"{subnet_region}/{subnet_name}"][ + 'used_ip_addresses'] += 1 + elif psc: + # PSC endpoint asset doesn't contain the subnet information in Asset Inventory + # We need to find the correct subnet with IP address matching + ip_address = ipaddress.ip_address(address) + for subnet_key, subnet_dict in all_subnets_dict[project_id].items(): + if ip_address in ipaddress.ip_network(subnet_dict['ip_cidr_range']): + all_subnets_dict[project_id][subnet_key]['used_ip_addresses'] += 1 + + response_reserved_ips = config["clients"][ + "asset_client"].search_all_resources( + request={ + "scope": f"organizations/{config['organization']}", + "asset_types": ["compute.googleapis.com/Address"], + "read_mask": read_mask, + }) + + # Counting IP addresses for GCE Reserved IPs (ex: PSC, Cloud DNS Inbound policies, reserved GCE IPs) + for asset in response_reserved_ips: + purpose = "" + status = "" + project_id = "" + network_name = "" + subnet_name = "" + subnet_region = "" + address = "" + prefixLength = "" + for versioned in asset.versioned_resources: + for field_name, field_value in versioned.resource.items(): + if field_name == 'purpose': + purpose = field_value + elif field_name == 'region': + subnet_region = field_value.split('/')[-1] + elif field_name == 'status': + status = field_value + elif field_name == 'address': + address = field_value + elif field_name == 'network': + network_name = field_value.split('/')[-1] + project_id = field_value.split('/')[6] + elif field_name == 'subnetwork': + subnet_name = field_value.split('/')[-1] + project_id = field_value.split('/')[6] + elif field_name == 'prefixLength': + prefixLength = field_value + + # Rserved IP addresses for GCE instances or PSC Forwarding Rule PENDING state + if purpose == "GCE_ENDPOINT" and status == "RESERVED": + all_subnets_dict[project_id][f"{subnet_region}/{subnet_name}"][ + 'used_ip_addresses'] += 1 + # Cloud DNS inbound policy + elif purpose == "DNS_RESOLVER": + all_subnets_dict[project_id][f"{subnet_region}/{subnet_name}"][ + 'used_ip_addresses'] += 1 + # PSA Range for Cloud SQL, MemoryStore, etc. + elif purpose == "VPC_PEERING": + # TODO: PSA range to be handled later + # print("PSA range to be handled later:", address, prefixLength, network_name) + continue + + +def get_subnets(config, metrics_dict): + ''' + Writes all subnet metrics to custom metrics. + + Parameters: + config (dict): The dict containing config like clients and limits + Returns: + None + ''' + + all_subnets_dict = get_all_subnets(config) + # Updates all_subnets_dict with the IP utilization info + compute_subnet_utilization(config, all_subnets_dict) + + for project_id in config["monitored_projects"]: + if project_id not in all_subnets_dict: + continue + for subnet_dict in all_subnets_dict[project_id].values(): + ip_utilization = 0 + if subnet_dict['used_ip_addresses'] > 0: + ip_utilization = subnet_dict['used_ip_addresses'] / subnet_dict[ + 'total_ip_addresses'] + + # Building unique identifier with subnet region/name + subnet_id = f"{subnet_dict['region']}/{subnet_dict['name']}" + metrics.write_data_to_metric( + config, project_id, subnet_dict['used_ip_addresses'], + metrics_dict["metrics_per_subnet"]["ip_usage_per_subnet"]["usage"] + ["name"], subnet_dict['network_name'], subnet_id) + metrics.write_data_to_metric( + config, project_id, subnet_dict['total_ip_addresses'], + metrics_dict["metrics_per_subnet"]["ip_usage_per_subnet"]["limit"] + ["name"], subnet_dict['network_name'], subnet_id) + metrics.write_data_to_metric( + config, project_id, ip_utilization, metrics_dict["metrics_per_subnet"] + ["ip_usage_per_subnet"]["utilization"]["name"], + subnet_dict['network_name'], subnet_id) + + print("Wrote metrics for subnet ip utilization for VPCs in project", + project_id) diff --git a/blueprints/cloud-operations/network-dashboard/dashboards/quotas-utilization.json b/blueprints/cloud-operations/network-dashboard/dashboards/quotas-utilization.json index e9059c3d..6db2499a 100644 --- a/blueprints/cloud-operations/network-dashboard/dashboards/quotas-utilization.json +++ b/blueprints/cloud-operations/network-dashboard/dashboards/quotas-utilization.json @@ -4,383 +4,456 @@ "columns": 12, "tiles": [ { - "width": 6, "height": 4, "widget": { "title": "internal_forwarding_rules_l4_utilization", "xyChart": { + "chartOptions": { + "mode": "COLOR" + }, "dataSets": [ { + "minAlignmentPeriod": "3600s", + "plotType": "LINE", + "targetAxis": "Y1", "timeSeriesQuery": { "timeSeriesFilter": { - "filter": "metric.type=\"custom.googleapis.com/internal_forwarding_rules_l4_utilization\" resource.type=\"global\"", "aggregation": { "alignmentPeriod": "3600s", "perSeriesAligner": "ALIGN_NEXT_OLDER" }, + "filter": "metric.type=\"custom.googleapis.com/internal_forwarding_rules_l4_utilization\" resource.type=\"global\"", "secondaryAggregation": { "alignmentPeriod": "1800s", "perSeriesAligner": "ALIGN_MEAN" } } - }, - "plotType": "LINE", - "minAlignmentPeriod": "3600s", - "targetAxis": "Y1" + } } ], "timeshiftDuration": "0s", "yAxis": { "label": "y1Axis", "scale": "LINEAR" - }, - "chartOptions": { - "mode": "COLOR" } } - } + }, + "width": 6 }, { - "yPos": 12, - "width": 6, "height": 4, "widget": { "title": "internal_forwarding_rules_l7_utilization", "xyChart": { + "chartOptions": { + "mode": "COLOR" + }, "dataSets": [ { + "minAlignmentPeriod": "3600s", + "plotType": "LINE", + "targetAxis": "Y1", "timeSeriesQuery": { "timeSeriesFilter": { - "filter": "metric.type=\"custom.googleapis.com/internal_forwarding_rules_l7_utilization\" resource.type=\"global\"", "aggregation": { "alignmentPeriod": "3600s", "perSeriesAligner": "ALIGN_NEXT_OLDER" }, + "filter": "metric.type=\"custom.googleapis.com/internal_forwarding_rules_l7_utilization\" resource.type=\"global\"", "secondaryAggregation": { "alignmentPeriod": "60s", "perSeriesAligner": "ALIGN_MEAN" } } - }, - "plotType": "LINE", - "minAlignmentPeriod": "3600s", - "targetAxis": "Y1" + } } ], "timeshiftDuration": "0s", "yAxis": { "label": "y1Axis", "scale": "LINEAR" - }, - "chartOptions": { - "mode": "COLOR" } } - } + }, + "width": 6, + "yPos": 12 }, { - "yPos": 8, - "width": 6, "height": 4, "widget": { "title": "number_of_instances_utilization", "xyChart": { + "chartOptions": { + "mode": "COLOR" + }, "dataSets": [ { + "minAlignmentPeriod": "3600s", + "plotType": "LINE", + "targetAxis": "Y1", "timeSeriesQuery": { "timeSeriesFilter": { - "filter": "metric.type=\"custom.googleapis.com/number_of_instances_utilization\" resource.type=\"global\"", "aggregation": { "alignmentPeriod": "3600s", "perSeriesAligner": "ALIGN_NEXT_OLDER" }, + "filter": "metric.type=\"custom.googleapis.com/number_of_instances_utilization\" resource.type=\"global\"", "secondaryAggregation": { "alignmentPeriod": "60s", "perSeriesAligner": "ALIGN_MEAN" } } - }, - "plotType": "LINE", - "minAlignmentPeriod": "3600s", - "targetAxis": "Y1" + } } ], "timeshiftDuration": "0s", "yAxis": { "label": "y1Axis", "scale": "LINEAR" - }, - "chartOptions": { - "mode": "COLOR" } } - } + }, + "width": 6, + "yPos": 8 }, { - "xPos": 6, - "yPos": 4, - "width": 6, "height": 4, "widget": { "title": "number_of_vpc_peerings_utilization", "xyChart": { + "chartOptions": { + "mode": "COLOR" + }, "dataSets": [ { + "minAlignmentPeriod": "3600s", + "plotType": "LINE", + "targetAxis": "Y1", "timeSeriesQuery": { "timeSeriesFilter": { - "filter": "metric.type=\"custom.googleapis.com/number_of_vpc_peerings_utilization\" resource.type=\"global\"", "aggregation": { "alignmentPeriod": "3600s", "perSeriesAligner": "ALIGN_NEXT_OLDER" }, + "filter": "metric.type=\"custom.googleapis.com/number_of_vpc_peerings_utilization\" resource.type=\"global\"", "secondaryAggregation": { "alignmentPeriod": "60s", "perSeriesAligner": "ALIGN_MEAN" } } - }, - "plotType": "LINE", - "minAlignmentPeriod": "3600s", - "targetAxis": "Y1" + } } ], "timeshiftDuration": "0s", "yAxis": { "label": "y1Axis", "scale": "LINEAR" - }, - "chartOptions": { - "mode": "COLOR" } } - } + }, + "width": 6, + "xPos": 6, + "yPos": 4 }, { - "yPos": 4, - "width": 6, "height": 4, "widget": { "title": "number_of_active_vpc_peerings_utilization", "xyChart": { + "chartOptions": { + "mode": "COLOR" + }, "dataSets": [ { + "minAlignmentPeriod": "3600s", + "plotType": "LINE", + "targetAxis": "Y1", "timeSeriesQuery": { "timeSeriesFilter": { - "filter": "metric.type=\"custom.googleapis.com/number_of_active_vpc_peerings_utilization\" resource.type=\"global\"", "aggregation": { "alignmentPeriod": "3600s", "perSeriesAligner": "ALIGN_NEXT_OLDER" }, + "filter": "metric.type=\"custom.googleapis.com/number_of_active_vpc_peerings_utilization\" resource.type=\"global\"", "secondaryAggregation": { "alignmentPeriod": "60s", "perSeriesAligner": "ALIGN_INTERPOLATE" } } - }, - "plotType": "LINE", - "minAlignmentPeriod": "3600s", - "targetAxis": "Y1" + } } ], "timeshiftDuration": "0s", "yAxis": { "label": "y1Axis", "scale": "LINEAR" - }, - "chartOptions": { - "mode": "COLOR" } } - } + }, + "width": 6, + "yPos": 4 }, { - "yPos": 16, - "width": 6, "height": 4, "widget": { "title": "subnet_IP_ranges_ppg_utilization", "xyChart": { + "chartOptions": { + "mode": "COLOR" + }, "dataSets": [ { + "minAlignmentPeriod": "3600s", + "plotType": "LINE", + "targetAxis": "Y1", "timeSeriesQuery": { "timeSeriesFilter": { - "filter": "metric.type=\"custom.googleapis.com/number_of_subnet_IP_ranges_ppg_utilization\" resource.type=\"global\"", "aggregation": { "alignmentPeriod": "3600s", "perSeriesAligner": "ALIGN_NEXT_OLDER" }, + "filter": "metric.type=\"custom.googleapis.com/number_of_subnet_IP_ranges_ppg_utilization\" resource.type=\"global\"", "secondaryAggregation": { "alignmentPeriod": "3600s", "perSeriesAligner": "ALIGN_MEAN" } } - }, - "plotType": "LINE", - "minAlignmentPeriod": "3600s", - "targetAxis": "Y1" + } } ], "timeshiftDuration": "0s", "yAxis": { "label": "y1Axis", "scale": "LINEAR" - }, - "chartOptions": { - "mode": "COLOR" } } - } + }, + "width": 6, + "yPos": 16 }, { - "xPos": 6, - "width": 6, "height": 4, "widget": { "title": "internal_forwarding_rules_l4_ppg_utilization", "xyChart": { + "chartOptions": { + "mode": "COLOR" + }, "dataSets": [ { + "minAlignmentPeriod": "3600s", + "plotType": "LINE", + "targetAxis": "Y1", "timeSeriesQuery": { "timeSeriesFilter": { - "filter": "metric.type=\"custom.googleapis.com/internal_forwarding_rules_l4_ppg_utilization\" resource.type=\"global\"", "aggregation": { "alignmentPeriod": "3600s", "perSeriesAligner": "ALIGN_NEXT_OLDER" }, + "filter": "metric.type=\"custom.googleapis.com/internal_forwarding_rules_l4_ppg_utilization\" resource.type=\"global\"", "secondaryAggregation": { "alignmentPeriod": "3600s", "perSeriesAligner": "ALIGN_MEAN" } } - }, - "plotType": "LINE", - "minAlignmentPeriod": "3600s", - "targetAxis": "Y1" + } } ], "timeshiftDuration": "0s", "yAxis": { "label": "y1Axis", "scale": "LINEAR" - }, - "chartOptions": { - "mode": "COLOR" } } - } + }, + "width": 6, + "xPos": 6 }, { - "xPos": 6, - "yPos": 12, - "width": 6, "height": 4, "widget": { "title": "internal_forwarding_rules_l7_ppg_utilization", "xyChart": { + "chartOptions": { + "mode": "COLOR" + }, "dataSets": [ { + "minAlignmentPeriod": "3600s", + "plotType": "LINE", + "targetAxis": "Y1", "timeSeriesQuery": { "timeSeriesFilter": { - "filter": "metric.type=\"custom.googleapis.com/internal_forwarding_rules_l7_ppg_utilization\" resource.type=\"global\"", "aggregation": { "alignmentPeriod": "3600s", "perSeriesAligner": "ALIGN_NEXT_OLDER" }, + "filter": "metric.type=\"custom.googleapis.com/internal_forwarding_rules_l7_ppg_utilization\" resource.type=\"global\"", "secondaryAggregation": { "alignmentPeriod": "60s", "perSeriesAligner": "ALIGN_MEAN" } } - }, - "plotType": "LINE", - "minAlignmentPeriod": "3600s", - "targetAxis": "Y1" + } } ], "timeshiftDuration": "0s", "yAxis": { "label": "y1Axis", "scale": "LINEAR" - }, - "chartOptions": { - "mode": "COLOR" } } - } + }, + "width": 6, + "xPos": 6, + "yPos": 12 }, { - "xPos": 6, - "yPos": 8, - "width": 6, "height": 4, "widget": { "title": "number_of_instances_ppg_utilization", "xyChart": { + "chartOptions": { + "mode": "COLOR" + }, "dataSets": [ { + "minAlignmentPeriod": "3600s", + "plotType": "LINE", + "targetAxis": "Y1", "timeSeriesQuery": { "timeSeriesFilter": { - "filter": "metric.type=\"custom.googleapis.com/number_of_instances_ppg_utilization\" resource.type=\"global\"", "aggregation": { "alignmentPeriod": "3600s", "perSeriesAligner": "ALIGN_NEXT_OLDER" - } + }, + "filter": "metric.type=\"custom.googleapis.com/number_of_instances_ppg_utilization\" resource.type=\"global\"" } - }, - "plotType": "LINE", - "minAlignmentPeriod": "3600s", - "targetAxis": "Y1" + } } ], "timeshiftDuration": "0s", "yAxis": { "label": "y1Axis", "scale": "LINEAR" - }, - "chartOptions": { - "mode": "COLOR" } } - } + }, + "width": 6, + "xPos": 6, + "yPos": 8 }, { - "xPos": 6, - "yPos": 16, - "width": 6, "height": 4, "widget": { "title": "dynamic_routes_per_network_utilization", "xyChart": { + "chartOptions": { + "mode": "COLOR" + }, "dataSets": [ { + "minAlignmentPeriod": "60s", + "plotType": "LINE", + "targetAxis": "Y1", "timeSeriesQuery": { "timeSeriesFilter": { - "filter": "metric.type=\"custom.googleapis.com/dynamic_routes_per_network_utilization\" resource.type=\"global\"", "aggregation": { "alignmentPeriod": "60s", "perSeriesAligner": "ALIGN_MEAN" }, - "secondaryAggregation": { - "alignmentPeriod": "60s" - } + "filter": "metric.type=\"custom.googleapis.com/dynamic_routes_per_network_utilization\" resource.type=\"global\"" } - }, - "plotType": "LINE", - "minAlignmentPeriod": "60s", - "targetAxis": "Y1" + } } ], "timeshiftDuration": "0s", "yAxis": { "label": "y1Axis", "scale": "LINEAR" - }, - "chartOptions": { - "mode": "COLOR" } } - } + }, + "width": 6, + "yPos": 20 + }, + { + "height": 4, + "widget": { + "title": "ip_addresses_per_subnet_utilization", + "xyChart": { + "chartOptions": { + "mode": "COLOR" + }, + "dataSets": [ + { + "minAlignmentPeriod": "60s", + "plotType": "LINE", + "targetAxis": "Y1", + "timeSeriesQuery": { + "timeSeriesFilter": { + "aggregation": { + "alignmentPeriod": "60s", + "perSeriesAligner": "ALIGN_MEAN" + }, + "filter": "metric.type=\"custom.googleapis.com/ip_addresses_per_subnet_utilization\" resource.type=\"global\"", + "secondaryAggregation": { + "alignmentPeriod": "60s" + } + } + } + } + ], + "timeshiftDuration": "0s", + "yAxis": { + "label": "y1Axis", + "scale": "LINEAR" + } + } + }, + "width": 6, + "xPos": 6, + "yPos": 16 + }, + { + "height": 4, + "widget": { + "title": "dynamic_routes_ppg_utilization", + "xyChart": { + "chartOptions": { + "mode": "COLOR" + }, + "dataSets": [ + { + "minAlignmentPeriod": "60s", + "plotType": "LINE", + "targetAxis": "Y1", + "timeSeriesQuery": { + "timeSeriesFilter": { + "aggregation": { + "alignmentPeriod": "60s", + "perSeriesAligner": "ALIGN_MEAN" + }, + "filter": "metric.type=\"custom.googleapis.com/dynamic_routes_per_peering_group_utilization\" resource.type=\"global\"", + "secondaryAggregation": { + "alignmentPeriod": "60s" + } + } + } + } + ], + "timeshiftDuration": "0s", + "yAxis": { + "label": "y1Axis", + "scale": "LINEAR" + } + } + }, + "width": 6, + "xPos": 6, + "yPos": 20 } ] - } + }, + "name": "projects/347834224817/dashboards/1bdcd06a-030d-4977-bf4b-f32231aa3b77" } \ No newline at end of file diff --git a/blueprints/cloud-operations/network-dashboard/main.tf b/blueprints/cloud-operations/network-dashboard/main.tf index 2102b370..a381d21d 100644 --- a/blueprints/cloud-operations/network-dashboard/main.tf +++ b/blueprints/cloud-operations/network-dashboard/main.tf @@ -15,8 +15,11 @@ */ locals { - project_id_list = toset(var.monitored_projects_list) - projects = join(",", local.project_id_list) + project_ids = toset(var.monitored_projects_list) + projects = join(",", local.project_ids) + + folder_ids = toset(var.monitored_folders_list) + folders = join(",", local.folder_ids) monitoring_project = var.monitoring_project_id == "" ? module.project-monitoring[0].project_id : var.monitoring_project_id } @@ -90,6 +93,7 @@ resource "google_cloud_scheduler_job" "job" { } } + module "cloud-function" { source = "../../../modules/cloud-function" project_id = local.monitoring_project @@ -116,11 +120,13 @@ module "cloud-function" { environment_variables = { MONITORED_PROJECTS_LIST = local.projects + MONITORED_FOLDERS_LIST = local.folders MONITORING_PROJECT_ID = local.monitoring_project ORGANIZATION_ID = var.organization_id } - service_account = module.service-account-function.email + service_account = module.service-account-function.email + ingress_settings = "ALLOW_INTERNAL_ONLY" trigger_config = { event = "google.pubsub.topic.publish" diff --git a/blueprints/cloud-operations/network-dashboard/variables.tf b/blueprints/cloud-operations/network-dashboard/variables.tf index 9d69469e..63a61ee9 100644 --- a/blueprints/cloud-operations/network-dashboard/variables.tf +++ b/blueprints/cloud-operations/network-dashboard/variables.tf @@ -32,15 +32,20 @@ variable "prefix" { default = "" } -# TODO: support folder instead of a list of projects? variable "monitored_projects_list" { type = list(string) description = "ID of the projects to be monitored (where limits and quotas data will be pulled)" } +variable "monitored_folders_list" { + type = list(string) + description = "ID of the projects to be monitored (where limits and quotas data will be pulled)" + default = [] +} + variable "schedule_cron" { description = "Cron format schedule to run the Cloud Function. Default is every 5 minutes." - default = "*/5 * * * *" + default = "*/10 * * * *" } variable "project_monitoring_services" {