From 1ec9dab065ce69e3da0476432373fbf2ec7bfd53 Mon Sep 17 00:00:00 2001 From: Rodrigue Chakode Date: Mon, 27 Jun 2022 00:10:03 +0200 Subject: [PATCH 01/32] review built-in charts docs - introducing accounting based on resource requests --- docs/built-in-dashboards-and-charts.md | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/docs/built-in-dashboards-and-charts.md b/docs/built-in-dashboards-and-charts.md index 67e4342..c4b93ae 100644 --- a/docs/built-in-dashboards-and-charts.md +++ b/docs/built-in-dashboards-and-charts.md @@ -5,7 +5,7 @@ This section describes the built-in dashboards and charts provided by `kube-opex - [Hourly Consolidated Usage Trends (7 days)](#hourly-consolidated-usage-trends-7-days) - [Hourly Usage/Requests Efficiency (7 days)](#hourly-usagerequests-efficiency-7-days) - [Daily Consumption Accounting (14 days)](#daily-consumption-accounting-14-days) - - [Monthly CPU and Memory Usage (12 months)](#monthly-cpu-and-memory-usage-12-months) + - [Monthly Consumption Accounting (12 months)](#monthly-consumption-accounting-12-months) - [Nodes' Occupation by Pods](#nodes-occupation-by-pods) - [Export Charts and Datasets (PNG, CSV, JSON)](#export-charts-and-datasets-png-csv-json) - [Dashboards and Visualization with Grafana](#dashboards-and-visualization-with-grafana) @@ -31,25 +31,29 @@ The date filter can be used to zoom out/in on a specific time range. These charts are based on data consolidated hourly thanks to sample metrics collected every five minutes from Kubernetes. ## Daily Consumption Accounting (14 days) -The daily accounting charts are provided per namespace for CPU and Memory resources and cover the last 14 days (2 weeks). +The daily accounting charts are provided per namespace for CPU and Memory resources and cover the last 14 days (2 weeks). -According to the [selected accounting model (](design-fundamentals.md#usage-accounting-models), the charts display one of the following metrics : +According to the [selected accounting model (](design-fundamentals.md#usage-accounting-models), the charts display the following metrics. The chart and the backed-data can be easily exported as an image or a CSV file (see [Export Charts and Datasets (PNG, CSV, JSON)](#export-charts-and-datasets-png-csv-json)). * Daily cumulative sum of actual hourly consumption per namespace. * Daily cumulative sum of the maximum between the actual hourly consumption and the requested capacities. -* Daily cumulative sum of actual hourly computedd from an actual cluster cost set statically based on a fixed hourly rate, or determinated dynamically from allocated resources on public clouds (nodes, storage, etc.). +* Daily cumulative sum of hourly cost computed from an actual cluster cost set statically based on a fixed hourly rate, or determinated dynamically from allocated resources on public clouds (nodes, storage, etc.). ![](../screenshots/sample-two-weeks-daily-usage.png) -## Monthly CPU and Memory Usage (12 months) -For the different namespaces discovered in the Kubernetes cluster, these charts show monthly cumulative usage for CPU and memory resources during the last 12 months. +## Monthly Consumption Accounting (12 months) -![](../screenshots/sample-one-year-monthly-usage.png) +The monthly accounting charts are provided per namespace for CPU and Memory resources and cover the last 12 months (1 year). + +According to the [selected accounting model (](design-fundamentals.md#usage-accounting-models), the charts display the following metrics. Each chart and/or the backed-data can be easily exported as an image or a CSV file (see [Export Charts and Datasets (PNG, CSV, JSON)](#export-charts-and-datasets-png-csv-json)). -The charts are based on data consolidated hourly thanks to sample metrics collected every five minutes from Kubernetes. +* Monthly cumulative sum of actual hourly consumption per namespace. +* Monthly cumulative sum of the maximum between the actual hourly consumption and the requested capacities. +* Monthly cumulative sum of hourly cost computed from an actual cluster cost set statically based on a fixed hourly rate, or determinated dynamically from allocated resources on public clouds (nodes, storage, etc.). + +![](../screenshots/sample-one-year-monthly-usage.png) -Depending on the [selected accounting model](design-fundamentals.md#usage-accounting-models), the values on these charts can be actual costs (`CHARGE_BACK` model), cumulative usage (sum of hourly consolidated usage, `CUMULATIVE_RATIO` model), or a percentage of the global cluster usage (`CHARGE_BACK` model, `100%` means the total cluster capacity). ## Nodes' Occupation by Pods For each node discovered in the Kubernetes cluster, this dashboard section displays the CPU and the memory resources currently consumed by running pods. The data are refreshed every five minutes. @@ -66,4 +70,4 @@ Any chart provided by kube-opex-analytics can be exported, either as PNG image, ![](../screenshots/export-menu.png) # Dashboards and Visualization with Grafana -In addition or alternatively to the built-in dashboards, it's also possible to [use Grafana for visualization](./prometheus-exporter-grafana-dashboard.md) thanks to the Prometheus exporter natively enabled by `kube-opex-analytics`. \ No newline at end of file +In addition or alternatively to the built-in dashboards, it's also possible to [use Grafana for visualization](./prometheus-exporter-grafana-dashboard.md) thanks to the Prometheus exporter natively enabled by `kube-opex-analytics`. From 29a62709d3219d834c3699f1587b1d5f18e001f0 Mon Sep 17 00:00:00 2001 From: Rodrigue Chakode Date: Tue, 28 Jun 2022 21:35:42 +0200 Subject: [PATCH 02/32] Updated highlights --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 068f3c4..5208635 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ Key features: -* **Consumption hourly trends, daily and monthly accounting per namespace.** This feature provides analytics metrics _tracking both actual usage and requested capacities_ over time. Metrics are namespaced-based, collected every 5 minutes, consolidated on a hourly basis for trends, from which daily and monthly accounting is processed. +* **Hourly consumption trends, daily and monthly accounting per namespace.** This feature provides analytics metrics _tracking both actual usage and requested capacities_ over time. Metrics are namespaced-based, collected every 5 minutes, consolidated on a hourly basis for trends, from which daily and monthly accounting is processed. * **Accounting of non-allocatable capacities.** At node and cluster levels, `kube-opex-analytics` tracks and consolidates the share of non-allocatable capacities and highlights them against usable capacities (i.e. capacities used by actual application workloads). In contrary to usable capacities, non-allocatable capacities are dedicated to Kubernetes operations (OS, kubelets, etc). * **Cluster usage accounting and capacity planning.** This feature makes it easy to account and visualize capacities consumed on a cluster, globally, instantly and over time. * **Usage/requests efficiency.** Based on hourly-consolidated trends, this functionality helps know how efficient resource requests set on Kubernetes workloads are, compared against the actual resource usage over time. From 732185cc8356fa4e6d559970289c12da2886a5e0 Mon Sep 17 00:00:00 2001 From: Mateus Caruccio Date: Tue, 28 Jun 2022 12:26:06 -0300 Subject: [PATCH 03/32] Add sugar includedNamespaces/excludedNamespaces to helm --- manifests/helm/templates/deployment.yaml | 14 ++++++++++++++ manifests/helm/values.yaml | 6 ++++++ 2 files changed, 20 insertions(+) diff --git a/manifests/helm/templates/deployment.yaml b/manifests/helm/templates/deployment.yaml index 3ebbd17..7f8f49c 100644 --- a/manifests/helm/templates/deployment.yaml +++ b/manifests/helm/templates/deployment.yaml @@ -44,6 +44,20 @@ spec: - name: {{ $key | quote }} value: {{ $val | quote }} {{- end }} + {{- end }} + {{- if .Values.includedNamespaces }} + {{- if not .Values.envs }} + env: + {{- end }} + - name: INCLUDED_NAMESPACES + value: {{ join "," $.Values.includedNamespaces }} + {{- end }} + {{- if .Values.excludedNamespaces }} + {{- if not .Values.envs }} + env: + {{- end }} + - name: EXCLUDED_NAMESPACES + value: {{ join "," $.Values.excludedNamespaces }} {{- end }} ports: - name: http diff --git a/manifests/helm/values.yaml b/manifests/helm/values.yaml index 8e38e3a..e169eb3 100644 --- a/manifests/helm/values.yaml +++ b/manifests/helm/values.yaml @@ -16,6 +16,12 @@ dataVolume: capacity: 4Gi # storageClass: default +# overrides envs.INCLUDED_NAMESPACES +includedNamespaces: [] + +# overrides envs.EXCLUDED_NAMESPACES +excludedNamespaces: [] + prometheusOperator: enabled: false labels: From 19c04197f62065eaee1459c7d87b4f53b00b2ddd Mon Sep 17 00:00:00 2001 From: Rodrigue Chakode Date: Fri, 1 Jul 2022 18:36:25 +0200 Subject: [PATCH 04/32] fixed dependabot.yml --- .github/dependabot.yml | 4 ++++ .github/workflows/dependabot.yml | 6 ------ 2 files changed, 4 insertions(+), 6 deletions(-) delete mode 100644 .github/workflows/dependabot.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 9c41570..38f94b5 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -9,3 +9,7 @@ updates: - rchakode reviewers: - rchakode +- package-ecosystem: "docker" + directory: "/" + schedule: + interval: "daily" diff --git a/.github/workflows/dependabot.yml b/.github/workflows/dependabot.yml deleted file mode 100644 index 313f059..0000000 --- a/.github/workflows/dependabot.yml +++ /dev/null @@ -1,6 +0,0 @@ -version: 2 -updates: - - package-ecosystem: "docker" - directory: "/" - schedule: - interval: "daily" From 07f1ea1786c606ce2261b536a27f50b487d842fa Mon Sep 17 00:00:00 2001 From: Nesrine Date: Wed, 18 May 2022 14:58:01 +0200 Subject: [PATCH 05/32] fixed after rebase --- backend.py | 89 ++++++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 73 insertions(+), 16 deletions(-) diff --git a/backend.py b/backend.py index bbcb9b1..4c2c8d8 100644 --- a/backend.py +++ b/backend.py @@ -46,6 +46,8 @@ def create_directory_if_not_exists(path): try: + # os.mkdir() method in Python is used to create a directory named path with the specified numeric mode. + # This method raises FileExistsError if the directory to be created already exists. os.makedirs(path) except OSError as e: if e.errno != errno.EEXIST: @@ -63,7 +65,9 @@ class Config: k8s_verify_ssl = (lambda v: v.lower() in ("yes", "true"))(os.getenv('KOA_K8S_API_VERIFY_SSL', 'true')) db_location = os.getenv('KOA_DB_LOCATION', ('%s/.kube-opex-analytics/db') % os.getenv('HOME', '/tmp')) polling_interval_sec = int(os.getenv('KOA_POLLING_INTERVAL_SEC', '300')) + # default cost model is CUMULATIVE_RATIO cost_model = os.getenv('KOA_COST_MODEL', 'CUMULATIVE_RATIO') + # default currency is $ billing_currency = os.getenv('KOA_BILLING_CURRENCY_SYMBOL', '$') enable_debug = (lambda v: v.lower() in ("yes", "true"))(os.getenv('KOA_ENABLE_DEBUG', 'false')) k8s_auth_token = os.getenv('KOA_K8S_AUTH_TOKEN', 'NO_ENV_AUTH_TOKEN') @@ -76,7 +80,10 @@ class Config: included_namespaces = [i for i in os.getenv('KOA_INCLUDED_NAMESPACES', '').replace(' ', ',').split(',') if i] excluded_namespaces = [i for i in os.getenv('KOA_EXCLUDED_NAMESPACES', '').replace(' ', ',').split(',') if i] + # constructer of the class Config def __init__(self): + + # load token if exists self.load_rbac_auth_token() # handle billing rate and cost model @@ -85,11 +92,23 @@ def __init__(self): except: self.billing_hourly_rate = -1.0 + # "frontend_data_location="static_content_location"/data = /static/data create_directory_if_not_exists(self.frontend_data_location) + + # partie affichage dynamique "Usage Trends" en fonction du cost_model + # modifiication du fichier /static/data/backend.json + with open(str('%s/backend.json' % self.frontend_data_location), 'w') as fd: - if self.cost_model == 'CHARGE_BACK': - cost_model_label = 'costs' + if self.cost_model == 'CHARGE_BACK' : + cost_model_label = 'costs' + cost_model_unit = self.billing_currency + + # ask M. Rodrigue if this is correct + # cost_model while using aks is called AKS_Billing + elif self.cost_model=='AKS_Billing': + cost_model_label = 'aks_costs' cost_model_unit = self.billing_currency + elif self.cost_model == 'RATIO': cost_model_label = 'normalized' cost_model_unit = '%' @@ -98,8 +117,12 @@ def __init__(self): cost_model_unit = '%' fd.write('{"cost_model":"%s", "currency":"%s"}' % (cost_model_label, cost_model_unit)) - # handle cacert file if applicable + # handle cacert file if applicable (verification du certificat ) + # if k8s_verify_ssl== True and k8s_ssl_cacert not None and k8s_ssl_cacert refers to an existing path: + if self.k8s_verify_ssl and self.k8s_ssl_cacert and os.path.exists(self.k8s_ssl_cacert): + # os.path.exists(path) Return True if path refers to an existing path or an open file descriptor. + self.koa_verify_ssl_option = self.k8s_ssl_cacert else: self.koa_verify_ssl_option = self.k8s_verify_ssl @@ -137,9 +160,12 @@ def usage_efficiency_db(ns): def configure_logger(debug_enabled): if debug_enabled: - log_level = logging.DEBUG + # The logging module keeps a record of the events that occur within a program, + # making it possible to see output related to any of the events that occur throughout the runtime of a piece of software. + log_level = logging.DEBUG # level= 10 else: - log_level = logging.WARN + log_level = logging.WARN # level=30 + logger = logging.getLogger('kube-opex-analytics') logger.setLevel(log_level) ch = logging.StreamHandler() @@ -158,6 +184,8 @@ def configure_logger(debug_enabled): class RrdPeriod(enum.IntEnum): +# class enum.IntEnum: Classe de base pour créer une énumération de constantes qui sont également des sous-classes de int. + PERIOD_5_MINS_SEC = 300 PERIOD_1_HOUR_SEC = 3600 PERIOD_1_DAY_SEC = 86400 @@ -236,6 +264,15 @@ def render(): return flask.render_template('index.html', koa_frontend_data_location=KOA_CONFIG.frontend_data_location, koa_version=KOA_CONFIG.version) +# AKS price compute +def get_Azure_price(node): + price=0.0 + api_endpoint= "https://prices.azure.com/api/retail/prices?$filter=armRegionName eq '"+ node.region +"' and skuName eq '"+node.instanceType[0].lower()+node.instanceType[1:]+"' and serviceName eq 'Virtual Machines' and unitOfMeasure eq '1 Hour'" + while api_endpoint is not None: + data_json=requests.get(api_endpoint).json() + item=data_json.get('Items')[0] + price=item.get('unitPrice') + return price class Node: def __init__(self): @@ -252,6 +289,11 @@ def __init__(self): self.containerRuntime = '' self.podsRunning = [] self.podsNotRunning = [] + self.region = '' + self.instanceType = '' + self.aksCluster = None + self.HourlyPrice= 0.0 + class Pod: @@ -343,6 +385,11 @@ def __init__(self): 'n': 1e-9, 'None': 1 } + self.managed_controlPlane_Price=0.10 + ### the pricing is similar for the managed control plane of all of AKS, EKS and GKE at $0.10/hour + self.hourlyRate=0.0 + + def decode_capacity(self, cap_input): data_length = len(cap_input) @@ -388,6 +435,15 @@ def extract_nodes(self, data): if metadata is not None: node.id = metadata.get('uid', None) node.name = metadata.get('name', None) + # If cluster is an AKS cluster + node.aksCluster= metadata['labels'].get('kubernetes.azure.com/cluster', None) + if node.aksCluster!=None : + self.hourlyRate=self.managed_controlPlane_Price + node.region = metadata['labels']['topology.kubernetes.io/region'] + node.instanceType = metadata['labels']['node.kubernetes.io/instance-type'] + node.HourlyPrice= get_Azure_price(node) + self.hourlyRate+=node.HourlyPrice + status = item.get('status', None) if status is not None: @@ -512,6 +568,7 @@ def consolidate_ns_usage(self): self.cpuUsageAllPods = 0.0 self.memUsageAllPods = 0.0 for pod in self.pods.values(): + # The hasattr() method returns true if an object has the given named attribute and false if it does not. if pod.nodeName is not None and hasattr(pod, 'cpuUsage') and hasattr(pod, 'memUsage'): self.cpuUsageAllPods += pod.cpuUsage self.memUsageAllPods += pod.memUsage @@ -520,23 +577,19 @@ def consolidate_ns_usage(self): if ns_pod_usage is not None: ns_pod_usage.cpu += pod.cpuUsage ns_pod_usage.mem += pod.memUsage - ns_pod_request = self.requestByNamespace.get(pod.namespace, None) if ns_pod_request is not None: ns_pod_request.cpu += pod.cpuRequest ns_pod_request.mem += pod.memRequest - pod_node = self.nodes.get(pod.nodeName, None) if pod_node is not None: pod_node.podsRunning.append(pod) - self.cpuCapacity += 0.0 self.memCapacity += 0.0 for node in self.nodes.values(): if hasattr(node, 'cpuCapacity') and hasattr(node, 'memCapacity'): self.cpuCapacity += node.cpuCapacity self.memCapacity += node.memCapacity - self.cpuAllocatable += 0.0 self.memAllocatable += 0.0 for node in self.nodes.values(): @@ -548,7 +601,6 @@ def dump_nodes(self): with open(str('%s/nodes.json' % KOA_CONFIG.frontend_data_location), 'w') as fd: fd.write(json.dumps(self.nodes, cls=JSONMarshaller)) - def compute_usage_percent_ratio(value, total): return round((100.0 * value) / total, KOA_CONFIG.db_round_decimals) @@ -791,10 +843,11 @@ def dump_histogram_analytics(dbfiles, period): with open(str('%s/memory_requests_period_%d.json' % (KOA_CONFIG.frontend_data_location, period)), 'w') as fd: fd.write('[' + ','.join(requests_export[1]) + ']') - +# pull_k8s(api_context) returns the response to the api_endpoint'http://localhost:8001/"api_context"' after authentication def pull_k8s(api_context): data = None api_endpoint = '%s%s' % (KOA_CONFIG.k8s_api_endpoint, api_context) + # api_endpoint='http://localhost:8001/"api_context"' headers = {} client_cert = None endpoint_info = urllib.parse.urlparse(KOA_CONFIG.k8s_api_endpoint) @@ -815,8 +868,8 @@ def pull_k8s(api_context): verify=KOA_CONFIG.koa_verify_ssl_option, headers=headers, cert=client_cert) - if http_req.status_code == 200: - data = http_req.text + if http_req.status_code == 200: # if the request is successful and the server responds with the requested data + data = http_req.text # data stores the respond else: KOA_LOGGER.error("call to %s returned error (%s)", api_endpoint, http_req.text) except Exception as ex: @@ -826,7 +879,6 @@ def pull_k8s(api_context): return data - def create_metrics_puller(): try: while True: @@ -836,6 +888,7 @@ def create_metrics_puller(): k8s_usage = K8sUsage() k8s_usage.extract_namespaces_and_initialize_usage(pull_k8s('/api/v1/namespaces')) + # extract_namespaces_and_initialize_usage extrait tous les namespaces du cluster et initialise cpu et mem à 0 k8s_usage.extract_nodes(pull_k8s('/api/v1/nodes')) k8s_usage.extract_node_metrics(pull_k8s('/apis/metrics.k8s.io/v1beta1/nodes')) k8s_usage.extract_pods(pull_k8s('/api/v1/pods')) @@ -855,9 +908,13 @@ def create_metrics_puller(): rrd.add_sample(timestamp_epoch=now_epoch, cpu_usage=cpu_non_allocatable, mem_usage=mem_non_allocatable) # handle billing data + if KOA_CONFIG.cost_model=="AKS_Billing": + Billing_hourly_rate=k8s_usage.hourlyRate + else: + Billing_hourly_rate=KOA_CONFIG.billing_hourly_rate rrd = Rrd(db_files_location=KOA_CONFIG.db_location, dbname=KOA_CONFIG.db_billing_hourly_rate) - rrd.add_sample(timestamp_epoch=now_epoch, cpu_usage=KOA_CONFIG.billing_hourly_rate, - mem_usage=KOA_CONFIG.billing_hourly_rate) + rrd.add_sample(timestamp_epoch=now_epoch, cpu_usage=Billing_hourly_rate, + mem_usage=Billing_hourly_rate) # handle resource request and usage by pods for ns, ns_usage in k8s_usage.usageByNamespace.items(): From 2e68d5fe19bffe5f9f863e9850a6dd6e63a4e3c2 Mon Sep 17 00:00:00 2001 From: Nesrine <88090963+NesrineSfaxi@users.noreply.github.com> Date: Fri, 20 May 2022 15:16:11 +0200 Subject: [PATCH 06/32] Update backend.py --- backend.py | 41 ++++++++++++++++++++++++----------------- 1 file changed, 24 insertions(+), 17 deletions(-) diff --git a/backend.py b/backend.py index 4c2c8d8..0cc7609 100644 --- a/backend.py +++ b/backend.py @@ -93,19 +93,17 @@ def __init__(self): self.billing_hourly_rate = -1.0 # "frontend_data_location="static_content_location"/data = /static/data - create_directory_if_not_exists(self.frontend_data_location) - - # partie affichage dynamique "Usage Trends" en fonction du cost_model - # modifiication du fichier /static/data/backend.json + create_directory_if_not_exists(self.frontend_data_billingta_location) with open(str('%s/backend.json' % self.frontend_data_location), 'w') as fd: + if self.cost_model == 'CHARGE_BACK' : cost_model_label = 'costs' cost_model_unit = self.billing_currency # ask M. Rodrigue if this is correct # cost_model while using aks is called AKS_Billing - elif self.cost_model=='AKS_Billing': + elif self.cost_model=='AKS_CHARGE_BACK': cost_model_label = 'aks_costs' cost_model_unit = self.billing_currency @@ -385,7 +383,7 @@ def __init__(self): 'n': 1e-9, 'None': 1 } - self.managed_controlPlane_Price=0.10 + self.managedControlPlanePrice = 0.10 ### the pricing is similar for the managed control plane of all of AKS, EKS and GKE at $0.10/hour self.hourlyRate=0.0 @@ -436,9 +434,9 @@ def extract_nodes(self, data): node.id = metadata.get('uid', None) node.name = metadata.get('name', None) # If cluster is an AKS cluster - node.aksCluster= metadata['labels'].get('kubernetes.azure.com/cluster', None) - if node.aksCluster!=None : - self.hourlyRate=self.managed_controlPlane_Price + node.aksCluster = metadata['labels'].get('kubernetes.azure.com/cluster', None) + if node.aksCluster != None : + self.hourlyRate=self.managedControlPlanePrice node.region = metadata['labels']['topology.kubernetes.io/region'] node.instanceType = metadata['labels']['node.kubernetes.io/instance-type'] node.HourlyPrice= get_Azure_price(node) @@ -766,6 +764,11 @@ def dump_histogram_analytics(dbfiles, period): requests_export = collections.defaultdict(list) requests_per_type_date = {} sum_requests_per_type_date = {} + + actual_cost_model = 'CUMULATIVE' + if KOA_CONFIG.cost_model in ['CHARGE_BACK', 'AKS_CHARGE_BACK']: + actual_cost_model = 'CHARGE_BACK' + for _, db in enumerate(dbfiles): rrd = Rrd(db_files_location=KOA_CONFIG.db_location, dbname=db) current_periodic_usage = rrd.dump_histogram_data(period=period) @@ -805,15 +808,19 @@ def dump_histogram_analytics(dbfiles, period): for res, usage_data_bundle in usage_per_type_date.items(): for date_key, db_usage_item in usage_data_bundle.items(): for db, usage_value in db_usage_item.items(): + if db != KOA_CONFIG.db_billing_hourly_rate: usage_cost = round(usage_value, KOA_CONFIG.db_round_decimals) - if KOA_CONFIG.cost_model == 'RATIO' or KOA_CONFIG.cost_model == 'CHARGE_BACK': + + if KOA_CONFIG.cost_model == 'RATIO' or actual_cost_model == 'CHARGE_BACK': usage_ratio = usage_value / sum_usage_per_type_date[res][date_key] usage_cost = round(100 * usage_ratio, KOA_CONFIG.db_round_decimals) - if KOA_CONFIG.cost_model == 'CHARGE_BACK': + + if actual_cost_model == 'CHARGE_BACK': usage_cost = round( usage_ratio * usage_per_type_date[res][date_key][KOA_CONFIG.db_billing_hourly_rate], KOA_CONFIG.db_round_decimals) + usage_export[res].append('{"stack":"%s","usage":%f,"date":"%s"}' % (db, usage_cost, date_key)) if Rrd.get_date_group(now_gmtime, period) == date_key: PROMETHEUS_PERIODIC_USAGE_EXPORTERS[period].labels(db, ResUsageType(res).name).set( @@ -908,13 +915,13 @@ def create_metrics_puller(): rrd.add_sample(timestamp_epoch=now_epoch, cpu_usage=cpu_non_allocatable, mem_usage=mem_non_allocatable) # handle billing data - if KOA_CONFIG.cost_model=="AKS_Billing": - Billing_hourly_rate=k8s_usage.hourlyRate - else: - Billing_hourly_rate=KOA_CONFIG.billing_hourly_rate + billing_hourly_rate = KOA_CONFIG.billing_hourly_rate + if KOA_CONFIG.cost_model == "AKS_CHARGE_BACK": + billing_hourly_rate = k8s_usage.hourlyRate + rrd = Rrd(db_files_location=KOA_CONFIG.db_location, dbname=KOA_CONFIG.db_billing_hourly_rate) - rrd.add_sample(timestamp_epoch=now_epoch, cpu_usage=Billing_hourly_rate, - mem_usage=Billing_hourly_rate) + rrd.add_sample(timestamp_epoch=now_epoch, cpu_usage=billing_hourly_rate, + mem_usage=billing_hourly_rate) # handle resource request and usage by pods for ns, ns_usage in k8s_usage.usageByNamespace.items(): From 928b46d53b58b43e5d04468cf9cf6cd728a21df6 Mon Sep 17 00:00:00 2001 From: Nesrine <88090963+NesrineSfaxi@users.noreply.github.com> Date: Fri, 20 May 2022 15:37:24 +0200 Subject: [PATCH 07/32] Update backend.py --- backend.py | 37 ++++++++++--------------------------- 1 file changed, 10 insertions(+), 27 deletions(-) diff --git a/backend.py b/backend.py index 0cc7609..8c21bb2 100644 --- a/backend.py +++ b/backend.py @@ -46,8 +46,6 @@ def create_directory_if_not_exists(path): try: - # os.mkdir() method in Python is used to create a directory named path with the specified numeric mode. - # This method raises FileExistsError if the directory to be created already exists. os.makedirs(path) except OSError as e: if e.errno != errno.EEXIST: @@ -65,9 +63,7 @@ class Config: k8s_verify_ssl = (lambda v: v.lower() in ("yes", "true"))(os.getenv('KOA_K8S_API_VERIFY_SSL', 'true')) db_location = os.getenv('KOA_DB_LOCATION', ('%s/.kube-opex-analytics/db') % os.getenv('HOME', '/tmp')) polling_interval_sec = int(os.getenv('KOA_POLLING_INTERVAL_SEC', '300')) - # default cost model is CUMULATIVE_RATIO cost_model = os.getenv('KOA_COST_MODEL', 'CUMULATIVE_RATIO') - # default currency is $ billing_currency = os.getenv('KOA_BILLING_CURRENCY_SYMBOL', '$') enable_debug = (lambda v: v.lower() in ("yes", "true"))(os.getenv('KOA_ENABLE_DEBUG', 'false')) k8s_auth_token = os.getenv('KOA_K8S_AUTH_TOKEN', 'NO_ENV_AUTH_TOKEN') @@ -80,10 +76,8 @@ class Config: included_namespaces = [i for i in os.getenv('KOA_INCLUDED_NAMESPACES', '').replace(' ', ',').split(',') if i] excluded_namespaces = [i for i in os.getenv('KOA_EXCLUDED_NAMESPACES', '').replace(' ', ',').split(',') if i] - # constructer of the class Config - def __init__(self): - # load token if exists + def __init__(self): self.load_rbac_auth_token() # handle billing rate and cost model @@ -92,7 +86,6 @@ def __init__(self): except: self.billing_hourly_rate = -1.0 - # "frontend_data_location="static_content_location"/data = /static/data create_directory_if_not_exists(self.frontend_data_billingta_location) with open(str('%s/backend.json' % self.frontend_data_location), 'w') as fd: @@ -101,9 +94,8 @@ def __init__(self): cost_model_label = 'costs' cost_model_unit = self.billing_currency - # ask M. Rodrigue if this is correct - # cost_model while using aks is called AKS_Billing - elif self.cost_model=='AKS_CHARGE_BACK': + # if cluster is an aks cluster then cost_model = 'AKS_CHARGE_BACK' + elif self.cost_model == 'AKS_CHARGE_BACK': cost_model_label = 'aks_costs' cost_model_unit = self.billing_currency @@ -115,12 +107,9 @@ def __init__(self): cost_model_unit = '%' fd.write('{"cost_model":"%s", "currency":"%s"}' % (cost_model_label, cost_model_unit)) - # handle cacert file if applicable (verification du certificat ) - # if k8s_verify_ssl== True and k8s_ssl_cacert not None and k8s_ssl_cacert refers to an existing path: + # handle cacert file if applicable if self.k8s_verify_ssl and self.k8s_ssl_cacert and os.path.exists(self.k8s_ssl_cacert): - # os.path.exists(path) Return True if path refers to an existing path or an open file descriptor. - self.koa_verify_ssl_option = self.k8s_ssl_cacert else: self.koa_verify_ssl_option = self.k8s_verify_ssl @@ -158,11 +147,9 @@ def usage_efficiency_db(ns): def configure_logger(debug_enabled): if debug_enabled: - # The logging module keeps a record of the events that occur within a program, - # making it possible to see output related to any of the events that occur throughout the runtime of a piece of software. - log_level = logging.DEBUG # level= 10 + log_level = logging.DEBUG else: - log_level = logging.WARN # level=30 + log_level = logging.WARN logger = logging.getLogger('kube-opex-analytics') logger.setLevel(log_level) @@ -182,7 +169,6 @@ def configure_logger(debug_enabled): class RrdPeriod(enum.IntEnum): -# class enum.IntEnum: Classe de base pour créer une énumération de constantes qui sont également des sous-classes de int. PERIOD_5_MINS_SEC = 300 PERIOD_1_HOUR_SEC = 3600 @@ -384,7 +370,7 @@ def __init__(self): 'None': 1 } self.managedControlPlanePrice = 0.10 - ### the pricing is similar for the managed control plane of all of AKS, EKS and GKE at $0.10/hour + # the pricing of the managed control plane is similar for all of AKS, EKS and GKE at $0.10/hour self.hourlyRate=0.0 @@ -566,7 +552,7 @@ def consolidate_ns_usage(self): self.cpuUsageAllPods = 0.0 self.memUsageAllPods = 0.0 for pod in self.pods.values(): - # The hasattr() method returns true if an object has the given named attribute and false if it does not. + if pod.nodeName is not None and hasattr(pod, 'cpuUsage') and hasattr(pod, 'memUsage'): self.cpuUsageAllPods += pod.cpuUsage self.memUsageAllPods += pod.memUsage @@ -850,11 +836,9 @@ def dump_histogram_analytics(dbfiles, period): with open(str('%s/memory_requests_period_%d.json' % (KOA_CONFIG.frontend_data_location, period)), 'w') as fd: fd.write('[' + ','.join(requests_export[1]) + ']') -# pull_k8s(api_context) returns the response to the api_endpoint'http://localhost:8001/"api_context"' after authentication def pull_k8s(api_context): data = None api_endpoint = '%s%s' % (KOA_CONFIG.k8s_api_endpoint, api_context) - # api_endpoint='http://localhost:8001/"api_context"' headers = {} client_cert = None endpoint_info = urllib.parse.urlparse(KOA_CONFIG.k8s_api_endpoint) @@ -875,8 +859,8 @@ def pull_k8s(api_context): verify=KOA_CONFIG.koa_verify_ssl_option, headers=headers, cert=client_cert) - if http_req.status_code == 200: # if the request is successful and the server responds with the requested data - data = http_req.text # data stores the respond + if http_req.status_code == 200: + data = http_req.text else: KOA_LOGGER.error("call to %s returned error (%s)", api_endpoint, http_req.text) except Exception as ex: @@ -895,7 +879,6 @@ def create_metrics_puller(): k8s_usage = K8sUsage() k8s_usage.extract_namespaces_and_initialize_usage(pull_k8s('/api/v1/namespaces')) - # extract_namespaces_and_initialize_usage extrait tous les namespaces du cluster et initialise cpu et mem à 0 k8s_usage.extract_nodes(pull_k8s('/api/v1/nodes')) k8s_usage.extract_node_metrics(pull_k8s('/apis/metrics.k8s.io/v1beta1/nodes')) k8s_usage.extract_pods(pull_k8s('/api/v1/pods')) From eab52196a90507377509457ed75fa6f1ccc61414 Mon Sep 17 00:00:00 2001 From: Nesrine <88090963+NesrineSfaxi@users.noreply.github.com> Date: Mon, 23 May 2022 10:43:10 +0200 Subject: [PATCH 08/32] Fixed frontend data location name --- backend.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend.py b/backend.py index 8c21bb2..54dc2e0 100644 --- a/backend.py +++ b/backend.py @@ -86,7 +86,7 @@ def __init__(self): except: self.billing_hourly_rate = -1.0 - create_directory_if_not_exists(self.frontend_data_billingta_location) + create_directory_if_not_exists(self.frontend_data_location) with open(str('%s/backend.json' % self.frontend_data_location), 'w') as fd: From dcce209c87e99ba74ad4c77271b65f5c2a4254f6 Mon Sep 17 00:00:00 2001 From: Nesrine <88090963+NesrineSfaxi@users.noreply.github.com> Date: Fri, 3 Jun 2022 17:44:52 +0200 Subject: [PATCH 09/32] Updated get_azure_price method --- backend.py | 36 ++++++++++++++++++++++++++---------- 1 file changed, 26 insertions(+), 10 deletions(-) diff --git a/backend.py b/backend.py index 54dc2e0..e188819 100644 --- a/backend.py +++ b/backend.py @@ -251,12 +251,23 @@ def render(): # AKS price compute def get_Azure_price(node): price=0.0 - api_endpoint= "https://prices.azure.com/api/retail/prices?$filter=armRegionName eq '"+ node.region +"' and skuName eq '"+node.instanceType[0].lower()+node.instanceType[1:]+"' and serviceName eq 'Virtual Machines' and unitOfMeasure eq '1 Hour'" - while api_endpoint is not None: + api_endpoint = "https://prices.azure.com/api/retail/prices?$filter=armRegionName eq '"+ node.region +"' and skuName eq '"+node.instanceType+"' and serviceName eq 'Virtual Machines'" + data_json=requests.get(api_endpoint).json() + if data_json.get("Count", None) == 0 : + api_endpoint = "https://prices.azure.com/api/retail/prices?$filter=armRegionName eq '"+ node.region +"' and skuName eq '"+node.instanceType[0].lower()+node.instanceType[1:]+"' and serviceName eq 'Virtual Machines'" + while price == 0.0 : data_json=requests.get(api_endpoint).json() - item=data_json.get('Items')[0] - price=item.get('unitPrice') - return price + for _, item in enumerate(data_json["Items"]): + if node.os == "windows" : + if item["type"] == "Consumption" and item["productName"].endswith('Windows'): + price=item.get('unitPrice') + elif node.os == "linux" : + if item["type"] == "Consumption" and not (item["productName"].endswith('Windows')): + price=item.get('unitPrice') + api_endpoint = data_json["NextPageLink"] + if api_endpoint is None : + break + return price class Node: def __init__(self): @@ -274,6 +285,7 @@ def __init__(self): self.podsRunning = [] self.podsNotRunning = [] self.region = '' + self.os = '' self.instanceType = '' self.aksCluster = None self.HourlyPrice= 0.0 @@ -369,7 +381,7 @@ def __init__(self): 'n': 1e-9, 'None': 1 } - self.managedControlPlanePrice = 0.10 + self.aksManagedControlPlanePrice = 0.10 # the pricing of the managed control plane is similar for all of AKS, EKS and GKE at $0.10/hour self.hourlyRate=0.0 @@ -419,12 +431,16 @@ def extract_nodes(self, data): if metadata is not None: node.id = metadata.get('uid', None) node.name = metadata.get('name', None) - # If cluster is an AKS cluster + node.aksCluster = metadata['labels'].get('kubernetes.azure.com/cluster', None) + node.region = metadata['labels']['topology.kubernetes.io/region'] + node.instanceType = metadata['labels']['node.kubernetes.io/instance-type'] + node.os = metadata['labels']["kubernetes.io/os"] + + + # If cluster is an AKS cluster if node.aksCluster != None : - self.hourlyRate=self.managedControlPlanePrice - node.region = metadata['labels']['topology.kubernetes.io/region'] - node.instanceType = metadata['labels']['node.kubernetes.io/instance-type'] + self.hourlyRate=self.aksManagedControlPlanePrice node.HourlyPrice= get_Azure_price(node) self.hourlyRate+=node.HourlyPrice From ffb491fabb898d011ca22675435a173a38cc9aa0 Mon Sep 17 00:00:00 2001 From: Nesrine <88090963+NesrineSfaxi@users.noreply.github.com> Date: Tue, 7 Jun 2022 15:14:06 +0200 Subject: [PATCH 10/32] Fixed syntax --- backend.py | 48 ++++++++++++++++++++++++------------------------ 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/backend.py b/backend.py index e188819..5661f6f 100644 --- a/backend.py +++ b/backend.py @@ -90,7 +90,7 @@ def __init__(self): with open(str('%s/backend.json' % self.frontend_data_location), 'w') as fd: - if self.cost_model == 'CHARGE_BACK' : + if self.cost_model == 'CHARGE_BACK': cost_model_label = 'costs' cost_model_unit = self.billing_currency @@ -248,27 +248,29 @@ def render(): return flask.render_template('index.html', koa_frontend_data_location=KOA_CONFIG.frontend_data_location, koa_version=KOA_CONFIG.version) -# AKS price compute + def get_Azure_price(node): - price=0.0 - api_endpoint = "https://prices.azure.com/api/retail/prices?$filter=armRegionName eq '"+ node.region +"' and skuName eq '"+node.instanceType+"' and serviceName eq 'Virtual Machines'" - data_json=requests.get(api_endpoint).json() - if data_json.get("Count", None) == 0 : - api_endpoint = "https://prices.azure.com/api/retail/prices?$filter=armRegionName eq '"+ node.region +"' and skuName eq '"+node.instanceType[0].lower()+node.instanceType[1:]+"' and serviceName eq 'Virtual Machines'" - while price == 0.0 : - data_json=requests.get(api_endpoint).json() + price = 0.0 + api_base = "https://prices.azure.com/api/retail/prices?$filter=armRegionName eq '" + api_endpoint = api_base + node.region + "' and skuName eq '" + node.instanceType + "' and serviceName eq 'Virtual Machines'" + data_json = requests.get(api_endpoint).json() + if data_json.get("Count", None) == 0: + api_endpoint = api_base + node.region + "' and skuName eq '" + node.instanceType[0].lower() + node.instanceType[1:] + "' and serviceName eq 'Virtual Machines'" + while price == 0.0: + data_json = requests.get(api_endpoint).json() for _, item in enumerate(data_json["Items"]): - if node.os == "windows" : + if node.os == "windows": if item["type"] == "Consumption" and item["productName"].endswith('Windows'): - price=item.get('unitPrice') - elif node.os == "linux" : + price = item.get('unitPrice') + elif node.os == "linux": if item["type"] == "Consumption" and not (item["productName"].endswith('Windows')): price=item.get('unitPrice') api_endpoint = data_json["NextPageLink"] - if api_endpoint is None : + if api_endpoint is None: break return price + class Node: def __init__(self): self.id = '' @@ -288,8 +290,7 @@ def __init__(self): self.os = '' self.instanceType = '' self.aksCluster = None - self.HourlyPrice= 0.0 - + self.HourlyPrice = 0.0 class Pod: @@ -383,10 +384,8 @@ def __init__(self): } self.aksManagedControlPlanePrice = 0.10 # the pricing of the managed control plane is similar for all of AKS, EKS and GKE at $0.10/hour - self.hourlyRate=0.0 - + self.hourlyRate = 0.0 - def decode_capacity(self, cap_input): data_length = len(cap_input) cap_unit = 'None' @@ -437,13 +436,11 @@ def extract_nodes(self, data): node.instanceType = metadata['labels']['node.kubernetes.io/instance-type'] node.os = metadata['labels']["kubernetes.io/os"] - # If cluster is an AKS cluster - if node.aksCluster != None : - self.hourlyRate=self.aksManagedControlPlanePrice - node.HourlyPrice= get_Azure_price(node) - self.hourlyRate+=node.HourlyPrice - + if node.aksCluster is not None: + self.hourlyRate = self.aksManagedControlPlanePrice + node.HourlyPrice = get_Azure_price(node) + self.hourlyRate += node.HourlyPrice status = item.get('status', None) if status is not None: @@ -601,6 +598,7 @@ def dump_nodes(self): with open(str('%s/nodes.json' % KOA_CONFIG.frontend_data_location), 'w') as fd: fd.write(json.dumps(self.nodes, cls=JSONMarshaller)) + def compute_usage_percent_ratio(value, total): return round((100.0 * value) / total, KOA_CONFIG.db_round_decimals) @@ -852,6 +850,7 @@ def dump_histogram_analytics(dbfiles, period): with open(str('%s/memory_requests_period_%d.json' % (KOA_CONFIG.frontend_data_location, period)), 'w') as fd: fd.write('[' + ','.join(requests_export[1]) + ']') + def pull_k8s(api_context): data = None api_endpoint = '%s%s' % (KOA_CONFIG.k8s_api_endpoint, api_context) @@ -886,6 +885,7 @@ def pull_k8s(api_context): return data + def create_metrics_puller(): try: while True: From 602b03c44935ef850120dca491579a66484e1dcd Mon Sep 17 00:00:00 2001 From: Nesrine <88090963+NesrineSfaxi@users.noreply.github.com> Date: Fri, 10 Jun 2022 16:53:35 +0200 Subject: [PATCH 11/32] Fix error related to "billing_hourly_rate" --- backend.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/backend.py b/backend.py index 5661f6f..314453d 100644 --- a/backend.py +++ b/backend.py @@ -914,8 +914,9 @@ def create_metrics_puller(): rrd.add_sample(timestamp_epoch=now_epoch, cpu_usage=cpu_non_allocatable, mem_usage=mem_non_allocatable) # handle billing data - billing_hourly_rate = KOA_CONFIG.billing_hourly_rate - if KOA_CONFIG.cost_model == "AKS_CHARGE_BACK": + if KOA_CONFIG.cost_model == "CHARGE_BACK": + billing_hourly_rate = KOA_CONFIG.billing_hourly_rate + elif KOA_CONFIG.cost_model == "AKS_CHARGE_BACK": billing_hourly_rate = k8s_usage.hourlyRate rrd = Rrd(db_files_location=KOA_CONFIG.db_location, dbname=KOA_CONFIG.db_billing_hourly_rate) From 6296aaccd839f6dcf7dc4691c82609db2d641d43 Mon Sep 17 00:00:00 2001 From: Nesrine <88090963+NesrineSfaxi@users.noreply.github.com> Date: Mon, 13 Jun 2022 13:42:19 +0200 Subject: [PATCH 12/32] Fixed syntax --- backend.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/backend.py b/backend.py index 314453d..e637c76 100644 --- a/backend.py +++ b/backend.py @@ -290,7 +290,7 @@ def __init__(self): self.os = '' self.instanceType = '' self.aksCluster = None - self.HourlyPrice = 0.0 + self.hourlyPrice = 0.0 class Pod: @@ -439,8 +439,8 @@ def extract_nodes(self, data): # If cluster is an AKS cluster if node.aksCluster is not None: self.hourlyRate = self.aksManagedControlPlanePrice - node.HourlyPrice = get_Azure_price(node) - self.hourlyRate += node.HourlyPrice + node.hourlyPrice = get_Azure_price(node) + self.hourlyRate += node.hourlyPrice status = item.get('status', None) if status is not None: @@ -765,7 +765,7 @@ def dump_histogram_analytics(dbfiles, period): requests_per_type_date = {} sum_requests_per_type_date = {} - actual_cost_model = 'CUMULATIVE' + actual_cost_model = 'CUMULATIVE_RATIO' if KOA_CONFIG.cost_model in ['CHARGE_BACK', 'AKS_CHARGE_BACK']: actual_cost_model = 'CHARGE_BACK' From a32d646853d72ebf75f4c8b4029164bf962605b9 Mon Sep 17 00:00:00 2001 From: Nesrine <88090963+NesrineSfaxi@users.noreply.github.com> Date: Thu, 16 Jun 2022 14:21:05 +0200 Subject: [PATCH 13/32] Fix error in handling billing data: removed billing_hourly_rate variable --- backend.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/backend.py b/backend.py index e637c76..134f974 100644 --- a/backend.py +++ b/backend.py @@ -249,7 +249,7 @@ def render(): koa_version=KOA_CONFIG.version) -def get_Azure_price(node): +def get_azure_price(node): price = 0.0 api_base = "https://prices.azure.com/api/retail/prices?$filter=armRegionName eq '" api_endpoint = api_base + node.region + "' and skuName eq '" + node.instanceType + "' and serviceName eq 'Virtual Machines'" @@ -439,7 +439,7 @@ def extract_nodes(self, data): # If cluster is an AKS cluster if node.aksCluster is not None: self.hourlyRate = self.aksManagedControlPlanePrice - node.hourlyPrice = get_Azure_price(node) + node.hourlyPrice = get_azure_price(node) self.hourlyRate += node.hourlyPrice status = item.get('status', None) @@ -914,14 +914,12 @@ def create_metrics_puller(): rrd.add_sample(timestamp_epoch=now_epoch, cpu_usage=cpu_non_allocatable, mem_usage=mem_non_allocatable) # handle billing data - if KOA_CONFIG.cost_model == "CHARGE_BACK": - billing_hourly_rate = KOA_CONFIG.billing_hourly_rate - elif KOA_CONFIG.cost_model == "AKS_CHARGE_BACK": - billing_hourly_rate = k8s_usage.hourlyRate + if KOA_CONFIG.cost_model in ["AKS_CHARGE_BACK", "GKE_CHARGE_BACK"] : + KOA_CONFIG.billing_hourly_rate = k8s_usage.hourlyRate rrd = Rrd(db_files_location=KOA_CONFIG.db_location, dbname=KOA_CONFIG.db_billing_hourly_rate) - rrd.add_sample(timestamp_epoch=now_epoch, cpu_usage=billing_hourly_rate, - mem_usage=billing_hourly_rate) + rrd.add_sample(timestamp_epoch=now_epoch, cpu_usage=KOA_CONFIG.billing_hourly_rate, + mem_usage=KOA_CONFIG.billing_hourly_rate) # handle resource request and usage by pods for ns, ns_usage in k8s_usage.usageByNamespace.items(): From d715e2636ff2d4930764b74191f8f15d05bfaf77 Mon Sep 17 00:00:00 2001 From: Nesrine <88090963+NesrineSfaxi@users.noreply.github.com> Date: Wed, 1 Jun 2022 16:46:50 +0200 Subject: [PATCH 14/32] Added a method to process GKE cluster costs --- backend.py | 88 ++++++++++++++++--- static/data/backend.json | 1 + static/data/cpu_rf_trends.json | 1 + static/data/cpu_usage_period_1209600.json | 1 + static/data/cpu_usage_period_31968000.json | 1 + static/data/cpu_usage_trends.json | 1 + static/data/memory_rf_trends.json | 1 + static/data/memory_usage_period_1209600.json | 1 + static/data/memory_usage_period_31968000.json | 1 + static/data/memory_usage_trends.json | 1 + static/data/nodes.json | 1 + 11 files changed, 84 insertions(+), 14 deletions(-) create mode 100644 static/data/backend.json create mode 100644 static/data/cpu_rf_trends.json create mode 100644 static/data/cpu_usage_period_1209600.json create mode 100644 static/data/cpu_usage_period_31968000.json create mode 100644 static/data/cpu_usage_trends.json create mode 100644 static/data/memory_rf_trends.json create mode 100644 static/data/memory_usage_period_1209600.json create mode 100644 static/data/memory_usage_period_31968000.json create mode 100644 static/data/memory_usage_trends.json create mode 100644 static/data/nodes.json diff --git a/backend.py b/backend.py index 134f974..d9ba942 100644 --- a/backend.py +++ b/backend.py @@ -98,6 +98,11 @@ def __init__(self): elif self.cost_model == 'AKS_CHARGE_BACK': cost_model_label = 'aks_costs' cost_model_unit = self.billing_currency + + # if cluster is a GKE cluster then cost_model = 'GKE_CHARGE_BACK' + elif self.cost_model == 'GKE_CHARGE_BACK': + cost_model_label = 'GKE_costs' + cost_model_unit = self.billing_currency elif self.cost_model == 'RATIO': cost_model_label = 'normalized' @@ -271,6 +276,46 @@ def get_azure_price(node): return price +# computing GKE node price +def gcp_search_price_per_page(node, data_json, instance_description): + price = 0.0 + for _, sku in enumerate(data_json['skus']): + if sku.get("description").startswith(instance_description): + if node.region in sku.get("serviceRegions") and sku["category"]["usageType"]=="OnDemand": + price_info = sku["pricingInfo"][0]["pricingExpression"]["tieredRates"][0] + units = float( price_info["unitPrice"]["units"]) + nanos = float( price_info["unitPrice"]["nanos"]) + price = units + nanos * 10 ** (-9) + return price + +def get_GCP_price(node, memory, cpu): + cpu_price = 0.0 + memory_price = 0.0 + price= 0.0 + base_api_endpoint= "https://cloudbilling.googleapis.com/v1/services/6F81-5844-456A/skus?key=" + + data_json=requests.get(base_api_endpoint).json() + instance_description_for_cpu = node.instanceType[:2].upper()+" Instance Core" + instance_description_for_memory = node.instanceType[:2].upper()+" Instance Ram" + next_page_token = data_json['nextPageToken'] + cpu_price= cpu * gcp_search_price_per_page(node, data_json, instance_description_for_cpu) + memory_price = memory * gcp_search_price_per_page(node, data_json, instance_description_for_memory) + + while cpu_price == 0.0 or memory_price == 0 : + api_endpoint=base_api_endpoint+"&pageToken="+next_page_token + data_json=requests.get(api_endpoint).json() + + cpu_price= cpu * gcp_search_price_per_page(node, data_json, instance_description_for_cpu) + memory_price = memory * gcp_search_price_per_page(node, data_json, instance_description_for_memory) + + if data_json['nextPageToken'] != "": + next_page_token = data_json['nextPageToken'] + else : + break + price = cpu_price + memory_price + return price + + class Node: def __init__(self): self.id = '' @@ -290,7 +335,9 @@ def __init__(self): self.os = '' self.instanceType = '' self.aksCluster = None - self.hourlyPrice = 0.0 + self.gcpCluster = None + self.hourlyPrice= 0.0 + class Pod: @@ -383,6 +430,7 @@ def __init__(self): 'None': 1 } self.aksManagedControlPlanePrice = 0.10 + self.gkeManagedControlPlanePrice = 0.10 # the pricing of the managed control plane is similar for all of AKS, EKS and GKE at $0.10/hour self.hourlyRate = 0.0 @@ -431,17 +479,6 @@ def extract_nodes(self, data): node.id = metadata.get('uid', None) node.name = metadata.get('name', None) - node.aksCluster = metadata['labels'].get('kubernetes.azure.com/cluster', None) - node.region = metadata['labels']['topology.kubernetes.io/region'] - node.instanceType = metadata['labels']['node.kubernetes.io/instance-type'] - node.os = metadata['labels']["kubernetes.io/os"] - - # If cluster is an AKS cluster - if node.aksCluster is not None: - self.hourlyRate = self.aksManagedControlPlanePrice - node.hourlyPrice = get_azure_price(node) - self.hourlyRate += node.hourlyPrice - status = item.get('status', None) if status is not None: node.containerRuntime = status['nodeInfo']['containerRuntimeVersion'] @@ -471,6 +508,29 @@ def extract_nodes(self, data): if cond['type'] == 'DiskPressure' and cond['status'] == 'True': node.state = 'DiskPressure' break + + # for managed clusters + node.region = metadata['labels']['topology.kubernetes.io/region'] + node.instanceType = metadata['labels']['node.kubernetes.io/instance-type'] + node.aksCluster = metadata['labels'].get('kubernetes.azure.com/cluster', None) + node.gcpCluster= metadata['labels'].get("cloud.google.com/gke-boot-disk", None) + + # AKS cluster processing + if node.aksCluster != None : + self.hourlyRate = self.aksManagedControlPlanePrice + node.hourlyPrice = get_azure_price(node) + self.hourlyRate += node.hourlyPrice + + # GKE cluster processing + if node.aksCluster != None : + self.hourlyRate=self.gkeManagedControlPlanePrice + memory = status["capacity"]["memory"] + memLengh = len(status["capacity"]["memory"]) + memoryInGibibytes = (float( memory[0:memLengh-2])*9.3132)*(10 ** (-7)) + cpu = float(status['capacity']['cpu']) + node.hourlyPrice = get_GCP_price(node, memoryInGibibytes, cpu) + self.hourlyRate += node.hourlyPrice + self.nodes[node.name] = node def extract_node_metrics(self, data): @@ -765,8 +825,8 @@ def dump_histogram_analytics(dbfiles, period): requests_per_type_date = {} sum_requests_per_type_date = {} - actual_cost_model = 'CUMULATIVE_RATIO' - if KOA_CONFIG.cost_model in ['CHARGE_BACK', 'AKS_CHARGE_BACK']: + actual_cost_model = 'CUMULATIVE' + if KOA_CONFIG.cost_model in ['CHARGE_BACK', 'AKS_CHARGE_BACK','GKE_CHARGE_BACK' ]: actual_cost_model = 'CHARGE_BACK' for _, db in enumerate(dbfiles): diff --git a/static/data/backend.json b/static/data/backend.json new file mode 100644 index 0000000..92aa07a --- /dev/null +++ b/static/data/backend.json @@ -0,0 +1 @@ +{"cost_model":"cumulative", "currency":"%"} \ No newline at end of file diff --git a/static/data/cpu_rf_trends.json b/static/data/cpu_rf_trends.json new file mode 100644 index 0000000..54eaa49 --- /dev/null +++ b/static/data/cpu_rf_trends.json @@ -0,0 +1 @@ +[{"name":"kube-system__rf","dateUTC":"2022-05-16T16:00:00Z","usage":0.095046},{"name":"kube-system__rf","dateUTC":"2022-05-16T17:00:00Z","usage":0.090844},{"name":"kube-system__rf","dateUTC":"2022-05-16T18:00:00Z","usage":0.094825},{"name":"kube-system__rf","dateUTC":"2022-05-16T19:00:00Z","usage":0.094944},{"name":"kube-system__rf","dateUTC":"2022-05-16T20:00:00Z","usage":0.093708},{"name":"kube-system__rf","dateUTC":"2022-05-16T21:00:00Z","usage":0.094208},{"name":"kube-system__rf","dateUTC":"2022-05-16T22:00:00Z","usage":0.093369},{"name":"kube-system__rf","dateUTC":"2022-05-16T23:00:00Z","usage":0.092525},{"name":"kube-system__rf","dateUTC":"2022-05-17T00:00:00Z","usage":0.094214},{"name":"kube-system__rf","dateUTC":"2022-05-17T01:00:00Z","usage":0.095764},{"name":"kube-system__rf","dateUTC":"2022-05-17T02:00:00Z","usage":0.094333},{"name":"kube-system__rf","dateUTC":"2022-05-17T03:00:00Z","usage":0.092525},{"name":"kube-system__rf","dateUTC":"2022-05-17T04:00:00Z","usage":0.093367},{"name":"kube-system__rf","dateUTC":"2022-05-17T05:00:00Z","usage":0.093369},{"name":"kube-system__rf","dateUTC":"2022-05-17T06:00:00Z","usage":0.095217},{"name":"kube-system__rf","dateUTC":"2022-05-17T07:00:00Z","usage":0.094886},{"name":"kube-system__rf","dateUTC":"2022-05-17T08:00:00Z","usage":0.091689},{"name":"default__rf","dateUTC":"2022-05-16T16:00:00Z","usage":1.000000},{"name":"default__rf","dateUTC":"2022-05-16T17:00:00Z","usage":1.000000},{"name":"default__rf","dateUTC":"2022-05-16T18:00:00Z","usage":1.000000},{"name":"default__rf","dateUTC":"2022-05-16T19:00:00Z","usage":1.000000},{"name":"default__rf","dateUTC":"2022-05-16T20:00:00Z","usage":1.000000},{"name":"default__rf","dateUTC":"2022-05-16T21:00:00Z","usage":1.000000},{"name":"default__rf","dateUTC":"2022-05-16T22:00:00Z","usage":1.000000},{"name":"default__rf","dateUTC":"2022-05-16T23:00:00Z","usage":1.000000},{"name":"default__rf","dateUTC":"2022-05-17T00:00:00Z","usage":1.000000},{"name":"default__rf","dateUTC":"2022-05-17T01:00:00Z","usage":1.000000},{"name":"default__rf","dateUTC":"2022-05-17T02:00:00Z","usage":1.000000},{"name":"default__rf","dateUTC":"2022-05-17T03:00:00Z","usage":1.000000},{"name":"default__rf","dateUTC":"2022-05-17T04:00:00Z","usage":1.000000},{"name":"default__rf","dateUTC":"2022-05-17T05:00:00Z","usage":1.000000},{"name":"default__rf","dateUTC":"2022-05-17T06:00:00Z","usage":1.000000},{"name":"default__rf","dateUTC":"2022-05-17T07:00:00Z","usage":1.000000},{"name":"default__rf","dateUTC":"2022-05-17T08:00:00Z","usage":1.000000},{"name":"kube-node-lease__rf","dateUTC":"2022-05-16T16:00:00Z","usage":1.000000},{"name":"kube-node-lease__rf","dateUTC":"2022-05-16T17:00:00Z","usage":1.000000},{"name":"kube-node-lease__rf","dateUTC":"2022-05-16T18:00:00Z","usage":1.000000},{"name":"kube-node-lease__rf","dateUTC":"2022-05-16T19:00:00Z","usage":1.000000},{"name":"kube-node-lease__rf","dateUTC":"2022-05-16T20:00:00Z","usage":1.000000},{"name":"kube-node-lease__rf","dateUTC":"2022-05-16T21:00:00Z","usage":1.000000},{"name":"kube-node-lease__rf","dateUTC":"2022-05-16T22:00:00Z","usage":1.000000},{"name":"kube-node-lease__rf","dateUTC":"2022-05-16T23:00:00Z","usage":1.000000},{"name":"kube-node-lease__rf","dateUTC":"2022-05-17T00:00:00Z","usage":1.000000},{"name":"kube-node-lease__rf","dateUTC":"2022-05-17T01:00:00Z","usage":1.000000},{"name":"kube-node-lease__rf","dateUTC":"2022-05-17T02:00:00Z","usage":1.000000},{"name":"kube-node-lease__rf","dateUTC":"2022-05-17T03:00:00Z","usage":1.000000},{"name":"kube-node-lease__rf","dateUTC":"2022-05-17T04:00:00Z","usage":1.000000},{"name":"kube-node-lease__rf","dateUTC":"2022-05-17T05:00:00Z","usage":1.000000},{"name":"kube-node-lease__rf","dateUTC":"2022-05-17T06:00:00Z","usage":1.000000},{"name":"kube-node-lease__rf","dateUTC":"2022-05-17T07:00:00Z","usage":1.000000},{"name":"kube-node-lease__rf","dateUTC":"2022-05-17T08:00:00Z","usage":1.000000},{"name":"kube-public__rf","dateUTC":"2022-05-16T16:00:00Z","usage":1.000000},{"name":"kube-public__rf","dateUTC":"2022-05-16T17:00:00Z","usage":1.000000},{"name":"kube-public__rf","dateUTC":"2022-05-16T18:00:00Z","usage":1.000000},{"name":"kube-public__rf","dateUTC":"2022-05-16T19:00:00Z","usage":1.000000},{"name":"kube-public__rf","dateUTC":"2022-05-16T20:00:00Z","usage":1.000000},{"name":"kube-public__rf","dateUTC":"2022-05-16T21:00:00Z","usage":1.000000},{"name":"kube-public__rf","dateUTC":"2022-05-16T22:00:00Z","usage":1.000000},{"name":"kube-public__rf","dateUTC":"2022-05-16T23:00:00Z","usage":1.000000},{"name":"kube-public__rf","dateUTC":"2022-05-17T00:00:00Z","usage":1.000000},{"name":"kube-public__rf","dateUTC":"2022-05-17T01:00:00Z","usage":1.000000},{"name":"kube-public__rf","dateUTC":"2022-05-17T02:00:00Z","usage":1.000000},{"name":"kube-public__rf","dateUTC":"2022-05-17T03:00:00Z","usage":1.000000},{"name":"kube-public__rf","dateUTC":"2022-05-17T04:00:00Z","usage":1.000000},{"name":"kube-public__rf","dateUTC":"2022-05-17T05:00:00Z","usage":1.000000},{"name":"kube-public__rf","dateUTC":"2022-05-17T06:00:00Z","usage":1.000000},{"name":"kube-public__rf","dateUTC":"2022-05-17T07:00:00Z","usage":1.000000},{"name":"kube-public__rf","dateUTC":"2022-05-17T08:00:00Z","usage":1.000000}] \ No newline at end of file diff --git a/static/data/cpu_usage_period_1209600.json b/static/data/cpu_usage_period_1209600.json new file mode 100644 index 0000000..8ba9a47 --- /dev/null +++ b/static/data/cpu_usage_period_1209600.json @@ -0,0 +1 @@ +[{"stack":"non-allocatable","usage":40.000000,"date":"16 May"},{"stack":"kube-system","usage":16.852008,"date":"16 May"},{"stack":"non-allocatable","usage":45.000000,"date":"17 May"},{"stack":"kube-system","usage":18.962345,"date":"17 May"}] \ No newline at end of file diff --git a/static/data/cpu_usage_period_31968000.json b/static/data/cpu_usage_period_31968000.json new file mode 100644 index 0000000..b66a15d --- /dev/null +++ b/static/data/cpu_usage_period_31968000.json @@ -0,0 +1 @@ +[{"stack":"non-allocatable","usage":85.000000,"date":"May 2022"},{"stack":"kube-system","usage":35.814353,"date":"May 2022"}] \ No newline at end of file diff --git a/static/data/cpu_usage_trends.json b/static/data/cpu_usage_trends.json new file mode 100644 index 0000000..7da2c5c --- /dev/null +++ b/static/data/cpu_usage_trends.json @@ -0,0 +1 @@ +[{"name":"non-allocatable","dateUTC":"2022-05-16T16:00:00Z","usage":5.000000},{"name":"non-allocatable","dateUTC":"2022-05-16T17:00:00Z","usage":5.000000},{"name":"non-allocatable","dateUTC":"2022-05-16T18:00:00Z","usage":5.000000},{"name":"non-allocatable","dateUTC":"2022-05-16T19:00:00Z","usage":5.000000},{"name":"non-allocatable","dateUTC":"2022-05-16T20:00:00Z","usage":5.000000},{"name":"non-allocatable","dateUTC":"2022-05-16T21:00:00Z","usage":5.000000},{"name":"non-allocatable","dateUTC":"2022-05-16T22:00:00Z","usage":5.000000},{"name":"non-allocatable","dateUTC":"2022-05-16T23:00:00Z","usage":5.000000},{"name":"non-allocatable","dateUTC":"2022-05-17T00:00:00Z","usage":5.000000},{"name":"non-allocatable","dateUTC":"2022-05-17T01:00:00Z","usage":5.000000},{"name":"non-allocatable","dateUTC":"2022-05-17T02:00:00Z","usage":5.000000},{"name":"non-allocatable","dateUTC":"2022-05-17T03:00:00Z","usage":5.000000},{"name":"non-allocatable","dateUTC":"2022-05-17T04:00:00Z","usage":5.000000},{"name":"non-allocatable","dateUTC":"2022-05-17T05:00:00Z","usage":5.000000},{"name":"non-allocatable","dateUTC":"2022-05-17T06:00:00Z","usage":5.000000},{"name":"non-allocatable","dateUTC":"2022-05-17T07:00:00Z","usage":5.000000},{"name":"non-allocatable","dateUTC":"2022-05-17T08:00:00Z","usage":5.000000},{"name":"kube-system","dateUTC":"2022-05-16T16:00:00Z","usage":2.103813},{"name":"kube-system","dateUTC":"2022-05-16T17:00:00Z","usage":2.093086},{"name":"kube-system","dateUTC":"2022-05-16T18:00:00Z","usage":2.131879},{"name":"kube-system","dateUTC":"2022-05-16T19:00:00Z","usage":2.129733},{"name":"kube-system","dateUTC":"2022-05-16T20:00:00Z","usage":2.097287},{"name":"kube-system","dateUTC":"2022-05-16T21:00:00Z","usage":2.106825},{"name":"kube-system","dateUTC":"2022-05-16T22:00:00Z","usage":2.101436},{"name":"kube-system","dateUTC":"2022-05-16T23:00:00Z","usage":2.087948},{"name":"kube-system","dateUTC":"2022-05-17T00:00:00Z","usage":2.099931},{"name":"kube-system","dateUTC":"2022-05-17T01:00:00Z","usage":2.128923},{"name":"kube-system","dateUTC":"2022-05-17T02:00:00Z","usage":2.118461},{"name":"kube-system","dateUTC":"2022-05-17T03:00:00Z","usage":2.094000},{"name":"kube-system","dateUTC":"2022-05-17T04:00:00Z","usage":2.105366},{"name":"kube-system","dateUTC":"2022-05-17T05:00:00Z","usage":2.109988},{"name":"kube-system","dateUTC":"2022-05-17T06:00:00Z","usage":2.118262},{"name":"kube-system","dateUTC":"2022-05-17T07:00:00Z","usage":2.114391},{"name":"kube-system","dateUTC":"2022-05-17T08:00:00Z","usage":2.073023}] \ No newline at end of file diff --git a/static/data/memory_rf_trends.json b/static/data/memory_rf_trends.json new file mode 100644 index 0000000..60fb60c --- /dev/null +++ b/static/data/memory_rf_trends.json @@ -0,0 +1 @@ +[{"name":"kube-system__rf","dateUTC":"2022-05-16T16:00:00Z","usage":0.571263},{"name":"kube-system__rf","dateUTC":"2022-05-16T17:00:00Z","usage":0.570842},{"name":"kube-system__rf","dateUTC":"2022-05-16T18:00:00Z","usage":0.570842},{"name":"kube-system__rf","dateUTC":"2022-05-16T19:00:00Z","usage":0.570000},{"name":"kube-system__rf","dateUTC":"2022-05-16T20:00:00Z","usage":0.570000},{"name":"kube-system__rf","dateUTC":"2022-05-16T21:00:00Z","usage":0.570844},{"name":"kube-system__rf","dateUTC":"2022-05-16T22:00:00Z","usage":0.570842},{"name":"kube-system__rf","dateUTC":"2022-05-16T23:00:00Z","usage":0.570844},{"name":"kube-system__rf","dateUTC":"2022-05-17T00:00:00Z","usage":0.570000},{"name":"kube-system__rf","dateUTC":"2022-05-17T01:00:00Z","usage":0.570000},{"name":"kube-system__rf","dateUTC":"2022-05-17T02:00:00Z","usage":0.570842},{"name":"kube-system__rf","dateUTC":"2022-05-17T03:00:00Z","usage":0.571683},{"name":"kube-system__rf","dateUTC":"2022-05-17T04:00:00Z","usage":0.570000},{"name":"kube-system__rf","dateUTC":"2022-05-17T05:00:00Z","usage":0.570844},{"name":"kube-system__rf","dateUTC":"2022-05-17T06:00:00Z","usage":0.571683},{"name":"kube-system__rf","dateUTC":"2022-05-17T07:00:00Z","usage":0.571683},{"name":"kube-system__rf","dateUTC":"2022-05-17T08:00:00Z","usage":0.570000},{"name":"default__rf","dateUTC":"2022-05-16T16:00:00Z","usage":1.000000},{"name":"default__rf","dateUTC":"2022-05-16T17:00:00Z","usage":1.000000},{"name":"default__rf","dateUTC":"2022-05-16T18:00:00Z","usage":1.000000},{"name":"default__rf","dateUTC":"2022-05-16T19:00:00Z","usage":1.000000},{"name":"default__rf","dateUTC":"2022-05-16T20:00:00Z","usage":1.000000},{"name":"default__rf","dateUTC":"2022-05-16T21:00:00Z","usage":1.000000},{"name":"default__rf","dateUTC":"2022-05-16T22:00:00Z","usage":1.000000},{"name":"default__rf","dateUTC":"2022-05-16T23:00:00Z","usage":1.000000},{"name":"default__rf","dateUTC":"2022-05-17T00:00:00Z","usage":1.000000},{"name":"default__rf","dateUTC":"2022-05-17T01:00:00Z","usage":1.000000},{"name":"default__rf","dateUTC":"2022-05-17T02:00:00Z","usage":1.000000},{"name":"default__rf","dateUTC":"2022-05-17T03:00:00Z","usage":1.000000},{"name":"default__rf","dateUTC":"2022-05-17T04:00:00Z","usage":1.000000},{"name":"default__rf","dateUTC":"2022-05-17T05:00:00Z","usage":1.000000},{"name":"default__rf","dateUTC":"2022-05-17T06:00:00Z","usage":1.000000},{"name":"default__rf","dateUTC":"2022-05-17T07:00:00Z","usage":1.000000},{"name":"default__rf","dateUTC":"2022-05-17T08:00:00Z","usage":1.000000},{"name":"kube-node-lease__rf","dateUTC":"2022-05-16T16:00:00Z","usage":1.000000},{"name":"kube-node-lease__rf","dateUTC":"2022-05-16T17:00:00Z","usage":1.000000},{"name":"kube-node-lease__rf","dateUTC":"2022-05-16T18:00:00Z","usage":1.000000},{"name":"kube-node-lease__rf","dateUTC":"2022-05-16T19:00:00Z","usage":1.000000},{"name":"kube-node-lease__rf","dateUTC":"2022-05-16T20:00:00Z","usage":1.000000},{"name":"kube-node-lease__rf","dateUTC":"2022-05-16T21:00:00Z","usage":1.000000},{"name":"kube-node-lease__rf","dateUTC":"2022-05-16T22:00:00Z","usage":1.000000},{"name":"kube-node-lease__rf","dateUTC":"2022-05-16T23:00:00Z","usage":1.000000},{"name":"kube-node-lease__rf","dateUTC":"2022-05-17T00:00:00Z","usage":1.000000},{"name":"kube-node-lease__rf","dateUTC":"2022-05-17T01:00:00Z","usage":1.000000},{"name":"kube-node-lease__rf","dateUTC":"2022-05-17T02:00:00Z","usage":1.000000},{"name":"kube-node-lease__rf","dateUTC":"2022-05-17T03:00:00Z","usage":1.000000},{"name":"kube-node-lease__rf","dateUTC":"2022-05-17T04:00:00Z","usage":1.000000},{"name":"kube-node-lease__rf","dateUTC":"2022-05-17T05:00:00Z","usage":1.000000},{"name":"kube-node-lease__rf","dateUTC":"2022-05-17T06:00:00Z","usage":1.000000},{"name":"kube-node-lease__rf","dateUTC":"2022-05-17T07:00:00Z","usage":1.000000},{"name":"kube-node-lease__rf","dateUTC":"2022-05-17T08:00:00Z","usage":1.000000},{"name":"kube-public__rf","dateUTC":"2022-05-16T16:00:00Z","usage":1.000000},{"name":"kube-public__rf","dateUTC":"2022-05-16T17:00:00Z","usage":1.000000},{"name":"kube-public__rf","dateUTC":"2022-05-16T18:00:00Z","usage":1.000000},{"name":"kube-public__rf","dateUTC":"2022-05-16T19:00:00Z","usage":1.000000},{"name":"kube-public__rf","dateUTC":"2022-05-16T20:00:00Z","usage":1.000000},{"name":"kube-public__rf","dateUTC":"2022-05-16T21:00:00Z","usage":1.000000},{"name":"kube-public__rf","dateUTC":"2022-05-16T22:00:00Z","usage":1.000000},{"name":"kube-public__rf","dateUTC":"2022-05-16T23:00:00Z","usage":1.000000},{"name":"kube-public__rf","dateUTC":"2022-05-17T00:00:00Z","usage":1.000000},{"name":"kube-public__rf","dateUTC":"2022-05-17T01:00:00Z","usage":1.000000},{"name":"kube-public__rf","dateUTC":"2022-05-17T02:00:00Z","usage":1.000000},{"name":"kube-public__rf","dateUTC":"2022-05-17T03:00:00Z","usage":1.000000},{"name":"kube-public__rf","dateUTC":"2022-05-17T04:00:00Z","usage":1.000000},{"name":"kube-public__rf","dateUTC":"2022-05-17T05:00:00Z","usage":1.000000},{"name":"kube-public__rf","dateUTC":"2022-05-17T06:00:00Z","usage":1.000000},{"name":"kube-public__rf","dateUTC":"2022-05-17T07:00:00Z","usage":1.000000},{"name":"kube-public__rf","dateUTC":"2022-05-17T08:00:00Z","usage":1.000000}] \ No newline at end of file diff --git a/static/data/memory_usage_period_1209600.json b/static/data/memory_usage_period_1209600.json new file mode 100644 index 0000000..64901ca --- /dev/null +++ b/static/data/memory_usage_period_1209600.json @@ -0,0 +1 @@ +[{"stack":"non-allocatable","usage":170.498520,"date":"16 May"},{"stack":"kube-system","usage":10.105812,"date":"16 May"},{"stack":"non-allocatable","usage":191.810835,"date":"17 May"},{"stack":"kube-system","usage":11.392899,"date":"17 May"}] \ No newline at end of file diff --git a/static/data/memory_usage_period_31968000.json b/static/data/memory_usage_period_31968000.json new file mode 100644 index 0000000..7b5d61c --- /dev/null +++ b/static/data/memory_usage_period_31968000.json @@ -0,0 +1 @@ +[{"stack":"non-allocatable","usage":362.309355,"date":"May 2022"},{"stack":"kube-system","usage":21.498711,"date":"May 2022"}] \ No newline at end of file diff --git a/static/data/memory_usage_trends.json b/static/data/memory_usage_trends.json new file mode 100644 index 0000000..a9b6897 --- /dev/null +++ b/static/data/memory_usage_trends.json @@ -0,0 +1 @@ +[{"name":"non-allocatable","dateUTC":"2022-05-16T16:00:00Z","usage":21.312315},{"name":"non-allocatable","dateUTC":"2022-05-16T17:00:00Z","usage":21.312315},{"name":"non-allocatable","dateUTC":"2022-05-16T18:00:00Z","usage":21.312315},{"name":"non-allocatable","dateUTC":"2022-05-16T19:00:00Z","usage":21.312315},{"name":"non-allocatable","dateUTC":"2022-05-16T20:00:00Z","usage":21.312315},{"name":"non-allocatable","dateUTC":"2022-05-16T21:00:00Z","usage":21.312315},{"name":"non-allocatable","dateUTC":"2022-05-16T22:00:00Z","usage":21.312315},{"name":"non-allocatable","dateUTC":"2022-05-16T23:00:00Z","usage":21.312315},{"name":"non-allocatable","dateUTC":"2022-05-17T00:00:00Z","usage":21.312315},{"name":"non-allocatable","dateUTC":"2022-05-17T01:00:00Z","usage":21.312315},{"name":"non-allocatable","dateUTC":"2022-05-17T02:00:00Z","usage":21.312315},{"name":"non-allocatable","dateUTC":"2022-05-17T03:00:00Z","usage":21.312315},{"name":"non-allocatable","dateUTC":"2022-05-17T04:00:00Z","usage":21.312315},{"name":"non-allocatable","dateUTC":"2022-05-17T05:00:00Z","usage":21.312315},{"name":"non-allocatable","dateUTC":"2022-05-17T06:00:00Z","usage":21.312315},{"name":"non-allocatable","dateUTC":"2022-05-17T07:00:00Z","usage":21.312315},{"name":"non-allocatable","dateUTC":"2022-05-17T08:00:00Z","usage":21.312315},{"name":"kube-system","dateUTC":"2022-05-16T16:00:00Z","usage":1.266366},{"name":"kube-system","dateUTC":"2022-05-16T17:00:00Z","usage":1.267275},{"name":"kube-system","dateUTC":"2022-05-16T18:00:00Z","usage":1.268300},{"name":"kube-system","dateUTC":"2022-05-16T19:00:00Z","usage":1.264139},{"name":"kube-system","dateUTC":"2022-05-16T20:00:00Z","usage":1.261430},{"name":"kube-system","dateUTC":"2022-05-16T21:00:00Z","usage":1.258346},{"name":"kube-system","dateUTC":"2022-05-16T22:00:00Z","usage":1.259619},{"name":"kube-system","dateUTC":"2022-05-16T23:00:00Z","usage":1.260337},{"name":"kube-system","dateUTC":"2022-05-17T00:00:00Z","usage":1.266378},{"name":"kube-system","dateUTC":"2022-05-17T01:00:00Z","usage":1.267609},{"name":"kube-system","dateUTC":"2022-05-17T02:00:00Z","usage":1.269841},{"name":"kube-system","dateUTC":"2022-05-17T03:00:00Z","usage":1.266712},{"name":"kube-system","dateUTC":"2022-05-17T04:00:00Z","usage":1.260553},{"name":"kube-system","dateUTC":"2022-05-17T05:00:00Z","usage":1.259613},{"name":"kube-system","dateUTC":"2022-05-17T06:00:00Z","usage":1.264562},{"name":"kube-system","dateUTC":"2022-05-17T07:00:00Z","usage":1.267643},{"name":"kube-system","dateUTC":"2022-05-17T08:00:00Z","usage":1.269989}] \ No newline at end of file diff --git a/static/data/nodes.json b/static/data/nodes.json new file mode 100644 index 0000000..9e26dfe --- /dev/null +++ b/static/data/nodes.json @@ -0,0 +1 @@ +{} \ No newline at end of file From f993d5e3cc332d2d4841078b501c015ba5cbff72 Mon Sep 17 00:00:00 2001 From: Nesrine <88090963+NesrineSfaxi@users.noreply.github.com> Date: Tue, 7 Jun 2022 12:24:20 +0200 Subject: [PATCH 15/32] Added k8s secret to store Google API Key --- backend.py | 6 +++++- .../kustomize/resources/kube-opex-analytics-secret.yaml | 7 +++++++ manifests/kustomize/resources/kube-opex-analytics-sts.yaml | 5 +++++ 3 files changed, 17 insertions(+), 1 deletion(-) create mode 100644 manifests/kustomize/resources/kube-opex-analytics-secret.yaml diff --git a/backend.py b/backend.py index d9ba942..2bebabd 100644 --- a/backend.py +++ b/backend.py @@ -75,6 +75,7 @@ class Config: k8s_ssl_client_cert_key = os.getenv('KOA_K8S_AUTH_CLIENT_CERT_KEY', 'NO_ENV_CLIENT_CERT_CERT') included_namespaces = [i for i in os.getenv('KOA_INCLUDED_NAMESPACES', '').replace(' ', ',').split(',') if i] excluded_namespaces = [i for i in os.getenv('KOA_EXCLUDED_NAMESPACES', '').replace(' ', ',').split(',') if i] + k8s_google_api_key = os.getenv('KOA_K8S_GOOGLE_API_KEY', 'NO_GOOGLE_API_KEY') def __init__(self): @@ -292,7 +293,7 @@ def get_GCP_price(node, memory, cpu): cpu_price = 0.0 memory_price = 0.0 price= 0.0 - base_api_endpoint= "https://cloudbilling.googleapis.com/v1/services/6F81-5844-456A/skus?key=" + base_api_endpoint= "https://cloudbilling.googleapis.com/v1/services/6F81-5844-456A/skus?key="+KOA_CONFIG.k8s_google_api_key data_json=requests.get(base_api_endpoint).json() instance_description_for_cpu = node.instanceType[:2].upper()+" Instance Core" @@ -1039,6 +1040,9 @@ def dump_analytics(): if KOA_CONFIG.cost_model == 'CHARGE_BACK' and KOA_CONFIG.billing_hourly_rate <= 0.0: KOA_LOGGER.fatal('invalid billing hourly rate for CHARGE_BACK cost allocation') sys.exit(1) +if KOA_CONFIG.k8s_google_api_key == 'NO_GOOGLE_API_KEY': + KOA_LOGGER.fatal('no google API key provided, unable to calculate GKE cluster cost') + sys.exit(1) if __name__ == '__main__': parser = argparse.ArgumentParser(description='Kubernetes Opex Analytics Backend') diff --git a/manifests/kustomize/resources/kube-opex-analytics-secret.yaml b/manifests/kustomize/resources/kube-opex-analytics-secret.yaml new file mode 100644 index 0000000..5bdac5a --- /dev/null +++ b/manifests/kustomize/resources/kube-opex-analytics-secret.yaml @@ -0,0 +1,7 @@ +apiVersion: v1 +kind: Secret +metadata: + name: kube_opex-analytics-secret +type: Opaque +data: + KOA_K8S_GOOGLE_API_KEY='google api key encoded using base64' diff --git a/manifests/kustomize/resources/kube-opex-analytics-sts.yaml b/manifests/kustomize/resources/kube-opex-analytics-sts.yaml index d3c851e..46892e5 100644 --- a/manifests/kustomize/resources/kube-opex-analytics-sts.yaml +++ b/manifests/kustomize/resources/kube-opex-analytics-sts.yaml @@ -62,6 +62,11 @@ spec: configMapKeyRef: key: KOA_COST_MODEL name: kube-opex-analytics-config + - name: "KOA_K8S_GOOGLE_API_KEY" + valueFrom: + secretKeyRef: + key: KOA_K8S_GOOGLE_API_KEY + name: kube-opex-analytics-secret ports: - name: http containerPort: 5483 From 9252a3a316e6444beefaede6caf242abc61fe59d Mon Sep 17 00:00:00 2001 From: Nesrine <88090963+NesrineSfaxi@users.noreply.github.com> Date: Tue, 7 Jun 2022 16:09:51 +0200 Subject: [PATCH 16/32] Fixed syntax --- backend.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/backend.py b/backend.py index 2bebabd..8d93146 100644 --- a/backend.py +++ b/backend.py @@ -292,12 +292,12 @@ def gcp_search_price_per_page(node, data_json, instance_description): def get_GCP_price(node, memory, cpu): cpu_price = 0.0 memory_price = 0.0 - price= 0.0 - base_api_endpoint= "https://cloudbilling.googleapis.com/v1/services/6F81-5844-456A/skus?key="+KOA_CONFIG.k8s_google_api_key + price = 0.0 + base_api_endpoint = "https://cloudbilling.googleapis.com/v1/services/6F81-5844-456A/skus?key=" + KOA_CONFIG.k8s_google_api_key data_json=requests.get(base_api_endpoint).json() - instance_description_for_cpu = node.instanceType[:2].upper()+" Instance Core" - instance_description_for_memory = node.instanceType[:2].upper()+" Instance Ram" + instance_description_for_cpu = node.instanceType[:2].upper() + " Instance Core" + instance_description_for_memory = node.instanceType[:2].upper() + " Instance Ram" next_page_token = data_json['nextPageToken'] cpu_price= cpu * gcp_search_price_per_page(node, data_json, instance_description_for_cpu) memory_price = memory * gcp_search_price_per_page(node, data_json, instance_description_for_memory) @@ -523,7 +523,7 @@ def extract_nodes(self, data): self.hourlyRate += node.hourlyPrice # GKE cluster processing - if node.aksCluster != None : + if node.aksCluster is not None : self.hourlyRate=self.gkeManagedControlPlanePrice memory = status["capacity"]["memory"] memLengh = len(status["capacity"]["memory"]) From de65055c63a5f08fbfa67352535ce21fb1eae1fe Mon Sep 17 00:00:00 2001 From: Nesrine <88090963+NesrineSfaxi@users.noreply.github.com> Date: Mon, 13 Jun 2022 15:52:21 +0200 Subject: [PATCH 17/32] Fixed google_api_key variable name --- backend.py | 14 +++++++------- .../resources/kube-opex-analytics-secret.yaml | 4 ++-- .../resources/kube-opex-analytics-sts.yaml | 2 +- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/backend.py b/backend.py index 8d93146..f2ac928 100644 --- a/backend.py +++ b/backend.py @@ -75,7 +75,7 @@ class Config: k8s_ssl_client_cert_key = os.getenv('KOA_K8S_AUTH_CLIENT_CERT_KEY', 'NO_ENV_CLIENT_CERT_CERT') included_namespaces = [i for i in os.getenv('KOA_INCLUDED_NAMESPACES', '').replace(' ', ',').split(',') if i] excluded_namespaces = [i for i in os.getenv('KOA_EXCLUDED_NAMESPACES', '').replace(' ', ',').split(',') if i] - k8s_google_api_key = os.getenv('KOA_K8S_GOOGLE_API_KEY', 'NO_GOOGLE_API_KEY') + google_api_key = os.getenv('KOA_GOOGLE_API_KEY', 'NO_GOOGLE_API_KEY') def __init__(self): @@ -289,11 +289,11 @@ def gcp_search_price_per_page(node, data_json, instance_description): price = units + nanos * 10 ** (-9) return price -def get_GCP_price(node, memory, cpu): +def get_gcp_price(node, memory, cpu): cpu_price = 0.0 memory_price = 0.0 price = 0.0 - base_api_endpoint = "https://cloudbilling.googleapis.com/v1/services/6F81-5844-456A/skus?key=" + KOA_CONFIG.k8s_google_api_key + base_api_endpoint = "https://cloudbilling.googleapis.com/v1/services/6F81-5844-456A/skus?key=" + KOA_CONFIG.google_api_key data_json=requests.get(base_api_endpoint).json() instance_description_for_cpu = node.instanceType[:2].upper() + " Instance Core" @@ -529,8 +529,8 @@ def extract_nodes(self, data): memLengh = len(status["capacity"]["memory"]) memoryInGibibytes = (float( memory[0:memLengh-2])*9.3132)*(10 ** (-7)) cpu = float(status['capacity']['cpu']) - node.hourlyPrice = get_GCP_price(node, memoryInGibibytes, cpu) - self.hourlyRate += node.hourlyPrice + node.HourlyPrice = get_gcp_price(node, memoryInGibibytes, cpu) + self.hourlyRate += node.HourlyPrice self.nodes[node.name] = node @@ -1040,8 +1040,8 @@ def dump_analytics(): if KOA_CONFIG.cost_model == 'CHARGE_BACK' and KOA_CONFIG.billing_hourly_rate <= 0.0: KOA_LOGGER.fatal('invalid billing hourly rate for CHARGE_BACK cost allocation') sys.exit(1) -if KOA_CONFIG.k8s_google_api_key == 'NO_GOOGLE_API_KEY': - KOA_LOGGER.fatal('no google API key provided, unable to calculate GKE cluster cost') +if KOA_CONFIG.google_api_key == 'NO_GOOGLE_API_KEY': + KOA_LOGGER.fatal('no google API key provided, unable to calculate GKE cluster hourly rate') sys.exit(1) if __name__ == '__main__': diff --git a/manifests/kustomize/resources/kube-opex-analytics-secret.yaml b/manifests/kustomize/resources/kube-opex-analytics-secret.yaml index 5bdac5a..72c9038 100644 --- a/manifests/kustomize/resources/kube-opex-analytics-secret.yaml +++ b/manifests/kustomize/resources/kube-opex-analytics-secret.yaml @@ -1,7 +1,7 @@ apiVersion: v1 kind: Secret metadata: - name: kube_opex-analytics-secret + name: kube-opex-analytics-secret type: Opaque data: - KOA_K8S_GOOGLE_API_KEY='google api key encoded using base64' + KOA_GOOGLE_API_KEY='google api key encoded using base64' diff --git a/manifests/kustomize/resources/kube-opex-analytics-sts.yaml b/manifests/kustomize/resources/kube-opex-analytics-sts.yaml index 46892e5..89e8ef9 100644 --- a/manifests/kustomize/resources/kube-opex-analytics-sts.yaml +++ b/manifests/kustomize/resources/kube-opex-analytics-sts.yaml @@ -65,7 +65,7 @@ spec: - name: "KOA_K8S_GOOGLE_API_KEY" valueFrom: secretKeyRef: - key: KOA_K8S_GOOGLE_API_KEY + key: KOA_GOOGLE_API_KEY name: kube-opex-analytics-secret ports: - name: http From f32465894d47ac02a1ce112725e988efa4e30325 Mon Sep 17 00:00:00 2001 From: Nesrine <88090963+NesrineSfaxi@users.noreply.github.com> Date: Wed, 15 Jun 2022 15:43:13 +0200 Subject: [PATCH 18/32] Fixed GKE clusters detection condition --- backend.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend.py b/backend.py index f2ac928..81b6497 100644 --- a/backend.py +++ b/backend.py @@ -523,7 +523,7 @@ def extract_nodes(self, data): self.hourlyRate += node.hourlyPrice # GKE cluster processing - if node.aksCluster is not None : + if node.gcpCluster is not None : self.hourlyRate=self.gkeManagedControlPlanePrice memory = status["capacity"]["memory"] memLengh = len(status["capacity"]["memory"]) From b3a6703a0c12a933d5adff8098a46deb8c10037a Mon Sep 17 00:00:00 2001 From: Nesrine <88090963+NesrineSfaxi@users.noreply.github.com> Date: Thu, 16 Jun 2022 16:54:30 +0200 Subject: [PATCH 19/32] Added config validation condition for GKE_CHARGE_BACK --- backend.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend.py b/backend.py index 81b6497..077e490 100644 --- a/backend.py +++ b/backend.py @@ -1040,7 +1040,7 @@ def dump_analytics(): if KOA_CONFIG.cost_model == 'CHARGE_BACK' and KOA_CONFIG.billing_hourly_rate <= 0.0: KOA_LOGGER.fatal('invalid billing hourly rate for CHARGE_BACK cost allocation') sys.exit(1) -if KOA_CONFIG.google_api_key == 'NO_GOOGLE_API_KEY': +if KOA_CONFIG.cost_model == 'GKE_CHARGE_BACK' and KOA_CONFIG.google_api_key == 'NO_GOOGLE_API_KEY': KOA_LOGGER.fatal('no google API key provided, unable to calculate GKE cluster hourly rate') sys.exit(1) From 9ab3431de0cb28d43a50e50aee31666b2854ed77 Mon Sep 17 00:00:00 2001 From: Nesrine <88090963+NesrineSfaxi@users.noreply.github.com> Date: Fri, 17 Jun 2022 17:14:43 +0200 Subject: [PATCH 20/32] Fixed title for Usage Accounting in GKE_CHARGE_BACK --- backend.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend.py b/backend.py index 077e490..86dac8c 100644 --- a/backend.py +++ b/backend.py @@ -102,7 +102,7 @@ def __init__(self): # if cluster is a GKE cluster then cost_model = 'GKE_CHARGE_BACK' elif self.cost_model == 'GKE_CHARGE_BACK': - cost_model_label = 'GKE_costs' + cost_model_label = 'gke_costs' cost_model_unit = self.billing_currency elif self.cost_model == 'RATIO': From 85266d9a8759c68b5385fb2e700965657c7ca479 Mon Sep 17 00:00:00 2001 From: Nesrine <88090963+NesrineSfaxi@users.noreply.github.com> Date: Fri, 24 Jun 2022 11:50:36 +0200 Subject: [PATCH 21/32] Fixed get_gcp_price method : avoided zero price --- backend.py | 16 ++++++++++++---- static/data/backend.json | 1 - static/data/cpu_rf_trends.json | 1 - static/data/cpu_usage_period_1209600.json | 1 - static/data/cpu_usage_period_31968000.json | 1 - static/data/cpu_usage_trends.json | 1 - static/data/memory_rf_trends.json | 1 - static/data/memory_usage_period_1209600.json | 1 - static/data/memory_usage_period_31968000.json | 1 - static/data/memory_usage_trends.json | 1 - static/data/nodes.json | 1 - 11 files changed, 12 insertions(+), 14 deletions(-) delete mode 100644 static/data/backend.json delete mode 100644 static/data/cpu_rf_trends.json delete mode 100644 static/data/cpu_usage_period_1209600.json delete mode 100644 static/data/cpu_usage_period_31968000.json delete mode 100644 static/data/cpu_usage_trends.json delete mode 100644 static/data/memory_rf_trends.json delete mode 100644 static/data/memory_usage_period_1209600.json delete mode 100644 static/data/memory_usage_period_31968000.json delete mode 100644 static/data/memory_usage_trends.json delete mode 100644 static/data/nodes.json diff --git a/backend.py b/backend.py index 86dac8c..c2774e7 100644 --- a/backend.py +++ b/backend.py @@ -299,16 +299,23 @@ def get_gcp_price(node, memory, cpu): instance_description_for_cpu = node.instanceType[:2].upper() + " Instance Core" instance_description_for_memory = node.instanceType[:2].upper() + " Instance Ram" next_page_token = data_json['nextPageToken'] + cpu_price= cpu * gcp_search_price_per_page(node, data_json, instance_description_for_cpu) memory_price = memory * gcp_search_price_per_page(node, data_json, instance_description_for_memory) - while cpu_price == 0.0 or memory_price == 0 : + while cpu_price == 0.0 : api_endpoint=base_api_endpoint+"&pageToken="+next_page_token data_json=requests.get(api_endpoint).json() - cpu_price= cpu * gcp_search_price_per_page(node, data_json, instance_description_for_cpu) - memory_price = memory * gcp_search_price_per_page(node, data_json, instance_description_for_memory) + if data_json['nextPageToken'] != "": + next_page_token = data_json['nextPageToken'] + else : + break + while memory_price == 0 : + api_endpoint=base_api_endpoint+"&pageToken="+next_page_token + data_json=requests.get(api_endpoint).json() + memory_price = memory * gcp_search_price_per_page(node, data_json, instance_description_for_memory) if data_json['nextPageToken'] != "": next_page_token = data_json['nextPageToken'] else : @@ -527,7 +534,8 @@ def extract_nodes(self, data): self.hourlyRate=self.gkeManagedControlPlanePrice memory = status["capacity"]["memory"] memLengh = len(status["capacity"]["memory"]) - memoryInGibibytes = (float( memory[0:memLengh-2])*9.3132)*(10 ** (-7)) + memoryInGibibytes = (float( memory[0:memLengh-2])*9.5367431640625)*(10 ** (-7)) + # 1 kiB = 9.5367431640625E-7 giB cpu = float(status['capacity']['cpu']) node.HourlyPrice = get_gcp_price(node, memoryInGibibytes, cpu) self.hourlyRate += node.HourlyPrice diff --git a/static/data/backend.json b/static/data/backend.json deleted file mode 100644 index 92aa07a..0000000 --- a/static/data/backend.json +++ /dev/null @@ -1 +0,0 @@ -{"cost_model":"cumulative", "currency":"%"} \ No newline at end of file diff --git a/static/data/cpu_rf_trends.json b/static/data/cpu_rf_trends.json deleted file mode 100644 index 54eaa49..0000000 --- a/static/data/cpu_rf_trends.json +++ /dev/null @@ -1 +0,0 @@ -[{"name":"kube-system__rf","dateUTC":"2022-05-16T16:00:00Z","usage":0.095046},{"name":"kube-system__rf","dateUTC":"2022-05-16T17:00:00Z","usage":0.090844},{"name":"kube-system__rf","dateUTC":"2022-05-16T18:00:00Z","usage":0.094825},{"name":"kube-system__rf","dateUTC":"2022-05-16T19:00:00Z","usage":0.094944},{"name":"kube-system__rf","dateUTC":"2022-05-16T20:00:00Z","usage":0.093708},{"name":"kube-system__rf","dateUTC":"2022-05-16T21:00:00Z","usage":0.094208},{"name":"kube-system__rf","dateUTC":"2022-05-16T22:00:00Z","usage":0.093369},{"name":"kube-system__rf","dateUTC":"2022-05-16T23:00:00Z","usage":0.092525},{"name":"kube-system__rf","dateUTC":"2022-05-17T00:00:00Z","usage":0.094214},{"name":"kube-system__rf","dateUTC":"2022-05-17T01:00:00Z","usage":0.095764},{"name":"kube-system__rf","dateUTC":"2022-05-17T02:00:00Z","usage":0.094333},{"name":"kube-system__rf","dateUTC":"2022-05-17T03:00:00Z","usage":0.092525},{"name":"kube-system__rf","dateUTC":"2022-05-17T04:00:00Z","usage":0.093367},{"name":"kube-system__rf","dateUTC":"2022-05-17T05:00:00Z","usage":0.093369},{"name":"kube-system__rf","dateUTC":"2022-05-17T06:00:00Z","usage":0.095217},{"name":"kube-system__rf","dateUTC":"2022-05-17T07:00:00Z","usage":0.094886},{"name":"kube-system__rf","dateUTC":"2022-05-17T08:00:00Z","usage":0.091689},{"name":"default__rf","dateUTC":"2022-05-16T16:00:00Z","usage":1.000000},{"name":"default__rf","dateUTC":"2022-05-16T17:00:00Z","usage":1.000000},{"name":"default__rf","dateUTC":"2022-05-16T18:00:00Z","usage":1.000000},{"name":"default__rf","dateUTC":"2022-05-16T19:00:00Z","usage":1.000000},{"name":"default__rf","dateUTC":"2022-05-16T20:00:00Z","usage":1.000000},{"name":"default__rf","dateUTC":"2022-05-16T21:00:00Z","usage":1.000000},{"name":"default__rf","dateUTC":"2022-05-16T22:00:00Z","usage":1.000000},{"name":"default__rf","dateUTC":"2022-05-16T23:00:00Z","usage":1.000000},{"name":"default__rf","dateUTC":"2022-05-17T00:00:00Z","usage":1.000000},{"name":"default__rf","dateUTC":"2022-05-17T01:00:00Z","usage":1.000000},{"name":"default__rf","dateUTC":"2022-05-17T02:00:00Z","usage":1.000000},{"name":"default__rf","dateUTC":"2022-05-17T03:00:00Z","usage":1.000000},{"name":"default__rf","dateUTC":"2022-05-17T04:00:00Z","usage":1.000000},{"name":"default__rf","dateUTC":"2022-05-17T05:00:00Z","usage":1.000000},{"name":"default__rf","dateUTC":"2022-05-17T06:00:00Z","usage":1.000000},{"name":"default__rf","dateUTC":"2022-05-17T07:00:00Z","usage":1.000000},{"name":"default__rf","dateUTC":"2022-05-17T08:00:00Z","usage":1.000000},{"name":"kube-node-lease__rf","dateUTC":"2022-05-16T16:00:00Z","usage":1.000000},{"name":"kube-node-lease__rf","dateUTC":"2022-05-16T17:00:00Z","usage":1.000000},{"name":"kube-node-lease__rf","dateUTC":"2022-05-16T18:00:00Z","usage":1.000000},{"name":"kube-node-lease__rf","dateUTC":"2022-05-16T19:00:00Z","usage":1.000000},{"name":"kube-node-lease__rf","dateUTC":"2022-05-16T20:00:00Z","usage":1.000000},{"name":"kube-node-lease__rf","dateUTC":"2022-05-16T21:00:00Z","usage":1.000000},{"name":"kube-node-lease__rf","dateUTC":"2022-05-16T22:00:00Z","usage":1.000000},{"name":"kube-node-lease__rf","dateUTC":"2022-05-16T23:00:00Z","usage":1.000000},{"name":"kube-node-lease__rf","dateUTC":"2022-05-17T00:00:00Z","usage":1.000000},{"name":"kube-node-lease__rf","dateUTC":"2022-05-17T01:00:00Z","usage":1.000000},{"name":"kube-node-lease__rf","dateUTC":"2022-05-17T02:00:00Z","usage":1.000000},{"name":"kube-node-lease__rf","dateUTC":"2022-05-17T03:00:00Z","usage":1.000000},{"name":"kube-node-lease__rf","dateUTC":"2022-05-17T04:00:00Z","usage":1.000000},{"name":"kube-node-lease__rf","dateUTC":"2022-05-17T05:00:00Z","usage":1.000000},{"name":"kube-node-lease__rf","dateUTC":"2022-05-17T06:00:00Z","usage":1.000000},{"name":"kube-node-lease__rf","dateUTC":"2022-05-17T07:00:00Z","usage":1.000000},{"name":"kube-node-lease__rf","dateUTC":"2022-05-17T08:00:00Z","usage":1.000000},{"name":"kube-public__rf","dateUTC":"2022-05-16T16:00:00Z","usage":1.000000},{"name":"kube-public__rf","dateUTC":"2022-05-16T17:00:00Z","usage":1.000000},{"name":"kube-public__rf","dateUTC":"2022-05-16T18:00:00Z","usage":1.000000},{"name":"kube-public__rf","dateUTC":"2022-05-16T19:00:00Z","usage":1.000000},{"name":"kube-public__rf","dateUTC":"2022-05-16T20:00:00Z","usage":1.000000},{"name":"kube-public__rf","dateUTC":"2022-05-16T21:00:00Z","usage":1.000000},{"name":"kube-public__rf","dateUTC":"2022-05-16T22:00:00Z","usage":1.000000},{"name":"kube-public__rf","dateUTC":"2022-05-16T23:00:00Z","usage":1.000000},{"name":"kube-public__rf","dateUTC":"2022-05-17T00:00:00Z","usage":1.000000},{"name":"kube-public__rf","dateUTC":"2022-05-17T01:00:00Z","usage":1.000000},{"name":"kube-public__rf","dateUTC":"2022-05-17T02:00:00Z","usage":1.000000},{"name":"kube-public__rf","dateUTC":"2022-05-17T03:00:00Z","usage":1.000000},{"name":"kube-public__rf","dateUTC":"2022-05-17T04:00:00Z","usage":1.000000},{"name":"kube-public__rf","dateUTC":"2022-05-17T05:00:00Z","usage":1.000000},{"name":"kube-public__rf","dateUTC":"2022-05-17T06:00:00Z","usage":1.000000},{"name":"kube-public__rf","dateUTC":"2022-05-17T07:00:00Z","usage":1.000000},{"name":"kube-public__rf","dateUTC":"2022-05-17T08:00:00Z","usage":1.000000}] \ No newline at end of file diff --git a/static/data/cpu_usage_period_1209600.json b/static/data/cpu_usage_period_1209600.json deleted file mode 100644 index 8ba9a47..0000000 --- a/static/data/cpu_usage_period_1209600.json +++ /dev/null @@ -1 +0,0 @@ -[{"stack":"non-allocatable","usage":40.000000,"date":"16 May"},{"stack":"kube-system","usage":16.852008,"date":"16 May"},{"stack":"non-allocatable","usage":45.000000,"date":"17 May"},{"stack":"kube-system","usage":18.962345,"date":"17 May"}] \ No newline at end of file diff --git a/static/data/cpu_usage_period_31968000.json b/static/data/cpu_usage_period_31968000.json deleted file mode 100644 index b66a15d..0000000 --- a/static/data/cpu_usage_period_31968000.json +++ /dev/null @@ -1 +0,0 @@ -[{"stack":"non-allocatable","usage":85.000000,"date":"May 2022"},{"stack":"kube-system","usage":35.814353,"date":"May 2022"}] \ No newline at end of file diff --git a/static/data/cpu_usage_trends.json b/static/data/cpu_usage_trends.json deleted file mode 100644 index 7da2c5c..0000000 --- a/static/data/cpu_usage_trends.json +++ /dev/null @@ -1 +0,0 @@ -[{"name":"non-allocatable","dateUTC":"2022-05-16T16:00:00Z","usage":5.000000},{"name":"non-allocatable","dateUTC":"2022-05-16T17:00:00Z","usage":5.000000},{"name":"non-allocatable","dateUTC":"2022-05-16T18:00:00Z","usage":5.000000},{"name":"non-allocatable","dateUTC":"2022-05-16T19:00:00Z","usage":5.000000},{"name":"non-allocatable","dateUTC":"2022-05-16T20:00:00Z","usage":5.000000},{"name":"non-allocatable","dateUTC":"2022-05-16T21:00:00Z","usage":5.000000},{"name":"non-allocatable","dateUTC":"2022-05-16T22:00:00Z","usage":5.000000},{"name":"non-allocatable","dateUTC":"2022-05-16T23:00:00Z","usage":5.000000},{"name":"non-allocatable","dateUTC":"2022-05-17T00:00:00Z","usage":5.000000},{"name":"non-allocatable","dateUTC":"2022-05-17T01:00:00Z","usage":5.000000},{"name":"non-allocatable","dateUTC":"2022-05-17T02:00:00Z","usage":5.000000},{"name":"non-allocatable","dateUTC":"2022-05-17T03:00:00Z","usage":5.000000},{"name":"non-allocatable","dateUTC":"2022-05-17T04:00:00Z","usage":5.000000},{"name":"non-allocatable","dateUTC":"2022-05-17T05:00:00Z","usage":5.000000},{"name":"non-allocatable","dateUTC":"2022-05-17T06:00:00Z","usage":5.000000},{"name":"non-allocatable","dateUTC":"2022-05-17T07:00:00Z","usage":5.000000},{"name":"non-allocatable","dateUTC":"2022-05-17T08:00:00Z","usage":5.000000},{"name":"kube-system","dateUTC":"2022-05-16T16:00:00Z","usage":2.103813},{"name":"kube-system","dateUTC":"2022-05-16T17:00:00Z","usage":2.093086},{"name":"kube-system","dateUTC":"2022-05-16T18:00:00Z","usage":2.131879},{"name":"kube-system","dateUTC":"2022-05-16T19:00:00Z","usage":2.129733},{"name":"kube-system","dateUTC":"2022-05-16T20:00:00Z","usage":2.097287},{"name":"kube-system","dateUTC":"2022-05-16T21:00:00Z","usage":2.106825},{"name":"kube-system","dateUTC":"2022-05-16T22:00:00Z","usage":2.101436},{"name":"kube-system","dateUTC":"2022-05-16T23:00:00Z","usage":2.087948},{"name":"kube-system","dateUTC":"2022-05-17T00:00:00Z","usage":2.099931},{"name":"kube-system","dateUTC":"2022-05-17T01:00:00Z","usage":2.128923},{"name":"kube-system","dateUTC":"2022-05-17T02:00:00Z","usage":2.118461},{"name":"kube-system","dateUTC":"2022-05-17T03:00:00Z","usage":2.094000},{"name":"kube-system","dateUTC":"2022-05-17T04:00:00Z","usage":2.105366},{"name":"kube-system","dateUTC":"2022-05-17T05:00:00Z","usage":2.109988},{"name":"kube-system","dateUTC":"2022-05-17T06:00:00Z","usage":2.118262},{"name":"kube-system","dateUTC":"2022-05-17T07:00:00Z","usage":2.114391},{"name":"kube-system","dateUTC":"2022-05-17T08:00:00Z","usage":2.073023}] \ No newline at end of file diff --git a/static/data/memory_rf_trends.json b/static/data/memory_rf_trends.json deleted file mode 100644 index 60fb60c..0000000 --- a/static/data/memory_rf_trends.json +++ /dev/null @@ -1 +0,0 @@ -[{"name":"kube-system__rf","dateUTC":"2022-05-16T16:00:00Z","usage":0.571263},{"name":"kube-system__rf","dateUTC":"2022-05-16T17:00:00Z","usage":0.570842},{"name":"kube-system__rf","dateUTC":"2022-05-16T18:00:00Z","usage":0.570842},{"name":"kube-system__rf","dateUTC":"2022-05-16T19:00:00Z","usage":0.570000},{"name":"kube-system__rf","dateUTC":"2022-05-16T20:00:00Z","usage":0.570000},{"name":"kube-system__rf","dateUTC":"2022-05-16T21:00:00Z","usage":0.570844},{"name":"kube-system__rf","dateUTC":"2022-05-16T22:00:00Z","usage":0.570842},{"name":"kube-system__rf","dateUTC":"2022-05-16T23:00:00Z","usage":0.570844},{"name":"kube-system__rf","dateUTC":"2022-05-17T00:00:00Z","usage":0.570000},{"name":"kube-system__rf","dateUTC":"2022-05-17T01:00:00Z","usage":0.570000},{"name":"kube-system__rf","dateUTC":"2022-05-17T02:00:00Z","usage":0.570842},{"name":"kube-system__rf","dateUTC":"2022-05-17T03:00:00Z","usage":0.571683},{"name":"kube-system__rf","dateUTC":"2022-05-17T04:00:00Z","usage":0.570000},{"name":"kube-system__rf","dateUTC":"2022-05-17T05:00:00Z","usage":0.570844},{"name":"kube-system__rf","dateUTC":"2022-05-17T06:00:00Z","usage":0.571683},{"name":"kube-system__rf","dateUTC":"2022-05-17T07:00:00Z","usage":0.571683},{"name":"kube-system__rf","dateUTC":"2022-05-17T08:00:00Z","usage":0.570000},{"name":"default__rf","dateUTC":"2022-05-16T16:00:00Z","usage":1.000000},{"name":"default__rf","dateUTC":"2022-05-16T17:00:00Z","usage":1.000000},{"name":"default__rf","dateUTC":"2022-05-16T18:00:00Z","usage":1.000000},{"name":"default__rf","dateUTC":"2022-05-16T19:00:00Z","usage":1.000000},{"name":"default__rf","dateUTC":"2022-05-16T20:00:00Z","usage":1.000000},{"name":"default__rf","dateUTC":"2022-05-16T21:00:00Z","usage":1.000000},{"name":"default__rf","dateUTC":"2022-05-16T22:00:00Z","usage":1.000000},{"name":"default__rf","dateUTC":"2022-05-16T23:00:00Z","usage":1.000000},{"name":"default__rf","dateUTC":"2022-05-17T00:00:00Z","usage":1.000000},{"name":"default__rf","dateUTC":"2022-05-17T01:00:00Z","usage":1.000000},{"name":"default__rf","dateUTC":"2022-05-17T02:00:00Z","usage":1.000000},{"name":"default__rf","dateUTC":"2022-05-17T03:00:00Z","usage":1.000000},{"name":"default__rf","dateUTC":"2022-05-17T04:00:00Z","usage":1.000000},{"name":"default__rf","dateUTC":"2022-05-17T05:00:00Z","usage":1.000000},{"name":"default__rf","dateUTC":"2022-05-17T06:00:00Z","usage":1.000000},{"name":"default__rf","dateUTC":"2022-05-17T07:00:00Z","usage":1.000000},{"name":"default__rf","dateUTC":"2022-05-17T08:00:00Z","usage":1.000000},{"name":"kube-node-lease__rf","dateUTC":"2022-05-16T16:00:00Z","usage":1.000000},{"name":"kube-node-lease__rf","dateUTC":"2022-05-16T17:00:00Z","usage":1.000000},{"name":"kube-node-lease__rf","dateUTC":"2022-05-16T18:00:00Z","usage":1.000000},{"name":"kube-node-lease__rf","dateUTC":"2022-05-16T19:00:00Z","usage":1.000000},{"name":"kube-node-lease__rf","dateUTC":"2022-05-16T20:00:00Z","usage":1.000000},{"name":"kube-node-lease__rf","dateUTC":"2022-05-16T21:00:00Z","usage":1.000000},{"name":"kube-node-lease__rf","dateUTC":"2022-05-16T22:00:00Z","usage":1.000000},{"name":"kube-node-lease__rf","dateUTC":"2022-05-16T23:00:00Z","usage":1.000000},{"name":"kube-node-lease__rf","dateUTC":"2022-05-17T00:00:00Z","usage":1.000000},{"name":"kube-node-lease__rf","dateUTC":"2022-05-17T01:00:00Z","usage":1.000000},{"name":"kube-node-lease__rf","dateUTC":"2022-05-17T02:00:00Z","usage":1.000000},{"name":"kube-node-lease__rf","dateUTC":"2022-05-17T03:00:00Z","usage":1.000000},{"name":"kube-node-lease__rf","dateUTC":"2022-05-17T04:00:00Z","usage":1.000000},{"name":"kube-node-lease__rf","dateUTC":"2022-05-17T05:00:00Z","usage":1.000000},{"name":"kube-node-lease__rf","dateUTC":"2022-05-17T06:00:00Z","usage":1.000000},{"name":"kube-node-lease__rf","dateUTC":"2022-05-17T07:00:00Z","usage":1.000000},{"name":"kube-node-lease__rf","dateUTC":"2022-05-17T08:00:00Z","usage":1.000000},{"name":"kube-public__rf","dateUTC":"2022-05-16T16:00:00Z","usage":1.000000},{"name":"kube-public__rf","dateUTC":"2022-05-16T17:00:00Z","usage":1.000000},{"name":"kube-public__rf","dateUTC":"2022-05-16T18:00:00Z","usage":1.000000},{"name":"kube-public__rf","dateUTC":"2022-05-16T19:00:00Z","usage":1.000000},{"name":"kube-public__rf","dateUTC":"2022-05-16T20:00:00Z","usage":1.000000},{"name":"kube-public__rf","dateUTC":"2022-05-16T21:00:00Z","usage":1.000000},{"name":"kube-public__rf","dateUTC":"2022-05-16T22:00:00Z","usage":1.000000},{"name":"kube-public__rf","dateUTC":"2022-05-16T23:00:00Z","usage":1.000000},{"name":"kube-public__rf","dateUTC":"2022-05-17T00:00:00Z","usage":1.000000},{"name":"kube-public__rf","dateUTC":"2022-05-17T01:00:00Z","usage":1.000000},{"name":"kube-public__rf","dateUTC":"2022-05-17T02:00:00Z","usage":1.000000},{"name":"kube-public__rf","dateUTC":"2022-05-17T03:00:00Z","usage":1.000000},{"name":"kube-public__rf","dateUTC":"2022-05-17T04:00:00Z","usage":1.000000},{"name":"kube-public__rf","dateUTC":"2022-05-17T05:00:00Z","usage":1.000000},{"name":"kube-public__rf","dateUTC":"2022-05-17T06:00:00Z","usage":1.000000},{"name":"kube-public__rf","dateUTC":"2022-05-17T07:00:00Z","usage":1.000000},{"name":"kube-public__rf","dateUTC":"2022-05-17T08:00:00Z","usage":1.000000}] \ No newline at end of file diff --git a/static/data/memory_usage_period_1209600.json b/static/data/memory_usage_period_1209600.json deleted file mode 100644 index 64901ca..0000000 --- a/static/data/memory_usage_period_1209600.json +++ /dev/null @@ -1 +0,0 @@ -[{"stack":"non-allocatable","usage":170.498520,"date":"16 May"},{"stack":"kube-system","usage":10.105812,"date":"16 May"},{"stack":"non-allocatable","usage":191.810835,"date":"17 May"},{"stack":"kube-system","usage":11.392899,"date":"17 May"}] \ No newline at end of file diff --git a/static/data/memory_usage_period_31968000.json b/static/data/memory_usage_period_31968000.json deleted file mode 100644 index 7b5d61c..0000000 --- a/static/data/memory_usage_period_31968000.json +++ /dev/null @@ -1 +0,0 @@ -[{"stack":"non-allocatable","usage":362.309355,"date":"May 2022"},{"stack":"kube-system","usage":21.498711,"date":"May 2022"}] \ No newline at end of file diff --git a/static/data/memory_usage_trends.json b/static/data/memory_usage_trends.json deleted file mode 100644 index a9b6897..0000000 --- a/static/data/memory_usage_trends.json +++ /dev/null @@ -1 +0,0 @@ -[{"name":"non-allocatable","dateUTC":"2022-05-16T16:00:00Z","usage":21.312315},{"name":"non-allocatable","dateUTC":"2022-05-16T17:00:00Z","usage":21.312315},{"name":"non-allocatable","dateUTC":"2022-05-16T18:00:00Z","usage":21.312315},{"name":"non-allocatable","dateUTC":"2022-05-16T19:00:00Z","usage":21.312315},{"name":"non-allocatable","dateUTC":"2022-05-16T20:00:00Z","usage":21.312315},{"name":"non-allocatable","dateUTC":"2022-05-16T21:00:00Z","usage":21.312315},{"name":"non-allocatable","dateUTC":"2022-05-16T22:00:00Z","usage":21.312315},{"name":"non-allocatable","dateUTC":"2022-05-16T23:00:00Z","usage":21.312315},{"name":"non-allocatable","dateUTC":"2022-05-17T00:00:00Z","usage":21.312315},{"name":"non-allocatable","dateUTC":"2022-05-17T01:00:00Z","usage":21.312315},{"name":"non-allocatable","dateUTC":"2022-05-17T02:00:00Z","usage":21.312315},{"name":"non-allocatable","dateUTC":"2022-05-17T03:00:00Z","usage":21.312315},{"name":"non-allocatable","dateUTC":"2022-05-17T04:00:00Z","usage":21.312315},{"name":"non-allocatable","dateUTC":"2022-05-17T05:00:00Z","usage":21.312315},{"name":"non-allocatable","dateUTC":"2022-05-17T06:00:00Z","usage":21.312315},{"name":"non-allocatable","dateUTC":"2022-05-17T07:00:00Z","usage":21.312315},{"name":"non-allocatable","dateUTC":"2022-05-17T08:00:00Z","usage":21.312315},{"name":"kube-system","dateUTC":"2022-05-16T16:00:00Z","usage":1.266366},{"name":"kube-system","dateUTC":"2022-05-16T17:00:00Z","usage":1.267275},{"name":"kube-system","dateUTC":"2022-05-16T18:00:00Z","usage":1.268300},{"name":"kube-system","dateUTC":"2022-05-16T19:00:00Z","usage":1.264139},{"name":"kube-system","dateUTC":"2022-05-16T20:00:00Z","usage":1.261430},{"name":"kube-system","dateUTC":"2022-05-16T21:00:00Z","usage":1.258346},{"name":"kube-system","dateUTC":"2022-05-16T22:00:00Z","usage":1.259619},{"name":"kube-system","dateUTC":"2022-05-16T23:00:00Z","usage":1.260337},{"name":"kube-system","dateUTC":"2022-05-17T00:00:00Z","usage":1.266378},{"name":"kube-system","dateUTC":"2022-05-17T01:00:00Z","usage":1.267609},{"name":"kube-system","dateUTC":"2022-05-17T02:00:00Z","usage":1.269841},{"name":"kube-system","dateUTC":"2022-05-17T03:00:00Z","usage":1.266712},{"name":"kube-system","dateUTC":"2022-05-17T04:00:00Z","usage":1.260553},{"name":"kube-system","dateUTC":"2022-05-17T05:00:00Z","usage":1.259613},{"name":"kube-system","dateUTC":"2022-05-17T06:00:00Z","usage":1.264562},{"name":"kube-system","dateUTC":"2022-05-17T07:00:00Z","usage":1.267643},{"name":"kube-system","dateUTC":"2022-05-17T08:00:00Z","usage":1.269989}] \ No newline at end of file diff --git a/static/data/nodes.json b/static/data/nodes.json deleted file mode 100644 index 9e26dfe..0000000 --- a/static/data/nodes.json +++ /dev/null @@ -1 +0,0 @@ -{} \ No newline at end of file From 0d50f00a0f5dfbd371bfc63a5d7983db4694271f Mon Sep 17 00:00:00 2001 From: Rodrigue Chakode Date: Wed, 31 Aug 2022 21:38:25 +0000 Subject: [PATCH 22/32] fixed lint --- backend.py | 105 ++++++++++++++++++++++++++--------------------------- 1 file changed, 52 insertions(+), 53 deletions(-) diff --git a/backend.py b/backend.py index c2774e7..0a8a066 100644 --- a/backend.py +++ b/backend.py @@ -77,7 +77,6 @@ class Config: excluded_namespaces = [i for i in os.getenv('KOA_EXCLUDED_NAMESPACES', '').replace(' ', ',').split(',') if i] google_api_key = os.getenv('KOA_GOOGLE_API_KEY', 'NO_GOOGLE_API_KEY') - def __init__(self): self.load_rbac_auth_token() @@ -92,17 +91,17 @@ def __init__(self): with open(str('%s/backend.json' % self.frontend_data_location), 'w') as fd: if self.cost_model == 'CHARGE_BACK': - cost_model_label = 'costs' + cost_model_label = 'costs' cost_model_unit = self.billing_currency # if cluster is an aks cluster then cost_model = 'AKS_CHARGE_BACK' elif self.cost_model == 'AKS_CHARGE_BACK': - cost_model_label = 'aks_costs' + cost_model_label = 'aks_costs' cost_model_unit = self.billing_currency - + # if cluster is a GKE cluster then cost_model = 'GKE_CHARGE_BACK' elif self.cost_model == 'GKE_CHARGE_BACK': - cost_model_label = 'gke_costs' + cost_model_label = 'gke_costs' cost_model_unit = self.billing_currency elif self.cost_model == 'RATIO': @@ -113,8 +112,8 @@ def __init__(self): cost_model_unit = '%' fd.write('{"cost_model":"%s", "currency":"%s"}' % (cost_model_label, cost_model_unit)) - # handle cacert file if applicable - + # handle cacert file if applicable + if self.k8s_verify_ssl and self.k8s_ssl_cacert and os.path.exists(self.k8s_ssl_cacert): self.koa_verify_ssl_option = self.k8s_ssl_cacert else: @@ -155,8 +154,8 @@ def configure_logger(debug_enabled): if debug_enabled: log_level = logging.DEBUG else: - log_level = logging.WARN - + log_level = logging.WARN + logger = logging.getLogger('kube-opex-analytics') logger.setLevel(log_level) ch = logging.StreamHandler() @@ -258,10 +257,10 @@ def render(): def get_azure_price(node): price = 0.0 api_base = "https://prices.azure.com/api/retail/prices?$filter=armRegionName eq '" - api_endpoint = api_base + node.region + "' and skuName eq '" + node.instanceType + "' and serviceName eq 'Virtual Machines'" + api_endpoint = api_base + node.region + "' and skuName eq '" + node.instanceType + "' and serviceName eq 'Virtual Machines'" # noqa: E501 data_json = requests.get(api_endpoint).json() if data_json.get("Count", None) == 0: - api_endpoint = api_base + node.region + "' and skuName eq '" + node.instanceType[0].lower() + node.instanceType[1:] + "' and serviceName eq 'Virtual Machines'" + api_endpoint = api_base + node.region + "' and skuName eq '" + node.instanceType[0].lower() + node.instanceType[1:] + "' and serviceName eq 'Virtual Machines'" # noqa: E501 while price == 0.0: data_json = requests.get(api_endpoint).json() for _, item in enumerate(data_json["Items"]): @@ -270,55 +269,56 @@ def get_azure_price(node): price = item.get('unitPrice') elif node.os == "linux": if item["type"] == "Consumption" and not (item["productName"].endswith('Windows')): - price=item.get('unitPrice') + price = item.get('unitPrice') api_endpoint = data_json["NextPageLink"] if api_endpoint is None: break return price -# computing GKE node price +# computing GKE node price def gcp_search_price_per_page(node, data_json, instance_description): price = 0.0 for _, sku in enumerate(data_json['skus']): - if sku.get("description").startswith(instance_description): - if node.region in sku.get("serviceRegions") and sku["category"]["usageType"]=="OnDemand": - price_info = sku["pricingInfo"][0]["pricingExpression"]["tieredRates"][0] - units = float( price_info["unitPrice"]["units"]) - nanos = float( price_info["unitPrice"]["nanos"]) - price = units + nanos * 10 ** (-9) + if sku.get("description").startswith(instance_description): + if node.region in sku.get("serviceRegions") and sku["category"]["usageType"] == "OnDemand": + price_info = sku["pricingInfo"][0]["pricingExpression"]["tieredRates"][0] + units = float(price_info["unitPrice"]["units"]) + nanos = float(price_info["unitPrice"]["nanos"]) + price = units + nanos * 1e-9 return price + def get_gcp_price(node, memory, cpu): cpu_price = 0.0 memory_price = 0.0 - price = 0.0 - base_api_endpoint = "https://cloudbilling.googleapis.com/v1/services/6F81-5844-456A/skus?key=" + KOA_CONFIG.google_api_key + price = 0.0 + base_api_endpoint = "https://cloudbilling.googleapis.com/v1/services/6F81-5844-456A/skus?key=" + KOA_CONFIG.google_api_key # noqa: E501 - data_json=requests.get(base_api_endpoint).json() + data_json = requests.get(base_api_endpoint).json() instance_description_for_cpu = node.instanceType[:2].upper() + " Instance Core" - instance_description_for_memory = node.instanceType[:2].upper() + " Instance Ram" + instance_description_for_memory = node.instanceType[:2].upper() + " Instance Ram" next_page_token = data_json['nextPageToken'] - cpu_price= cpu * gcp_search_price_per_page(node, data_json, instance_description_for_cpu) + cpu_price = cpu * gcp_search_price_per_page(node, data_json, instance_description_for_cpu) memory_price = memory * gcp_search_price_per_page(node, data_json, instance_description_for_memory) - - while cpu_price == 0.0 : - api_endpoint=base_api_endpoint+"&pageToken="+next_page_token - data_json=requests.get(api_endpoint).json() - cpu_price= cpu * gcp_search_price_per_page(node, data_json, instance_description_for_cpu) + + while cpu_price == 0.0: + api_endpoint = base_api_endpoint + "&pageToken=" + next_page_token + data_json = requests.get(api_endpoint).json() + cpu_price = cpu * gcp_search_price_per_page(node, data_json, instance_description_for_cpu) if data_json['nextPageToken'] != "": - next_page_token = data_json['nextPageToken'] - else : + next_page_token = data_json['nextPageToken'] + else: break - while memory_price == 0 : - api_endpoint=base_api_endpoint+"&pageToken="+next_page_token - data_json=requests.get(api_endpoint).json() + while memory_price == 0: + api_endpoint = base_api_endpoint + "&pageToken=" + next_page_token + data_json = requests.get(api_endpoint).json() memory_price = memory * gcp_search_price_per_page(node, data_json, instance_description_for_memory) if data_json['nextPageToken'] != "": - next_page_token = data_json['nextPageToken'] - else : + next_page_token = data_json['nextPageToken'] + else: break price = cpu_price + memory_price return price @@ -344,8 +344,7 @@ def __init__(self): self.instanceType = '' self.aksCluster = None self.gcpCluster = None - self.hourlyPrice= 0.0 - + self.hourlyPrice = 0.0 class Pod: @@ -441,7 +440,7 @@ def __init__(self): self.gkeManagedControlPlanePrice = 0.10 # the pricing of the managed control plane is similar for all of AKS, EKS and GKE at $0.10/hour self.hourlyRate = 0.0 - + def decode_capacity(self, cap_input): data_length = len(cap_input) cap_unit = 'None' @@ -486,7 +485,7 @@ def extract_nodes(self, data): if metadata is not None: node.id = metadata.get('uid', None) node.name = metadata.get('name', None) - + status = item.get('status', None) if status is not None: node.containerRuntime = status['nodeInfo']['containerRuntimeVersion'] @@ -517,24 +516,24 @@ def extract_nodes(self, data): node.state = 'DiskPressure' break - # for managed clusters + # for managed clusters node.region = metadata['labels']['topology.kubernetes.io/region'] node.instanceType = metadata['labels']['node.kubernetes.io/instance-type'] node.aksCluster = metadata['labels'].get('kubernetes.azure.com/cluster', None) - node.gcpCluster= metadata['labels'].get("cloud.google.com/gke-boot-disk", None) + node.gcpCluster = metadata['labels'].get("cloud.google.com/gke-boot-disk", None) # AKS cluster processing - if node.aksCluster != None : + if node.aksCluster is not None: self.hourlyRate = self.aksManagedControlPlanePrice node.hourlyPrice = get_azure_price(node) self.hourlyRate += node.hourlyPrice - + # GKE cluster processing - if node.gcpCluster is not None : - self.hourlyRate=self.gkeManagedControlPlanePrice + if node.gcpCluster is not None: + self.hourlyRate = self.gkeManagedControlPlanePrice memory = status["capacity"]["memory"] memLengh = len(status["capacity"]["memory"]) - memoryInGibibytes = (float( memory[0:memLengh-2])*9.5367431640625)*(10 ** (-7)) + memoryInGibibytes = (float(memory[0:(memLengh - 2)]) * 9.5367431640625) * 1e-7 # 1 kiB = 9.5367431640625E-7 giB cpu = float(status['capacity']['cpu']) node.HourlyPrice = get_gcp_price(node, memoryInGibibytes, cpu) @@ -835,9 +834,9 @@ def dump_histogram_analytics(dbfiles, period): sum_requests_per_type_date = {} actual_cost_model = 'CUMULATIVE' - if KOA_CONFIG.cost_model in ['CHARGE_BACK', 'AKS_CHARGE_BACK','GKE_CHARGE_BACK' ]: + if KOA_CONFIG.cost_model in ['CHARGE_BACK', 'AKS_CHARGE_BACK', 'GKE_CHARGE_BACK']: actual_cost_model = 'CHARGE_BACK' - + for _, db in enumerate(dbfiles): rrd = Rrd(db_files_location=KOA_CONFIG.db_location, dbname=db) current_periodic_usage = rrd.dump_histogram_data(period=period) @@ -877,7 +876,7 @@ def dump_histogram_analytics(dbfiles, period): for res, usage_data_bundle in usage_per_type_date.items(): for date_key, db_usage_item in usage_data_bundle.items(): for db, usage_value in db_usage_item.items(): - + if db != KOA_CONFIG.db_billing_hourly_rate: usage_cost = round(usage_value, KOA_CONFIG.db_round_decimals) @@ -944,7 +943,7 @@ def pull_k8s(api_context): headers=headers, cert=client_cert) if http_req.status_code == 200: - data = http_req.text + data = http_req.text else: KOA_LOGGER.error("call to %s returned error (%s)", api_endpoint, http_req.text) except Exception as ex: @@ -983,9 +982,9 @@ def create_metrics_puller(): rrd.add_sample(timestamp_epoch=now_epoch, cpu_usage=cpu_non_allocatable, mem_usage=mem_non_allocatable) # handle billing data - if KOA_CONFIG.cost_model in ["AKS_CHARGE_BACK", "GKE_CHARGE_BACK"] : + if KOA_CONFIG.cost_model in ["AKS_CHARGE_BACK", "GKE_CHARGE_BACK"]: KOA_CONFIG.billing_hourly_rate = k8s_usage.hourlyRate - + rrd = Rrd(db_files_location=KOA_CONFIG.db_location, dbname=KOA_CONFIG.db_billing_hourly_rate) rrd.add_sample(timestamp_epoch=now_epoch, cpu_usage=KOA_CONFIG.billing_hourly_rate, mem_usage=KOA_CONFIG.billing_hourly_rate) From 7392af38fb68c7b9e35ce69e258f3b32b09cf312 Mon Sep 17 00:00:00 2001 From: Rodrigue Chakode Date: Sat, 17 Sep 2022 12:26:40 +0200 Subject: [PATCH 23/32] Fix/misc fixes and refactoringn next to the GCP and Azure dynamic pricing integration (#77) * misc fixes and refactoring * fixes regression when running against GKE without API key * code refactoring to improve quality * fixed lint --- backend.py | 123 ++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 83 insertions(+), 40 deletions(-) diff --git a/backend.py b/backend.py index 0a8a066..09b9c88 100644 --- a/backend.py +++ b/backend.py @@ -250,78 +250,121 @@ def download_dataset(path): @app.route('/') def render(): + """Render the index.html page based on Flash template.""" return flask.render_template('index.html', koa_frontend_data_location=KOA_CONFIG.frontend_data_location, koa_version=KOA_CONFIG.version) +def get_http_resource_or_return_none_on_error(url): + """Get a HTTP resource on its URL and return none on error.""" + data = None + try: + req = requests.get(url, params=None) + except requests.exceptions.Timeout: + + KOA_LOGGER.error("Timeout while querying %s", url) + except requests.exceptions.TooManyRedirects: + KOA_LOGGER.error("TooManyRedirects while querying %s", url) + except requests.exceptions.RequestException as ex: + exception_type = type(ex).__name__ + KOA_LOGGER.error("HTTP error (%s) => %s", exception_type, traceback.format_exc()) + + if req.status_code != 200: + KOA_LOGGER.error("Call to URL %s returned error => %s", url, req.content) + else: + data = req.content + + return data + + def get_azure_price(node): + """Query Azure pricing API to compute node price based on its computing resources (e.g. vCPU, RAM).""" + api_base = "https://prices.azure.com/api/retail/prices?$filter=armRegionName" + api_endpoint = "{} eq '{}' and skuName eq '{}' and serviceName eq 'Virtual Machines'".format(api_base, node.region, node.instanceType) # noqa: E501 + + pricing_data = get_http_resource_or_return_none_on_error(api_endpoint) + if pricing_data is None: + return 0.0 + + pricing_json = pricing_data.json() + if pricing_json.get("Count", 0) == 0: + api_endpoint = "{} eq '{}' and skuName eq '{}{}' and serviceName eq 'Virtual Machines'".format(api_base, node.region, node.instanceType[0].lower(), node.instanceType[1:]) # noqa: E501 + price = 0.0 - api_base = "https://prices.azure.com/api/retail/prices?$filter=armRegionName eq '" - api_endpoint = api_base + node.region + "' and skuName eq '" + node.instanceType + "' and serviceName eq 'Virtual Machines'" # noqa: E501 - data_json = requests.get(api_endpoint).json() - if data_json.get("Count", None) == 0: - api_endpoint = api_base + node.region + "' and skuName eq '" + node.instanceType[0].lower() + node.instanceType[1:] + "' and serviceName eq 'Virtual Machines'" # noqa: E501 while price == 0.0: - data_json = requests.get(api_endpoint).json() - for _, item in enumerate(data_json["Items"]): + pricing_data = get_http_resource_or_return_none_on_error(api_endpoint) + if pricing_data is None: + break + + pricing_json = pricing_data.json() + for _, item in enumerate(pricing_json["Items"]): if node.os == "windows": if item["type"] == "Consumption" and item["productName"].endswith('Windows'): price = item.get('unitPrice') elif node.os == "linux": if item["type"] == "Consumption" and not (item["productName"].endswith('Windows')): price = item.get('unitPrice') - api_endpoint = data_json["NextPageLink"] + + api_endpoint = pricing_json.get("NextPageLink", None) if api_endpoint is None: break + return price -# computing GKE node price -def gcp_search_price_per_page(node, data_json, instance_description): +def gcp_search_price_per_page(node, skus, instance_description): + """Compute GKE node price.""" price = 0.0 - for _, sku in enumerate(data_json['skus']): + for _, sku in skus: if sku.get("description").startswith(instance_description): if node.region in sku.get("serviceRegions") and sku["category"]["usageType"] == "OnDemand": price_info = sku["pricingInfo"][0]["pricingExpression"]["tieredRates"][0] units = float(price_info["unitPrice"]["units"]) nanos = float(price_info["unitPrice"]["nanos"]) price = units + nanos * 1e-9 + return price def get_gcp_price(node, memory, cpu): + """Query GCE pricing API to compute node price based on its computing capacities (e.g. vCPU, RAM).""" cpu_price = 0.0 memory_price = 0.0 - price = 0.0 - base_api_endpoint = "https://cloudbilling.googleapis.com/v1/services/6F81-5844-456A/skus?key=" + KOA_CONFIG.google_api_key # noqa: E501 - - data_json = requests.get(base_api_endpoint).json() - instance_description_for_cpu = node.instanceType[:2].upper() + " Instance Core" - instance_description_for_memory = node.instanceType[:2].upper() + " Instance Ram" - next_page_token = data_json['nextPageToken'] - - cpu_price = cpu * gcp_search_price_per_page(node, data_json, instance_description_for_cpu) - memory_price = memory * gcp_search_price_per_page(node, data_json, instance_description_for_memory) - - while cpu_price == 0.0: - api_endpoint = base_api_endpoint + "&pageToken=" + next_page_token - data_json = requests.get(api_endpoint).json() - cpu_price = cpu * gcp_search_price_per_page(node, data_json, instance_description_for_cpu) - if data_json['nextPageToken'] != "": - next_page_token = data_json['nextPageToken'] - else: + instance_cpu_desc = node.instanceType[:2].upper() + " Instance Core" + instance_memory_desc = node.instanceType[:2].upper() + " Instance Ram" + + base_api_endpoint = "https://cloudbilling.googleapis.com/v1/services/6F81-5844-456A/skus?key={}".format(KOA_CONFIG.google_api_key) # noqa: E501 + + pricing_data = get_http_resource_or_return_none_on_error(base_api_endpoint) + if pricing_data is None: + return 0.0 + + pricing_json = pricing_data.json() + skus = pricing_json.get('skus', None) + if skus is not None: + cpu_price = cpu * gcp_search_price_per_page(node, skus, instance_cpu_desc) + memory_price = memory * gcp_search_price_per_page(node, skus, instance_memory_desc) + + next_page_token = pricing_json.get('nextPageToken', None) + while next_page_token is not None and next_page_token != "": + api_endpoint = "{}&pageToken={}".format(base_api_endpoint, next_page_token) + + pricing_data = get_http_resource_or_return_none_on_error(api_endpoint) + if pricing_data is None: break - while memory_price == 0: - api_endpoint = base_api_endpoint + "&pageToken=" + next_page_token - data_json = requests.get(api_endpoint).json() - memory_price = memory * gcp_search_price_per_page(node, data_json, instance_description_for_memory) - if data_json['nextPageToken'] != "": - next_page_token = data_json['nextPageToken'] - else: + pricing_json = pricing_data.json() + skus = pricing_json.get('skus', None) + if skus is not None: + cpu_price += cpu * gcp_search_price_per_page(node, skus, instance_cpu_desc) + memory_price += memory * gcp_search_price_per_page(node, skus, instance_memory_desc) + + if cpu_price != 0.0 and memory_price != 0.0: break - price = cpu_price + memory_price - return price + + next_page_token = pricing_json.get('nextPageToken', None) + + return cpu_price + memory_price class Node: @@ -529,12 +572,11 @@ def extract_nodes(self, data): self.hourlyRate += node.hourlyPrice # GKE cluster processing - if node.gcpCluster is not None: + if node.gcpCluster is not None and KOA_CONFIG.google_api_key != "NO_GOOGLE_API_KEY": self.hourlyRate = self.gkeManagedControlPlanePrice memory = status["capacity"]["memory"] memLengh = len(status["capacity"]["memory"]) memoryInGibibytes = (float(memory[0:(memLengh - 2)]) * 9.5367431640625) * 1e-7 - # 1 kiB = 9.5367431640625E-7 giB cpu = float(status['capacity']['cpu']) node.HourlyPrice = get_gcp_price(node, memoryInGibibytes, cpu) self.hourlyRate += node.HourlyPrice @@ -594,6 +636,7 @@ def extract_pods(self, data): pod.nodeName = item['spec']['nodeName'] pod.cpuRequest = 0.0 pod.memRequest = 0.0 + # TODO: extract initContainers for _, container in enumerate(item.get('spec').get('containers')): resources = container.get('resources', None) From 9c53b3a8ba5ed219db2ccacc326ba0ebfc2bb698 Mon Sep 17 00:00:00 2001 From: Rodrigue Chakode Date: Sat, 17 Sep 2022 10:41:58 +0000 Subject: [PATCH 24/32] added missing default config settings in manifests --- manifests/helm/templates/deployment.yaml | 5 +++++ manifests/helm/values.yaml | 2 ++ manifests/kustomize/kustomization.yaml | 3 ++- .../kustomize/resources/kube-opex-analytics-config.yaml | 4 +++- .../kustomize/resources/kube-opex-analytics-secret.yaml | 7 ------- .../kustomize/resources/kube-opex-analytics-secrets.yaml | 7 +++++++ manifests/kustomize/resources/kube-opex-analytics-sts.yaml | 2 +- 7 files changed, 20 insertions(+), 10 deletions(-) delete mode 100644 manifests/kustomize/resources/kube-opex-analytics-secret.yaml create mode 100644 manifests/kustomize/resources/kube-opex-analytics-secrets.yaml diff --git a/manifests/helm/templates/deployment.yaml b/manifests/helm/templates/deployment.yaml index 7f8f49c..2f35156 100644 --- a/manifests/helm/templates/deployment.yaml +++ b/manifests/helm/templates/deployment.yaml @@ -44,6 +44,11 @@ spec: - name: {{ $key | quote }} value: {{ $val | quote }} {{- end }} + - name: "KOA_GOOGLE_API_KEY" + valueFrom: + secretKeyRef: + name: kube-opex-analytics-secrets + key: KOA_GOOGLE_API_KEY {{- end }} {{- if .Values.includedNamespaces }} {{- if not .Values.envs }} diff --git a/manifests/helm/values.yaml b/manifests/helm/values.yaml index e169eb3..200daee 100644 --- a/manifests/helm/values.yaml +++ b/manifests/helm/values.yaml @@ -10,6 +10,8 @@ envs: KOA_BILLING_CURRENCY_SYMBOL: $ KOA_K8S_API_VERIFY_SSL: true KOA_K8S_CACERT: /run/secrets/kubernetes.io/serviceaccount/ca.crt + KOA_INCLUDED_NAMESPACES: '' + KOA_EXCLUDED_NAMESPACES: '' dataVolume: persist: true diff --git a/manifests/kustomize/kustomization.yaml b/manifests/kustomize/kustomization.yaml index e6cd5ad..c3d2a72 100644 --- a/manifests/kustomize/kustomization.yaml +++ b/manifests/kustomize/kustomization.yaml @@ -6,10 +6,11 @@ namespace: kube-opex-analytics resources: - resources/kube-opex-analytics-rbac.yaml - resources/kube-opex-analytics-config.yaml + - resources/kube-opex-analytics-secrets.yaml - resources/kube-opex-analytics-sts.yaml - resources/kube-opex-analytics-service.yaml - resources/kube-opex-analytics-tests.yaml images: - name: kube-opex-analytics newName: rchakode/kube-opex-analytics - newTag: 22.02.3 + newTag: 2022-09-17-33a3620 diff --git a/manifests/kustomize/resources/kube-opex-analytics-config.yaml b/manifests/kustomize/resources/kube-opex-analytics-config.yaml index 7a54d18..24880f4 100644 --- a/manifests/kustomize/resources/kube-opex-analytics-config.yaml +++ b/manifests/kustomize/resources/kube-opex-analytics-config.yaml @@ -8,4 +8,6 @@ data: KOA_BILLING_CURRENCY_SYMBOL: '$' KOA_K8S_API_ENDPOINT: 'https://kubernetes.default' KOA_K8S_API_VERIFY_SSL: 'true' - KOA_K8S_CACERT: '/run/secrets/kubernetes.io/serviceaccount/ca.crt' \ No newline at end of file + KOA_K8S_CACERT: '/run/secrets/kubernetes.io/serviceaccount/ca.crt' + KOA_INCLUDED_NAMESPACES: '' + KOA_EXCLUDED_NAMESPACES: '' diff --git a/manifests/kustomize/resources/kube-opex-analytics-secret.yaml b/manifests/kustomize/resources/kube-opex-analytics-secret.yaml deleted file mode 100644 index 72c9038..0000000 --- a/manifests/kustomize/resources/kube-opex-analytics-secret.yaml +++ /dev/null @@ -1,7 +0,0 @@ -apiVersion: v1 -kind: Secret -metadata: - name: kube-opex-analytics-secret -type: Opaque -data: - KOA_GOOGLE_API_KEY='google api key encoded using base64' diff --git a/manifests/kustomize/resources/kube-opex-analytics-secrets.yaml b/manifests/kustomize/resources/kube-opex-analytics-secrets.yaml new file mode 100644 index 0000000..79c41d5 --- /dev/null +++ b/manifests/kustomize/resources/kube-opex-analytics-secrets.yaml @@ -0,0 +1,7 @@ +apiVersion: v1 +kind: Secret +metadata: + name: kube-opex-analytics-secrets +type: Opaque +data: + KOA_GOOGLE_API_KEY: 'S09BX0dPT0dMRV9BUElfS0VZ' diff --git a/manifests/kustomize/resources/kube-opex-analytics-sts.yaml b/manifests/kustomize/resources/kube-opex-analytics-sts.yaml index 89e8ef9..154ce08 100644 --- a/manifests/kustomize/resources/kube-opex-analytics-sts.yaml +++ b/manifests/kustomize/resources/kube-opex-analytics-sts.yaml @@ -66,7 +66,7 @@ spec: valueFrom: secretKeyRef: key: KOA_GOOGLE_API_KEY - name: kube-opex-analytics-secret + name: kube-opex-analytics-secrets ports: - name: http containerPort: 5483 From 7554be785cf6a7e90ca7d782aa14d893ec83a2e5 Mon Sep 17 00:00:00 2001 From: Rodrigue Chakode Date: Wed, 5 Oct 2022 23:24:43 +0200 Subject: [PATCH 25/32] added variable KOA_LISTENER to configure backend port (#79) --- backend.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/backend.py b/backend.py index 09b9c88..fc2c8d9 100644 --- a/backend.py +++ b/backend.py @@ -80,6 +80,12 @@ class Config: def __init__(self): self.load_rbac_auth_token() + # check listener port + try: + self.listener_port = int(os.getenv('KOA_LISTENER_PORT')) + except: + self.listener_port = 5483 + # handle billing rate and cost model try: self.billing_hourly_rate = float(os.getenv('KOA_BILLING_HOURLY_RATE')) @@ -1104,6 +1110,6 @@ def dump_analytics(): th_exporter.start() if not KOA_CONFIG.enable_debug: - waitress_serve(wsgi_dispatcher, listen='0.0.0.0:5483') + waitress_serve(wsgi_dispatcher, listen='0.0.0.0:{}'.format(KOA_CONFIG.listener_port)) else: - app.run(host='0.0.0.0', port=5483) + app.run(host='0.0.0.0', port=KOA_CONFIG.listener_port) From 3adac2ead3c16306d8b7b723f31129ecce0b145f Mon Sep 17 00:00:00 2001 From: Christophe Camel Date: Fri, 7 Oct 2022 20:28:52 +0200 Subject: [PATCH 26/32] added github-actions ecosystem to dependabot configuration (#80) --- .github/dependabot.yml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 38f94b5..eede421 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -13,3 +13,12 @@ updates: directory: "/" schedule: interval: "daily" +- package-ecosystem: github-actions + directory: "/" + schedule: + interval: daily + open-pull-requests-limit: 5 + assignees: + - rchakode + reviewers: + - rchakode From 4b163120f2c1eb16ac0bb14ff2dfba9b60bafc5b Mon Sep 17 00:00:00 2001 From: Rodrigue Chakode Date: Sat, 8 Oct 2022 06:24:50 +0200 Subject: [PATCH 27/32] Kustomize: Fixed unexpected change on current version --- manifests/kustomize/kustomization.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/manifests/kustomize/kustomization.yaml b/manifests/kustomize/kustomization.yaml index c3d2a72..1366087 100644 --- a/manifests/kustomize/kustomization.yaml +++ b/manifests/kustomize/kustomization.yaml @@ -13,4 +13,4 @@ resources: images: - name: kube-opex-analytics newName: rchakode/kube-opex-analytics - newTag: 2022-09-17-33a3620 + newTag: 22.02.3 From 40f6b1faf06ba78ee1902c877689caa821e0797e Mon Sep 17 00:00:00 2001 From: Rodrigue Chakode Date: Sat, 8 Oct 2022 07:02:07 +0200 Subject: [PATCH 28/32] Feat/auto pricing (#78) * added missing default config settings in manifests * updated deployment manifests with dynamic pricing settings * enable auto cost pricing computation * automatically handle cost analytics for AKS and GKE * Fixed typo in a variable * Make lint happy --- backend.py | 162 ++++++++++++++++++++++++++--------------------------- 1 file changed, 79 insertions(+), 83 deletions(-) diff --git a/backend.py b/backend.py index fc2c8d9..d7e5b6d 100644 --- a/backend.py +++ b/backend.py @@ -18,7 +18,6 @@ import json import logging import os -import sys import threading import time import traceback @@ -45,6 +44,7 @@ def create_directory_if_not_exists(path): + """Create the given directory if it does not exist.""" try: os.makedirs(path) except OSError as e: @@ -77,8 +77,32 @@ class Config: excluded_namespaces = [i for i in os.getenv('KOA_EXCLUDED_NAMESPACES', '').replace(' ', ',').split(',') if i] google_api_key = os.getenv('KOA_GOOGLE_API_KEY', 'NO_GOOGLE_API_KEY') + def process_cost_model_config(self): + cost_model_label = 'cumulative' + cost_model_unit = '%' + if self.cost_model == 'CHARGE_BACK': + cost_model_label = 'costs' + cost_model_unit = self.billing_currency + elif self.cost_model == 'RATIO': + cost_model_label = 'normalized' + cost_model_unit = '%' + return cost_model_label, cost_model_unit + + def process_billing_hourly_rate_config(self): + """Process KOA_BILLING_HOURLY_RATE config setting.""" + try: + self.billing_hourly_rate = float(os.getenv('KOA_BILLING_HOURLY_RATE', -1)) + except: + self.billing_hourly_rate = float(-1.0) + def __init__(self): + self.billing_hourly_rate = 0.0 self.load_rbac_auth_token() + self.process_cost_model_config() + create_directory_if_not_exists(self.frontend_data_location) + cost_model_label, cost_model_unit = self.process_cost_model_config() + with open(str('%s/backend.json' % self.frontend_data_location), 'w') as fd: + fd.write('{"cost_model":"%s", "currency":"%s"}' % (cost_model_label, cost_model_unit)) # check listener port try: @@ -86,40 +110,7 @@ def __init__(self): except: self.listener_port = 5483 - # handle billing rate and cost model - try: - self.billing_hourly_rate = float(os.getenv('KOA_BILLING_HOURLY_RATE')) - except: - self.billing_hourly_rate = -1.0 - - create_directory_if_not_exists(self.frontend_data_location) - - with open(str('%s/backend.json' % self.frontend_data_location), 'w') as fd: - - if self.cost_model == 'CHARGE_BACK': - cost_model_label = 'costs' - cost_model_unit = self.billing_currency - - # if cluster is an aks cluster then cost_model = 'AKS_CHARGE_BACK' - elif self.cost_model == 'AKS_CHARGE_BACK': - cost_model_label = 'aks_costs' - cost_model_unit = self.billing_currency - - # if cluster is a GKE cluster then cost_model = 'GKE_CHARGE_BACK' - elif self.cost_model == 'GKE_CHARGE_BACK': - cost_model_label = 'gke_costs' - cost_model_unit = self.billing_currency - - elif self.cost_model == 'RATIO': - cost_model_label = 'normalized' - cost_model_unit = '%' - else: - cost_model_label = 'cumulative' - cost_model_unit = '%' - fd.write('{"cost_model":"%s", "currency":"%s"}' % (cost_model_label, cost_model_unit)) - # handle cacert file if applicable - if self.k8s_verify_ssl and self.k8s_ssl_cacert and os.path.exists(self.k8s_ssl_cacert): self.koa_verify_ssl_option = self.k8s_ssl_cacert else: @@ -141,8 +132,9 @@ def allow_namespace(namespace): return no_namespace_included or all_namespaces_enabled or namespace_matched def load_rbac_auth_token(self): + """Load the service account token when applicable.""" try: - with open('/var/run/secrets/kubernetes.io/serviceaccount/token', 'r') as rbac_token_file: + with open('/var/run/secrets/kubernetes.io/serviceaccount/token', 'r', encoding=None) as rbac_token_file: self.k8s_rbac_auth_token = rbac_token_file.read() except: self.k8s_rbac_auth_token = 'NO_ENV_TOKEN_FILE' @@ -164,11 +156,11 @@ def configure_logger(debug_enabled): logger = logging.getLogger('kube-opex-analytics') logger.setLevel(log_level) - ch = logging.StreamHandler() - ch.setLevel(log_level) - formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') - ch.setFormatter(formatter) - logger.addHandler(ch) + logger_handler = logging.StreamHandler() + logger_handler.setLevel(log_level) + logger_formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') + logger_handler.setFormatter(logger_formatter) + logger.addHandler(logger_handler) return logger @@ -180,6 +172,7 @@ def configure_logger(debug_enabled): class RrdPeriod(enum.IntEnum): + """Class RrdPeriod handles RRD settings.""" PERIOD_5_MINS_SEC = 300 PERIOD_1_HOUR_SEC = 3600 @@ -190,8 +183,6 @@ class RrdPeriod(enum.IntEnum): # initialize Prometheus exporter - - PROMETHEUS_HOURLY_USAGE_EXPORTER = prometheus_client.Gauge('koa_namespace_hourly_usage', 'Current hourly resource usage per namespace', ['namespace', 'resource']) @@ -257,7 +248,8 @@ def download_dataset(path): @app.route('/') def render(): """Render the index.html page based on Flash template.""" - return flask.render_template('index.html', koa_frontend_data_location=KOA_CONFIG.frontend_data_location, + return flask.render_template('index.html', + koa_frontend_data_location=KOA_CONFIG.frontend_data_location, koa_version=KOA_CONFIG.version) @@ -485,10 +477,13 @@ def __init__(self): 'n': 1e-9, 'None': 1 } - self.aksManagedControlPlanePrice = 0.10 - self.gkeManagedControlPlanePrice = 0.10 - # the pricing of the managed control plane is similar for all of AKS, EKS and GKE at $0.10/hour + + self.cloudCostAvailable = None self.hourlyRate = 0.0 + self.managedControlPlanePrice = { + "AKS": 0.10, + "GKE": 0.10 + } def decode_capacity(self, cap_input): data_length = len(cap_input) @@ -538,7 +533,6 @@ def extract_nodes(self, data): status = item.get('status', None) if status is not None: node.containerRuntime = status['nodeInfo']['containerRuntimeVersion'] - node.cpuCapacity = self.decode_capacity(status['capacity']['cpu']) node.cpuAllocatable = self.decode_capacity(status['allocatable']['cpu']) node.memCapacity = self.decode_capacity(status['capacity']['memory']) @@ -565,7 +559,7 @@ def extract_nodes(self, data): node.state = 'DiskPressure' break - # for managed clusters + # check managed cluster settings node.region = metadata['labels']['topology.kubernetes.io/region'] node.instanceType = metadata['labels']['node.kubernetes.io/instance-type'] node.aksCluster = metadata['labels'].get('kubernetes.azure.com/cluster', None) @@ -573,22 +567,20 @@ def extract_nodes(self, data): # AKS cluster processing if node.aksCluster is not None: - self.hourlyRate = self.aksManagedControlPlanePrice + self.cloudCostAvailable = "AKS" node.hourlyPrice = get_azure_price(node) self.hourlyRate += node.hourlyPrice # GKE cluster processing if node.gcpCluster is not None and KOA_CONFIG.google_api_key != "NO_GOOGLE_API_KEY": - self.hourlyRate = self.gkeManagedControlPlanePrice - memory = status["capacity"]["memory"] - memLengh = len(status["capacity"]["memory"]) - memoryInGibibytes = (float(memory[0:(memLengh - 2)]) * 9.5367431640625) * 1e-7 - cpu = float(status['capacity']['cpu']) - node.HourlyPrice = get_gcp_price(node, memoryInGibibytes, cpu) + self.cloudCostAvailable = "GKE" + node.HourlyPrice = get_gcp_price(node, node.memCapacity * 9.5367431640625e-7, node.cpuCapacity) self.hourlyRate += node.HourlyPrice self.nodes[node.name] = node + self.hourlyRate += self.managedControlPlanePrice.get(self.cloudCostAvailable, 0.0) + def extract_node_metrics(self, data): # exit if not valid data if data is None: @@ -866,7 +858,7 @@ def dump_trend_analytics(dbfiles, category='usage'): fd.write('[' + ','.join(res_usage[1]) + ']') @staticmethod - def dump_histogram_analytics(dbfiles, period): + def dump_histogram_analytics(dbfiles, period, cost_model): """ Dump usage history data. @@ -882,10 +874,6 @@ def dump_histogram_analytics(dbfiles, period): requests_per_type_date = {} sum_requests_per_type_date = {} - actual_cost_model = 'CUMULATIVE' - if KOA_CONFIG.cost_model in ['CHARGE_BACK', 'AKS_CHARGE_BACK', 'GKE_CHARGE_BACK']: - actual_cost_model = 'CHARGE_BACK' - for _, db in enumerate(dbfiles): rrd = Rrd(db_files_location=KOA_CONFIG.db_location, dbname=db) current_periodic_usage = rrd.dump_histogram_data(period=period) @@ -929,11 +917,11 @@ def dump_histogram_analytics(dbfiles, period): if db != KOA_CONFIG.db_billing_hourly_rate: usage_cost = round(usage_value, KOA_CONFIG.db_round_decimals) - if KOA_CONFIG.cost_model == 'RATIO' or actual_cost_model == 'CHARGE_BACK': + if KOA_CONFIG.cost_model == 'RATIO' or cost_model == 'CHARGE_BACK': usage_ratio = usage_value / sum_usage_per_type_date[res][date_key] usage_cost = round(100 * usage_ratio, KOA_CONFIG.db_round_decimals) - if actual_cost_model == 'CHARGE_BACK': + if cost_model == 'CHARGE_BACK': usage_cost = round( usage_ratio * usage_per_type_date[res][date_key][KOA_CONFIG.db_billing_hourly_rate], KOA_CONFIG.db_round_decimals) @@ -948,15 +936,15 @@ def dump_histogram_analytics(dbfiles, period): if KOA_CONFIG.cost_model == 'RATIO' or KOA_CONFIG.cost_model == 'CHARGE_BACK': req_ratio = req_value / sum_requests_per_type_date[res][date_key] req_cost = round(100 * req_ratio, KOA_CONFIG.db_round_decimals) + if KOA_CONFIG.cost_model == 'CHARGE_BACK': req_cost = round( req_ratio * usage_per_type_date[res][date_key][KOA_CONFIG.db_billing_hourly_rate], KOA_CONFIG.db_round_decimals) - requests_export[res].append('{"stack":"%s","usage":%f,"date":"%s"}' - % (db, req_cost, date_key)) + + requests_export[res].append('{"stack":"%s","usage":%f,"date":"%s"}' % (db, req_cost, date_key)) if Rrd.get_date_group(now_gmtime, period) == date_key: - PROMETHEUS_PERIODIC_REQUESTS_EXPORTERS[period].labels(db, ResUsageType(res).name).set( - req_cost) + PROMETHEUS_PERIODIC_REQUESTS_EXPORTERS[period].labels(db, ResUsageType(res).name).set(req_cost) # noqa: E501 with open(str('%s/cpu_usage_period_%d.json' % (KOA_CONFIG.frontend_data_location, period)), 'w') as fd: fd.write('[' + ','.join(usage_export[0]) + ']') @@ -1030,13 +1018,17 @@ def create_metrics_puller(): rrd = Rrd(db_files_location=KOA_CONFIG.db_location, dbname=KOA_CONFIG.db_non_allocatable) rrd.add_sample(timestamp_epoch=now_epoch, cpu_usage=cpu_non_allocatable, mem_usage=mem_non_allocatable) - # handle billing data - if KOA_CONFIG.cost_model in ["AKS_CHARGE_BACK", "GKE_CHARGE_BACK"]: - KOA_CONFIG.billing_hourly_rate = k8s_usage.hourlyRate + hourly_rate = -1 + if KOA_CONFIG.billing_hourly_rate > 0: + hourly_rate = KOA_CONFIG.billing_hourly_rate + else: + if k8s_usage.cloudCostAvailable is not None: + hourly_rate = k8s_usage.hourlyRate rrd = Rrd(db_files_location=KOA_CONFIG.db_location, dbname=KOA_CONFIG.db_billing_hourly_rate) - rrd.add_sample(timestamp_epoch=now_epoch, cpu_usage=KOA_CONFIG.billing_hourly_rate, - mem_usage=KOA_CONFIG.billing_hourly_rate) + rrd.add_sample(timestamp_epoch=now_epoch, + cpu_usage=hourly_rate, + mem_usage=hourly_rate) # handle resource request and usage by pods for ns, ns_usage in k8s_usage.usageByNamespace.items(): @@ -1065,7 +1057,7 @@ def create_metrics_puller(): KOA_LOGGER.error("%s Exception in create_metrics_puller => %s", exception_type, traceback.format_exc()) -def dump_analytics(): +def dump_analytics(cost_model_by_user=None): try: export_interval = round(1.5 * KOA_CONFIG.polling_interval_sec) while True: @@ -1084,25 +1076,29 @@ def dump_analytics(): Rrd.dump_trend_analytics(ns_dbfiles, 'usage') Rrd.dump_trend_analytics(rf_dbfiles, 'rf') - Rrd.dump_histogram_analytics(dbfiles=ns_dbfiles, period=RrdPeriod.PERIOD_14_DAYS_SEC) - Rrd.dump_histogram_analytics(dbfiles=ns_dbfiles, period=RrdPeriod.PERIOD_YEAR_SEC) + + cost_model_selected = cost_model_by_user + if cost_model_by_user is None: + cost_model_selected = KOA_CONFIG.cost_model + else: + if cost_model_by_user not in ['CUMULATIVE', 'RATIO', 'CHARGE_BACK']: + cost_model_selected = 'CUMULATIVE' + KOA_LOGGER.warning("Unexpected cost model => %s (using default => CUMULATIVE)", cost_model_by_user) + + Rrd.dump_histogram_analytics(dbfiles=ns_dbfiles, period=RrdPeriod.PERIOD_14_DAYS_SEC, cost_model=cost_model_selected) # noqa: E501 + Rrd.dump_histogram_analytics(dbfiles=ns_dbfiles, period=RrdPeriod.PERIOD_YEAR_SEC, cost_model=cost_model_selected) # noqa: E501 time.sleep(export_interval) except Exception as ex: exception_type = type(ex).__name__ KOA_LOGGER.error("%s Exception in dump_analytics => %s", exception_type, traceback.format_exc()) -# validating configs -if KOA_CONFIG.cost_model == 'CHARGE_BACK' and KOA_CONFIG.billing_hourly_rate <= 0.0: - KOA_LOGGER.fatal('invalid billing hourly rate for CHARGE_BACK cost allocation') - sys.exit(1) -if KOA_CONFIG.cost_model == 'GKE_CHARGE_BACK' and KOA_CONFIG.google_api_key == 'NO_GOOGLE_API_KEY': - KOA_LOGGER.fatal('no google API key provided, unable to calculate GKE cluster hourly rate') - sys.exit(1) - if __name__ == '__main__': + if KOA_CONFIG.cost_model == 'CHARGE_BACK' and KOA_CONFIG.billing_hourly_rate <= 0.0: + KOA_LOGGER.warning('Unexpected hourly rate for CHARGE_BACK => %f', KOA_CONFIG.billing_hourly_rate) + parser = argparse.ArgumentParser(description='Kubernetes Opex Analytics Backend') - parser.add_argument('-v', '--version', action='version', version='%(prog)s ' + KOA_CONFIG.version) + parser.add_argument('-v', '--version', action='version', version='%(prog)s {}'.format(KOA_CONFIG.version)) args = parser.parse_args() th_puller = threading.Thread(target=create_metrics_puller) th_exporter = threading.Thread(target=dump_analytics) From 29849c847184b18a56507fa5178574378d3be67c Mon Sep 17 00:00:00 2001 From: Rodrigue Chakode Date: Sat, 8 Oct 2022 07:05:24 +0200 Subject: [PATCH 29/32] fixed current version in kustomize manifests --- manifests/kustomize/kustomization.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/manifests/kustomize/kustomization.yaml b/manifests/kustomize/kustomization.yaml index 1366087..270c810 100644 --- a/manifests/kustomize/kustomization.yaml +++ b/manifests/kustomize/kustomization.yaml @@ -13,4 +13,4 @@ resources: images: - name: kube-opex-analytics newName: rchakode/kube-opex-analytics - newTag: 22.02.3 + newTag: 22.06.0 From 88c1b3bf37d07e1e3896014bb9b52a27b1e2304d Mon Sep 17 00:00:00 2001 From: Rodrigue Chakode Date: Sat, 8 Oct 2022 07:07:07 +0200 Subject: [PATCH 30/32] fixed current version in helm manifests --- manifests/helm/Chart.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/manifests/helm/Chart.yaml b/manifests/helm/Chart.yaml index a0eb0c2..ea6aba7 100644 --- a/manifests/helm/Chart.yaml +++ b/manifests/helm/Chart.yaml @@ -2,6 +2,6 @@ apiVersion: v1 appVersion: "1.0" description: Helm chart for Kubernetes Opex Analtytics name: kube-opex-analytics -version: 22.02.3 +version: 22.06.0 From 1837be1408e8f92584fa0b3561b164b7679e591a Mon Sep 17 00:00:00 2001 From: Rodrigue Chakode Date: Mon, 31 Oct 2022 14:54:54 +0000 Subject: [PATCH 31/32] make token file configurable --- backend.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/backend.py b/backend.py index d7e5b6d..3f5737f 100644 --- a/backend.py +++ b/backend.py @@ -66,6 +66,7 @@ class Config: cost_model = os.getenv('KOA_COST_MODEL', 'CUMULATIVE_RATIO') billing_currency = os.getenv('KOA_BILLING_CURRENCY_SYMBOL', '$') enable_debug = (lambda v: v.lower() in ("yes", "true"))(os.getenv('KOA_ENABLE_DEBUG', 'false')) + k8s_auth_token_file = os.getenv('KOA_K8S_AUTH_TOKEN_FILE', '/var/run/secrets/kubernetes.io/serviceaccount/token') k8s_auth_token = os.getenv('KOA_K8S_AUTH_TOKEN', 'NO_ENV_AUTH_TOKEN') k8s_auth_token_type = os.getenv('KOA_K8S_AUTH_TOKEN_TYPE', 'Bearer') k8s_auth_username = os.getenv('KOA_K8S_AUTH_USERNAME', 'NO_ENV_AUTH_USERNAME') @@ -134,7 +135,7 @@ def allow_namespace(namespace): def load_rbac_auth_token(self): """Load the service account token when applicable.""" try: - with open('/var/run/secrets/kubernetes.io/serviceaccount/token', 'r', encoding=None) as rbac_token_file: + with open(KOA_CONFIG.k8s_auth_token_file, 'r', encoding=None) as rbac_token_file: self.k8s_rbac_auth_token = rbac_token_file.read() except: self.k8s_rbac_auth_token = 'NO_ENV_TOKEN_FILE' From 0e5677b7cb2b5a60942f77c3fbc324d302aab7ff Mon Sep 17 00:00:00 2001 From: Rodrigue Chakode Date: Sat, 3 Dec 2022 14:49:08 +0000 Subject: [PATCH 32/32] bump version to 22.12.0 --- backend.py | 2 +- manifests/helm/Chart.yaml | 2 +- manifests/kustomize/kustomization.yaml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/backend.py b/backend.py index 3f5737f..dee0148 100644 --- a/backend.py +++ b/backend.py @@ -53,7 +53,7 @@ def create_directory_if_not_exists(path): class Config: - version = '22.02.3' + version = '22.12.0' db_round_decimals = 6 db_non_allocatable = 'non-allocatable' db_billing_hourly_rate = '.billing-hourly-rate' diff --git a/manifests/helm/Chart.yaml b/manifests/helm/Chart.yaml index ea6aba7..89ba6e0 100644 --- a/manifests/helm/Chart.yaml +++ b/manifests/helm/Chart.yaml @@ -2,6 +2,6 @@ apiVersion: v1 appVersion: "1.0" description: Helm chart for Kubernetes Opex Analtytics name: kube-opex-analytics -version: 22.06.0 +version: 22.12.0 diff --git a/manifests/kustomize/kustomization.yaml b/manifests/kustomize/kustomization.yaml index 270c810..c5e82a4 100644 --- a/manifests/kustomize/kustomization.yaml +++ b/manifests/kustomize/kustomization.yaml @@ -13,4 +13,4 @@ resources: images: - name: kube-opex-analytics newName: rchakode/kube-opex-analytics - newTag: 22.06.0 + newTag: 22.12.0