diff --git a/c7n/cli.py b/c7n/cli.py index 12d4fd72afb..b2742855da8 100644 --- a/c7n/cli.py +++ b/c7n/cli.py @@ -328,9 +328,9 @@ def _setup_logger(options): external_log_level = logging.INFO logging.getLogger('botocore').setLevel(external_log_level) - logging.getLogger('urllib3').setLevel(external_log_level) logging.getLogger('s3transfer').setLevel(external_log_level) logging.getLogger('urllib3').setLevel(logging.ERROR) + logging.getLogger('gql').setLevel(logging.WARNING) def main(): diff --git a/c7n/filters/cost.py b/c7n/filters/cost.py new file mode 100644 index 00000000000..6c6a8821f2d --- /dev/null +++ b/c7n/filters/cost.py @@ -0,0 +1,107 @@ +import os + +from c7n.cache import NullCache +from gql import Client, gql +from gql.transport.requests import RequestsHTTPTransport + +from .core import OPERATORS, Filter + + +class Cost(Filter): + """Annotate resource monthly cost with Infracost pricing API. + It aims to provide an approximate cost for a generic case. + For example, it only grabs price of on-demand, no pre-installed softwares for EC2 instances. + + Please use INFRACOST_API_ENDPOINT and INFRACOST_API_KEY to specify API config. + + .. code-block:: yaml + + policies: + - name: ec2-cost + resource: ec2 + filters: + - type: cost + op: greater-than + value: 4 + quantity: 730 + + + reference: https://www.infracost.io/docs/cloud_pricing_api/overview/ + """ + + schema = { + 'type': 'object', + 'additionalProperties': False, + 'required': ['type'], + 'properties': { + 'api_endpoint': {'type': 'string'}, + 'api_key': {'type': 'string'}, + # 'currency': {'type': 'number'}, + 'quantity': {'type': 'number'}, + 'op': {'$ref': '#/definitions/filters_common/comparison_operators'}, + 'type': {'enum': ['cost']}, + 'value': {'type': 'number'}, + }, + } + schema_alias = True + ANNOTATION_KEY = "c7n:Cost" + + def __init__(self, data, manager=None): + super().__init__(data, manager) + self.cache = manager._cache or NullCache({}) + self.api_endpoint = data.get( + "api_endpoint", + os.environ.get("INFRACOST_API_ENDPOINT", "https://pricing.api.infracost.io"), + ) + self.api_key = data.get("api_key", os.environ.get("INFRACOST_API_KEY")) + + def get_permissions(self): + return ("ec2:DescribeInstances",) + + def validate(self): + name = self.__class__.__name__ + if self.api_endpoint is None: + raise ValueError("%s Filter requires Infracost pricing_api_endpoint" % name) + + if self.api_key is None: + raise ValueError("%s Filter requires Infracost api_key" % name) + return super(Cost, self).validate() + + def process_resource(self, resource, client, query): + params = self.get_params(resource) + cache_key = str(params) + + with self.cache: + price = self.cache.get(cache_key) + if not price: + price = self.get_infracost(client, query, params) + # TODO support configurable currency + price["USD"] = float(price["USD"]) * self.data.get("quantity", 1) + self.cache.save(cache_key, price) + + resource[self.ANNOTATION_KEY] = price + op = self.data.get('operator', 'ge') + value = self.data.get('value', -1) + return OPERATORS[op](price["USD"], value) + + def get_infracost(self, client, query, params): + result = client.execute(query, variable_values=params) + self.log.info(f"Infracost {params}: {result}") + return result["products"][0]["prices"][0] + + def process(self, resources, event=None): + transport = RequestsHTTPTransport( + url=self.api_endpoint + "/graphql", + headers={'X-Api-Key': self.api_key}, + verify=True, + retries=3, + ) + client = Client(transport=transport, fetch_schema_from_transport=True) + query = gql(self.get_query()) + return [r for r in resources if self.process_resource(r, client, query)] + + def get_query(self): + raise NotImplementedError("use subclass") + + def get_params(self, resource): + raise NotImplementedError("use subclass") diff --git a/c7n/resources/ec2.py b/c7n/resources/ec2.py index bdf6a2cfa22..59ee4aed1d1 100644 --- a/c7n/resources/ec2.py +++ b/c7n/resources/ec2.py @@ -23,6 +23,7 @@ from c7n.filters import ( FilterRegistry, AgeFilter, ValueFilter, Filter, DefaultVpcBase ) +from c7n.filters.cost import Cost from c7n.filters.offhours import OffHour, OnHour import c7n.filters.vpc as net_filters @@ -177,6 +178,43 @@ class VpcFilter(net_filters.VpcFilter): RelatedIdsExpression = "VpcId" +@filters.register('cost') +class Ec2Cost(Cost): + + def get_query(self): + # reference: https://gql.readthedocs.io/en/stable/usage/variables.html + return """ + query ($region: String, $instanceType: String) { + products( + filter: { + vendorName: "aws", + service: "AmazonEC2", + productFamily: "Compute Instance", + region: $region, + attributeFilters: [ + { key: "instanceType", value: $instanceType } + { key: "operatingSystem", value: "Linux" } + { key: "tenancy", value: "Shared" } + { key: "capacitystatus", value: "Used" } + { key: "preInstalledSw", value: "NA" } + ] + }, + ) { + prices( + filter: {purchaseOption: "on_demand"} + ) { USD, unit, description, purchaseOption } + } + } + """ + + def get_params(self, resource): + params = { + "region": resource["Placement"]["AvailabilityZone"][:-1], + "instanceType": resource["InstanceType"], + } + return params + + @filters.register('check-permissions') class ComputePermissions(CheckPermissions): diff --git a/poetry.lock b/poetry.lock index fdd83035f9f..37aef8ae288 100644 --- a/poetry.lock +++ b/poetry.lock @@ -21,10 +21,10 @@ optional = false python-versions = ">=3.5" [package.extras] -dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "mypy (>=0.900,!=0.940)", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit", "cloudpickle"] -docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"] -tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "mypy (>=0.900,!=0.940)", "pytest-mypy-plugins", "zope.interface", "cloudpickle"] -tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "mypy (>=0.900,!=0.940)", "pytest-mypy-plugins", "cloudpickle"] +dev = ["cloudpickle", "coverage[toml] (>=5.0.2)", "furo", "hypothesis", "mypy (>=0.900,!=0.940)", "pre-commit", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "sphinx", "sphinx-notfound-page", "zope.interface"] +docs = ["furo", "sphinx", "sphinx-notfound-page", "zope.interface"] +tests = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy (>=0.900,!=0.940)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "zope.interface"] +tests_no_zope = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy (>=0.900,!=0.940)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins"] [[package]] name = "aws-xray-sdk" @@ -38,6 +38,14 @@ python-versions = "*" botocore = ">=1.11.3" wrapt = "*" +[[package]] +name = "backoff" +version = "2.1.2" +description = "Function decoration for backoff and retry" +category = "main" +optional = false +python-versions = ">=3.7,<4.0" + [[package]] name = "bleach" version = "5.0.1" @@ -52,18 +60,18 @@ webencodings = "*" [package.extras] css = ["tinycss2 (>=1.1.0,<1.2)"] -dev = ["build (==0.8.0)", "flake8 (==4.0.1)", "hashin (==0.17.0)", "pip-tools (==6.6.2)", "pytest (==7.1.2)", "Sphinx (==4.3.2)", "tox (==3.25.0)", "twine (==4.0.1)", "wheel (==0.37.1)", "black (==22.3.0)", "mypy (==0.961)"] +dev = ["Sphinx (==4.3.2)", "black (==22.3.0)", "build (==0.8.0)", "flake8 (==4.0.1)", "hashin (==0.17.0)", "mypy (==0.961)", "pip-tools (==6.6.2)", "pytest (==7.1.2)", "tox (==3.25.0)", "twine (==4.0.1)", "wheel (==0.37.1)"] [[package]] name = "boto3" -version = "1.24.77" +version = "1.24.79" description = "The AWS SDK for Python" category = "main" optional = false python-versions = ">= 3.7" [package.dependencies] -botocore = ">=1.27.77,<1.28.0" +botocore = ">=1.27.79,<1.28.0" jmespath = ">=0.7.1,<2.0.0" s3transfer = ">=0.6.0,<0.7.0" @@ -72,7 +80,7 @@ crt = ["botocore[crt] (>=1.21.0,<2.0a0)"] [[package]] name = "botocore" -version = "1.27.77" +version = "1.27.79" description = "Low-level, data-driven core of boto 3." category = "main" optional = false @@ -90,7 +98,7 @@ crt = ["awscrt (==0.14.0)"] name = "certifi" version = "2022.9.14" description = "Python package for providing Mozilla's CA Bundle." -category = "dev" +category = "main" optional = false python-versions = ">=3.6" @@ -109,7 +117,7 @@ pycparser = "*" name = "charset-normalizer" version = "2.1.1" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." -category = "dev" +category = "main" optional = false python-versions = ">=3.6.0" @@ -163,11 +171,11 @@ cffi = ">=1.12" [package.extras] docs = ["sphinx (>=1.6.5,!=1.8.0,!=3.1.0,!=3.1.1)", "sphinx-rtd-theme"] -docstest = ["pyenchant (>=1.6.11)", "twine (>=1.12.0)", "sphinxcontrib-spelling (>=4.0.1)"] +docstest = ["pyenchant (>=1.6.11)", "sphinxcontrib-spelling (>=4.0.1)", "twine (>=1.12.0)"] pep8test = ["black", "flake8", "flake8-import-order", "pep8-naming"] sdist = ["setuptools-rust (>=0.11.4)"] ssh = ["bcrypt (>=3.1.5)"] -test = ["pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-subtests", "pytest-xdist", "pretend", "iso8601", "pytz", "hypothesis (>=1.11.4,!=3.79.2)"] +test = ["hypothesis (>=1.11.4,!=3.79.2)", "iso8601", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-subtests", "pytest-xdist", "pytz"] [[package]] name = "docutils" @@ -213,11 +221,48 @@ python-versions = ">=3.6" [package.dependencies] python-dateutil = ">=2.7" +[[package]] +name = "gql" +version = "3.4.0" +description = "GraphQL client for Python" +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +backoff = ">=1.11.1,<3.0" +graphql-core = ">=3.2,<3.3" +requests = {version = ">=2.26,<3", optional = true, markers = "extra == \"requests\""} +requests-toolbelt = {version = ">=0.9.1,<1", optional = true, markers = "extra == \"requests\""} +urllib3 = {version = ">=1.26", optional = true, markers = "extra == \"requests\""} +yarl = ">=1.6,<2.0" + +[package.extras] +aiohttp = ["aiohttp (>=3.7.1,<3.9.0)"] +all = ["aiohttp (>=3.7.1,<3.9.0)", "botocore (>=1.21,<2)", "requests (>=2.26,<3)", "requests-toolbelt (>=0.9.1,<1)", "urllib3 (>=1.26)", "websockets (>=10,<11)", "websockets (>=9,<10)"] +botocore = ["botocore (>=1.21,<2)"] +dev = ["aiofiles", "aiohttp (>=3.7.1,<3.9.0)", "black (==22.3.0)", "botocore (>=1.21,<2)", "check-manifest (>=0.42,<1)", "flake8 (==3.8.1)", "isort (==4.3.21)", "mock (==4.0.2)", "mypy (==0.910)", "parse (==1.15.0)", "pytest (==6.2.5)", "pytest-asyncio (==0.16.0)", "pytest-console-scripts (==1.3.1)", "pytest-cov (==3.0.0)", "requests (>=2.26,<3)", "requests-toolbelt (>=0.9.1,<1)", "sphinx (>=3.0.0,<4)", "sphinx-argparse (==0.2.5)", "sphinx-rtd-theme (>=0.4,<1)", "types-aiofiles", "types-mock", "types-requests", "urllib3 (>=1.26)", "vcrpy (==4.0.2)", "websockets (>=10,<11)", "websockets (>=9,<10)"] +requests = ["requests (>=2.26,<3)", "requests-toolbelt (>=0.9.1,<1)", "urllib3 (>=1.26)"] +test = ["aiofiles", "aiohttp (>=3.7.1,<3.9.0)", "botocore (>=1.21,<2)", "mock (==4.0.2)", "parse (==1.15.0)", "pytest (==6.2.5)", "pytest-asyncio (==0.16.0)", "pytest-console-scripts (==1.3.1)", "pytest-cov (==3.0.0)", "requests (>=2.26,<3)", "requests-toolbelt (>=0.9.1,<1)", "urllib3 (>=1.26)", "vcrpy (==4.0.2)", "websockets (>=10,<11)", "websockets (>=9,<10)"] +test_no_transport = ["aiofiles", "mock (==4.0.2)", "parse (==1.15.0)", "pytest (==6.2.5)", "pytest-asyncio (==0.16.0)", "pytest-console-scripts (==1.3.1)", "pytest-cov (==3.0.0)", "vcrpy (==4.0.2)"] +websockets = ["websockets (>=10,<11)", "websockets (>=9,<10)"] + +[[package]] +name = "graphql-core" +version = "3.2.2" +description = "GraphQL implementation for Python, a port of GraphQL.js, the JavaScript reference implementation for GraphQL." +category = "main" +optional = false +python-versions = ">=3.6,<4" + +[package.dependencies] +typing-extensions = {version = ">=4.2,<5", markers = "python_version < \"3.8\""} + [[package]] name = "idna" version = "3.4" description = "Internationalized Domain Names in Applications (IDNA)" -category = "dev" +category = "main" optional = false python-versions = ">=3.5" @@ -234,9 +279,9 @@ typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""} zipp = ">=0.5" [package.extras] -docs = ["sphinx", "jaraco.packaging (>=9)", "rst.linker (>=1.9)"] +docs = ["jaraco.packaging (>=9)", "rst.linker (>=1.9)", "sphinx"] perf = ["ipython"] -testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.3)", "packaging", "pyfakefs", "flufl.flake8", "pytest-perf (>=0.9.2)", "pytest-black (>=0.3.7)", "pytest-mypy (>=0.9.1)", "importlib-resources (>=1.3)"] +testing = ["flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf (>=0.9.2)"] [[package]] name = "importlib-resources" @@ -250,8 +295,8 @@ python-versions = ">=3.7" zipp = {version = ">=3.1.0", markers = "python_version < \"3.10\""} [package.extras] -testing = ["pytest-mypy (>=0.9.1)", "pytest-black (>=0.3.7)", "pytest-enabler (>=1.3)", "pytest-cov", "pytest-flake8", "pytest-checkdocs (>=2.4)", "pytest (>=6)"] -docs = ["jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "jaraco.packaging (>=9)", "sphinx"] +docs = ["jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx"] +testing = ["pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"] [[package]] name = "iniconfig" @@ -273,8 +318,8 @@ python-versions = ">=3.7" more-itertools = "*" [package.extras] -docs = ["sphinx", "jaraco.packaging (>=9)", "rst.linker (>=1.9)", "jaraco.tidelift (>=1.4)"] -testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-black (>=0.3.7)", "pytest-mypy (>=0.9.1)"] +docs = ["jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx"] +testing = ["pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"] [[package]] name = "jeepney" @@ -285,8 +330,8 @@ optional = false python-versions = ">=3.7" [package.extras] -test = ["pytest", "pytest-trio", "pytest-asyncio (>=0.17)", "testpath", "trio", "async-timeout"] -trio = ["trio", "async-generator"] +test = ["async-timeout", "pytest", "pytest-asyncio (>=0.17)", "pytest-trio", "testpath", "trio"] +trio = ["async_generator", "trio"] [[package]] name = "jmespath" @@ -351,8 +396,8 @@ pywin32-ctypes = {version = "<0.1.0 || >0.1.0,<0.1.1 || >0.1.1", markers = "sys_ SecretStorage = {version = ">=3.2", markers = "sys_platform == \"linux\""} [package.extras] -docs = ["sphinx", "jaraco.packaging (>=9)", "rst.linker (>=1.9)", "jaraco.tidelift (>=1.4)"] -testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "flake8 (<5)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-black (>=0.3.7)", "pytest-mypy (>=0.9.1)"] +docs = ["jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx"] +testing = ["flake8 (<5)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"] [[package]] name = "mccabe" @@ -371,7 +416,7 @@ optional = false python-versions = ">=3.6" [package.extras] -build = ["twine", "wheel", "blurb"] +build = ["blurb", "twine", "wheel"] docs = ["sphinx"] test = ["pytest (<5.4)", "pytest-cov"] @@ -387,7 +432,7 @@ python-versions = ">=3.5" name = "multidict" version = "6.0.2" description = "multidict implementation" -category = "dev" +category = "main" optional = false python-versions = ">=3.7" @@ -411,10 +456,10 @@ optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" [package.extras] -testing = ["nose", "coverage"] +testing = ["coverage", "nose"] [[package]] -name = "pkgutil-resolve-name" +name = "pkgutil_resolve_name" version = "1.3.10" description = "Resolve a name to an object." category = "main" @@ -458,7 +503,7 @@ pywin32 = {version = ">=226", markers = "platform_system == \"Windows\""} [package.extras] docs = ["sphinx (>=1.7.1)"] redis = ["redis"] -tests = ["pytest (>=5.4.1)", "pytest-cov (>=2.8.1)", "pytest-timeout (>=2.1.0)", "sphinx (>=3.0.3)", "pytest-mypy (>=0.8.0)", "redis"] +tests = ["pytest (>=5.4.1)", "pytest-cov (>=2.8.1)", "pytest-mypy (>=0.8.0)", "pytest-timeout (>=2.1.0)", "redis", "sphinx (>=3.0.3)"] [[package]] name = "psutil" @@ -469,7 +514,7 @@ optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [package.extras] -test = ["ipaddress", "mock", "enum34", "pywin32", "wmi"] +test = ["enum34", "ipaddress", "mock", "pywin32", "wmi"] [[package]] name = "py" @@ -504,7 +549,7 @@ optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [[package]] -name = "pygments" +name = "Pygments" version = "2.13.0" description = "Pygments is a syntax highlighting package written in Python." category = "dev" @@ -523,7 +568,7 @@ optional = false python-versions = ">=3.6.8" [package.extras] -diagrams = ["railroad-diagrams", "jinja2"] +diagrams = ["jinja2", "railroad-diagrams"] [[package]] name = "pyrsistent" @@ -567,7 +612,7 @@ coverage = {version = ">=5.2.1", extras = ["toml"]} pytest = ">=4.6" [package.extras] -testing = ["fields", "hunter", "process-tests", "six", "pytest-xdist", "virtualenv"] +testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"] [[package]] name = "pytest-forked" @@ -667,7 +712,7 @@ optional = false python-versions = "*" [[package]] -name = "pyyaml" +name = "PyYAML" version = "6.0" description = "YAML parser and emitter for Python" category = "main" @@ -694,7 +739,7 @@ md = ["cmarkgfm (>=0.8.0)"] name = "requests" version = "2.28.1" description = "Python HTTP for Humans." -category = "dev" +category = "main" optional = false python-versions = ">=3.7, <4" @@ -712,7 +757,7 @@ use_chardet_on_py3 = ["chardet (>=3.0.2,<6)"] name = "requests-toolbelt" version = "0.9.1" description = "A utility belt for advanced users of python-requests" -category = "dev" +category = "main" optional = false python-versions = "*" @@ -745,7 +790,7 @@ botocore = ">=1.12.36,<2.0a.0" crt = ["botocore[crt] (>=1.20.29,<2.0a.0)"] [[package]] -name = "secretstorage" +name = "SecretStorage" version = "3.3.3" description = "Python bindings to FreeDesktop.org Secret Service API" category = "dev" @@ -784,7 +829,7 @@ optional = false python-versions = ">=3.7" [package.extras] -tests = ["pytest-cov", "pytest"] +tests = ["pytest", "pytest-cov"] [[package]] name = "tomli" @@ -848,8 +893,8 @@ optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*, <4" [package.extras] -brotli = ["brotlicffi (>=0.8.0)", "brotli (>=1.0.9)", "brotlipy (>=0.6.0)"] -secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "urllib3-secure-extra", "ipaddress"] +brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)", "brotlipy (>=0.6.0)"] +secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "ipaddress", "pyOpenSSL (>=0.14)", "urllib3-secure-extra"] socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] [[package]] @@ -886,7 +931,7 @@ python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" name = "yarl" version = "1.8.1" description = "Yet another URL library" -category = "dev" +category = "main" optional = false python-versions = ">=3.7" @@ -904,13 +949,13 @@ optional = false python-versions = ">=3.7" [package.extras] -testing = ["pytest-mypy (>=0.9.1)", "pytest-black (>=0.3.7)", "func-timeout", "jaraco.itertools", "pytest-enabler (>=1.3)", "pytest-cov", "pytest-flake8", "pytest-checkdocs (>=2.4)", "pytest (>=6)"] -docs = ["jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "jaraco.packaging (>=9)", "sphinx"] +docs = ["jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx"] +testing = ["func-timeout", "jaraco.itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"] [metadata] lock-version = "1.1" python-versions = "^3.7" -content-hash = "5b19269242626f6246590dfe7c896684aead35f7a8ca9e2f7255c7b672f58e0d" +content-hash = "f04afb3ff241a275628b4fbac7fde65dba3e511a0f8091f430ec328ed976d415" [metadata.files] argcomplete = [ @@ -925,17 +970,21 @@ aws-xray-sdk = [ {file = "aws-xray-sdk-2.10.0.tar.gz", hash = "sha256:9b14924fd0628cf92936055864655354003f0b1acc3e1c3ffde6403d0799dd7a"}, {file = "aws_xray_sdk-2.10.0-py2.py3-none-any.whl", hash = "sha256:7551e81a796e1a5471ebe84844c40e8edf7c218db33506d046fec61f7495eda4"}, ] +backoff = [ + {file = "backoff-2.1.2-py3-none-any.whl", hash = "sha256:b135e6d7c7513ba2bfd6895bc32bc8c66c6f3b0279b4c6cd866053cfd7d3126b"}, + {file = "backoff-2.1.2.tar.gz", hash = "sha256:407f1bc0f22723648a8880821b935ce5df8475cf04f7b6b5017ae264d30f6069"}, +] bleach = [ {file = "bleach-5.0.1-py3-none-any.whl", hash = "sha256:085f7f33c15bd408dd9b17a4ad77c577db66d76203e5984b1bd59baeee948b2a"}, {file = "bleach-5.0.1.tar.gz", hash = "sha256:0d03255c47eb9bd2f26aa9bb7f2107732e7e8fe195ca2f64709fcf3b0a4a085c"}, ] boto3 = [ - {file = "boto3-1.24.77-py3-none-any.whl", hash = "sha256:fc16e50263c24631d5fe75464e76f3a1b454b4c0015c864948a61691f9702a6e"}, - {file = "boto3-1.24.77.tar.gz", hash = "sha256:16646de3303779d6dc9c192d8a863095244de81d2e0b94f50692fbde767c6f1b"}, + {file = "boto3-1.24.79-py3-none-any.whl", hash = "sha256:c05f82633b086a7aa6dba9edec56ba8137835d6eb2bfca98bedb32d93eb657a9"}, + {file = "boto3-1.24.79.tar.gz", hash = "sha256:973a23d629a7aed77a662fcd55505c210bc48642cddfc64a1a9f3dbd18468d19"}, ] botocore = [ - {file = "botocore-1.27.77-py3-none-any.whl", hash = "sha256:d88509ed291b95525205cc06ca87b54d077aae996827039f5e32375949c5aaf7"}, - {file = "botocore-1.27.77.tar.gz", hash = "sha256:77a43e970d0762080b4b79a7e00ea572ef2ae7a9f578c3c8e7f0a344ee4b4e6d"}, + {file = "botocore-1.27.79-py3-none-any.whl", hash = "sha256:10be90eb6ece83fc915b1bb15d2561a0ecefd33a7c1612a8f78da006f99d58f0"}, + {file = "botocore-1.27.79.tar.gz", hash = "sha256:1187a685f0205b8acdde873fc3b081b036bed9104c91e9702b176e7b76ea63d0"}, ] certifi = [ {file = "certifi-2022.9.14-py3-none-any.whl", hash = "sha256:e232343de1ab72c2aa521b625c80f699e356830fd0e2c620b465b304b17b0516"}, @@ -1115,6 +1164,14 @@ freezegun = [ {file = "freezegun-1.2.2-py3-none-any.whl", hash = "sha256:ea1b963b993cb9ea195adbd893a48d573fda951b0da64f60883d7e988b606c9f"}, {file = "freezegun-1.2.2.tar.gz", hash = "sha256:cd22d1ba06941384410cd967d8a99d5ae2442f57dfafeff2fda5de8dc5c05446"}, ] +gql = [ + {file = "gql-3.4.0-py2.py3-none-any.whl", hash = "sha256:59c8a0b8f0a2f3b0b2ff970c94de86f82f65cb1da3340bfe57143e5f7ea82f71"}, + {file = "gql-3.4.0.tar.gz", hash = "sha256:ca81aa8314fa88a8c57dd1ce34941278e0c352d762eb721edcba0387829ea7c0"}, +] +graphql-core = [ + {file = "graphql-core-3.2.2.tar.gz", hash = "sha256:c56635ce67757069f317da85efcfd2f3f86cf1d26a06daf64402d89ed3a527c5"}, + {file = "graphql_core-3.2.2-py3-none-any.whl", hash = "sha256:4672711cb0f10013df464e450bda006fc449eca90d17a54a7c1103322e246c49"}, +] idna = [ {file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"}, {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"}, @@ -1240,7 +1297,7 @@ pkginfo = [ {file = "pkginfo-1.8.3-py2.py3-none-any.whl", hash = "sha256:848865108ec99d4901b2f7e84058b6e7660aae8ae10164e015a6dcf5b242a594"}, {file = "pkginfo-1.8.3.tar.gz", hash = "sha256:a84da4318dd86f870a9447a8c98340aa06216bfc6f2b7bdc4b8766984ae1867c"}, ] -pkgutil-resolve-name = [ +pkgutil_resolve_name = [ {file = "pkgutil_resolve_name-1.3.10-py3-none-any.whl", hash = "sha256:ca27cc078d25c5ad71a9de0a7a330146c4e014c2462d9af19c6b828280649c5e"}, {file = "pkgutil_resolve_name-1.3.10.tar.gz", hash = "sha256:357d6c9e6a755653cfd78893817c0853af365dd51ec97f3d358a819373bbd174"}, ] @@ -1305,7 +1362,7 @@ pyflakes = [ {file = "pyflakes-2.3.1-py2.py3-none-any.whl", hash = "sha256:7893783d01b8a89811dd72d7dfd4d84ff098e5eed95cfa8905b22bbffe52efc3"}, {file = "pyflakes-2.3.1.tar.gz", hash = "sha256:f5bc8ecabc05bb9d291eb5203d6810b49040f6ff446a756326104746cc00c1db"}, ] -pygments = [ +Pygments = [ {file = "Pygments-2.13.0-py3-none-any.whl", hash = "sha256:f643f331ab57ba3c9d89212ee4a2dabc6e94f117cf4eefde99a0574720d14c42"}, {file = "Pygments-2.13.0.tar.gz", hash = "sha256:56a8508ae95f98e2b9bdf93a6be5ae3f7d8af858b43e02c5a2ff083726be40c1"}, ] @@ -1388,7 +1445,7 @@ pywin32-ctypes = [ {file = "pywin32-ctypes-0.2.0.tar.gz", hash = "sha256:24ffc3b341d457d48e8922352130cf2644024a4ff09762a2261fd34c36ee5942"}, {file = "pywin32_ctypes-0.2.0-py2.py3-none-any.whl", hash = "sha256:9dc2d991b3479cc2df15930958b674a48a227d5361d413827a4cfd0b5876fc98"}, ] -pyyaml = [ +PyYAML = [ {file = "PyYAML-6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53"}, {file = "PyYAML-6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c"}, {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc"}, @@ -1396,6 +1453,13 @@ pyyaml = [ {file = "PyYAML-6.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5"}, {file = "PyYAML-6.0-cp310-cp310-win32.whl", hash = "sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513"}, {file = "PyYAML-6.0-cp310-cp310-win_amd64.whl", hash = "sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a"}, + {file = "PyYAML-6.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d4b0ba9512519522b118090257be113b9468d804b19d63c71dbcf4a48fa32358"}, + {file = "PyYAML-6.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:81957921f441d50af23654aa6c5e5eaf9b06aba7f0a19c18a538dc7ef291c5a1"}, + {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:afa17f5bc4d1b10afd4466fd3a44dc0e245382deca5b3c353d8b757f9e3ecb8d"}, + {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dbad0e9d368bb989f4515da330b88a057617d16b6a8245084f1b05400f24609f"}, + {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:432557aa2c09802be39460360ddffd48156e30721f5e8d917f01d31694216782"}, + {file = "PyYAML-6.0-cp311-cp311-win32.whl", hash = "sha256:bfaef573a63ba8923503d27530362590ff4f576c626d86a9fed95822a8255fd7"}, + {file = "PyYAML-6.0-cp311-cp311-win_amd64.whl", hash = "sha256:01b45c0191e6d66c470b6cf1b9531a771a83c1c4208272ead47a3ae4f2f603bf"}, {file = "PyYAML-6.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86"}, {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f"}, {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92"}, @@ -1443,7 +1507,7 @@ s3transfer = [ {file = "s3transfer-0.6.0-py3-none-any.whl", hash = "sha256:06176b74f3a15f61f1b4f25a1fc29a4429040b7647133a463da8fa5bd28d5ecd"}, {file = "s3transfer-0.6.0.tar.gz", hash = "sha256:2ed07d3866f523cc561bf4a00fc5535827981b117dd7876f036b0c1aca42c947"}, ] -secretstorage = [ +SecretStorage = [ {file = "SecretStorage-3.3.3-py3-none-any.whl", hash = "sha256:f356e6628222568e3af06f2eba8df495efa13b3b63081dafd4f7d9a7b7bc9f99"}, {file = "SecretStorage-3.3.3.tar.gz", hash = "sha256:2403533ef369eca6d2ba81718576c5e0f564d5cca1b58f73a8b23e7d4eeebd77"}, ] diff --git a/pyproject.toml b/pyproject.toml index 837480e12a5..1df99a6f115 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -26,6 +26,7 @@ python = "^3.7" boto3 = "^1.12.31" jsonschema = ">=3.0.0" argcomplete = ">=1.12.3" +gql = {extras = ["requests"], version = "^3.4.0"} python-dateutil = "^2.8.2" pyyaml = ">=5.4.0" tabulate = "^0.8.6" diff --git a/tests/data/placebo/ec2_cost/ec2.DescribeInstances_1.json b/tests/data/placebo/ec2_cost/ec2.DescribeInstances_1.json new file mode 100644 index 00000000000..1d0568f5925 --- /dev/null +++ b/tests/data/placebo/ec2_cost/ec2.DescribeInstances_1.json @@ -0,0 +1,481 @@ +{ + "status_code": 200, + "data": { + "Reservations": [ + { + "Groups": [], + "Instances": [ + { + "AmiLaunchIndex": 0, + "ImageId": "ami-053cb79d5964bdd39", + "InstanceId": "i-07a22308186a50b34", + "InstanceType": "t3.nano", + "KeyName": "dev-tools", + "LaunchTime": { + "__class__": "datetime", + "year": 2022, + "month": 9, + "day": 18, + "hour": 22, + "minute": 10, + "second": 13, + "microsecond": 0 + }, + "Monitoring": { + "State": "disabled" + }, + "Placement": { + "AvailabilityZone": "ap-southeast-2a", + "GroupName": "", + "Tenancy": "default" + }, + "PrivateDnsName": "ip-172-28-6-198.ap-southeast-2.compute.internal", + "PrivateIpAddress": "172.23.6.118", + "ProductCodes": [], + "PublicDnsName": "ec2-13-211-66-239.ap-southeast-2.compute.amazonaws.com", + "PublicIpAddress": "13.201.61.239", + "State": { + "Code": 16, + "Name": "running" + }, + "StateTransitionReason": "", + "SubnetId": "subnet-04bd57fc7a6389db9", + "VpcId": "vpc-0281ef7ef92c05654", + "Architecture": "x86_64", + "BlockDeviceMappings": [ + { + "DeviceName": "/dev/sda1", + "Ebs": { + "AttachTime": { + "__class__": "datetime", + "year": 2022, + "month": 8, + "day": 16, + "hour": 0, + "minute": 33, + "second": 47, + "microsecond": 0 + }, + "DeleteOnTermination": true, + "Status": "attached", + "VolumeId": "vol-0726425acd87e3931" + } + } + ], + "ClientToken": "", + "EbsOptimized": true, + "EnaSupport": true, + "Hypervisor": "xen", + "IamInstanceProfile": { + "Arn": "arn:aws:iam::644160558196:instance-profile/AmazonSSMRoleForInstancesQuickSetup", + "Id": "AIPA5AT757NYGUJQCF4TO" + }, + "NetworkInterfaces": [ + { + "Association": { + "IpOwnerId": "amazon", + "PublicDnsName": "ec2-13-211-66-239.ap-southeast-2.compute.amazonaws.com", + "PublicIp": "13.211.66.239" + }, + "Attachment": { + "AttachTime": { + "__class__": "datetime", + "year": 2022, + "month": 8, + "day": 16, + "hour": 0, + "minute": 33, + "second": 46, + "microsecond": 0 + }, + "AttachmentId": "eni-attach-0e996903398267ee8", + "DeleteOnTermination": true, + "DeviceIndex": 0, + "Status": "attached", + "NetworkCardIndex": 0 + }, + "Description": "", + "Groups": [ + { + "GroupName": "nct-jeevse", + "GroupId": "sg-073f0d531e7f134f9" + } + ], + "Ipv6Addresses": [], + "MacAddress": "02:cb:5b:3b:65:18", + "NetworkInterfaceId": "eni-068c53c17fa2ca5db", + "OwnerId": "644160558196", + "PrivateDnsName": "ip-172-28-6-198.ap-southeast-2.compute.internal", + "PrivateIpAddress": "172.28.6.198", + "PrivateIpAddresses": [ + { + "Association": { + "IpOwnerId": "amazon", + "PublicDnsName": "ec2-13-211-66-239.ap-southeast-2.compute.amazonaws.com", + "PublicIp": "13.211.66.239" + }, + "Primary": true, + "PrivateDnsName": "ip-172-28-6-198.ap-southeast-2.compute.internal", + "PrivateIpAddress": "172.28.6.198" + } + ], + "SourceDestCheck": true, + "Status": "in-use", + "SubnetId": "subnet-04bd57fc7a6389db9", + "VpcId": "vpc-0281ef7ef92c05654", + "InterfaceType": "interface" + } + ], + "RootDeviceName": "/dev/sda1", + "RootDeviceType": "ebs", + "SecurityGroups": [ + { + "GroupName": "nct-jeevse", + "GroupId": "sg-073f0d531e7f134f9" + } + ], + "SourceDestCheck": true, + "Tags": [], + "VirtualizationType": "hvm", + "CpuOptions": { + "CoreCount": 1, + "ThreadsPerCore": 2 + }, + "CapacityReservationSpecification": { + "CapacityReservationPreference": "open" + }, + "HibernationOptions": { + "Configured": false + }, + "MetadataOptions": { + "State": "applied", + "HttpTokens": "optional", + "HttpPutResponseHopLimit": 1, + "HttpEndpoint": "enabled", + "HttpProtocolIpv6": "disabled", + "InstanceMetadataTags": "disabled" + }, + "EnclaveOptions": { + "Enabled": false + }, + "PlatformDetails": "Linux/UNIX", + "UsageOperation": "RunInstances", + "UsageOperationUpdateTime": { + "__class__": "datetime", + "year": 2022, + "month": 8, + "day": 16, + "hour": 0, + "minute": 33, + "second": 46, + "microsecond": 0 + }, + "PrivateDnsNameOptions": { + "HostnameType": "ip-name", + "EnableResourceNameDnsARecord": false, + "EnableResourceNameDnsAAAARecord": false + }, + "MaintenanceOptions": { + "AutoRecovery": "default" + } + } + ], + "OwnerId": "6441658196", + "ReservationId": "r-03a7c319fe5646c39" + }, + { + "Groups": [], + "Instances": [ + { + "AmiLaunchIndex": 0, + "ImageId": "ami-00fcd54ac6970a3", + "InstanceId": "i-0b05ca2c3121136", + "InstanceType": "t3.medium", + "LaunchTime": { + "__class__": "datetime", + "year": 2022, + "month": 9, + "day": 21, + "hour": 11, + "minute": 22, + "second": 43, + "microsecond": 0 + }, + "Monitoring": { + "State": "enabled" + }, + "Placement": { + "AvailabilityZone": "ap-southeast-2b", + "GroupName": "", + "Tenancy": "default" + }, + "PrivateDnsName": "ip-172-18-153-8.ap-southeast-2.compute.internal", + "PrivateIpAddress": "172.18.13.8", + "ProductCodes": [], + "PublicDnsName": "", + "State": { + "Code": 16, + "Name": "running" + }, + "StateTransitionReason": "", + "SubnetId": "subnet-012ba337a6326388", + "VpcId": "vpc-0e27c47dd1f9c4fb9", + "Architecture": "x86_64", + "BlockDeviceMappings": [ + { + "DeviceName": "/dev/xvda", + "Ebs": { + "AttachTime": { + "__class__": "datetime", + "year": 2022, + "month": 9, + "day": 21, + "hour": 11, + "minute": 22, + "second": 43, + "microsecond": 0 + }, + "DeleteOnTermination": true, + "Status": "attached", + "VolumeId": "vol-0a80eca485754d137" + } + }, + { + "DeviceName": "/dev/xvdbr", + "Ebs": { + "AttachTime": { + "__class__": "datetime", + "year": 2022, + "month": 9, + "day": 21, + "hour": 11, + "minute": 24, + "second": 5, + "microsecond": 0 + }, + "DeleteOnTermination": false, + "Status": "attached", + "VolumeId": "vol-02683f4a30fa41ef0" + } + }, + { + "DeviceName": "/dev/xvdbx", + "Ebs": { + "AttachTime": { + "__class__": "datetime", + "year": 2022, + "month": 9, + "day": 21, + "hour": 11, + "minute": 24, + "second": 5, + "microsecond": 0 + }, + "DeleteOnTermination": false, + "Status": "attached", + "VolumeId": "vol-0feaeb2a2bab6fd69" + } + } + ], + "ClientToken": "fleet-a33c6d94-bc8c-c4af-0c30-8b08c7a344f9-0", + "EbsOptimized": false, + "EnaSupport": true, + "Hypervisor": "xen", + "IamInstanceProfile": { + "Arn": "arn:aws:iam::644160558196:instance-profile/eks-90c1a09b-9d3d-97ef-9fe6-90ff47c39cbc", + "Id": "AIPA5AT757NYJSCJ6HOML" + }, + "NetworkInterfaces": [ + { + "Attachment": { + "AttachTime": { + "__class__": "datetime", + "year": 2022, + "month": 9, + "day": 21, + "hour": 11, + "minute": 24, + "second": 11, + "microsecond": 0 + }, + "AttachmentId": "eni-attach-066a79718da83efdf", + "DeleteOnTermination": true, + "DeviceIndex": 2, + "Status": "attached", + "NetworkCardIndex": 0 + }, + "Description": "aws-K8S-i-0b05ca2c3f121136", + "Groups": [ + { + "GroupName": "nct-nie-eks-dev-node-20220915124154233900000002", + "GroupId": "sg-0d5d2011974988a14" + }, + { + "GroupName": "ng1-eks-node-group-20220915124154235600000004", + "GroupId": "sg-0881320f1bab69d87" + } + ], + "Ipv6Addresses": [], + "MacAddress": "06:b4:cb:70:20:44", + "NetworkInterfaceId": "eni-0a085d9afea280f36", + "OwnerId": "644160558196", + "PrivateDnsName": "ip-172-18-154-200.ap-southeast-2.compute.internal", + "PrivateIpAddress": "172.18.154.200", + "PrivateIpAddresses": [], + "SourceDestCheck": true, + "Status": "in-use", + "SubnetId": "subnet-012ba337a6a326388", + "VpcId": "vpc-0e27c47dd1f9c4fb9", + "InterfaceType": "interface" + }, + { + "Attachment": { + "AttachTime": { + "__class__": "datetime", + "year": 2022, + "month": 9, + "day": 21, + "hour": 11, + "minute": 24, + "second": 0, + "microsecond": 0 + }, + "AttachmentId": "eni-attach-09b535de87a0c75a5", + "DeleteOnTermination": true, + "DeviceIndex": 1, + "Status": "attached", + "NetworkCardIndex": 0 + }, + "Description": "aws-K8S-i-0b05ca2c3fd121136", + "Groups": [ + { + "GroupName": "nct-nie-eks-dev-node-20220915124154233900000002", + "GroupId": "sg-0d5d2011974988a14" + }, + { + "GroupName": "ng1-eks-node-group-20220915124154235600000004", + "GroupId": "sg-0881320f1bab69d87" + } + ], + "Ipv6Addresses": [], + "MacAddress": "06:ac:ad:a4:ec:fe", + "NetworkInterfaceId": "eni-08a0ca11769cdd75e", + "OwnerId": "644160558196", + "PrivateDnsName": "ip-172-18-152-216.ap-southeast-2.compute.internal", + "PrivateIpAddress": "172.18.152.216", + "PrivateIpAddresses": [], + "SourceDestCheck": true, + "Status": "in-use", + "SubnetId": "subnet-012ba337a6a326388", + "VpcId": "vpc-0e27c47dd1f9c4fb9", + "InterfaceType": "interface" + }, + { + "Attachment": { + "AttachTime": { + "__class__": "datetime", + "year": 2022, + "month": 9, + "day": 21, + "hour": 11, + "minute": 22, + "second": 43, + "microsecond": 0 + }, + "AttachmentId": "eni-attach-0d122a8e7a0d55761", + "DeleteOnTermination": true, + "DeviceIndex": 0, + "Status": "attached", + "NetworkCardIndex": 0 + }, + "Description": "", + "Groups": [ + { + "GroupName": "nct-nie-eks-dev-node-20220915124154233900000002", + "GroupId": "sg-0d5d2011974988a14" + }, + { + "GroupName": "ng1-eks-node-group-20220915124154235600000004", + "GroupId": "sg-0881320f1bab69d87" + } + ], + "Ipv6Addresses": [], + "MacAddress": "06:36:b1:c8:6a:9e", + "NetworkInterfaceId": "eni-0e90e3fc4951e5973", + "OwnerId": "644160558196", + "PrivateDnsName": "ip-172-18-153-8.ap-southeast-2.compute.internal", + "PrivateIpAddress": "172.18.153.8", + "PrivateIpAddresses": [], + "SourceDestCheck": true, + "Status": "in-use", + "SubnetId": "subnet-012ba337a6a326388", + "VpcId": "vpc-0e27c47dd1f9c4fb9", + "InterfaceType": "interface" + } + ], + "RootDeviceName": "/dev/xvda", + "RootDeviceType": "ebs", + "SecurityGroups": [ + { + "GroupName": "nct-nie-eks-dev-node-20220915124154233900000002", + "GroupId": "sg-0d5d2011974988a14" + }, + { + "GroupName": "ng1-eks-node-group-20220915124154235600000004", + "GroupId": "sg-0881320f1bab69d87" + } + ], + "SourceDestCheck": true, + "Tags": [], + "VirtualizationType": "hvm", + "CpuOptions": { + "CoreCount": 1, + "ThreadsPerCore": 2 + }, + "CapacityReservationSpecification": { + "CapacityReservationPreference": "open" + }, + "HibernationOptions": { + "Configured": false + }, + "MetadataOptions": { + "State": "applied", + "HttpTokens": "required", + "HttpPutResponseHopLimit": 2, + "HttpEndpoint": "enabled", + "HttpProtocolIpv6": "disabled", + "InstanceMetadataTags": "disabled" + }, + "EnclaveOptions": { + "Enabled": false + }, + "PlatformDetails": "Linux/UNIX", + "UsageOperation": "RunInstances", + "UsageOperationUpdateTime": { + "__class__": "datetime", + "year": 2022, + "month": 9, + "day": 21, + "hour": 11, + "minute": 22, + "second": 43, + "microsecond": 0 + }, + "PrivateDnsNameOptions": { + "HostnameType": "ip-name", + "EnableResourceNameDnsARecord": false, + "EnableResourceNameDnsAAAARecord": false + }, + "MaintenanceOptions": { + "AutoRecovery": "default" + } + } + ], + "OwnerId": "644160558196", + "RequesterId": "644160558196", + "ReservationId": "r-06a7d998e80c96b5f" + } + ], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/test_ec2.py b/tests/test_ec2.py index 18af3b301b3..c7e61ba1386 100644 --- a/tests/test_ec2.py +++ b/tests/test_ec2.py @@ -5,6 +5,7 @@ import time import datetime +from unittest.mock import patch from dateutil import tz import jmespath from mock import mock @@ -129,6 +130,35 @@ def test_ec2_stop_protection_filter_permissions(test): ) +def test_ec2_cost(test): + aws_region = 'ap-southeast-2' + session_factory = test.replay_flight_data('ec2_cost', region=aws_region) + policy = test.load_policy( + { + "name": "ec2-cost", + "resource": "ec2", + "filters": [{ + "type": "cost", + "op": "greater-than", + "value": 5, + "quantity": 730, + }] + }, + session_factory=session_factory, + config={'region': aws_region}, + ) + with patch("c7n.filters.cost.Cost.get_infracost") as infracost: + infracost.side_effect = [ + {'USD': 0.0066, 'description': '$0.0066 per On Demand Linux t3.nano Instance Hour'}, + {'USD': 0.0528, 'description': '$0.0528 per On Demand Linux t3.medium Instance Hour'}, + ] + resources = policy.run() + test.assertEqual(len(resources), 1) + assert resources[0]["c7n:Cost"].items() >= { + 'USD': 38.544, 'description': '$0.0528 per On Demand Linux t3.medium Instance Hour' + }.items() + + @pytest.mark.parametrize( 'botocore_version', ['1.26.6', '1.25.8', '0.27.27']