From d2157f1a2057c9dd21e00ece77c3d1adabc08b1a Mon Sep 17 00:00:00 2001 From: Mario Apra Date: Wed, 26 Jun 2024 23:17:53 +0100 Subject: [PATCH 1/4] feat: Add __main__.py for running the package from the command line --- awslimitchecker/__main__.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 awslimitchecker/__main__.py diff --git a/awslimitchecker/__main__.py b/awslimitchecker/__main__.py new file mode 100644 index 00000000..f7b5c80a --- /dev/null +++ b/awslimitchecker/__main__.py @@ -0,0 +1,20 @@ +# The idea of this file is to enable running the package with +# `python -m awslimitchecker` or `python -m awslimitchecker [args]`. +# This enables debugging the package when installed in editable mode +# (`pip install -e .`) and running the package from the command line. + +import sys + +from . import runner + + +def main(args=None): + if args is None: + args = sys.argv[1:] + # Parse arguments and run your package's main functionality + print("Running awslimitchecker with arguments:", args) + runner.console_entry_point() + + +if __name__ == "__main__": + main() From 9152ebefd675c718dd25b2ba3e61b0c8d4c45ee9 Mon Sep 17 00:00:00 2001 From: Mario Apra Date: Thu, 27 Jun 2024 17:23:45 +0100 Subject: [PATCH 2/4] 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 | 1 - awslimitchecker/runner.py | 17 +++++++++++++---- setup.py | 3 ++- 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/awslimitchecker/limit.py b/awslimitchecker/limit.py index 65e054cf..c8031e56 100644 --- a/awslimitchecker/limit.py +++ b/awslimitchecker/limit.py @@ -433,7 +433,6 @@ class instance. Return True if usage is within thresholds, or false if continue pct = (usage / (limit * 1.0)) * 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..e9cb9145 100644 --- a/awslimitchecker/runner.py +++ b/awslimitchecker/runner.py @@ -43,6 +43,7 @@ import json import boto3 import time +import tabulate from .checker import AwsLimitChecker from .utils import StoreKeyValuePair, dict2cols, issue_string_tuple @@ -319,12 +320,20 @@ 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 = int(data.quotas_limit) if data.quotas_limit else "" + use = usage.value + use_percent = "{:.0f} %".format((use / limit) * 100) if isinstance(limit, (int, float)) else "-" + 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/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 = [ From 2b26d5a9fe32e5ff4f864deeae0f5ad2a9cd0a3c Mon Sep 17 00:00:00 2001 From: Mario Apra Date: Thu, 27 Jun 2024 21:59:06 +0100 Subject: [PATCH 3/4] chore: Sort imports in runner.py for better organization and readability --- awslimitchecker/runner.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/awslimitchecker/runner.py b/awslimitchecker/runner.py index e9cb9145..25fc1732 100644 --- a/awslimitchecker/runner.py +++ b/awslimitchecker/runner.py @@ -37,19 +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 From a30e892e330f6da6837248fa50f3ab0f3296f0f6 Mon Sep 17 00:00:00 2001 From: Mario Apra Date: Fri, 28 Jun 2024 15:03:02 +0100 Subject: [PATCH 4/4] Fix test_limit and test_runner --- awslimitchecker/limit.py | 2 +- awslimitchecker/runner.py | 19 ++++++-- awslimitchecker/tests/test_limit.py | 72 ++++++++++++++++++---------- awslimitchecker/tests/test_runner.py | 49 ++++++++----------- 4 files changed, 83 insertions(+), 59 deletions(-) diff --git a/awslimitchecker/limit.py b/awslimitchecker/limit.py index c8031e56..b8226ce7 100644 --- a/awslimitchecker/limit.py +++ b/awslimitchecker/limit.py @@ -431,7 +431,7 @@ 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: all_ok = False elif pct >= crit_pct: diff --git a/awslimitchecker/runner.py b/awslimitchecker/runner.py index 25fc1732..38c07de5 100644 --- a/awslimitchecker/runner.py +++ b/awslimitchecker/runner.py @@ -330,11 +330,22 @@ def show_usage(self): service = svc limit_name = lim resource = usage.resource_id or '-' - limit = int(data.quotas_limit) if data.quotas_limit else "" + limit = "" + if data.quotas_limit: + limit = int(data.quotas_limit) use = usage.value - use_percent = "{:.0f} %".format((use / limit) * 100) if isinstance(limit, (int, float)) else "-" - table.append([f"{service}/{limit_name}", resource, str(use), use_percent, str(limit)]) - print(tabulate.tabulate(table, headers=headers, tablefmt="simple_outline")) + 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):