From de441465c8fe36b83005627054d65b660c78e575 Mon Sep 17 00:00:00 2001 From: Mario Apra Date: Thu, 27 Jun 2024 17:23:45 +0100 Subject: [PATCH] Improve report by adding limit and percentage - #384 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The idea here is to improve the visualization of the reporting by adding not only the usage, but the limit and how close to the limit it is. Also, to make it easier to read, print it all in a table format. Example before: ``` EC2/All F Spot Instance Requests 0 EC2/All G Spot Instance Requests 0 EC2/All Inf Spot Instance Requests 0 EC2/All P Spot Instance Requests 0 EC2/All Standard (A, C, D, H, I, M, R, T, Z) Spot Instance Requests 0 EC2/All X Spot Instance Requests 0 EC2/Elastic IP addresses (EIPs) 0 EC2/Max active spot fleets per region 0 EC2/Max launch specifications per spot fleet EC2/Max target capacity for all spot fleets in region 0 EC2/Max target capacity per spot fleet EC2/Rules per VPC security group max: sg-0648da97fdeebf980=102 (sg-0df2ad106237174ea=1, sg-07e5e4f72cea0dfe5=1, sg-0b9911c871252d8e7=1, sg-0d055747ec7c4140e=1, sg-09551693a5250b7f4=1, sg-0781c381e1b5d378a=1, sg-0fd94d543ec6cf4ee=1, sg-05e714614de7941cd=1, sg-03cc3b695a6366ffd=1, sg-083eae2676c6f8b92=1, sg-0dc2ee2f21505bf2c=1, sg-0e1729f5b07df6416=1, sg-0de5f793ac0da38e7=1, sg-0d79228bbb3f97b6d=1, sg-0c7bcbb474d8afbf5=1, sg-0d6aa6e6209d923b5=1, sg-086b6c19c0860460b=1, sg-0c56e7af849a12622=1, sg-086a3c13d414aef8b=1, sg-0853c55cd9df708de=1, sg-0e1ed9e1ceca720a1=1, sg-099ef683e4d35e2ee=1, sg-0adc28eb7faa40ecf=1, sg-0dabf1e2c30f5998f=1, sg-06aedf016149fbe14=1, sg-0f9148df50a48a291=1, sg-006e95a00de69f1b8=1, sg-0f9f697bf9ef106a5=1, ``` Example after: ``` ┌──────────────────────────────────────────────────────────────────────────┬───────────────────────┬───────────┬───────────┬───────────┐ │ Service Limit │ Resource │ Usage # │ Usage % │ Limit │ ├──────────────────────────────────────────────────────────────────────────┼───────────────────────┼───────────┼───────────┼───────────┤ │ EC2/All F Spot Instance Requests │ - │ 0 │ 0 % │ 128 │ │ EC2/All G Spot Instance Requests │ - │ 0 │ - │ │ │ EC2/All Inf Spot Instance Requests │ - │ 0 │ 0 % │ 64 │ │ EC2/All P Spot Instance Requests │ - │ 0 │ - │ │ │ EC2/All Standard (A, C, D, H, I, M, R, T, Z) Spot Instance Requests │ - │ 0 │ 0 % │ 640 │ │ EC2/All X Spot Instance Requests │ - │ 0 │ 0 % │ 128 │ │ EC2/Elastic IP addresses (EIPs) │ - │ 0 │ - │ │ │ EC2/Max active spot fleets per region │ - │ 0 │ - │ │ │ EC2/Max target capacity for all spot fleets in region │ - │ 0 │ - │ │ │ EC2/Rules per VPC security group │ sg-058d83cc259d4e69c │ 3 │ 2 % │ 120 │ │ EC2/Rules per VPC security group │ sg-0783065c4d2b78c83 │ 3 │ 2 % │ 120 │ │ EC2/Rules per VPC security group │ sg-0df2ad106237174ea │ 1 │ 1 % │ 120 │ │ EC2/Rules per VPC security group │ sg-07e5e4f72cea0dfe5 │ 1 │ 1 % │ 120 │ │ EC2/Rules per VPC security group │ sg-0b9911c871252d8e7 │ 1 │ 1 % │ 120 │ │ EC2/Rules per VPC security group │ sg-0229a5ed0b61ce2bd │ 2 │ 2 % │ 120 │ │ EC2/Rules per VPC security group │ sg-0b25ba5186ce99117 │ 2 │ 2 % │ 120 │ ``` --- awslimitchecker/limit.py | 3 +- awslimitchecker/runner.py | 41 ++++++++++++---- awslimitchecker/tests/test_limit.py | 72 ++++++++++++++++++---------- awslimitchecker/tests/test_runner.py | 49 ++++++++----------- setup.py | 3 +- 5 files changed, 101 insertions(+), 67 deletions(-) diff --git a/awslimitchecker/limit.py b/awslimitchecker/limit.py index 65e054cf..b8226ce7 100644 --- a/awslimitchecker/limit.py +++ b/awslimitchecker/limit.py @@ -431,9 +431,8 @@ class instance. Return True if usage is within thresholds, or false if limit = u.get_maximum() or self.get_limit() if limit is None or limit == 0: continue - pct = (usage / (limit * 1.0)) * 100 + pct = (usage / limit) * 100 if crit_int is not None and usage >= crit_int: - self._criticals.append(u) all_ok = False elif pct >= crit_pct: self._criticals.append(u) diff --git a/awslimitchecker/runner.py b/awslimitchecker/runner.py index df61babf..bc0b9030 100644 --- a/awslimitchecker/runner.py +++ b/awslimitchecker/runner.py @@ -37,18 +37,20 @@ ############################################################################## """ -import sys import argparse -import logging import json -import boto3 +import logging +import sys import time +import boto3 +import tabulate + +from .alerts import AlertProvider from .checker import AwsLimitChecker -from .utils import StoreKeyValuePair, dict2cols, issue_string_tuple -from .limit import SOURCE_TA, SOURCE_API, SOURCE_QUOTAS +from .limit import SOURCE_API, SOURCE_QUOTAS, SOURCE_TA from .metrics import MetricsProvider -from .alerts import AlertProvider +from .utils import StoreKeyValuePair, dict2cols, issue_string_tuple try: from urllib.parse import urlparse @@ -319,12 +321,31 @@ def show_usage(self): service=self.service_name, use_ta=(not self.skip_ta)) limits = self.checker.get_limits( service=self.service_name, use_ta=(not self.skip_ta)) - data = {} + headers = ['Service Limit', 'Resource', 'Usage #', 'Usage %', 'Limit'] + table = [] for svc in sorted(limits.keys()): for lim in sorted(limits[svc].keys()): - data["{s}/{l}".format(s=svc, l=lim)] = '{v}'.format( - v=limits[svc][lim].get_current_usage_str()) - print(dict2cols(data)) + data = limits[svc][lim] + for usage in data.get_current_usage(): + service = svc + limit_name = lim + resource = usage.resource_id or '-' + limit = "" + if data.quotas_limit: + limit = int(data.get_limit()) + use = usage.value + use_percent = "-" + if isinstance(limit, (int, float)): + use_percent = "{:.0f} %".format((use / limit) * 100) + table.append([ + f"{service}/{limit_name}", + resource, + str(use), + use_percent, + str(limit), + ]) + print(tabulate.tabulate( + table, headers=headers, tablefmt="simple_outline")) def check_thresholds(self, metrics=None): have_warn = False diff --git a/awslimitchecker/tests/test_limit.py b/awslimitchecker/tests/test_limit.py index be7dd1c3..cbbd4a47 100644 --- a/awslimitchecker/tests/test_limit.py +++ b/awslimitchecker/tests/test_limit.py @@ -602,23 +602,33 @@ def test_int_warn(self): assert mock_get_limit.mock_calls == [call(), call(), call()] def test_int_warn_crit(self): - limit = AwsLimit('limitname', self.mock_svc, 100, 1, 2) - u1 = AwsLimitUsage(limit, 4, resource_id='foo4bar') - u2 = AwsLimitUsage(limit, 1, resource_id='foo3bar') - u3 = AwsLimitUsage(limit, 7, resource_id='foo2bar') + limit = AwsLimit( + name='limitname', + service=self.mock_svc, + default_limit=10, + def_warning_threshold=40, + def_critical_threshold=60, + ) + u1 = AwsLimitUsage( + limit=limit, + value=4, + resource_id='foo4bar', + ) + u2 = AwsLimitUsage( + limit=limit, + value=1, + resource_id='foo3bar', + ) + u3 = AwsLimitUsage( + limit=limit, + value=7, + resource_id='foo2bar', + ) limit._current_usage = [u1, u2, u3] - with patch('awslimitchecker.limit.AwsLimit.' - '_get_thresholds') as mock_get_thresh: - with patch('awslimitchecker.limit.AwsLimit.get_' - 'limit') as mock_get_limit: - mock_get_thresh.return_value = (4, 40, 6, 80) - mock_get_limit.return_value = 100 - res = limit.check_thresholds() + res = limit.check_thresholds() assert res is False assert limit._warnings == [u1] assert limit._criticals == [u3] - assert mock_get_thresh.mock_calls == [call()] - assert mock_get_limit.mock_calls == [call(), call(), call()] def test_pct_crit(self): limit = AwsLimit('limitname', self.mock_svc, 100, 1, 2) @@ -640,23 +650,33 @@ def test_pct_crit(self): assert mock_get_limit.mock_calls == [call(), call(), call()] def test_int_crit(self): - limit = AwsLimit('limitname', self.mock_svc, 100, 1, 2) - u1 = AwsLimitUsage(limit, 9, resource_id='foo4bar') - u2 = AwsLimitUsage(limit, 3, resource_id='foo3bar') - u3 = AwsLimitUsage(limit, 95, resource_id='foo2bar') + limit = AwsLimit( + name='limitname', + service=self.mock_svc, + default_limit=10, + def_warning_threshold=60, + def_critical_threshold=80, + ) + u1 = AwsLimitUsage( + limit=limit, + value=9, + resource_id='foo4bar', + ) + u2 = AwsLimitUsage( + limit=limit, + value=3, + resource_id='foo3bar', + ) + u3 = AwsLimitUsage( + limit=limit, + value=95, + resource_id='foo2bar', + ) limit._current_usage = [u1, u2, u3] - with patch('awslimitchecker.limit.AwsLimit.' - '_get_thresholds') as mock_get_thresh: - with patch('awslimitchecker.limit.AwsLimit.get_' - 'limit') as mock_get_limit: - mock_get_thresh.return_value = (6, 40, 8, 80) - mock_get_limit.return_value = 100 - res = limit.check_thresholds() + res = limit.check_thresholds() assert res is False assert limit._warnings == [] assert limit._criticals == [u1, u3] - assert mock_get_thresh.mock_calls == [call()] - assert mock_get_limit.mock_calls == [call(), call(), call()] def test_pct_warn_crit(self): limit = AwsLimit('limitname', self.mock_svc, 100, 1, 2) diff --git a/awslimitchecker/tests/test_runner.py b/awslimitchecker/tests/test_runner.py index e80f472d..fb385141 100644 --- a/awslimitchecker/tests/test_runner.py +++ b/awslimitchecker/tests/test_runner.py @@ -769,22 +769,18 @@ def test_default(self, capsys): mock_checker = Mock(spec_set=AwsLimitChecker) mock_checker.get_limits.return_value = limits self.cls.checker = mock_checker - with patch('awslimitchecker.runner.dict2cols') as mock_d2c: - mock_d2c.return_value = 'd2cval' + with patch('awslimitchecker.runner.tabulate') as mock_tabulate: self.cls.show_usage() - out, err = capsys.readouterr() - assert out == 'd2cval\n' - assert mock_checker.mock_calls == [ - call.find_usage(service=None, use_ta=True), - call.get_limits(service=None, use_ta=True) - ] - assert mock_d2c.mock_calls == [ - call({ - 'SvcBar/bar limit2': '22', - 'SvcBar/barlimit1': '11', - 'SvcFoo/foo limit3': '33', - }) - ] + assert mock_checker.mock_calls == [ + call.find_usage(service=None, use_ta=True), + call.get_limits(service=None, use_ta=True) + ] + assert len(mock_tabulate.method_calls) == 1 + assert mock_tabulate.method_calls[0].args == ([ + ['SvcBar/bar limit2', '-', '22', '-', ''], + ['SvcBar/barlimit1', '-', '11', '-', ''], + ['SvcFoo/foo limit3', '-', '33', '-', ''] + ],) def test_one_service(self, capsys): limits = { @@ -796,20 +792,17 @@ def test_one_service(self, capsys): self.cls.checker = mock_checker self.cls.service_name = ['SvcFoo'] self.cls.skip_ta = True - with patch('awslimitchecker.runner.dict2cols') as mock_d2c: - mock_d2c.return_value = 'd2cval' + with patch('awslimitchecker.runner.tabulate') as mock_tabulate: self.cls.show_usage() - out, err = capsys.readouterr() - assert out == 'd2cval\n' - assert mock_checker.mock_calls == [ - call.find_usage(service=['SvcFoo'], use_ta=False), - call.get_limits(service=['SvcFoo'], use_ta=False) - ] - assert mock_d2c.mock_calls == [ - call({ - 'SvcFoo/foo limit3': '33', - }) - ] + out, err = capsys.readouterr() + assert mock_checker.mock_calls == [ + call.find_usage(service=['SvcFoo'], use_ta=False), + call.get_limits(service=['SvcFoo'], use_ta=False) + ] + assert len(mock_tabulate.method_calls) == 1 + assert mock_tabulate.method_calls[0].args == ( + [['SvcFoo/foo limit3', '-', '33', '-', '']], + ) class TestCheckThresholds(RunnerTester): diff --git a/setup.py b/setup.py index e598b380..e05715c5 100644 --- a/setup.py +++ b/setup.py @@ -50,7 +50,8 @@ 'python-dateutil', 'versionfinder>=0.1.1', 'pytz', - 'urllib3' + 'urllib3', + 'tabulate>=0.9.0', ] classifiers = [