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" {