diff --git a/Makefile b/Makefile index 17fb0fa65ec..6cc32b733bc 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ SELF_MAKE := $(lastword $(MAKEFILE_LIST)) -PKG_SET = tools/c7n_tencent tools/c7n_huawei tools/c7n_aliyun tools/c7n_gcp tools/c7n_azure tools/c7n_kube tools/c7n_mailer tools/c7n_logexporter tools/c7n_policystream tools/c7n_trailcreator tools/c7n_org tools/c7n_sphinxext +PKG_SET = tools/c7n_tencent tools/c7n_huawei tools/c7n_aliyun tools/c7n_gcp tools/c7n_azure tools/c7n_kube tools/c7n_openstack tools/c7n_vsphere tools/c7n_mailer tools/c7n_logexporter tools/c7n_policystream tools/c7n_trailcreator tools/c7n_org tools/c7n_sphinxext install: python3 -m venv . diff --git a/build.sh b/build.sh index 37a4f94521c..548f92f6cc6 100644 --- a/build.sh +++ b/build.sh @@ -1,4 +1,4 @@ echo "构建custodian镜像 ..." -docker build -t registry.cn-qingdao.aliyuncs.com/x-lab/riskscanner/custodian:master . -docker push registry.cn-qingdao.aliyuncs.com/x-lab/riskscanner/custodian:master +docker build -t registry.cn-qingdao.aliyuncs.com/x-lab/custodian:1.4 . +docker push registry.cn-qingdao.aliyuncs.com/x-lab/custodian:1.4 diff --git a/c7n/commands.py b/c7n/commands.py index 77c82b9364d..9f0c190c9d3 100644 --- a/c7n/commands.py +++ b/c7n/commands.py @@ -553,4 +553,14 @@ def version_cmd(options): packages.append('c7n_azure') if 'k8s' in found: packages.append('c7n_kube') + if 'aliyun' in found: + packages.append('c7n_aliyun') + if 'tencent' in found: + packages.append('c7n_tencent') + if 'huawei' in found: + packages.append('c7n_huawei') + if 'openstack' in found: + packages.append('c7n_openstack') + if 'vsphere' in found: + packages.append('c7n_vsphere') print(generate_requirements(packages)) diff --git a/c7n/resources/__init__.py b/c7n/resources/__init__.py index 2338e942354..e67abc55360 100644 --- a/c7n/resources/__init__.py +++ b/c7n/resources/__init__.py @@ -49,7 +49,7 @@ def should_load_provider(name, provider_types): return False -PROVIDER_NAMES = ('aws', 'azure', 'gcp', 'k8s', 'aliyun', 'huawei', 'tencent') +PROVIDER_NAMES = ('aws', 'azure', 'gcp', 'k8s', 'aliyun', 'huawei', 'tencent', 'openstack', 'vsphere') def load_available(resources=True): @@ -73,7 +73,6 @@ def load_available(resources=True): def load_providers(provider_types): global LOADED - # Even though we're lazy loading resources we still need to import # those that are making available generic filters/actions if should_load_provider('aws', provider_types): @@ -108,4 +107,11 @@ def load_providers(provider_types): from c7n_tencent.entry import initialize_tencent initialize_tencent() + if should_load_provider('openstack', provider_types): + from c7n_openstack.entry import initialize_openstack + initialize_openstack() + + if should_load_provider('vsphere', provider_types): + from c7n_vsphere.entry import initialize_vsphere + initialize_vsphere() LOADED.update(provider_types) diff --git a/docker/cli b/docker/cli index 0b5ccc4558f..871652c5820 100644 --- a/docker/cli +++ b/docker/cli @@ -1,4 +1,4 @@ -FROM registry.fit2cloud.com/fit2cloud2/fabric8-java-alpine-openjdk8-jre:py37 as build-env +FROM registry.cn-qingdao.aliyuncs.com/x-lab/fabric8-java-alpine-openjdk8-jre:py37 as build-env USER root @@ -8,7 +8,7 @@ RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories # pre-requisite distro deps, and build env setup RUN apk add --update \ curl \ - && apk add gcc g++ make libffi-dev openssl-dev libtool --no-cache \ + && apk add gcc g++ make git libffi-dev openssl-dev libxml2-dev libxslt-dev musl-dev cargo libtool --no-cache \ && python3 -m venv /usr/local \ && curl -sSL https://nginx-qa.fit2cloud.com/tools/get-poetry.py | python3 @@ -26,17 +26,23 @@ ADD tools/c7n_gcp /src/tools/c7n_gcp RUN rm -R tools/c7n_gcp/tests ADD tools/c7n_azure /src/tools/c7n_azure RUN rm -R tools/c7n_azure/tests_azure -ADD tools/c7n_kube /src/tools/c7n_kube -RUN rm -R tools/c7n_kube/tests +#ADD tools/c7n_kube /src/tools/c7n_kube +#RUN rm -R tools/c7n_kube/tests ADD tools/c7n_aliyun /src/tools/c7n_aliyun RUN rm -R tools/c7n_aliyun/test ADD tools/c7n_huawei /src/tools/c7n_huawei RUN rm -R tools/c7n_huawei/test ADD tools/c7n_tencent /src/tools/c7n_tencent RUN rm -R tools/c7n_tencent/test +ADD tools/c7n_openstack /src/tools/c7n_openstack +RUN rm -R tools/c7n_openstack/test +ADD tools/c7n_vsphere /src/tools/c7n_vsphere +RUN rm -R tools/c7n_vsphere/test # Install requested providers -ARG providers="azure gcp kube aliyun huawei tencent" +ARG providers="azure gcp aliyun huawei tencent openstack vsphere" +RUN . /usr/local/bin/activate && \ + pip install --upgrade git+https://github.com/vmware/vsphere-automation-sdk-python.git RUN . /usr/local/bin/activate && \ for pkg in $providers; do cd tools/c7n_$pkg && \ $HOME/.poetry/bin/poetry lock && \ @@ -44,13 +50,13 @@ RUN . /usr/local/bin/activate && \ RUN mkdir /output -FROM registry.fit2cloud.com/fit2cloud2/fabric8-java-alpine-openjdk8-jre:py37 +FROM registry.cn-qingdao.aliyuncs.com/x-lab/fabric8-java-alpine-openjdk8-jre:py37 COPY --from=build-env /src /src COPY --from=build-env /usr/local /usr/local COPY --from=build-env /output /output -RUN rm -Rf /var/cache/apk +RUN rm -Rf /var/cache/apk/* USER root WORKDIR /home/custodian diff --git a/docker/cli-distroless b/docker/cli-distroless index c3df08d8c48..4b462cba277 100644 --- a/docker/cli-distroless +++ b/docker/cli-distroless @@ -25,9 +25,19 @@ ADD tools/c7n_azure /src/tools/c7n_azure RUN rm -R tools/c7n_azure/tests_azure ADD tools/c7n_kube /src/tools/c7n_kube RUN rm -R tools/c7n_kube/tests +ADD tools/c7n_aliyun /src/tools/c7n_aliyun +RUN rm -R tools/c7n_aliyun/test +ADD tools/c7n_huawei /src/tools/c7n_huawei +RUN rm -R tools/c7n_huawei/test +ADD tools/c7n_tencent /src/tools/c7n_tencent +RUN rm -R tools/c7n_tencent/test +ADD tools/c7n_openstack /src/tools/c7n_openstack +RUN rm -R tools/c7n_openstack/test +ADD tools/c7n_vsphere /src/tools/c7n_vsphere +RUN rm -R tools/c7n_vsphere/test # Install requested providers -ARG providers="azure gcp kube" +ARG providers="azure gcp kube aliyun huawei tencent openstack vsphere" RUN . /usr/local/bin/activate && \ for pkg in $providers; do cd tools/c7n_$pkg && \ $HOME/.poetry/bin/poetry install && cd ../../; done diff --git a/docker/mailer b/docker/mailer index 20a221a1806..35d2b5ee695 100644 --- a/docker/mailer +++ b/docker/mailer @@ -25,9 +25,19 @@ ADD tools/c7n_azure /src/tools/c7n_azure RUN rm -R tools/c7n_azure/tests_azure ADD tools/c7n_kube /src/tools/c7n_kube RUN rm -R tools/c7n_kube/tests +ADD tools/c7n_aliyun /src/tools/c7n_aliyun +RUN rm -R tools/c7n_aliyun/test +ADD tools/c7n_huawei /src/tools/c7n_huawei +RUN rm -R tools/c7n_huawei/test +ADD tools/c7n_tencent /src/tools/c7n_tencent +RUN rm -R tools/c7n_tencent/test +ADD tools/c7n_openstack /src/tools/c7n_openstack +RUN rm -R tools/c7n_openstack/test +ADD tools/c7n_vsphere /src/tools/c7n_vsphere +RUN rm -R tools/c7n_vsphere/test # Install requested providers -ARG providers="azure gcp kube" +ARG providers="azure gcp kube aliyun huawei tencent openstack vsphere" RUN . /usr/local/bin/activate && \ for pkg in $providers; do cd tools/c7n_$pkg && \ $HOME/.poetry/bin/poetry install && cd ../../; done diff --git a/docker/mailer-distroless b/docker/mailer-distroless index b8d9881e93b..09e45496e72 100644 --- a/docker/mailer-distroless +++ b/docker/mailer-distroless @@ -25,9 +25,19 @@ ADD tools/c7n_azure /src/tools/c7n_azure RUN rm -R tools/c7n_azure/tests_azure ADD tools/c7n_kube /src/tools/c7n_kube RUN rm -R tools/c7n_kube/tests +ADD tools/c7n_aliyun /src/tools/c7n_aliyun +RUN rm -R tools/c7n_aliyun/test +ADD tools/c7n_huawei /src/tools/c7n_huawei +RUN rm -R tools/c7n_huawei/test +ADD tools/c7n_tencent /src/tools/c7n_tencent +RUN rm -R tools/c7n_tencent/test +ADD tools/c7n_openstack /src/tools/c7n_openstack +RUN rm -R tools/c7n_openstack/test +ADD tools/c7n_vsphere /src/tools/c7n_vsphere +RUN rm -R tools/c7n_vsphere/test # Install requested providers -ARG providers="azure gcp kube" +ARG providers="azure gcp kube aliyun huawei tencent openstack vsphere" RUN . /usr/local/bin/activate && \ for pkg in $providers; do cd tools/c7n_$pkg && \ $HOME/.poetry/bin/poetry install && cd ../../; done diff --git a/docker/org b/docker/org index 5651be72669..972bb653647 100644 --- a/docker/org +++ b/docker/org @@ -25,9 +25,19 @@ ADD tools/c7n_azure /src/tools/c7n_azure RUN rm -R tools/c7n_azure/tests_azure ADD tools/c7n_kube /src/tools/c7n_kube RUN rm -R tools/c7n_kube/tests +ADD tools/c7n_aliyun /src/tools/c7n_aliyun +RUN rm -R tools/c7n_aliyun/test +ADD tools/c7n_huawei /src/tools/c7n_huawei +RUN rm -R tools/c7n_huawei/test +ADD tools/c7n_tencent /src/tools/c7n_tencent +RUN rm -R tools/c7n_tencent/test +ADD tools/c7n_openstack /src/tools/c7n_openstack +RUN rm -R tools/c7n_openstack/test +ADD tools/c7n_vsphere /src/tools/c7n_vsphere +RUN rm -R tools/c7n_vsphere/test # Install requested providers -ARG providers="azure gcp kube" +ARG providers="azure gcp kube aliyun huawei tencent openstack vsphere" RUN . /usr/local/bin/activate && \ for pkg in $providers; do cd tools/c7n_$pkg && \ $HOME/.poetry/bin/poetry install && cd ../../; done diff --git a/docker/org-distroless b/docker/org-distroless index f4758489dfc..d99e7d159e2 100644 --- a/docker/org-distroless +++ b/docker/org-distroless @@ -25,9 +25,19 @@ ADD tools/c7n_azure /src/tools/c7n_azure RUN rm -R tools/c7n_azure/tests_azure ADD tools/c7n_kube /src/tools/c7n_kube RUN rm -R tools/c7n_kube/tests +ADD tools/c7n_aliyun /src/tools/c7n_aliyun +RUN rm -R tools/c7n_aliyun/test +ADD tools/c7n_huawei /src/tools/c7n_huawei +RUN rm -R tools/c7n_huawei/test +ADD tools/c7n_tencent /src/tools/c7n_tencent +RUN rm -R tools/c7n_tencent/test +ADD tools/c7n_openstack /src/tools/c7n_openstack +RUN rm -R tools/c7n_openstack/test +ADD tools/c7n_vsphere /src/tools/c7n_vsphere +RUN rm -R tools/c7n_vsphere/test # Install requested providers -ARG providers="azure gcp kube" +ARG providers="azure gcp kube aliyun huawei tencent openstack vsphere" RUN . /usr/local/bin/activate && \ for pkg in $providers; do cd tools/c7n_$pkg && \ $HOME/.poetry/bin/poetry install && cd ../../; done diff --git a/docker/policystream b/docker/policystream index c91284b1687..b2bfdc4dc74 100644 --- a/docker/policystream +++ b/docker/policystream @@ -25,9 +25,19 @@ ADD tools/c7n_azure /src/tools/c7n_azure RUN rm -R tools/c7n_azure/tests_azure ADD tools/c7n_kube /src/tools/c7n_kube RUN rm -R tools/c7n_kube/tests +ADD tools/c7n_aliyun /src/tools/c7n_aliyun +RUN rm -R tools/c7n_aliyun/test +ADD tools/c7n_huawei /src/tools/c7n_huawei +RUN rm -R tools/c7n_huawei/test +ADD tools/c7n_tencent /src/tools/c7n_tencent +RUN rm -R tools/c7n_tencent/test +ADD tools/c7n_openstack /src/tools/c7n_openstack +RUN rm -R tools/c7n_openstack/test +ADD tools/c7n_vsphere /src/tools/c7n_vsphere +RUN rm -R tools/c7n_vsphere/test # Install requested providers -ARG providers="azure gcp kube" +ARG providers="azure gcp kube aliyun huawei tencent openstack vsphere" RUN . /usr/local/bin/activate && \ for pkg in $providers; do cd tools/c7n_$pkg && \ $HOME/.poetry/bin/poetry install && cd ../../; done diff --git a/docker/policystream-distroless b/docker/policystream-distroless index 3983b8d8ccd..8edd4b47b4f 100644 --- a/docker/policystream-distroless +++ b/docker/policystream-distroless @@ -25,9 +25,19 @@ ADD tools/c7n_azure /src/tools/c7n_azure RUN rm -R tools/c7n_azure/tests_azure ADD tools/c7n_kube /src/tools/c7n_kube RUN rm -R tools/c7n_kube/tests +ADD tools/c7n_aliyun /src/tools/c7n_aliyun +RUN rm -R tools/c7n_aliyun/test +ADD tools/c7n_huawei /src/tools/c7n_huawei +RUN rm -R tools/c7n_huawei/test +ADD tools/c7n_tencent /src/tools/c7n_tencent +RUN rm -R tools/c7n_tencent/test +ADD tools/c7n_openstack /src/tools/c7n_openstack +RUN rm -R tools/c7n_openstack/test +ADD tools/c7n_vsphere /src/tools/c7n_vsphere +RUN rm -R tools/c7n_vsphere/test # Install requested providers -ARG providers="azure gcp kube" +ARG providers="azure gcp kube aliyun huawei tencent openstack vsphere" RUN . /usr/local/bin/activate && \ for pkg in $providers; do cd tools/c7n_$pkg && \ $HOME/.poetry/bin/poetry install && cd ../../; done diff --git a/requirements-dev.txt b/requirements-dev.txt index 4ca0e19a65f..e5d3f7f73a8 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -8,6 +8,10 @@ -r tools/c7n_mailer/requirements.txt -r tools/c7n_org/requirements.txt -r tools/c7n_aliyun/requirements.txt +-r tools/c7n_huawei/requirements.txt +-r tools/c7n_tencent/requirements.txt +-r tools/c7n_openstack/requirements.txt +-r tools/c7n_vsphere/requirements.txt # Setup source directories as editable/development distributions -e . @@ -23,6 +27,14 @@ -e tools/c7n_org # Local package required for c7n_aliyun tests -e tools/c7n_aliyun +# Local package required for c7n_huawei tests +-e tools/c7n_huawei +# Local package required for c7n_tencent tests +-e tools/c7n_tencent +# Local package required for c7n_vsphere tests +-e tools/c7n_openstack +# Local package required for c7n_vsphere tests +-e tools/c7n_vsphere # we don't export dev requirements of subpackages (due to editable/distribution above) # so explicitly list them here.. diff --git a/tests/test_docker.py b/tests/test_docker.py index 64847490cbb..33547300db2 100644 --- a/tests/test_docker.py +++ b/tests/test_docker.py @@ -162,7 +162,7 @@ def test_image_metadata(image_name): def test_cli_providers_available(): providers = os.environ.get("CUSTODIAN_PROVIDERS", None) if providers is None: - providers = {"aws", "azure", "gcp", "k8s"} + providers = {"aws", "azure", "gcp", "k8s", "aliyun", "huawei", "tencent", "openstack", "vsphere"} elif providers == "": providers = {"aws"} else: diff --git a/tests/test_packaging.py b/tests/test_packaging.py index cae6b2213f7..a1e5c2f2ed0 100644 --- a/tests/test_packaging.py +++ b/tests/test_packaging.py @@ -19,7 +19,8 @@ @pytest.mark.parametrize("package", [ "c7n", "c7n_azure", "c7n_gcp", "c7n_kube", "c7n_org", "c7n_mailer", "policystream", "c7n_trailcreator", - "c7n_logexporter", "c7n_sphinxext"]) + "c7n_logexporter", "c7n_sphinxext", "c7n_aliyun", + "c7n_huawei", "c7n_tencent", "c7n_vsphere", "c7n_vsphere"]) def test_package_metadata(package): try: m = __import__(package) diff --git a/tools/c7n_aliyun/.gitignore b/tools/c7n_aliyun/.gitignore index 1e7bb232c6a..36ec3a3b659 100644 --- a/tools/c7n_aliyun/.gitignore +++ b/tools/c7n_aliyun/.gitignore @@ -2,3 +2,4 @@ *py~ __pycache__ *.egg-info +aliyun.py diff --git a/tools/c7n_aliyun/c7n_aliyun/client.py b/tools/c7n_aliyun/c7n_aliyun/client.py index d743cdc8b47..d39bfc77dca 100644 --- a/tools/c7n_aliyun/c7n_aliyun/client.py +++ b/tools/c7n_aliyun/c7n_aliyun/client.py @@ -61,26 +61,26 @@ def get_oss_region(self, regionId): return region REGION_ENDPOINT = { - 'cn-hangzhou': 'oss-cn-hangzhou.aliyuncs.com', - 'cn-shanghai': 'oss-cn-shanghai.aliyuncs.com', - 'cn-qingdao': 'oss-cn-qingdao.aliyuncs.com', - 'cn-beijing': 'oss-cn-beijing.aliyuncs.com', - 'cn-zhangjiakou': 'oss-cn-zhangjiakou.aliyuncs.com', - 'cn-huhehaote': 'oss-cn-huhehaote.aliyuncs.com', - 'cn-shenzhen': 'oss-cn-shenzhen.aliyuncs.com', - 'cn-hongkong': 'oss-cn-hongkong.aliyuncs.com', - 'us-west-1': 'oss-us-west-1.aliyuncs.com', - 'us-east-1': 'oss-us-east-1.aliyuncs.com', - 'ap-southeast-1': 'oss-ap-southeast-1.aliyuncs.com', - 'ap-southeast-2': 'oss-ap-southeast-2.aliyuncs.com', - 'ap-southeast-3': 'oss-ap-southeast-3.aliyuncs.com', - 'ap-southeast-5': 'oss-ap-southeast-5.aliyuncs.com', - 'ap-northeast-1': 'oss-ap-northeast-1.aliyuncs.com', - 'ap-south-1': 'oss-ap-south-1.aliyuncs.com', - 'eu-central-1': 'oss-eu-central-1.aliyuncs.com', - 'eu-west-1': 'oss-eu-west-1.aliyuncs.com', - 'me-east-1': 'oss-me-east-1.aliyuncs.com', - 'cn-wulanchabu': 'oss-cn-wulanchabu.aliyuncs.com', - 'cn-heyuan': 'oss-cn-heyuan.aliyuncs.com', - 'cn-chengdu': 'oss-cn-chengdu.aliyuncs.com' - } \ No newline at end of file + 'cn-hangzhou': 'oss-cn-hangzhou.aliyuncs.com', + 'cn-shanghai': 'oss-cn-shanghai.aliyuncs.com', + 'cn-qingdao': 'oss-cn-qingdao.aliyuncs.com', + 'cn-beijing': 'oss-cn-beijing.aliyuncs.com', + 'cn-zhangjiakou': 'oss-cn-zhangjiakou.aliyuncs.com', + 'cn-huhehaote': 'oss-cn-huhehaote.aliyuncs.com', + 'cn-shenzhen': 'oss-cn-shenzhen.aliyuncs.com', + 'cn-hongkong': 'oss-cn-hongkong.aliyuncs.com', + 'us-west-1': 'oss-us-west-1.aliyuncs.com', + 'us-east-1': 'oss-us-east-1.aliyuncs.com', + 'ap-southeast-1': 'oss-ap-southeast-1.aliyuncs.com', + 'ap-southeast-2': 'oss-ap-southeast-2.aliyuncs.com', + 'ap-southeast-3': 'oss-ap-southeast-3.aliyuncs.com', + 'ap-southeast-5': 'oss-ap-southeast-5.aliyuncs.com', + 'ap-northeast-1': 'oss-ap-northeast-1.aliyuncs.com', + 'ap-south-1': 'oss-ap-south-1.aliyuncs.com', + 'eu-central-1': 'oss-eu-central-1.aliyuncs.com', + 'eu-west-1': 'oss-eu-west-1.aliyuncs.com', + 'me-east-1': 'oss-me-east-1.aliyuncs.com', + 'cn-wulanchabu': 'oss-cn-wulanchabu.aliyuncs.com', + 'cn-heyuan': 'oss-cn-heyuan.aliyuncs.com', + 'cn-chengdu': 'oss-cn-chengdu.aliyuncs.com' +} \ No newline at end of file diff --git a/tools/c7n_aliyun/c7n_aliyun/filters/filter.py b/tools/c7n_aliyun/c7n_aliyun/filters/filter.py index 1456589336c..3334762cae5 100644 --- a/tools/c7n_aliyun/c7n_aliyun/filters/filter.py +++ b/tools/c7n_aliyun/c7n_aliyun/filters/filter.py @@ -1,6 +1,5 @@ import datetime import json -import logging from concurrent.futures import as_completed from datetime import timedelta @@ -308,7 +307,7 @@ def process(self, resources, event=None): fattrs = list(sorted(self.perm_attrs.intersection(self.data.keys()))) self.ports = 'Ports' in self.data and self.data['Ports'] or () self.only_ports = ( - 'OnlyPorts' in self.data and self.data['OnlyPorts'] or ()) + 'OnlyPorts' in self.data and self.data['OnlyPorts'] or ()) for f in fattrs: fv = self.data.get(f) if isinstance(fv, dict): @@ -462,13 +461,15 @@ class MetricsFilter(Filter): """Supports metrics filters on resources. .. code-block:: yaml - - name: ecs-underutilized - resource: ecs + policies: + - name: aliyun-ecs-underutilized + resource: aliyun.ecs filters: - type: metrics name: CPUUtilization - days: 4 + days: 7 period: 86400 + statistics: Average value: 30 op: less-than @@ -483,8 +484,9 @@ class MetricsFilter(Filter): .. code-block:: yaml - - name: elb-low-request-count - resource: elb + policies: + - name: aliyun-elb-low-request-count + resource: aliyun.elb filters: - type: metrics name: RequestCount @@ -575,7 +577,12 @@ def process(self, resources, event=None): return matched def get_dimensions(self, resource): - return [{self.model.dimension: resource[self.model.dimension]}] + start = self.model.dimension[:1] + end = self.model.dimension[1:] + key = start.lower() + end + if key!='instanceId': + key = 'instanceId' + return [{key: resource[self.model.dimension]}] def get_user_dimensions(self): dims = [] @@ -625,7 +632,7 @@ def process_resource_set(self, resource_set): collected_metrics[key].append({'timestamp': self.start, self.statistics: self.data['missing-value'], 'c7n_aliyun:detail': 'Fill value for missing data'}) else: collected_metrics[key].append({'startTime': self.start, 'endTime': self.end, self.statistics: 0, - 'detail': 'The read and write monitoring value within the specified time range is 0 (or no data)'}) + 'detail': 'The read and write monitoring value within the specified time range is 0 (or no data)'}) if self.model.service == "disk": for data in collected_metrics[key]: if self.op(data[data_usage], self.value): diff --git a/tools/c7n_aliyun/c7n_aliyun/query.py b/tools/c7n_aliyun/c7n_aliyun/query.py index 8e68cd3a9ad..5e38cd25c8e 100644 --- a/tools/c7n_aliyun/c7n_aliyun/query.py +++ b/tools/c7n_aliyun/c7n_aliyun/query.py @@ -39,27 +39,66 @@ def filter(self, resource_manager, **params): if extra_args: params.update(extra_args) + pageNumber = 1 + pageSize = 100 + res = [] + buckets = [] if m.service == 'oss': - request = resource_manager.get_request() - result = client.list_buckets() - buckets = [] - for b in result.buckets: - if request in b.__dict__['location']: - b.__dict__['F2CId'] = b.__dict__[m.id] - buckets.append(b.__dict__) + marker_param = "" + while 1 <= pageNumber: + request = resource_manager.get_request() + result = client.list_buckets(marker=marker_param) + for b in result.buckets: + if request in b.__dict__['location']: + b.__dict__['F2CId'] = b.__dict__[m.id] + buckets.append(b.__dict__) + if len(result.buckets) == pageSize: + marker_param = result.next_marker + else: + return buckets return buckets - else: - request = resource_manager.get_request() - if request: + elif m.service == 'ram': + marker_param = None + while 1 <= pageNumber: + request = resource_manager.get_request() + request.set_accept_format('json') + if marker_param is not None: + request.set_Marker(marker_param) result = client.do_action_with_exception(request) false = "false" true = "true" - res = jmespath.search(path, eval(result)) - for data in res: + response = jmespath.search(path, eval(result)) + for data in response: data['F2CId'] = data[m.id] - return res - else: - return None + res = res + response + if len(response) == pageSize: + marker_param = eval(result).get('Marker', None) + else: + return res + return res + else: + + while 1 <= pageNumber: + request = resource_manager.get_request() + request.set_accept_format('json') + request.set_PageSize(pageSize) + request.set_PageNumber(pageNumber) + if request: + result = client.do_action_with_exception(request) + false = "false" + true = "true" + response = jmespath.search(path, eval(result)) + for data in response: + data['F2CId'] = data[m.id] + else: + response = [] + res = res + response + if len(response) == pageSize: + pageNumber += 1 + else: + return res + return res + def _invoke_client_enum(self, client, request, params, path): result = client.do_action_with_exception(request) diff --git a/tools/c7n_aliyun/c7n_aliyun/resources/disk.py b/tools/c7n_aliyun/c7n_aliyun/resources/disk.py index 9a81d2eba42..d5f5f1e04dc 100644 --- a/tools/c7n_aliyun/c7n_aliyun/resources/disk.py +++ b/tools/c7n_aliyun/c7n_aliyun/resources/disk.py @@ -58,7 +58,8 @@ class AliyunDiskFilter(AliyunDiskFilter): **{'value': {'type': 'boolean'}}) def get_request(self, i): - if i['Status'] != type: + encrypted = self.data.get('type', '') + if i.get('Encrypted', '').lower() != str(encrypted).lower(): return False return i @@ -83,8 +84,7 @@ class AliyunDiskFilter(AliyunDiskFilter): schema = type_schema('Available') def get_request(self, i): - encrypted = self.data['value'] - if i['Encrypted'].lower() != str(encrypted).lower(): + if i.get('Status', '') != 'Available': return False return i diff --git a/tools/c7n_aliyun/c7n_aliyun/resources/ecs.py b/tools/c7n_aliyun/c7n_aliyun/resources/ecs.py index 53311a1e534..2407cb006a5 100644 --- a/tools/c7n_aliyun/c7n_aliyun/resources/ecs.py +++ b/tools/c7n_aliyun/c7n_aliyun/resources/ecs.py @@ -106,6 +106,7 @@ def get_request(self, r): request.set_MetricName(self.metric) return request + @Ecs.filter_registry.register('instance-network-type') class InstanceNetworkTypeEcsFilter(AliyunEcsFilter): """Filters @@ -132,3 +133,86 @@ def get_request(self, i): if self.data['value'] == i['InstanceNetworkType']: return False return i + +@Ecs.filter_registry.register('vpc-type') +class VpcTypeEcsFilter(AliyunEcsFilter): + """Filters + :Example: + .. code-block:: yaml + + policies: + # 检测您账号下的Ecs实例指定属于哪些VPC, 属于则合规,不属于则"不合规"。 + - name: aliyun-ecs-vpc-type + resource: aliyun.ecs + filters: + - type: vpc-type + vpcIds: ["111", "222"] + """ + schema = type_schema( + 'vpc-type', + **{'vpcIds': {'type': 'array', 'items': {'type': 'string'}}}) + + def get_request(self, i): + vpcId = i['VpcAttributes']['VpcId'] + if vpcId in self.data['vpcIds']: + return False + return i + +@Ecs.filter_registry.register('stopped') +class AliyunEcsFilter(AliyunEcsFilter): + """Filters + + :Example: + + .. code-block:: yaml + + policies: + - name: aliyun-ecs + resource: aliyun.ecs + filters: + - type: stopped + """ + # 实例状态。取值范围: + # + # Pending:创建中 + # Running:运行中 + # Starting:启动中 + # Stopping:停止中 + # Stopped:已停止 + schema = type_schema('Stopped') + + def get_request(self, i): + status = i.get('Status', '') + if 'Stopped' != status: + return False + return i + +@Ecs.filter_registry.register('instance-age') +class EcsAgeFilter(AliyunAgeFilter): + """Filters instances based on their age (in days) + + :Example: + + .. code-block:: yaml + + policies: + - name: ecs-30-days-plus + resource: aliyun.ecs + filters: + - type: instance-age + op: ge + minutes: 1 + """ + + date_attribute = "LaunchTime" + ebs_key_func = operator.itemgetter('AttachTime') + + schema = type_schema( + 'instance-age', + op={'$ref': '#/definitions/filters_common/comparison_operators'}, + days={'type': 'number'}, + hours={'type': 'number'}, + minutes={'type': 'number'}) + + def get_resource_date(self, i): + return i['CreationTime'] \ No newline at end of file diff --git a/tools/c7n_aliyun/c7n_aliyun/resources/mongodb.py b/tools/c7n_aliyun/c7n_aliyun/resources/mongodb.py index 36742dbe16b..2a87ec8704f 100644 --- a/tools/c7n_aliyun/c7n_aliyun/resources/mongodb.py +++ b/tools/c7n_aliyun/c7n_aliyun/resources/mongodb.py @@ -14,12 +14,8 @@ import logging import os -import jmespath -from aliyunsdkr_kvstore.request.v20150101.DescribeInstancesRequest import DescribeInstancesRequest -from aliyunsdkdds.request.v20151201.DescribeSecurityIpsRequest import DescribeSecurityIpsRequest -from aliyunsdkdds.request.v20151201.DescribeSecurityGroupConfigurationRequest import DescribeSecurityGroupConfigurationRequest - - +from aliyunsdkdds.request.v20151201.DescribeDBInstancesRequest import DescribeDBInstancesRequest +from aliyunsdkdds.request.v20151201.DescribeShardingNetworkAddressRequest import DescribeShardingNetworkAddressRequest from c7n.utils import type_schema from c7n_aliyun.filters.filter import AliyunRdsFilter @@ -37,11 +33,11 @@ class MongoDB(QueryResourceManager): class resource_type(TypeInfo): service = 'mongodb' - enum_spec = (None, 'Instances.KVStoreInstance', None) + enum_spec = (None, 'DBInstances.DBInstance', None) id = 'InstanceId' def get_request(self): - return DescribeInstancesRequest() + return DescribeDBInstancesRequest() @MongoDB.filter_registry.register('network-type') class NetworkTypeMongoDBFilter(AliyunRdsFilter): @@ -89,23 +85,28 @@ class InternetAccessMongoDBFilter(AliyunRdsFilter): **{'value': {'type': 'boolean'}}) def get_request(self, i): - request1 = DescribeSecurityIpsRequest() - request1.set_accept_format('json') - response1 = Session.client(self, service).do_action_with_exception(request1) - string1 = str(response1, encoding="utf-8").replace("false", "False") - SecurityIpGroups = jmespath.search('SecurityIpGroups.SecurityIpGroup', eval(string1)) - request2 = DescribeSecurityGroupConfigurationRequest() - request2.set_accept_format('json') - response2 = Session.client(self, service).do_action_with_exception(request2) - string2 = str(response2, encoding="utf-8").replace("false", "False") - - RdsEcsSecurityGroupRel = jmespath.search('Items.RdsEcsSecurityGroupRel', eval(string2)) - if self.data['value']: - if len(SecurityIpGroups) == 0 and len(RdsEcsSecurityGroupRel) == 0: - return False + request = DescribeShardingNetworkAddressRequest() + request.set_accept_format('json') + + request.set_DBInstanceId(i.get('DBInstanceId', '')) + response = Session.client(self, service).do_action_with_exception(request) + string = str(response, encoding="utf-8").replace("false", "False").replace("true", "True") + data = eval(string) + CompatibleConnections = data.get('CompatibleConnections', {}).get('CompatibleConnection', []) + NetworkAddresses = data.get('NetworkAddresses', {}).get('NetworkAddress', []) + if self.data.get('value', '') == True: + for c in CompatibleConnections: + if c.get('NetworkType', '') == 'Public': + return i + for n in NetworkAddresses: + if n.get('NetworkType', '') == 'Public': + return i else: - return False - i['SecurityIpGroups'] = SecurityIpGroups - i['RdsEcsSecurityGroupRel'] = RdsEcsSecurityGroupRel - return i \ No newline at end of file + for c in CompatibleConnections: + if c.get('NetworkType', '') != 'Public': + return i + for n in NetworkAddresses: + if n.get('NetworkType', '') != 'Public': + return i + return None \ No newline at end of file diff --git a/tools/c7n_aliyun/c7n_aliyun/resources/oss.py b/tools/c7n_aliyun/c7n_aliyun/resources/oss.py index e6cc3f0e74e..ba869296069 100644 --- a/tools/c7n_aliyun/c7n_aliyun/resources/oss.py +++ b/tools/c7n_aliyun/c7n_aliyun/resources/oss.py @@ -179,7 +179,7 @@ class BucketRefererOssFilter(AliyunOssFilter): def get_request(self, i): try: auth = oss2.Auth(accessKeyId, accessSecret) - bucket = oss2.Bucket(auth, i['extranet_endpoint'], i['name']) + bucket = oss2.Bucket(auth, i.get('extranet_endpoint', ''), i.get('name', '')) result = bucket.get_bucket_referer() if self.data['value']: if result.referers: diff --git a/tools/c7n_aliyun/c7n_aliyun/resources/polardb.py b/tools/c7n_aliyun/c7n_aliyun/resources/polardb.py index 5c6eaccfe94..32cb855344a 100644 --- a/tools/c7n_aliyun/c7n_aliyun/resources/polardb.py +++ b/tools/c7n_aliyun/c7n_aliyun/resources/polardb.py @@ -40,6 +40,30 @@ class resource_type(TypeInfo): def get_request(self): return DescribeDBClustersRequest() +@PolarDB.filter_registry.register('vpc-type') +class VpcTypePolarDBFilter(AliyunRdsFilter): + """Filters + :Example: + .. code-block:: yaml + + policies: + # 检测您账号下的Polardb实例指定属于哪些VPC, 属于则合规,不属于则"不合规"。 + - name: aliyun-polardb-vpc-type + resource: aliyun.polardb + filters: + - type: vpc-type + vpcIds: ["111", "222"] + """ + schema = type_schema( + 'vpc-type', + **{'vpcIds': {'type': 'array', 'items': {'type': 'string'}}}) + + def get_request(self, i): + vpcId = i['VpcId'] + if vpcId in self.data['vpcIds']: + return False + return i + @PolarDB.filter_registry.register('dbcluster-network-type') class DBClusterNetworkTypePolarDBFilter(AliyunRdsFilter): """Filters @@ -88,9 +112,9 @@ class InternetAccessPolarDBFilter(AliyunRdsFilter): def get_request(self, i): request = DescribeDBClusterAccessWhitelistRequest() request.set_accept_format('json') - request.set_DBClusterId(i['DBClusterId']) + request.set_DBClusterId(i.get('DBClusterId', '')) response = Session.client(self, service).do_action_with_exception(request) - string = str(response, encoding="utf-8").replace("false", "False") + string = str(response, encoding="utf-8").replace("false", "False").replace("true", "True") data = eval(string) DBClusterSecurityGroups = jmespath.search('DBClusterSecurityGroups.DBClusterSecurityGroup', data) DBClusterIPArray = jmespath.search('Items.DBClusterIPArray', data) diff --git a/tools/c7n_aliyun/c7n_aliyun/resources/ram.py b/tools/c7n_aliyun/c7n_aliyun/resources/ram.py index 3d7e762b43d..2ab7a086240 100644 --- a/tools/c7n_aliyun/c7n_aliyun/resources/ram.py +++ b/tools/c7n_aliyun/c7n_aliyun/resources/ram.py @@ -61,7 +61,7 @@ def get_request(self, i): request.set_accept_format('json') request.set_UserName(i['UserName']) response = Session.client(self, service).do_action_with_exception(request) - string = str(response, encoding="utf-8").replace("false", "False") + string = str(response, encoding="utf-8").replace("false", "False").replace("true", "True") data = jmespath.search(self.mfa_bind_required, eval(string)) if data != self.data['value']: return False diff --git a/tools/c7n_aliyun/c7n_aliyun/resources/rds.py b/tools/c7n_aliyun/c7n_aliyun/resources/rds.py index 34c91b05b2c..b0907b3d29a 100644 --- a/tools/c7n_aliyun/c7n_aliyun/resources/rds.py +++ b/tools/c7n_aliyun/c7n_aliyun/resources/rds.py @@ -44,7 +44,6 @@ class resource_type(TypeInfo): def get_request(self): request = DescribeDBInstancesRequest() - request.set_accept_format('json') return request @Rds.filter_registry.register('available-zones') @@ -79,7 +78,7 @@ def get_request(self, i): response = Session.client(self, service).do_action_with_exception(request) - string = str(response, encoding="utf-8").replace("false", "False") + string = str(response, encoding="utf-8").replace("false", "False").replace("true", "True") data = eval(string) if self.data['value']==True: flag = len(data['AvailableZones']) > 0 @@ -117,7 +116,7 @@ def get_request(self, i): request.set_DBInstanceId(i['DBInstanceId']) response = Session.client(self, service).do_action_with_exception(request) - string = str(response, encoding="utf-8").replace("false", "False") + string = str(response, encoding="utf-8").replace("false", "False").replace("true", "True") data = eval(string) DBInstanceAttributes = data['Items']['DBInstanceAttribute'] for obj in DBInstanceAttributes: @@ -154,7 +153,7 @@ def get_request(self, i): request.set_DBInstanceId(i['DBInstanceId']) response = Session.client(self, service).do_action_with_exception(request) - string = str(response, encoding="utf-8").replace("false", "False") + string = str(response, encoding="utf-8").replace("false", "False").replace("true", "True") data = eval(string) DBInstanceAttributes = data['Items']['DBInstanceAttribute'] for obj in DBInstanceAttributes: @@ -187,6 +186,8 @@ class RdsMetricsFilter(MetricsFilter): def get_request(self, rds): request = DescribeMetricListRequest() request.set_accept_format('json') + dimensions = self.get_dimensions(rds) + request.set_Dimensions(dimensions) request.set_StartTime(self.start) request.set_Period(self.period) request.set_Namespace(self.namespace) @@ -221,6 +222,30 @@ def get_request(self, i): return False return i +@Rds.filter_registry.register('vpc-type') +class VpcTypeRdsFilter(AliyunRdsFilter): + """Filters + :Example: + .. code-block:: yaml + + policies: + # 检测您账号下的Rds实例指定属于哪些VPC, 属于则合规,不属于则"不合规"。 + - name: aliyun-rds-vpc-type + resource: aliyun.rds + filters: + - type: vpc-type + vpcIds: ["111", "222"] + """ + schema = type_schema( + 'vpc-type', + **{'vpcIds': {'type': 'array', 'items': {'type': 'string'}}}) + + def get_request(self, i): + vpcId = i['VpcId'] + if vpcId in self.data['vpcIds']: + return False + return i + @Rds.filter_registry.register('instance-network-type') class InstanceNetworkTypeRdsFilter(AliyunRdsFilter): """Filters @@ -267,26 +292,27 @@ class InternetAccessRdsFilter(AliyunRdsFilter): **{'value': {'type': 'boolean'}}) def get_request(self, i): - request1 = DescribeSecurityGroupConfigurationRequest() - request1.set_accept_format('json') - request1.set_DBInstanceId(i['DBInstanceId']) - response1 = Session.client(self, service).do_action_with_exception(request1) - - string1 = str(response1, encoding="utf-8").replace("false", "False") - EcsSecurityGroupRelation = jmespath.search('Items.EcsSecurityGroupRelation', eval(string1)) - - request2 = DescribeDBInstanceIPArrayListRequest() - request2.set_accept_format('json') - request2.set_DBInstanceId(i['DBInstanceId']) - response2 = Session.client(self, service).do_action_with_exception(request2) - string2 = str(response2, encoding="utf-8").replace("false", "False") - - DBInstanceIPArray = jmespath.search('Items.DBInstanceIPArray', eval(string2)) - if self.data['value']: - if len(EcsSecurityGroupRelation) == 0 and len(DBInstanceIPArray) == 0: - return False + DBInstanceNetType = i.get('DBInstanceNetType', '') + if self.data.get('value', ''): + if DBInstanceNetType == "Internet": + return i + else: + return None else: - return False - i['EcsSecurityGroupRelation'] = EcsSecurityGroupRelation - i['DBInstanceIPArray'] = DBInstanceIPArray - return i \ No newline at end of file + if DBInstanceNetType == "Intranet": + return i + else: + return None + return i + + def is_internal_ip(self, ip): + ip = self.ip_into_int(ip) + net_a = self.ip_into_int('10.255.255.255') >> 24 + net_b = self.ip_into_int('172.31.255.255') >> 20 + net_c = self.ip_into_int('192.168.255.255') >> 16 + return ip >> 24 == net_a or ip >>20 == net_b or ip >> 16 == net_c + + def ip_into_int(self, ip): + # 先把 192.168.31.46 用map分割'.'成数组,然后用reduuce+lambda转成10进制的 3232243502 + # (((((192 * 256) + 168) * 256) + 31) * 256) + 46 + return self.reduce(lambda x,y:(x<<8)+y,map(int,ip.split('.'))) \ No newline at end of file diff --git a/tools/c7n_aliyun/c7n_aliyun/resources/redis.py b/tools/c7n_aliyun/c7n_aliyun/resources/redis.py index 811899ce0f5..17232500a9f 100644 --- a/tools/c7n_aliyun/c7n_aliyun/resources/redis.py +++ b/tools/c7n_aliyun/c7n_aliyun/resources/redis.py @@ -88,23 +88,16 @@ class InternetAccessMongoDBFilter(AliyunRedisFilter): **{'value': {'type': 'boolean'}}) def get_request(self, i): - request1 = DescribeSecurityIpsRequest() - request1.set_accept_format('json') - response1 = Session.client(self, service).do_action_with_exception(request1) - string1 = str(response1, encoding="utf-8").replace("false", "False") - SecurityIpGroups = jmespath.search('SecurityIpGroups.SecurityIpGroup', eval(string1)) - request2 = DescribeSecurityGroupConfigurationRequest() - request2.set_accept_format('json') - response2 = Session.client(self, service).do_action_with_exception(request2) - string2 = str(response2, encoding="utf-8").replace("false", "False") - - EcsSecurityGroupRelation = jmespath.search('Items.EcsSecurityGroupRelation', eval(string2)) - if self.data['value']: - if len(SecurityIpGroups) == 0 and len(EcsSecurityGroupRelation) == 0: - return False + DBInstanceNetType = i.get('DBInstanceNetType', '') + if self.data.get('value', ''): + if DBInstanceNetType == "Internet": + return i + else: + return None else: - return False - i['SecurityIpGroups'] = SecurityIpGroups - i['EcsSecurityGroupRelation'] = EcsSecurityGroupRelation + if DBInstanceNetType == "Intranet": + return i + else: + return None return i \ No newline at end of file diff --git a/tools/c7n_aliyun/c7n_aliyun/resources/slb.py b/tools/c7n_aliyun/c7n_aliyun/resources/slb.py index 2057ad0de8d..8638b835ca1 100644 --- a/tools/c7n_aliyun/c7n_aliyun/resources/slb.py +++ b/tools/c7n_aliyun/c7n_aliyun/resources/slb.py @@ -21,6 +21,7 @@ from aliyunsdkslb.request.v20140515.DescribeHealthStatusRequest import DescribeHealthStatusRequest from aliyunsdkslb.request.v20140515.DescribeLoadBalancerAttributeRequest import DescribeLoadBalancerAttributeRequest from aliyunsdkslb.request.v20140515.DescribeLoadBalancersRequest import DescribeLoadBalancersRequest +from aliyunsdkecs.request.v20140526.DescribeVpcsRequest import DescribeVpcsRequest from c7n.utils import local_session from c7n.utils import type_schema @@ -69,7 +70,7 @@ def get_request(self, i): request.set_LoadBalancerId(i['LoadBalancerId']) response = Session.client(self, service).do_action_with_exception(request) - string = str(response, encoding="utf-8").replace("false", "False") + string = str(response, encoding="utf-8").replace("false", "False").replace("true", "True") data = eval(string) if self.data['value'] in jmespath.search(self.listener_protocol_key, data): return False @@ -138,6 +139,41 @@ def get_request(self, i): return i return False +@Slb.filter_registry.register('listener-type') +class AliyunSlbListener(AliyunSlbListenerFilter): + """Filters + + :Example: + + .. code-block:: yaml + + policies: + # 检测您账号下的SLB负载均衡开启HTTPS或HTTP监听视为合规,否则不合规。 + - name: aliyun-slb-listener-type + resource: aliyun.slb + filters: + - type: listener-type + value: ["https", "http"] + """ + schema = type_schema( + 'listener-type', + **{'value': {'type': 'array', 'items': {'type': 'string'}}}) + + def get_request(self, i): + request = DescribeLoadBalancerAttributeRequest() + request.set_accept_format('json') + request.set_LoadBalancerId(i['LoadBalancerId']) + client = local_session( + self.manager.session_factory).client(self.manager.get_model().service) + response = json.loads(client.do_action(request)) + ListenerPortAndProtocal = response.get('ListenerPortsAndProtocal').get('ListenerPortAndProtocal') + if len(ListenerPortAndProtocal) == 0: + return i + for ListenerProtocol in ListenerPortAndProtocal: + if ListenerProtocol in self.data['value']: + return False + return False + @Slb.filter_registry.register('metrics') class SlbMetricsFilter(MetricsFilter): @@ -159,6 +195,8 @@ def get_request(self, r): request.set_accept_format('json') request.set_StartTime(self.start) request.set_Period(self.period) + dimensions = self.get_dimensions(r) + request.set_Dimensions(dimensions) request.set_Namespace(self.namespace) request.set_MetricName(self.metric) return request @@ -210,6 +248,30 @@ def get_request(self, i): return False return i +@Slb.filter_registry.register('vpc-type') +class VpcSlbFilter(AliyunSlbFilter): + """Filters + :Example: + .. code-block:: yaml + + policies: + # 检测您账号下SLB负载均衡实例指定属于哪些VPC, 属于则合规,不属于则"不合规"。 + - name: aliyun-slb-vpc-type + resource: aliyun.slb + filters: + - type: vpc-type + vpcIds: ["111", "222"] + """ + schema = type_schema( + 'vpc-type', + **{'vpcIds': {'type': 'array', 'items': {'type': 'string'}}}) + + def get_request(self, i): + vpcId = i['VpcId'] + if vpcId in self.data['vpcIds']: + return False + return i + @Slb.filter_registry.register('bandwidth') class BandwidthSlbFilter(AliyunSlbFilter): """Filters @@ -233,7 +295,7 @@ def get_request(self, i): request.set_accept_format('json') request.set_LoadBalancerId(i['LoadBalancerId']) response = Session.client(self, service).do_action_with_exception(request) - string = str(response, encoding="utf-8").replace("false", "False") + string = str(response, encoding="utf-8").replace("false", "False").replace("true", "True") data = eval(string) if self.data['value'] < data['Bandwidth']: return False @@ -259,23 +321,16 @@ class AclsSlbFilter(AliyunSlbFilter): **{'value': {'type': 'boolean'}}) def get_request(self, i): - request = DescribeAccessControlListsRequest() - request.set_accept_format('json') - response = Session.client(self, service).do_action_with_exception(request) - string = str(response, encoding="utf-8").replace("false", "False") - data = eval(string) - if self.data['value']: - if len(data) == 0: - return False + AddressType = i.get('AddressType', '') + if self.data.get('value', ''): + if AddressType == "Internet": + return i + else: + return None + else: + if AddressType == "Intranet": + return i else: - for obj in data['Acls']['Acl']: - req = DescribeAccessControlListAttributeRequest() - req.set_accept_format('json') - req.set_AclId(obj['AclId']) - res = Session.client(self, service).do_action_with_exception(req) - string = str(res, encoding="utf-8").replace("false", "False") - data2 = eval(string) - if len(data2) == 0: - return False + return None return i diff --git a/tools/c7n_aliyun/c7n_aliyun/resources/vpc.py b/tools/c7n_aliyun/c7n_aliyun/resources/vpc.py index bb8dfc97932..84e1462fbb1 100644 --- a/tools/c7n_aliyun/c7n_aliyun/resources/vpc.py +++ b/tools/c7n_aliyun/c7n_aliyun/resources/vpc.py @@ -133,18 +133,72 @@ class IPPermission(AliyunSgFilter): def get_request(self, sg): service = 'security-group' - request = DescribeSecurityGroupAttributeRequest(); + request = DescribeSecurityGroupAttributeRequest() request.set_SecurityGroupId(sg["SecurityGroupId"]) request.set_Direction("ingress") request.set_accept_format('json') response = Session.client(self, service).do_action_with_exception(request) - string = str(response, encoding="utf-8").replace("false", "False") + string = str(response, encoding="utf-8").replace("false", "False").replace("true", "True") data = eval(string) for cidr in jmespath.search(self.ip_permissions_key, data): if cidr['SourceCidrIp'] == self.data['value']: + sg['cidr'] = cidr return sg return False +@SecurityGroup.filter_registry.register('source-ports') +class IPPermission(AliyunSgFilter): + + """Filters + :Example: + .. code-block:: yaml + + policies: + # 账号下ECS安全组配置允许所有端口访问视为不合规,否则为合规 + - name: aliyun-sg-ports + resource: aliyun.security-group + filters: + - type: source-ports + SourceCidrIp: "0.0.0.0/0" + PortRange: -1/-1 + """ + + ip_permissions_key = "Permissions.Permission" + schema = type_schema( + 'source-ports', + **{'SourceCidrIp': {'type': 'string'}, + 'PortRange': {'type': 'string'}} + ) + + def get_request(self, sg): + service = 'security-group' + request = DescribeSecurityGroupAttributeRequest(); + request.set_SecurityGroupId(sg["SecurityGroupId"]) + request.set_Direction("ingress") + request.set_accept_format('json') + response = Session.client(self, service).do_action_with_exception(request) + string = str(response, encoding="utf-8").replace("false", "False").replace("true", "True") + data = eval(string) + for ports in jmespath.search(self.ip_permissions_key, data): + if ports['SourceCidrIp'] == self.data['SourceCidrIp']: + if ports['PortRange'] == self.data['PortRange']: + return sg + values = self.data['PortRange'].split('/') + if values[0].replace("”", "") == "-1": + fromPort = 0 + toPort = 65535 + else: + fromPort = int(values[0].replace("”", "")) + toPort = int(values[1].replace("”", "")) + if '/' in ports['PortRange']: + strs = ports['PortRange'].split('/') + port1 = int(strs[0]) + port2 = int(strs[1]) + if (fromPort >= port1 and fromPort <= port2) \ + or (toPort >= port1 and toPort <= port2): + return sg + return False + @SecurityGroup.filter_registry.register('ingress') class IPPermission(SGPermission): @@ -157,7 +211,7 @@ class IPPermission(SGPermission): schema['properties'].update(SGPermissionSchema) def process_self_cidrs(self, perm): - self.process_cidrs(perm, "SourceCidrIp", "Ipv6SourceCidrIp") + return self.process_cidrs(perm, "SourceCidrIp", "Ipv6SourceCidrIp") def securityGroupAttributeRequst(self, sg): requst = DescribeSecurityGroupAttributeRequest(); @@ -185,4 +239,4 @@ def securityGroupAttributeRequst(self, sg): return requst def process_self_cidrs(self, perm): - self.process_cidrs(perm, "DestCidrIp", "Ipv6DestCidrIp") + return self.process_cidrs(perm, "DestCidrIp", "Ipv6DestCidrIp") diff --git a/tools/c7n_aliyun/setup.py b/tools/c7n_aliyun/setup.py index 97ada937f56..3de1453cdea 100644 --- a/tools/c7n_aliyun/setup.py +++ b/tools/c7n_aliyun/setup.py @@ -4,36 +4,36 @@ from setuptools import setup packages = \ -['c7n_aliyun', 'c7n_aliyun.actions', 'c7n_aliyun.filters', 'c7n_aliyun.resources'] + ['c7n_aliyun', 'c7n_aliyun.actions', 'c7n_aliyun.filters', 'c7n_aliyun.resources'] package_data = \ -{'': ['*']} + {'': ['*']} install_requires = \ -['argcomplete (>=1.11.1,<2.0.0)', - 'attrs (>=19.3.0,<20.0.0)', - 'boto3 (>=1.14.8,<2.0.0)', - 'botocore (>=1.17.8,<2.0.0)', - 'c7n (>=0.9.3,<0.10.0)', - 'docutils (>=0.15.2,<0.16.0)', - 'google-api-python-client>=1.7,<2.0', - 'google-auth>=1.11.0,<2.0.0', - 'google-cloud-logging>=1.14,<2.0', - 'google-cloud-monitoring>=0.34.0,<0.35.0', - 'google-cloud-storage>=1.28.1,<2.0.0', - 'importlib-metadata (>=1.6.1,<2.0.0)', - 'jmespath (>=0.10.0,<0.11.0)', - 'jsonschema (>=3.2.0,<4.0.0)', - 'pyrsistent (>=0.16.0,<0.17.0)', - 'python-dateutil (>=2.8.1,<3.0.0)', - 'pyyaml (>=5.3.1,<6.0.0)', - 'ratelimiter>=1.2.0,<2.0.0', - 'retrying>=1.3.3,<2.0.0', - 's3transfer (>=0.3.3,<0.4.0)', - 'six (>=1.15.0,<2.0.0)', - 'tabulate (>=0.8.7,<0.9.0)', - 'urllib3 (>=1.25.9,<2.0.0)', - 'zipp (>=3.1.0,<4.0.0)'] + ['argcomplete (>=1.11.1,<2.0.0)', + 'attrs (>=19.3.0,<20.0.0)', + 'boto3 (>=1.14.8,<2.0.0)', + 'botocore (>=1.17.8,<2.0.0)', + 'c7n (>=0.9.3,<0.10.0)', + 'docutils (>=0.15.2,<0.16.0)', + 'google-api-python-client>=1.7,<2.0', + 'google-auth>=1.11.0,<2.0.0', + 'google-cloud-logging>=1.14,<2.0', + 'google-cloud-monitoring>=0.34.0,<0.35.0', + 'google-cloud-storage>=1.28.1,<2.0.0', + 'importlib-metadata (>=1.6.1,<2.0.0)', + 'jmespath (>=0.10.0,<0.11.0)', + 'jsonschema (>=3.2.0,<4.0.0)', + 'pyrsistent (>=0.16.0,<0.17.0)', + 'python-dateutil (>=2.8.1,<3.0.0)', + 'pyyaml (>=5.3.1,<6.0.0)', + 'ratelimiter>=1.2.0,<2.0.0', + 'retrying>=1.3.3,<2.0.0', + 's3transfer (>=0.3.3,<0.4.0)', + 'six (>=1.15.0,<2.0.0)', + 'tabulate (>=0.8.7,<0.9.0)', + 'urllib3 (>=1.25.9,<2.0.0)', + 'zipp (>=3.1.0,<4.0.0)'] setup_kwargs = { 'name': 'c7n_aliyun', diff --git a/tools/c7n_aliyun/test/aliyun.py b/tools/c7n_aliyun/test/aliyun.py index 9b8ffd1a640..88802fd8818 100644 --- a/tools/c7n_aliyun/test/aliyun.py +++ b/tools/c7n_aliyun/test/aliyun.py @@ -303,4 +303,4 @@ def get_instance_monitor(): # get_slb_listener() # get_instance_monitor() # get_disk() - list_slb() \ No newline at end of file + # list_slb() \ No newline at end of file diff --git a/tools/c7n_azure/c7n_azure/query.py b/tools/c7n_azure/c7n_azure/query.py index 770a6c0f12f..0c876adab21 100644 --- a/tools/c7n_azure/c7n_azure/query.py +++ b/tools/c7n_azure/c7n_azure/query.py @@ -55,9 +55,6 @@ def filter(self, resource_manager, **params): op = getattr(getattr(resource_manager.get_client(), enum_op), list_op) result = op(**params) - for obj in result: - obj.F2CId = obj.id - if isinstance(result, Iterable): return [r.serialize(True) for r in result] elif hasattr(result, 'value'): diff --git a/tools/c7n_huawei/.gitignore b/tools/c7n_huawei/.gitignore index 1e7bb232c6a..8b7f5425938 100644 --- a/tools/c7n_huawei/.gitignore +++ b/tools/c7n_huawei/.gitignore @@ -2,3 +2,4 @@ *py~ __pycache__ *.egg-info +huawei.py \ No newline at end of file diff --git a/tools/c7n_huawei/c7n_huawei/filters/filter.py b/tools/c7n_huawei/c7n_huawei/filters/filter.py index 844e207bc14..5bd8c54a359 100644 --- a/tools/c7n_huawei/c7n_huawei/filters/filter.py +++ b/tools/c7n_huawei/c7n_huawei/filters/filter.py @@ -1,9 +1,7 @@ import datetime -import json from concurrent.futures import as_completed from datetime import timedelta -import jmespath from dateutil.parser import parse from dateutil.tz import tzutc from openstack import utils @@ -15,6 +13,7 @@ from c7n.utils import local_session, chunks from c7n.utils import type_schema + class HuaweiEcsFilter(Filter): schema = None @@ -296,13 +295,12 @@ def process(self, resources, event=None): def process_ports(self, perm): found = None - if perm['port_range_max'] and perm['port_range_min']: + if perm['port_range_max'] is not None and perm['port_range_min'] is not None: FromPort = int(perm['port_range_max']) ToPort = int(perm['port_range_min']) for port in self.ports: if port >= FromPort and port <= ToPort: - found = True - break + return True found = False only_found = False for port in self.only_ports: @@ -404,8 +402,8 @@ def __call__(self, resource): if perm['direction'] != self.direction: continue perm_matches = {} - perm_matches['ports'] = self.process_ports(perm) perm_matches['cidrs'] = self.process_self_cidrs(perm) + perm_matches['ports'] = self.process_ports(perm) perm_match_values = list(filter( lambda x: x is not None, perm_matches.values())) # account for one python behavior any([]) == False, all([]) == True diff --git a/tools/c7n_huawei/c7n_huawei/query.py b/tools/c7n_huawei/c7n_huawei/query.py index 8c84d828340..9342d846987 100644 --- a/tools/c7n_huawei/c7n_huawei/query.py +++ b/tools/c7n_huawei/c7n_huawei/query.py @@ -43,9 +43,12 @@ def filter(self, resource_manager, **params): if extra_args: params.update(extra_args) + offset = 1 + limit = 100 + res = [] + buckets = [] if m.service == 'obs': result = resource_manager.get_request() - buckets = [] if result is None: return buckets for b in result.body.buckets: @@ -53,17 +56,19 @@ def filter(self, resource_manager, **params): buckets.append(b) return buckets else: - request = resource_manager.get_request() - if request: - result = request - else: - return None - if path is None: - return result - res = jmespath.search(path, eval(str(result))) - if res is not None: - for data in res: - data['F2CId'] = data[m.id] + while 1 <= offset: + request = resource_manager.get_request() + request.limit = limit + request.offset = offset + response = jmespath.search(path, eval(str(request).replace('null', 'None').replace('false', 'False').replace('true', 'True'))) + if response is not None: + for data in response: + data['F2CId'] = data[m.id] + res = res + response + if len(response) == limit: + offset += 1 + else: + return res return res def _invoke_client_enum(self, client, request, params, path): diff --git a/tools/c7n_huawei/c7n_huawei/resources/dds.py b/tools/c7n_huawei/c7n_huawei/resources/dds.py index fd23ca0f17b..cca4878cbb3 100644 --- a/tools/c7n_huawei/c7n_huawei/resources/dds.py +++ b/tools/c7n_huawei/c7n_huawei/resources/dds.py @@ -62,12 +62,12 @@ class InternetAccessRedisFilter(HuaweiRedisFilter): **{'value': {'type': 'boolean'}}) def get_request(self, i): - groups = i['groups'] - if self.data['value']: + groups = i.get('groups', []) + if self.data.get('value', ''): if len(groups) > 0: for group in groups: - if len(group['nodes']) > 0: - if group['nodes']['public_ip']: + if len(group.get('nodes', [])) > 0: + if group.get('nodes', []).get('public_ip', None): return i return None else: @@ -75,8 +75,8 @@ def get_request(self, i): return i else: for group in groups: - if len(group['nodes']) > 0: - if group['nodes']['public_ip']: + if len(group.get('nodes', [])) > 0: + if group.get('nodes', []).get('public_ip', None): return i return None diff --git a/tools/c7n_huawei/c7n_huawei/resources/disk.py b/tools/c7n_huawei/c7n_huawei/resources/disk.py index 50e321dbf18..6309c5cf025 100644 --- a/tools/c7n_huawei/c7n_huawei/resources/disk.py +++ b/tools/c7n_huawei/c7n_huawei/resources/disk.py @@ -65,8 +65,8 @@ class EncryptedDiskFilter(HuaweiDiskFilter): **{'value': {'type': 'boolean'}}) def get_request(self, disk): - encrypted = self.data['value'] - if disk['encrypted'] != encrypted: + encrypted = self.data.get('value', '') + if disk.get('encrypted', '') != encrypted: return False return disk @@ -101,6 +101,6 @@ class HuaweiDiskFilter(HuaweiDiskFilter): schema = type_schema('available') def get_request(self, disk): - if disk['status'] != 'available': + if disk.get('status', '') != 'available': return False return disk \ No newline at end of file diff --git a/tools/c7n_huawei/c7n_huawei/resources/ecs.py b/tools/c7n_huawei/c7n_huawei/resources/ecs.py index 12b5a89c7f7..816e90e5fed 100644 --- a/tools/c7n_huawei/c7n_huawei/resources/ecs.py +++ b/tools/c7n_huawei/c7n_huawei/resources/ecs.py @@ -74,7 +74,7 @@ def get_request(self, i): else: for addrs in list(data.values()): for addr in addrs: - if addr['os_ext_ip_stype'] == 'floating': + if addr.get('os_ext_ip_stype', '') is not None and addr.get('os_ext_ip_stype', '') == 'floating': return i return False @@ -102,7 +102,7 @@ class EcsAgeFilter(HuaweiAgeFilter): def get_resource_date(self, i): # '2020-07-27T05:55:32.000000' - return i['os_srv_us_glaunched_at'] + return i.get('os_srv_us_glaunched_at', '2021-07-27T05:55:32.000000') @Ecs.filter_registry.register('instance-network-type') class InstanceNetworkTypeEcsFilter(HuaweiEcsFilter): @@ -123,8 +123,8 @@ class InstanceNetworkTypeEcsFilter(HuaweiEcsFilter): **{'value': {'type': 'string'}}) def get_request(self, i): - if self.data['value'] == 'vpc': - if i['metadata']['vpc_id'] is not None: + if self.data.get('value', '') == 'vpc': + if i.get('metadata', {}).get('vpc_id', '') is not None: return False return i @@ -154,7 +154,7 @@ def get_request(self, dimensions): listMetricsDimensionDimensionsMetrics= [ MetricsDimension( name="instance_id", - value=dimensions[0]['id'] + value=dimensions[0].get('id', '') ) ] listMetricInfoMetricsbody = [ diff --git a/tools/c7n_huawei/c7n_huawei/resources/eip.py b/tools/c7n_huawei/c7n_huawei/resources/eip.py index c944e79d949..eba3766a6e6 100644 --- a/tools/c7n_huawei/c7n_huawei/resources/eip.py +++ b/tools/c7n_huawei/c7n_huawei/resources/eip.py @@ -62,7 +62,7 @@ class HuaweiEipFilter(HuaweiEipFilter): schema = type_schema('DOWN') def get_request(self, i): - if i['status'] != "DOWN": + if i.get('status', '') != "DOWN": return False return i @@ -85,6 +85,6 @@ class BandwidthEipFilter(HuaweiEipFilter): **{'value': {'type': 'number'}}) def get_request(self, i): - if self.data['value'] < i['bandwidth_size']: + if self.datai.get('value', 0) < i.get('bandwidth_size', 0): return False return i diff --git a/tools/c7n_huawei/c7n_huawei/resources/elb.py b/tools/c7n_huawei/c7n_huawei/resources/elb.py index e80a6db090c..00d009143a7 100644 --- a/tools/c7n_huawei/c7n_huawei/resources/elb.py +++ b/tools/c7n_huawei/c7n_huawei/resources/elb.py @@ -69,7 +69,7 @@ def get_request(self, i): request = ShowListenerRequest() request.listener_id = data response = Session.client(self, service).show_listener(request) - if jmespath.search('protocol', response) == self.data['value']: + if jmespath.search('protocol', response) == self.data.get('value', ''): return False return i @@ -91,7 +91,7 @@ class UnusedElbFilter(HuaweiElbFilter): schema = type_schema('unused') def get_request(self, i): - listeners = i['pools'] + listeners = i.get('pools', []) # elb 查询elb下是否有监听 if len(listeners) > 0: return False @@ -117,10 +117,10 @@ class AddressTypeElbFilter(HuaweiElbFilter): **{'value': {'type': 'string'}}) def get_request(self, i): - if self.data['value'] == 'internet': - if i['vip_address'] is not None: + if self.data.get('value', '') == 'internet': + if i.get('vip_address', '') is not None: return False else: - if i['vip_address'] is None: + if i.get('vip_address', '') is None: return False return i \ No newline at end of file diff --git a/tools/c7n_huawei/c7n_huawei/resources/iam.py b/tools/c7n_huawei/c7n_huawei/resources/iam.py index 41141a93705..9bd1e4fb0b4 100644 --- a/tools/c7n_huawei/c7n_huawei/resources/iam.py +++ b/tools/c7n_huawei/c7n_huawei/resources/iam.py @@ -68,10 +68,10 @@ class HuaweiIamLoginFilter(HuaweiIamFilter): def get_request(self, i): try: request = ShowUserLoginProtectRequest() - request.user_id = i['id'] + request.user_id = i.get('id', '') response = Session.client(self, service).show_user_login_protect(request) i['login_protect'] = jmespath.search('login_protect', eval(str(response))) - if self.data['value'] == i['login_protect']['enabled']: + if self.data.get('value', '') == i.get('login_protect', {}).get('enabled', ''): return False return i except Exception as e: diff --git a/tools/c7n_huawei/c7n_huawei/resources/obs.py b/tools/c7n_huawei/c7n_huawei/resources/obs.py index 911e27cba91..b3c6c16ac93 100644 --- a/tools/c7n_huawei/c7n_huawei/resources/obs.py +++ b/tools/c7n_huawei/c7n_huawei/resources/obs.py @@ -101,12 +101,12 @@ def process_bucket(self, b): # BUCKET_OWNER_FULL_CONTROL 桶或对象所有者拥有完全控制权限。 acl = Session.client(self, service).getBucketAcl(b.name) b['permission'] = acl.body.grants - if self.data['value'] == 'read': + if self.data.get('value', '') == 'read': for obj in acl.body.grants: if obj.grantee.group == 'Everyone': if obj.permission == 'READ' or obj.permission == 'READ_ACP': return b - if self.data['value'] == 'write': + if self.data.get('value', '') == 'write': for obj in acl.body.grants: if obj.grantee.group == 'Everyone': if obj.permission == 'WRITE' or obj.permission == 'WRITE_ACP': diff --git a/tools/c7n_huawei/c7n_huawei/resources/rds.py b/tools/c7n_huawei/c7n_huawei/resources/rds.py index 08024b0c634..a2aac938629 100644 --- a/tools/c7n_huawei/c7n_huawei/resources/rds.py +++ b/tools/c7n_huawei/c7n_huawei/resources/rds.py @@ -61,7 +61,9 @@ class HuaweiRdsFilter(HuaweiRdsFilter): schema = type_schema('internet') def get_request(self, i): - public_ips = i['public_ips'] + if i.get('public_ips', '') is None: + return None + public_ips = i.get('public_ips', '') if len(public_ips) == 0: return None return i @@ -78,7 +80,7 @@ def get_request(self, dimensions): new_dimensions.append( { "name": "rds_cluster_id", - "value": str(dimension['id']) + "value": str(dimension.get('id', '')) }) try: listMetricsDimensionDimensionsMetrics = [] @@ -86,7 +88,7 @@ def get_request(self, dimensions): listMetricsDimensionDimensionsMetrics.append( MetricsDimension( name="rds_cluster_id", - value=str(dimension['id']) + value=str(dimension.get('id', '')) ) ) listMetricInfoMetricsbody = [ @@ -127,8 +129,8 @@ class InternetAccessRdsFilter(HuaweiRdsFilter): **{'value': {'type': 'boolean'}}) def get_request(self, i): - public_ips = i['public_ips'] - if self.data['value'] == True: + public_ips = i.get('public_ips', '') + if self.data.get('value', '') == True: if len(public_ips) == 0: return None return i @@ -200,7 +202,7 @@ class InstanceNetworkTypeRdsFilter(HuaweiRdsFilter): **{'value': {'type': 'string'}}) def get_request(self, i): - if i['vpc_id'] is not None: + if i.get('vpc_id', '') is not None: return False return i @@ -225,6 +227,6 @@ class InstanceNetworkTypeRdsFilter(HuaweiRdsFilter): **{'value': {'type': 'string'}}) def get_request(self, i): - if i['charge_info']['charge_mode'] == self.data['value']: + if i['charge_info']['charge_mode'] == self.data.get('value', ''): return False return i \ No newline at end of file diff --git a/tools/c7n_huawei/c7n_huawei/resources/redis.py b/tools/c7n_huawei/c7n_huawei/resources/redis.py index e5e7a50bf4c..43afd210db1 100644 --- a/tools/c7n_huawei/c7n_huawei/resources/redis.py +++ b/tools/c7n_huawei/c7n_huawei/resources/redis.py @@ -63,7 +63,7 @@ class InternetAccessRedisFilter(HuaweiRedisFilter): def get_request(self, i): public_ips = i['publicip_id'] - if self.data['value']: + if self.data.get('value', ''): if public_ips is None or (len(public_ips) == 0 and i['enable_ssl'] == False): return i return False @@ -94,6 +94,6 @@ class NoPasswordAccessRedisFilter(HuaweiRedisFilter): **{'value': {'type': 'boolean'}}) def get_request(self, i): - if i['no_password_access'] == self.data['value']: + if i['no_password_access'] == self.data.get('value', ''): return i return False diff --git a/tools/c7n_huawei/c7n_huawei/resources/securitygroup.py b/tools/c7n_huawei/c7n_huawei/resources/securitygroup.py index 4b139852678..0cf0a88312f 100644 --- a/tools/c7n_huawei/c7n_huawei/resources/securitygroup.py +++ b/tools/c7n_huawei/c7n_huawei/resources/securitygroup.py @@ -81,7 +81,7 @@ class IPPermission(SGPermission): schema['properties'].update(SGPermissionSchema) def process_self_cidrs(self, perm): - self.process_cidrs(perm, "remote_ip_prefix", "remote_ip_prefix") + return self.process_cidrs(perm, "remote_ip_prefix", "remote_ip_prefix") def securityGroupAttributeRequst(self, sg): @@ -103,7 +103,7 @@ def securityGroupAttributeRequst(self, sg): return sg def process_self_cidrs(self, perm): - self.process_cidrs(perm, "DestCidrIp", "Ipv6DestCidrIp") + return self.process_cidrs(perm, "DestCidrIp", "Ipv6DestCidrIp") @SecurityGroup.filter_registry.register('source-cidr-ip') class HuaweiSgSourceCidrIp(HuaweiSgFilter): @@ -128,6 +128,6 @@ class HuaweiSgSourceCidrIp(HuaweiSgFilter): def get_request(self, sg): for cidr in sg[self.ip_permissions_key]: - if cidr['remote_ip_prefix'] == self.data['value']: + if cidr.get('remote_ip_prefix', '') is not None and cidr.get('remote_ip_prefix', '') == self.data.get('value', ''): return sg return False \ No newline at end of file diff --git a/tools/c7n_huawei/test/huawei.py b/tools/c7n_huawei/test/huawei.py index ba9207c4d77..d03e21bfb93 100644 --- a/tools/c7n_huawei/test/huawei.py +++ b/tools/c7n_huawei/test/huawei.py @@ -325,6 +325,6 @@ def list_elb(): # list_sg() # list_rds() # list_cdn() - list_iam() + # list_iam() # list_obs() # list_redis() \ No newline at end of file diff --git a/tools/c7n_mailer/tests/test_misc.py b/tools/c7n_mailer/tests/test_misc.py index fe619d01d87..ef52c7bb9e2 100644 --- a/tools/c7n_mailer/tests/test_misc.py +++ b/tools/c7n_mailer/tests/test_misc.py @@ -36,6 +36,10 @@ def test_mailer_handle(self): https_proxy ] ) + # Clear http proxy + MAILER_CONFIG['http_proxy'] = '' + MAILER_CONFIG['https_proxy'] = '' + config = handle.config_setup(MAILER_CONFIG) def test_sqs_queue_processor(self): mailer_sqs_queue_processor = sqs_queue_processor.MailerSqsQueueProcessor( diff --git a/tools/c7n_openstack/.gitignore b/tools/c7n_openstack/.gitignore new file mode 100644 index 00000000000..1e7bb232c6a --- /dev/null +++ b/tools/c7n_openstack/.gitignore @@ -0,0 +1,4 @@ +*pyc +*py~ +__pycache__ +*.egg-info diff --git a/tools/c7n_openstack/c7n_openstack/__init__.py b/tools/c7n_openstack/c7n_openstack/__init__.py new file mode 100644 index 00000000000..cdce4e86dfd --- /dev/null +++ b/tools/c7n_openstack/c7n_openstack/__init__.py @@ -0,0 +1,2 @@ +# Copyright The Cloud Custodian Authors. +# SPDX-License-Identifier: Apache-2.0 \ No newline at end of file diff --git a/tools/c7n_openstack/c7n_openstack/actions/__init__.py b/tools/c7n_openstack/c7n_openstack/actions/__init__.py new file mode 100644 index 00000000000..cdce4e86dfd --- /dev/null +++ b/tools/c7n_openstack/c7n_openstack/actions/__init__.py @@ -0,0 +1,2 @@ +# Copyright The Cloud Custodian Authors. +# SPDX-License-Identifier: Apache-2.0 \ No newline at end of file diff --git a/tools/c7n_openstack/c7n_openstack/actions/core.py b/tools/c7n_openstack/c7n_openstack/actions/core.py new file mode 100644 index 00000000000..6263705fb9b --- /dev/null +++ b/tools/c7n_openstack/c7n_openstack/actions/core.py @@ -0,0 +1,117 @@ +# Copyright 2018 Capital One Services, LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from c7n.actions import Action as BaseAction +from c7n.utils import local_session, chunks + + +class Action(BaseAction): + pass + + +class MethodAction(Action): + """Invoke an api call on each resource. + + Quite a number of procedural actions are simply invoking an api + call on a filtered set of resources. The exact handling is mostly + boilerplate at that point following an 80/20 rule. This class is + an encapsulation of the 80%. + """ + + # method we'll be invoking + method_spec = () + + # batch size + chunk_size = 20 + + # implicitly filter resources by state, (attr_name, (valid_enum)) + attr_filter = () + + # error codes that can be safely ignored + ignore_error_codes = () + + permissions = () + method_perm = None + + def validate(self): + if not self.method_spec: + raise NotImplementedError("subclass must define method_spec") + return self + + def filter_resources(self, resources): + rcount = len(resources) + attr_name, valid_enum = self.attr_filter + resources = [r for r in resources if r.get(attr_name) in valid_enum] + if len(resources) != rcount: + self.log.warning( + "policy:%s action:%s implicitly filtered %d resources to %d by attr:%s", + self.manager.ctx.policy.name, + self.type, + rcount, + len(resources), + attr_name, + ) + return resources + + def process(self, resources): + + if self.attr_filter: + resources = self.filter_resources(resources) + model = self.manager.get_model() + session = local_session(self.manager.session_factory) + client = self.get_client(session, model) + for resource_set in chunks(resources, self.chunk_size): + self.process_resource_set(client, model, resource_set) + + def process_resource_set(self, client, model, resources): + result_key = self.method_spec.get('result_key') + annotation_key = self.method_spec.get('annotation_key') + for resource in resources: + requst = self.get_request(resource) + result = self.invoke_api(client, requst) + if result_key and annotation_key: + resource[annotation_key] = result.get(result_key) + + def invoke_api(self, client, requst): + try: + return client.do_action(requst) + except: + raise + + def get_permissions(self): + if self.permissions: + return self.permissions + m = self.manager.resource_type + method = self.method_perm + if not method and 'op' not in self.method_spec: + return () + if not method: + method = self.method_spec['op'] + component = m.component + if '.' in component: + component = component.split('.')[-1] + return ("{}.{}.{}".format( + m.perm_service or m.service, component, method),) + + def get_operation_name(self, model, resource): + return self.method_spec['op'] + + def get_resource_params(self, model, resource): + raise NotImplementedError("subclass responsibility") + + def get_request(self, resource): + raise NotImplementedError("subclass responsibility") + + def get_client(self, session, model): + return session.client(model.service) diff --git a/tools/c7n_openstack/c7n_openstack/actions/cscc.py b/tools/c7n_openstack/c7n_openstack/actions/cscc.py new file mode 100644 index 00000000000..2981090db7f --- /dev/null +++ b/tools/c7n_openstack/c7n_openstack/actions/cscc.py @@ -0,0 +1,243 @@ +# Copyright 2018-2019 Capital One Services, LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import datetime +import hashlib +import json +from urllib.parse import urlparse + +from c7n_openstack.provider import resources as openstack_resources + +from c7n.exceptions import PolicyExecutionError, PolicyValidationError +from c7n.utils import local_session, type_schema +from .core import MethodAction + + +class PostFinding(MethodAction): + """Post finding for matched resources to Cloud Security Command Center. + + + :Example: + + .. code-block:: yaml + + policies: + - name: openstack-instances-with-label + resource: openstack.instance + filters: + - "tag:name": "bad-instance" + actions: + - type: post-finding + org-domain: example.io + category: MEDIUM_INTERNET_SECURITY + + The source for custodian can either be specified inline to the policy, or + custodian can generate one at runtime if it doesn't exist given a org-domain + or org-id. + + Finding updates are not currently supported, due to upstream api issues. + """ + schema = type_schema( + 'post-finding', + **{ + 'source': { + 'type': 'string', + 'description': 'qualified name of source to post to CSCC as'}, + 'org-domain': {'type': 'string'}, + 'org-id': {'type': 'integer'}, + 'category': {'type': 'string'}}) + schema_alias = True + method_spec = {'op': 'create', 'result': 'name', 'annotation_key': 'c7n:Finding'} + + # create throws error if already exists, patch method has bad docs. + ignore_error_codes = (409,) + + CustodianSourceName = 'CloudCustodian' + DefaultCategory = 'Custodian' + Service = 'securitycenter' + ServiceVersion = 'v1beta1' + + _source = None + + # security center permission model is pretty obtuse to correct + permissions = ( + 'securitycenter.findings.list', + 'securitycenter.findings.update', + 'resourcemanager.organizations.get', + 'securitycenter.assetsecuritymarks.update', + 'securitycenter.sources.update', + 'securitycenter.sources.list' + ) + + def validate(self): + if not any([self.data.get(k) for k in ('source', 'org-domain', 'org-id')]): + raise PolicyValidationError( + "policy:%s CSCC post-finding requires one of source, org-domain, org-id" % ( + self.manager.ctx.policy.name)) + + def process(self, resources): + self.initialize_source() + return super(PostFinding, self).process(resources) + + def get_client(self, session, model): + return session.client( + self.Service, self.ServiceVersion, 'organizations.sources.findings') + + def get_resource_params(self, model, resource): + return self.get_finding(resource) + + def initialize_source(self): + # Ideally we'll be given a source, but we'll attempt to auto create it + # if given an org_domain or org_id. + if self._source: + return self._source + elif 'source' in self.data: + self._source = self.data['source'] + return self._source + + session = local_session(self.manager.session_factory) + + # Resolve Organization Id + if 'org-id' in self.data: + org_id = self.data['org-id'] + else: + orgs = session.client('cloudresourcemanager', 'v1', 'organizations') + res = orgs.execute_query( + 'search', {'body': { + 'filter': 'domain:%s' % self.data['org-domain']}}).get( + 'organizations') + if not res: + raise PolicyExecutionError("Could not determine organization id") + org_id = res[0]['name'].rsplit('/', 1)[-1] + + # Resolve Source + client = session.client(self.Service, self.ServiceVersion, 'organizations.sources') + source = None + res = [s for s in + client.execute_query( + 'list', {'parent': 'organizations/{}'.format(org_id)}).get('sources') + if s['displayName'] == self.CustodianSourceName] + if res: + source = res[0]['name'] + + if source is None: + source = client.execute_command( + 'create', + {'parent': 'organizations/{}'.format(org_id), + 'body': { + 'displayName': self.CustodianSourceName, + 'description': 'Cloud Management Rules Engine'}}).get('name') + self.log.info( + "policy:%s resolved cscc source: %s, update policy with this source value", + self.manager.ctx.policy.name, + source) + self._source = source + return self._source + + def get_name(self, r): + """Given an arbitrary resource attempt to resolve back to a qualified name.""" + namer = ResourceNameAdapters[self.manager.resource_type.service] + return namer(r) + + def get_finding(self, resource): + policy = self.manager.ctx.policy + resource_name = self.get_name(resource) + # ideally we could be using shake, but its py3.6+ only + finding_id = hashlib.sha256( + b"%s%s" % ( + policy.name.encode('utf8'), + resource_name.encode('utf8'))).hexdigest()[:32] + + finding = { + 'name': '{}/findings/{}'.format(self._source, finding_id), + 'resourceName': resource_name, + 'state': 'ACTIVE', + 'category': self.data.get('category', self.DefaultCategory), + 'eventTime': datetime.datetime.utcnow().isoformat('T') + 'Z', + 'sourceProperties': { + 'resource_type': self.manager.type, + 'title': policy.data.get('title', policy.name), + 'policy_name': policy.name, + 'policy': json.dumps(policy.data) + } + } + + request = { + 'parent': self._source, + 'findingId': finding_id[:31], + 'body': finding} + return request + + @classmethod + def register_resource(klass, registry, resource_class): + if resource_class.resource_type.service not in ResourceNameAdapters: + return + if 'post-finding' in resource_class.action_registry: + return + resource_class.action_registry.register('post-finding', klass) + + +# CSCC uses its own notion of resource id, if we want our findings on +# a resource to be linked from the asset view we need to post w/ the +# same resource name. If this conceptulization of resource name is +# standard, then we should move these to resource types with +# appropriate hierarchies by service. + + +def name_compute(r): + prefix = urlparse(r['selfLink']).path.strip('/').split('/')[2:][:-1] + return "//compute.googleapis.com/{}/{}".format( + "/".join(prefix), + r['id']) + + +def name_iam(r): + return "//iam.googleapis.com/projects/{}/serviceAccounts/{}".format( + r['projectId'], + r['uniqueId']) + + +def name_resourcemanager(r): + rid = r.get('projectNumber') + if rid is not None: + rtype = 'projects' + else: + rid = r.get('organizationId') + rtype = 'organizations' + return "//cloudresourcemanager.googleapis.com/{}/{}".format( + rtype, rid) + + +def name_container(r): + return "//container.googleapis.com/{}".format( + "/".join(urlparse(r['selfLink']).path.strip('/').split('/')[1:])) + + +def name_storage(r): + return "//storage.googleapis.com/{}".format(r['name']) + + +def name_appengine(r): + return "//appengine.googleapis.com/{}".format(r['name']) + + +ResourceNameAdapters = { + 'appengine': name_appengine, + 'cloudresourcemanager': name_resourcemanager, + 'compute': name_compute, + 'container': name_container, + 'iam': name_iam, + 'storage': name_storage, +} + +openstack_resources.subscribe(PostFinding.register_resource) diff --git a/tools/c7n_openstack/c7n_openstack/client.py b/tools/c7n_openstack/c7n_openstack/client.py new file mode 100644 index 00000000000..6a28cda1466 --- /dev/null +++ b/tools/c7n_openstack/c7n_openstack/client.py @@ -0,0 +1,40 @@ +# Copyright The Cloud Custodian Authors. +# SPDX-License-Identifier: Apache-2.0 +# Copyright 2017 The Forseti Security Authors. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import logging +import os + +import openstack + +log = logging.getLogger('custodian.openstack.client') + + +class Session: + def __init__(self, regionId=None): + self.http_proxy = os.getenv('HTTPS_PROXY') + self.cloud_name = os.getenv('OS_CLOUD_NAME') + if not regionId: + regionId = os.getenv('OS_DEFAULT_REGION') + self.regionId = regionId + + def client(self): + if self.cloud_name: + log.debug(f"Connect to OpenStack cloud {self.cloud_name}") + else: + log.debug(("OpenStack cloud name not set, " + "try to get openstack credential from environment")) + cloud = openstack.connect(cloud=self.cloud_name) + return cloud \ No newline at end of file diff --git a/tools/c7n_openstack/c7n_openstack/entry.py b/tools/c7n_openstack/c7n_openstack/entry.py new file mode 100644 index 00000000000..8fce605657a --- /dev/null +++ b/tools/c7n_openstack/c7n_openstack/entry.py @@ -0,0 +1,32 @@ +# Copyright The Cloud Custodian Authors. +# SPDX-License-Identifier: Apache-2.0 + +import logging +from c7n_openstack.resources import ( + project, + flavor, + server, + user, + volume, + image, + network, + router +) + +log = logging.getLogger('custodian.openstack') + +ALL = [ + flavor, + project, + server, + user, + volume, + image, + network, + router +] + + +def initialize_openstack(): + """openstack entry point + """ \ No newline at end of file diff --git a/tools/c7n_openstack/c7n_openstack/filters/__init__.py b/tools/c7n_openstack/c7n_openstack/filters/__init__.py new file mode 100644 index 00000000000..cdce4e86dfd --- /dev/null +++ b/tools/c7n_openstack/c7n_openstack/filters/__init__.py @@ -0,0 +1,2 @@ +# Copyright The Cloud Custodian Authors. +# SPDX-License-Identifier: Apache-2.0 \ No newline at end of file diff --git a/tools/c7n_openstack/c7n_openstack/filters/filter.py b/tools/c7n_openstack/c7n_openstack/filters/filter.py new file mode 100644 index 00000000000..7bd925e3d89 --- /dev/null +++ b/tools/c7n_openstack/c7n_openstack/filters/filter.py @@ -0,0 +1,340 @@ +# Copyright The Cloud Custodian Authors. +# SPDX-License-Identifier: Apache-2.0 +import json + +import jmespath + +from c7n.exceptions import PolicyValidationError +from c7n.filters.core import Filter +from c7n.filters.core import ValueFilter + + +class PolicyFilter: + pass + +class Filter(Filter): + + def validate(self): + return self + + def __call__(self, i): + return i + +class OpenstackFilter(Filter): + + def validate(self): + return self + + def __call__(self, i): + return self.get_request(i) + +class SGPermission(Filter): + """Filter for verifying security group ingress and egress permissions + + All attributes of a security group permission are available as + value filters. + + If multiple attributes are specified the permission must satisfy + all of them. Note that within an attribute match against a list value + of a permission we default to or. + + If a group has any permissions that match all conditions, then it + matches the filter. + + Permissions that match on the group are annotated onto the group and + can subsequently be used by the remove-permission action. + + We have specialized handling for matching `Ports` in ingress/egress + permission From/To range. The following example matches on ingress + rules which allow for a range that includes all of the given ports. + + .. code-block:: yaml + + - type: ingress + Ports: [22, 443, 80] + + As well for verifying that a rule only allows for a specific set of ports + as in the following example. The delta between this and the previous + example is that if the permission allows for any ports not specified here, + then the rule will match. ie. OnlyPorts is a negative assertion match, + it matches when a permission includes ports outside of the specified set. + + .. code-block:: yaml + + - type: ingress + OnlyPorts: [22] + + For simplifying ipranges handling which is specified as a list on a rule + we provide a `Cidr` key which can be used as a value type filter evaluated + against each of the rules. If any iprange cidr match then the permission + matches. + + .. code-block:: yaml + + - type: ingress + IpProtocol: -1 + FromPort: 445 + + We also have specialized handling for matching self-references in + ingress/egress permissions. The following example matches on ingress + rules which allow traffic its own same security group. + + .. code-block:: yaml + + - type: ingress + SelfReference: True + + As well for assertions that a ingress/egress permission only matches + a given set of ports, *note* OnlyPorts is an inverse match. + + .. code-block:: yaml + + - type: egress + OnlyPorts: [22, 443, 80] + + - type: egress + Cidr: + value_type: cidr + op: in + value: x.y.z + + `Cidr` can match ipv4 rules and `CidrV6` can match ipv6 rules. In + this example we are blocking global inbound connections to SSH or + RDP. + + .. code-block:: yaml + + - type: ingress + Ports: [22, 3389] + Cidr: + value: + - "0.0.0.0/0" + - "::/0" + op: in + + `SGReferences` can be used to filter out SG references in rules. + In this example we want to block ingress rules that reference a SG + that is tagged with `Access: Public`. + + .. code-block:: yaml + + - type: ingress + SGReferences: + key: "tag:Access" + value: "Public" + op: equal + + We can also filter SG references based on the VPC that they are + within. In this example we want to ensure that our outbound rules + that reference SGs are only referencing security groups within a + specified VPC. + + .. code-block:: yaml + + - type: egress + SGReferences: + key: 'VpcId' + value: 'vpc-11a1a1aa' + op: equal + + Likewise, we can also filter SG references by their description. + For example, we can prevent egress rules from referencing any + SGs that have a description of "default - DO NOT USE". + + .. code-block:: yaml + + - type: egress + SGReferences: + key: 'Description' + value: 'default - DO NOT USE' + op: equal + + """ + + perm_attrs = { + 'IpProtocol', "Priority", 'Policy'} + filter_attrs = { + 'Cidr', 'CidrV6', 'Ports', 'OnlyPorts', + 'SelfReference', 'Description', 'SGReferences'} + attrs = perm_attrs.union(filter_attrs) + attrs.add('match-operator') + attrs.add('match-operator') + + def validate(self): + delta = set(self.data.keys()).difference(self.attrs) + delta.remove('type') + if delta: + raise PolicyValidationError("Unknown keys %s on %s" % ( + ", ".join(delta), self.manager.data)) + return self + + def process(self, resources, event=None): + self.vfilters = [] + fattrs = list(sorted(self.perm_attrs.intersection(self.data.keys()))) + self.ports = 'Ports' in self.data and self.data['Ports'] or () + self.only_ports = ( + 'OnlyPorts' in self.data and self.data['OnlyPorts'] or ()) + for f in fattrs: + fv = self.data.get(f) + if isinstance(fv, dict): + fv['key'] = f + else: + fv = {f: fv} + vf = ValueFilter(fv, self.manager) + vf.annotate = False + + self.vfilters.append(vf) + + return super(SGPermission, self).process(resources, event) + + def process_ports(self, perm): + found = None + if perm['remote_ip_prefix'] == '0.0.0.0/0' or perm['remote_ip_prefix'] == '::/0': + return True + if perm['port_range_min'] is None or perm['port_range_max'] is None : + return True + FromPort = int(perm['port_range_min']) + ToPort = int(perm['port_range_max']) + for port in self.ports: + if port >= FromPort and port <= ToPort: + found = True + break + elif FromPort == -1 and ToPort == -1: + found = True + break + else: + found = False + only_found = False + for port in self.only_ports: + if port == FromPort and port == ToPort: + only_found = True + if self.only_ports and not only_found: + found = found is None or found and True or False + if self.only_ports and only_found: + found = False + return found + + + def _process_cidr(self, cidr_key, cidr_type, SourceCidrIp, perm): + found = None + SourceCidrIp = perm.get(SourceCidrIp, "") + if not SourceCidrIp: + return False + SourceCidrIp = {cidr_type: SourceCidrIp} + match_range = self.data[cidr_key] + if isinstance(match_range, dict): + match_range['key'] = cidr_type + else: + match_range = {cidr_type: match_range} + vf = ValueFilter(match_range, self.manager) + vf.annotate = False + found = vf(SourceCidrIp) + if found: + found = True + else: + found = False + return found + + def process_cidrs(self, perm, ipv4Cidr, ipv6Cidr): + found_v6 = found_v4 = None + if 'CidrV6' in self.data: + found_v6 = self._process_cidr('CidrV6', 'CidrIpv6', ipv6Cidr, perm) + if 'Cidr' in self.data: + found_v4 = self._process_cidr('Cidr', 'CidrIp', ipv4Cidr, perm) + match_op = self.data.get('match-operator', 'and') == 'and' and all or any + cidr_match = [k for k in (found_v6, found_v4) if k is not None] + if not cidr_match: + return None + return match_op(cidr_match) + + def process_description(self, perm): + if 'Description' not in self.data: + return None + + d = dict(self.data['Description']) + d['key'] = 'Description' + + vf = ValueFilter(d, self.manager) + vf.annotate = False + + for k in ('Ipv6Ranges', 'IpRanges', 'UserIdGroupPairs', 'PrefixListIds'): + if k not in perm or not perm[k]: + continue + return vf(perm[k][0]) + return False + + def process_self_reference(self, perm, sg_id): + found = None + ref_match = self.data.get('SelfReference') + if ref_match is not None: + found = False + if 'UserIdGroupPairs' in perm and 'SelfReference' in self.data: + self_reference = sg_id in [p['GroupId'] + for p in perm['UserIdGroupPairs']] + if ref_match is False and not self_reference: + found = True + if ref_match is True and self_reference: + found = True + return found + + def process_sg_references(self, perm, owner_id): + sg_refs = self.data.get('SGReferences') + if not sg_refs: + return None + + sg_perm = perm.get('UserIdGroupPairs', []) + if not sg_perm: + return False + + sg_group_ids = [p['GroupId'] for p in sg_perm if p['UserId'] == owner_id] + sg_resources = self.manager.get_resources(sg_group_ids) + vf = ValueFilter(sg_refs, self.manager) + vf.annotate = False + + for sg in sg_resources: + if vf(sg): + return True + return False + + def __call__(self, resource): + result = self.securityGroupAttributeRequst(resource) + matched = [] + match_op = self.data.get('match-operator', 'and') == 'and' and all or any + for perm in jmespath.search(self.ip_permissions_key, result): + perm_matches = {} + perm_matches['ports'] = self.process_ports(perm) + perm_matches['cidrs'] = self.process_self_cidrs(perm) + perm_match_values = list(filter( + lambda x: x is not None, perm_matches.values())) + # account for one python behavior any([]) == False, all([]) == True + if match_op == all and not perm_match_values: + continue + + match = match_op(perm_match_values) + if match: + matched.append(perm) + if matched: + return True + +SGPermissionSchema = { + 'match-operator': {'type': 'string', 'enum': ['or', 'and']}, + 'Ports': {'type': 'array', 'items': {'type': 'integer'}}, + 'OnlyPorts': {'type': 'array', 'items': {'type': 'integer'}}, + 'Policy': {}, + 'IpProtocol': { + 'oneOf': [ + {'enum': ["-1", -1, 'TCP', 'UDP', 'ICMP', 'ICMPV6']}, + {'$ref': '#/definitions/filters/value'} + ] + }, + 'FromPort': {'oneOf': [ + {'$ref': '#/definitions/filters/value'}, + {'type': 'integer'}]}, + 'ToPort': {'oneOf': [ + {'$ref': '#/definitions/filters/value'}, + {'type': 'integer'}]}, + 'IpRanges': {}, + 'Cidr': {}, + 'CidrV6': {}, + 'SGReferences': {} +} \ No newline at end of file diff --git a/tools/c7n_openstack/c7n_openstack/filters/labels.py b/tools/c7n_openstack/c7n_openstack/filters/labels.py new file mode 100644 index 00000000000..4d791f5e77b --- /dev/null +++ b/tools/c7n_openstack/c7n_openstack/filters/labels.py @@ -0,0 +1,119 @@ +# Copyright 2019 Karol Lassak +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from datetime import datetime, timedelta + +from c7n.filters import Filter, FilterValidationError +from c7n.filters.offhours import Time +from c7n.utils import type_schema + +DEFAULT_TAG = "custodian_status" + + +class LabelActionFilter(Filter): + """Filter resources for label specified future action + + Filters resources by a 'custodian_status' label which specifies a future + date for an action. + + The filter parses the label values looking for an 'op@date' + string. The date is parsed and compared to do today's date, the + filter succeeds if today's date is gte to the target date. + + The optional 'skew' parameter provides for incrementing today's + date a number of days into the future. An example use case might + be sending a final notice email a few days before terminating an + instance, or snapshotting a volume prior to deletion. + + The optional 'skew_hours' parameter provides for incrementing the current + time a number of hours into the future. + + Optionally, the 'tz' parameter can get used to specify the timezone + in which to interpret the clock (default value is 'utc') + + :example: + + .. code-block :: yaml + + policies: + - name: vm-stop-marked + resource: openstack.instance + filters: + - type: marked-for-op + # The default label used is custodian_status + # but that is configurable + label: custodian_status + op: stop + # Another optional label is skew + tz: utc + + + """ + schema = type_schema( + 'marked-for-op', + label={'type': 'string'}, + tz={'type': 'string'}, + skew={'type': 'number', 'minimum': 0}, + skew_hours={'type': 'number', 'minimum': 0}, + op={'type': 'string'}) + + def validate(self): + op = self.data.get('op') + if self.manager and op not in self.manager.action_registry.keys(): + raise FilterValidationError( + "Invalid marked-for-op op:%s in %s" % (op, self.manager.data)) + + tz = Time.get_tz(self.data.get('tz', 'utc')) + if not tz: + raise FilterValidationError( + "Invalid timezone specified '%s' in %s" % ( + self.data.get('tz'), self.manager.data)) + return self + + def process(self, resources, event=None): + self.label = self.data.get('label', DEFAULT_TAG) + self.op = self.data.get('op', 'stop') + self.skew = self.data.get('skew', 0) + self.skew_hours = self.data.get('skew_hours', 0) + self.tz = Time.get_tz(self.data.get('tz', 'utc')) + return super(LabelActionFilter, self).process(resources, event) + + def __call__(self, i): + v = i.get('labels', {}).get(self.label, None) + + if v is None: + return False + if '-' not in v or '_' not in v: + return False + + msg, action, action_date_str = v.rsplit('-', 2) + + if action != self.op: + return False + + try: + action_date = datetime.strptime(action_date_str, '%Y_%m_%d__%H_%M') + except Exception: + self.log.error("could not parse label:%s value:%s on %s" % ( + self.label, v, i['name'])) + return False + + # current_date must match timezones with the parsed date string + if action_date.tzinfo: + action_date = action_date.astimezone(self.tz) + current_date = datetime.now(tz=self.tz) + else: + current_date = datetime.now() + + return current_date >= (action_date - timedelta(days=self.skew, hours=self.skew_hours)) diff --git a/tools/c7n_openstack/c7n_openstack/handler.py b/tools/c7n_openstack/c7n_openstack/handler.py new file mode 100644 index 00000000000..97343cda997 --- /dev/null +++ b/tools/c7n_openstack/c7n_openstack/handler.py @@ -0,0 +1,56 @@ +# Copyright The Cloud Custodian Authors. +# SPDX-License-Identifier: Apache-2.0 + +import json +import logging +import os +import uuid + +# Load resource plugins +from c7n_openstack.entry import initialize_openstack + +from c7n.config import Config +from c7n.loader import PolicyLoader + +initialize_openstack() + +log = logging.getLogger('custodian.openstack.functions') + +logging.getLogger().setLevel(logging.INFO) + + +def run(event, context=None): + # policies file should always be valid in functions so do loading naively + with open('config.json') as f: + policy_config = json.load(f) + + if not policy_config or not policy_config.get('policies'): + log.error('Invalid policy config') + return False + + # setup execution options + options = Config.empty(**policy_config.pop('execution-options', {})) + options.update( + policy_config['policies'][0].get('mode', {}).get('execution-options', {})) + # if output_dir specified use that, otherwise make a temp directory + if not options.output_dir: + options['output_dir'] = get_tmp_output_dir() + + loader = PolicyLoader(options) + policies = loader.load_data(policy_config, 'config.json', validate=False) + if policies: + for p in policies: + log.info("running policy %s", p.name) + p.validate() + p.push(event, context) + return True + + +def get_tmp_output_dir(): + output_dir = '/tmp/' + str(uuid.uuid4()) + if not os.path.exists(output_dir): + try: + os.mkdir(output_dir) + except OSError as error: + log.warning("Unable to make output directory: {}".format(error)) + return output_dir diff --git a/tools/c7n_openstack/c7n_openstack/mu.py b/tools/c7n_openstack/c7n_openstack/mu.py new file mode 100644 index 00000000000..cdce4e86dfd --- /dev/null +++ b/tools/c7n_openstack/c7n_openstack/mu.py @@ -0,0 +1,2 @@ +# Copyright The Cloud Custodian Authors. +# SPDX-License-Identifier: Apache-2.0 \ No newline at end of file diff --git a/tools/c7n_openstack/c7n_openstack/output.py b/tools/c7n_openstack/c7n_openstack/output.py new file mode 100644 index 00000000000..cdce4e86dfd --- /dev/null +++ b/tools/c7n_openstack/c7n_openstack/output.py @@ -0,0 +1,2 @@ +# Copyright The Cloud Custodian Authors. +# SPDX-License-Identifier: Apache-2.0 \ No newline at end of file diff --git a/tools/c7n_openstack/c7n_openstack/policy.py b/tools/c7n_openstack/c7n_openstack/policy.py new file mode 100644 index 00000000000..8157478dffb --- /dev/null +++ b/tools/c7n_openstack/c7n_openstack/policy.py @@ -0,0 +1,182 @@ +# Copyright 2018 Capital One Services, LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import logging + +from c7n_openstack import mu +from dateutil.tz import tz + +from c7n.exceptions import PolicyValidationError +from c7n.policy import execution, ServerlessExecutionMode, PullMode +from c7n.utils import local_session, type_schema + +DEFAULT_REGION = 'us-central1' + + +class FunctionMode(ServerlessExecutionMode): + + schema = type_schema( + 'openstack', + **{'execution-options': {'$ref': '#/definitions/basic_dict'}, + 'timeout': {'type': 'string'}, + 'memory-size': {'type': 'integer'}, + 'labels': {'$ref': '#/definitions/string_dict'}, + 'network': {'type': 'string'}, + 'max-instances': {'type': 'integer'}, + 'service-account': {'type': 'string'}, + 'environment': {'$ref': '#/definitions/string_dict'}} + ) + + def __init__(self, policy): + self.policy = policy + self.log = logging.getLogger('custodian.openstack.funcexec') + self.region = policy.options.regions[0] if len(policy.options.regions) else DEFAULT_REGION + + def run(self): + raise NotImplementedError("subclass responsibility") + + def provision(self): + self.log.info("Provisioning policy function %s", self.policy.name) + manager = mu.CloudFunctionManager(self.policy.session_factory, self.region) + return manager.publish(self._get_function()) + + def deprovision(self): + manager = mu.CloudFunctionManager(self.policy.session_factory, self.region) + return manager.remove(self._get_function()) + + def validate(self): + pass + + def _get_function(self): + raise NotImplementedError("subclass responsibility") + + +@execution.register('openstack-periodic') +class PeriodicMode(FunctionMode, PullMode): + """Deploy a policy as a Cloud Functions triggered by Cloud Scheduler + at user defined cron interval via Pub/Sub. + + Default region the function is deployed to is ``us-central1``. In + case you want to change that, use the cli ``--region`` flag. + """ + + schema = type_schema( + 'openstack-periodic', + rinherit=FunctionMode.schema, + required=['schedule'], + **{'trigger-type': {'enum': ['http', 'pubsub']}, + 'tz': {'type': 'string'}, + 'schedule': {'type': 'string'}}) + + def validate(self): + mode = self.policy.data['mode'] + if 'tz' in mode: + error = PolicyValidationError( + "policy:%s openstack-periodic invalid tz:%s" % ( + self.policy.name, mode['tz'])) + # We can't catch all errors statically, our local tz retrieval + # then the form openstack is using, ie. not all the same aliases are + # defined. + tzinfo = tz.gettz(mode['tz']) + if tzinfo is None: + raise error + + def _get_function(self): + events = [mu.PeriodicEvent( + local_session(self.policy.session_factory), + self.policy.data['mode'], + self.region + )] + return mu.PolicyFunction(self.policy, events=events) + + def run(self, event, context): + return PullMode.run(self) + + +@execution.register('openstack-audit') +class ApiAuditMode(FunctionMode): + """Custodian policy execution on openstack api audit logs events. + + Deploys as a Cloud Function triggered by api calls. This allows + you to apply your policies as soon as an api call occurs. Audit + logs creates an event for every api call that occurs in your openstack + account. See `openstack Audit Logs + `_ for more + details. + + Default region the function is deployed to is + ``us-central1``. In case you want to change that, use the cli + ``--region`` flag. + """ + + schema = type_schema( + 'openstack-audit', + methods={'type': 'array', 'items': {'type': 'string'}}, + required=['methods'], + rinherit=FunctionMode.schema) + + def resolve_resources(self, event): + """Resolve a openstack resource from its audit trail metadata. + """ + if self.policy.resource_manager.resource_type.get_requires_event: + return [self.policy.resource_manager.get_resource(event)] + resource_info = event.get('resource') + if resource_info is None or 'labels' not in resource_info: + self.policy.log.warning("Could not find resource information in event") + return + # copy resource name, the api doesn't like resource ids, just names. + if 'resourceName' in event['protoPayload']: + resource_info['labels']['resourceName'] = event['protoPayload']['resourceName'] + + resource = self.policy.resource_manager.get_resource(resource_info['labels']) + return [resource] + + def _get_function(self): + events = [mu.ApiSubscriber( + local_session(self.policy.session_factory), + self.policy.data['mode'])] + return mu.PolicyFunction(self.policy, events=events) + + def validate(self): + if not self.policy.resource_manager.resource_type.get: + raise PolicyValidationError( + "Resource:%s does not implement retrieval method" % ( + self.policy.resource_type)) + + def run(self, event, context): + """Execute a openstack serverless model""" + from c7n.actions import EventAction + + resources = self.resolve_resources(event) + if not resources: + return + + resources = self.policy.resource_manager.filter_resources( + resources, event) + + self.policy.log.info("Filtered resources %d" % len(resources)) + + if not resources: + return + + self.policy.ctx.metrics.put_metric( + 'ResourceCount', len(resources), 'Count', Scope="Policy", + buffer=False) + + for action in self.policy.resource_manager.actions: + if isinstance(action, EventAction): + action.process(resources, event) + else: + action.process(resources) + + return resources diff --git a/tools/c7n_openstack/c7n_openstack/provider.py b/tools/c7n_openstack/c7n_openstack/provider.py new file mode 100644 index 00000000000..f46a3013875 --- /dev/null +++ b/tools/c7n_openstack/c7n_openstack/provider.py @@ -0,0 +1,34 @@ +# Copyright The Cloud Custodian Authors. +# SPDX-License-Identifier: Apache-2.0 + +from c7n.registry import PluginRegistry +from c7n.provider import Provider, clouds + +from .resources.resource_map import ResourceMap +from .client import Session + +import logging + +log = logging.getLogger('custodian.openstack') + + +@clouds.register('openstack') +class OpenStack(Provider): + + display_name = 'openstack' + resource_prefix = 'openstack' + resources = PluginRegistry('%s.resources' % resource_prefix) + resource_map = ResourceMap + + def initialize(self, options): + return options + + def initialize_policies(self, policy_collection, options): + return policy_collection + + def get_session_factory(self, options): + """Get a credential/session factory for api usage.""" + return Session + + +resources = OpenStack.resources \ No newline at end of file diff --git a/tools/c7n_openstack/c7n_openstack/query.py b/tools/c7n_openstack/c7n_openstack/query.py new file mode 100644 index 00000000000..613684f808f --- /dev/null +++ b/tools/c7n_openstack/c7n_openstack/query.py @@ -0,0 +1,113 @@ +# Copyright The Cloud Custodian Authors. +# SPDX-License-Identifier: Apache-2.0 + +import logging + +from c7n.actions import ActionRegistry +from c7n.filters import FilterRegistry +from c7n.manager import ResourceManager +from c7n.query import sources +from c7n.utils import local_session + +log = logging.getLogger('custodian.openstack.query') + + +class ResourceQuery: + def __init__(self, session_factory): + self.session_factory = session_factory + + def filter(self, resource_manager, **params): + m = resource_manager.resource_type + session = local_session(self.session_factory) + client = session.client() + + enum_op, extra_args = m.enum_spec + if extra_args: + params.update(extra_args) + return self._invoke_client_enum(client, enum_op, params) + + def _invoke_client_enum(self, client, enum_op, params): + res = getattr(client, enum_op)(**params) + return res + + +@sources.register('describe-openstack') +class DescribeSource: + def __init__(self, manager): + self.manager = manager + self.query = ResourceQuery(manager.session_factory) + + def get_resources(self, query): + if query is None: + query = {} + return self.query.filter(self.manager, **query) + + def get_permissions(self): + return () + + def augment(self, resources): + return resources + + +class QueryMeta(type): + """metaclass to have consistent action/filter registry for new resources""" + def __new__(cls, name, parents, attrs): + if 'filter_registry' not in attrs: + attrs['filter_registry'] = FilterRegistry( + '%s.filters' % name.lower()) + if 'action_registry' not in attrs: + attrs['action_registry'] = ActionRegistry( + '%s.actions' % name.lower()) + + return super(QueryMeta, cls).__new__(cls, name, parents, attrs) + + +class QueryResourceManager(ResourceManager, metaclass=QueryMeta): + def __init__(self, data, options): + super(QueryResourceManager, self).__init__(data, options) + self.source = self.get_source(self.source_type) + + def get_permissions(self): + return () + + def get_source(self, source_type): + return sources.get(source_type)(self) + + def get_client(self): + client = local_session(self.session_factory).client() + return client + + def get_model(self): + return self.resource_type + + def get_cache_key(self, query): + return {'source_type': self.source_type, 'query': query} + + @property + def source_type(self): + return self.data.get('source', 'describe-openstack') + + def get_resource_query(self): + if 'query' in self.data: + return {'filter': self.data.get('query')} + + def resources(self, query=None): + q = query or self.get_resource_query() + key = self.get_cache_key(q) + resources = self.augment(self.source.get_resources(q)) + self._cache.save(key, resources) + return self.filter_resources(resources) + + def augment(self, resources): + return resources + + +class TypeMeta(type): + def __repr__(cls): + return "" % ( + cls.group, + cls.version) + + +class TypeInfo(metaclass=TypeMeta): + enum_spec = () \ No newline at end of file diff --git a/tools/c7n_openstack/c7n_openstack/resources/__init__.py b/tools/c7n_openstack/c7n_openstack/resources/__init__.py new file mode 100644 index 00000000000..cdce4e86dfd --- /dev/null +++ b/tools/c7n_openstack/c7n_openstack/resources/__init__.py @@ -0,0 +1,2 @@ +# Copyright The Cloud Custodian Authors. +# SPDX-License-Identifier: Apache-2.0 \ No newline at end of file diff --git a/tools/c7n_openstack/c7n_openstack/resources/flavor.py b/tools/c7n_openstack/c7n_openstack/resources/flavor.py new file mode 100644 index 00000000000..6b18ea92d17 --- /dev/null +++ b/tools/c7n_openstack/c7n_openstack/resources/flavor.py @@ -0,0 +1,69 @@ +# Copyright The Cloud Custodian Authors. +# SPDX-License-Identifier: Apache-2.0 +# +from c7n.filters import Filter +from c7n.utils import type_schema +from c7n_openstack.provider import resources +from c7n_openstack.query import QueryResourceManager, TypeInfo + + +@resources.register('flavor') +class Flavor(QueryResourceManager): + class resource_type(TypeInfo): + enum_spec = ('list_flavors', None) + id = 'id' + name = 'name' + default_report_fields = ['id', 'name', 'vcpus', 'ram', 'disk'] + +@Flavor.filter_registry.register('system') +class FlavorFilter(Filter): + """Filters Flavors based on their system + :example: + .. code-block:: yaml + policies: + - name: openstack-flavor + resource: openstack.flavor + filters: + - type: system + is_public: true + ram: 512 + vcpus: 1 + disk: 1 + """ + + schema = type_schema( + 'system', + is_public={'type': 'boolean'}, + ram={'type': 'number'}, + vcpus={'type': 'number'}, + disk={'type': 'number'}, + ) + + def _match_(self, flavor, is_public, ram, vcpus, disk): + if is_public: + if is_public != flavor.is_public: + return True + else: + if is_public == flavor.is_public: + return True + if ram: + if flavor.ram > ram: + return True + if vcpus: + if flavor.vcpus > vcpus: + return True + if disk: + if flavor.disk > disk: + return True + return False + + def process(self, resources, event=None): + results = [] + is_public = self.data.get('is_public', None) + ram = self.data.get('ram', None) + vcpus = self.data.get('vcpus', None) + disk = self.data.get('disk', None) + for flavor in resources: + if self._match_(flavor, is_public, ram, vcpus, disk): + results.append(flavor) + return results \ No newline at end of file diff --git a/tools/c7n_openstack/c7n_openstack/resources/image.py b/tools/c7n_openstack/c7n_openstack/resources/image.py new file mode 100644 index 00000000000..7dfbe6981bf --- /dev/null +++ b/tools/c7n_openstack/c7n_openstack/resources/image.py @@ -0,0 +1,62 @@ +# Copyright The Cloud Custodian Authors. +# SPDX-License-Identifier: Apache-2.0 +# +from c7n.filters import Filter +from c7n.utils import local_session +from c7n.utils import type_schema +from c7n_openstack.provider import resources +from c7n_openstack.query import QueryResourceManager, TypeInfo + + +@resources.register('image') +class Image(QueryResourceManager): + class resource_type(TypeInfo): + enum_spec = ('list_images', None) + id = 'id' + name = 'name' + default_report_fields = ['id', 'name', 'status', 'visibility'] + +@Image.filter_registry.register('status') +class StatusFilter(Filter): + """Filters Images based on their status + :example: + .. code-block:: yaml + policies: + - name: openstack-image + resource: openstack.image + filters: + - not: + - type: status + image_name: centos + visibility: private + status: active + """ + schema = type_schema( + 'status', + image_name={'type': 'string'}, + visibility={'type': 'string'}, + status={'type': 'string'} + ) + + def process(self, resources, event=None): + results = [] + image_name = self.data.get('image_name', None) + visibility = self.data.get('visibility', None) + status = self.data.get('status', None) + + for image in resources: + matched = True + if not image: + if status == "absent": + results.append(image) + continue + if image_name is not None and image_name != image.name: + matched = False + if visibility is not None and visibility != image.visibility: + matched = False + if status is not None and status != image.status: + matched = False + if matched: + results.append(image) + return results + diff --git a/tools/c7n_openstack/c7n_openstack/resources/network.py b/tools/c7n_openstack/c7n_openstack/resources/network.py new file mode 100644 index 00000000000..b3c414ff7ce --- /dev/null +++ b/tools/c7n_openstack/c7n_openstack/resources/network.py @@ -0,0 +1,59 @@ +# Copyright The Cloud Custodian Authors. +# SPDX-License-Identifier: Apache-2.0 +# +from c7n_openstack.provider import resources +from c7n_openstack.query import QueryResourceManager, TypeInfo +from c7n.filters import Filter +from c7n.utils import type_schema + +@resources.register('network') +class Network(QueryResourceManager): + class resource_type(TypeInfo): + enum_spec = ('list_networks', None) + id = 'id' + name = 'name' + default_report_fields = ['id', 'name', 'status', 'shared'] + +@Network.filter_registry.register('system') +class NetworkFilter(Filter): + """Filters Networks based on their system + :example: + .. code-block:: yaml + policies: + - name: openstack-network + resource: openstack.network + filters: + - not: + - type: system + status: ACTIVE + shared: false + port_security_enabled: true + """ + schema = type_schema( + 'system', + status={'type': 'string'}, + shared={'type': 'boolean'}, + port_security_enabled={'type': 'boolean'}, + ) + + def _match_(self, network, status, shared, port_security_enabled): + if status: + if status != network.status: + return True + if shared: + if shared != network.shared: + return True + if port_security_enabled: + if port_security_enabled != network.port_security_enabled: + return True + return False + + def process(self, resources, event=None): + results = [] + status = self.data.get('status', None) + shared = self.data.get('shared', None) + port_security_enabled = self.data.get('port_security_enabled', None) + for network in resources: + if self._match_(network, status, shared, port_security_enabled): + results.append(network) + return results diff --git a/tools/c7n_openstack/c7n_openstack/resources/project.py b/tools/c7n_openstack/c7n_openstack/resources/project.py new file mode 100644 index 00000000000..ea8eaebb640 --- /dev/null +++ b/tools/c7n_openstack/c7n_openstack/resources/project.py @@ -0,0 +1,52 @@ +# Copyright The Cloud Custodian Authors. +# SPDX-License-Identifier: Apache-2.0 +# +from c7n_openstack.query import QueryResourceManager, TypeInfo +from c7n_openstack.provider import resources +from c7n.utils import local_session +from c7n.utils import type_schema +from c7n.filters import Filter + + +@resources.register('project') +class Project(QueryResourceManager): + class resource_type(TypeInfo): + id = 'id' + name = 'name' + enum_spec = ('list_projects', None) + default_report_fields = ['id', 'name'] + +@Project.filter_registry.register('user') +class UserFilter(Filter): + """Filters Projects based on their user + :example: + .. code-block:: yaml + policies: + - name: demo + resource: openstack.project + filters: + - type: user + system_scope: true + """ + schema = type_schema( + 'user', + system_scope={'type': 'boolean'}, + ) + + def process(self, resources, event=None): + results = [] + system_scope = self.data.get('system_scope', None) + openstack = local_session(self.manager.session_factory).client() + for project in resources: + users = openstack.list_users() + params = [] + for user in users: + if system_scope: + if user.default_project_id != project.id: + params.append(user) + else: + if user.default_project_id == project.id: + params.append(user) + if len(params) == 0: + results.append(project) + return results \ No newline at end of file diff --git a/tools/c7n_openstack/c7n_openstack/resources/resource_map.py b/tools/c7n_openstack/c7n_openstack/resources/resource_map.py new file mode 100644 index 00000000000..d2bb97fb068 --- /dev/null +++ b/tools/c7n_openstack/c7n_openstack/resources/resource_map.py @@ -0,0 +1,13 @@ +# Copyright The Cloud Custodian Authors. +# SPDX-License-Identifier: Apache-2.0 +ResourceMap = { + "openstack.project": "c7n_openstack.resources.project.Project", + "openstack.server": "c7n_openstack.resources.server.Server", + "openstack.flavor": "c7n_openstack.resources.server.Flavor", + "openstack.user": "c7n_openstack.resources.user.User", + "openstack.volume": "c7n_openstack.resources.volume.Volume", + "openstack.image": "c7n_openstack.resources.image.Image", + "openstack.network": "c7n_openstack.resources.network.Network", + "openstack.security-groups": "c7n_openstack.resources.security-groups.SecurityGroups", + "openstack.router": "c7n_openstack.resources.router.Router", +} \ No newline at end of file diff --git a/tools/c7n_openstack/c7n_openstack/resources/router.py b/tools/c7n_openstack/c7n_openstack/resources/router.py new file mode 100644 index 00000000000..5e6ad93a1f4 --- /dev/null +++ b/tools/c7n_openstack/c7n_openstack/resources/router.py @@ -0,0 +1,54 @@ +# Copyright The Cloud Custodian Authors. +# SPDX-License-Identifier: Apache-2.0 +# +from c7n.utils import type_schema +from c7n_openstack.filters.filter import Filter +from c7n_openstack.provider import resources +from c7n_openstack.query import QueryResourceManager, TypeInfo + + +@resources.register('router') +class Router(QueryResourceManager): + class resource_type(TypeInfo): + enum_spec = ('list_routers', None) + id = 'id' + name = 'name' + default_report_fields = ['id', 'name', 'status', 'ha'] + +@Router.filter_registry.register('system') +class RouterFilter(Filter): + """Filters Routers based on their system + :example: + .. code-block:: yaml + policies: + - name: openstack-router + resource: openstack.router + filters: + - not: + - type: system + status: ACTIVE + ha: true + """ + schema = type_schema( + 'system', + status={'type': 'string'}, + ha={'type': 'boolean'}, + ) + + def _match_(self, router, status, ha): + if ha: + if ha != router.ha: + return True + if status: + if status != router.status: + return True + return False + + def process(self, resources, event=None): + results = [] + status = self.data.get('status', None) + ha = self.data.get('ha', None) + for router in resources: + if self._match_(router, status, ha): + results.append(router) + return results diff --git a/tools/c7n_openstack/c7n_openstack/resources/security-groups.py b/tools/c7n_openstack/c7n_openstack/resources/security-groups.py new file mode 100644 index 00000000000..aebb23c8572 --- /dev/null +++ b/tools/c7n_openstack/c7n_openstack/resources/security-groups.py @@ -0,0 +1,94 @@ +# Copyright The Cloud Custodian Authors. +# SPDX-License-Identifier: Apache-2.0 +# +import jmespath + +from c7n.utils import type_schema +from c7n_openstack.filters.filter import SGPermission +from c7n_openstack.provider import resources +from c7n_openstack.query import QueryResourceManager, TypeInfo +from c7n_openstack.filters.filter import SGPermissionSchema, OpenstackFilter + + +@resources.register('security-groups') +class SecurityGroups(QueryResourceManager): + class resource_type(TypeInfo): + enum_spec = ('list_security_groups', None) + id = 'id' + name = 'name' + default_report_fields = ['id', 'name'] + +@SecurityGroups.filter_registry.register('ingress') +class IPIngressPermission(SGPermission): + """Filters SecurityGroups based on their system + :example: + .. code-block:: yaml + policies: + # 扫描开放以下高危端口的安全组: + - name: openstack-security-groups + resource: openstack.security-groups + filters: + - or: + - type: ingress + IpProtocol: "-1" + Ports: [20,21,22,25,80,773,765, 1733,1737,3306,3389,7333,5732,5500] + Cidr: "0.0.0.0/0" + - type: ingress + IpProtocol: "-1" + Ports: [20,21,22,25,80,773,765, 1733,1737,3306,3389,7333,5732,5500] + CidrV6: "::/0" + """ + ip_permissions_key = "security_group_rules" + schema = { + 'type': 'object', + 'additionalProperties': False, + 'properties': {'type': {'enum': ['ingress']}}, + 'required': ['type']} + schema['properties'].update(SGPermissionSchema) + + def process_self_cidrs(self, perm): + return self.process_cidrs(perm, 'remote_ip_prefix', 'remote_ip_prefix') + + def securityGroupAttributeRequst(self, sg): + self.direction = 'Ingress' + return sg + +@SecurityGroups.filter_registry.register('egress') +class IPEgressPermission(SGPermission): + schema = { + 'type': 'object', + 'additionalProperties': False, + 'properties': {'type': {'enum': ['egress']}}, + 'required': ['type']} + schema['properties'].update(SGPermissionSchema) + + def securityGroupAttributeRequst(self, sg): + self.direction = 'Egress' + return sg + +@SecurityGroups.filter_registry.register('source-cidr-ip') +class SourceCidrIp(OpenstackFilter): + + """Filters + :Example: + .. code-block:: yaml + + policies: + # 账号下安全组配置不为“0.0.0.0/0”,视为“合规”。 + - name: openstack-sg-source-cidr-ip + resource: openstack.security-groups + filters: + - type: source-cidr-ip + value: "0.0.0.0/0" + """ + + ip_permissions_key = "security_group_rules" + schema = type_schema( + 'source-cidr-ip', + **{'value': {'type': 'string'}}) + + def get_request(self, sg): + for cidr in jmespath.search(self.ip_permissions_key, sg): + if cidr['remote_ip_prefix'] == self.data['value']: + return sg + return False diff --git a/tools/c7n_openstack/c7n_openstack/resources/server.py b/tools/c7n_openstack/c7n_openstack/resources/server.py new file mode 100644 index 00000000000..c3f220cf6f9 --- /dev/null +++ b/tools/c7n_openstack/c7n_openstack/resources/server.py @@ -0,0 +1,232 @@ +# Copyright The Cloud Custodian Authors. +# SPDX-License-Identifier: Apache-2.0 +# +from c7n_openstack.query import QueryResourceManager, TypeInfo +from c7n_openstack.provider import resources +from c7n.utils import local_session +from c7n.utils import type_schema +from c7n.filters import Filter +from c7n.filters import AgeFilter + + +@resources.register('server') +class Server(QueryResourceManager): + class resource_type(TypeInfo): + enum_spec = ('list_servers', None) + id = 'id' + name = 'name' + + set_server_metadata = "set_server_metadata" + delete_server_metadata = "delete_server_metadata" + add_server_tag = "add_server_tag" + set_server_tag = "set_server_tag" + delete_server_tag = "delete_server_tag" + + default_report_fields = ['id', 'name', 'status', 'tenant_id'] + + +@Server.filter_registry.register('image') +class ImageFilter(Filter): + """Filters Servers based on their image attributes + :example: + .. code-block:: yaml + policies: + - name: openstack-image + resource: openstack.server + filters: + - not: + - type: image + image_name: centos + visibility: private + status: active + """ + schema = type_schema( + 'image', + image_name={'type': 'string'}, + visibility={'type': 'string'}, + status={'type': 'string'}) + + def process(self, resources, event=None): + results = [] + client = local_session(self.manager.session_factory).client() + image_name = self.data.get('image_name', None) + visibility = self.data.get('visibility', None) + status = self.data.get('status', None) + + images = client.list_images() + for r in resources: + image = find_object_by_property(images, 'id', r.image.id) + r.image = image + matched = True + if not image: + if status == "absent": + results.append(r) + continue + if image_name is not None and image_name != image.name: + matched = False + if visibility is not None and visibility != image.visibility: + matched = False + if status is not None and status != image.status: + matched = False + if matched: + results.append(r) + return results + + +@Server.filter_registry.register('flavor') +class FlavorFilter(Filter): + """Filters Servers based on their flavor attributes + :example: + .. code-block:: yaml + policies: + - name: openstack-server-flavor + resource: openstack.server + filters: + - type: flavor + is_public: true + ram: 512 + vcpus: 1 + disk: 1 + """ + schema = type_schema( + 'flavor', + flavor_name={'type': 'string'}, + flavor_id={'type': 'string'}, + vcpus={'type': 'integer'}, + ram={'type': 'integer'}, + swap={'type': 'integer'}, + disk={'type': 'integer'}, + ephemeral={'type': 'integer'}, + is_public={'type': 'boolean'}, + ) + + def server_match_flavor(self, server, flavor_name, flavor_id, + vcpus, ram, disk, ephemeral, is_public): + openstack = local_session(self.manager.session_factory).client() + server_flavor_name = server.flavor.original_name + flavor = openstack.get_flavor(server_flavor_name) + if not flavor: + return False + if flavor_name and flavor.name != flavor_name: + return False + if flavor_id and flavor.id != flavor_id: + return False + if vcpus and flavor.vcpus > int(vcpus): + return False + if ram and flavor.ram > int(ram): + return False + if disk and flavor.disk > int(disk): + return False + if ephemeral and flavor.ephemeral != int(ephemeral): + return False + if is_public is not None and flavor.is_public != is_public: + return False + return True + + def process(self, resources, event=None): + results = [] + flavor_name = self.data.get('flavor_name', None) + flavor_id = self.data.get('flavor_id', None) + vcpus = self.data.get('vcpus', None) + ram = self.data.get('ram', None) + disk = self.data.get('disk', None) + ephemeral = self.data.get('ephemeral', None) + is_public = self.data.get('is_public', None) + for server in resources: + if self.server_match_flavor(server, flavor_name, flavor_id, + vcpus, ram, disk, ephemeral, + is_public): + results.append(server) + return results + + +@Server.filter_registry.register('age') +class AgeFilter(AgeFilter): + + date_attribute = "launched_at" + + schema = type_schema( + 'age', + op={'$ref': '#/definitions/filters_common/comparison_operators'}, + days={'type': 'number'}, + hours={'type': 'number'}, + minutes={'type': 'number'}) + + def get_resource_data(self, i): + if i.get("launched_at"): + return i.get("launched_at") + return i.get("created_at") + + +@Server.filter_registry.register('tags') +class TagsFilter(Filter): + """Filters Servers based on their tags + :example: + .. code-block:: yaml + policies: + - name: openstack-server-tags + resource: openstack.server + filters: + - type: tags + tags: + - key: a + value: b + """ + tags_definition = { + 'type': 'array', + 'items': { + 'type': 'object', + 'properties': { + 'key': {'type': 'string'}, + 'value': {'type': 'string'} + }, + 'required': ['key', 'value'], + } + } + schema = type_schema( + 'tags', + tags=tags_definition, + op={'type': 'string', 'enum': ['any', 'all']}, + ) + + def match_any_tags(self, server, tags): + for t in tags: + str_tag = "%s=%s" % (t.get('key'), t.get('value')) + if str_tag in server.tags: + return True + return False + + def match_all_tags(self, server, tags): + for t in tags: + str_tag = "%s=%s" % (t.get('key'), t.get('value')) + if str_tag not in server.tags: + return False + return True + + def process(self, resources, event=None): + results = [] + tags = self.data.get('tags', []) + op = self.data.get('op', 'all') + match_fn = { + 'any': self.match_any_tags, + 'all': self.match_all_tags + } + for server in resources: + if match_fn[op](server, tags): + results.append(server) + return results + + +def find_object_by_property(collection, k, v): + result = [] + for d in collection: + if hasattr(d, k): + value = getattr(d, k) + else: + value = d.get(k) + if (v is None and value is None) or value == v: + result.append(d) + if not result: + return None + assert(len(result) == 1) + return result[0] \ No newline at end of file diff --git a/tools/c7n_openstack/c7n_openstack/resources/user.py b/tools/c7n_openstack/c7n_openstack/resources/user.py new file mode 100644 index 00000000000..029664cbf15 --- /dev/null +++ b/tools/c7n_openstack/c7n_openstack/resources/user.py @@ -0,0 +1,108 @@ +# Copyright The Cloud Custodian Authors. +# SPDX-License-Identifier: Apache-2.0 +# +from c7n_openstack.query import QueryResourceManager, TypeInfo +from c7n_openstack.provider import resources +from c7n.utils import local_session +from c7n.utils import type_schema +from c7n.filters import Filter + + +@resources.register('user') +class User(QueryResourceManager): + class resource_type(TypeInfo): + enum_spec = ('list_users', None) + id = 'id' + name = 'name' + default_report_fields = ['id', 'name', 'enabled', 'description'] + + +@User.filter_registry.register('project') +class ProjectFilter(Filter): + """Filters Users based on their project + :example: + .. code-block:: yaml + policies: + - name: demo + resource: openstack.user + filters: + - type: project + default_project_id: '' + """ + schema = type_schema( + 'project', + default_project_id={'type': 'string'}, + ) + def process(self, resources, event=None): + results = [] + default_project_id = self.data.get('default_project_id', None) + for user in resources: + if default_project_id != '' and user.default_project_id != default_project_id: + results.append(user) + elif default_project_id == '' and user.default_project_id is None: + results.append(user) + return results + +@User.filter_registry.register('role') +class RoleFilter(Filter): + """Filters Users based on their role + :example: + .. code-block:: yaml + policies: + - name: demo + resource: openstack.user + filters: + - type: role + role_name: admin + system_scope: true + """ + schema = type_schema( + 'role', + role_name={'type': 'string'}, + role_id={'type': 'string'}, + project_name={'type': 'string'}, + project_id={'type': 'string'}, + system_scope={'type': 'boolean'}, + ) + + def user_match_role(self, assignments, user_id, + role_id, project_id, system_scope): + for p in assignments: + if user_id and p.get('user', '') != user_id: + continue + if system_scope and p.get('project'): + continue + if project_id and p.get('project', '') != project_id: + continue + if role_id and p.id != role_id: + continue + return True + return False + + def process(self, resources, event=None): + results = [] + openstack = local_session(self.manager.session_factory).client() + role_name = self.data.get('role_name', None) + role_id = self.data.get('role_id', None) + project_name = self.data.get('project_name', None) + project_id = self.data.get('project_id', None) + system_scope = self.data.get('system_scope', False) + if not role_id and role_name: + role = openstack.get_role(role_name) + if role: + role_id = role.id + else: + raise ValueError(f"Role {role_name} doesn't exists") + if not project_id and project_name: + project = openstack.get_project(project_name) + if project: + project_id = project.id + else: + raise ValueError(f"Project {project_name} doesn't exists") + assignments = openstack.list_role_assignments() + for user in resources: + user_id = user.id + if self.user_match_role(assignments, user_id, role_id, + project_id, system_scope): + results.append(user) + return results \ No newline at end of file diff --git a/tools/c7n_openstack/c7n_openstack/resources/volume.py b/tools/c7n_openstack/c7n_openstack/resources/volume.py new file mode 100644 index 00000000000..eb43f43a628 --- /dev/null +++ b/tools/c7n_openstack/c7n_openstack/resources/volume.py @@ -0,0 +1,61 @@ +# Copyright The Cloud Custodian Authors. +# SPDX-License-Identifier: Apache-2.0 +# +from c7n.filters import Filter +from c7n.utils import type_schema +from c7n_openstack.provider import resources +from c7n_openstack.query import QueryResourceManager, TypeInfo + + +@resources.register('volume') +class Volume(QueryResourceManager): + class resource_type(TypeInfo): + enum_spec = ('list_volumes', None) + id = 'id' + name = 'name' + default_report_fields = ['id', 'name', 'status', 'size'] + +@Volume.filter_registry.register('status') +class StatusFilter(Filter): + """Filters Volumes based on their status + :example: + .. code-block:: yaml + policies: + - name: openstack-volume + resource: openstack.volume + filters: + - type: status + volume_status: 'in-use' + system_scope: true + """ + schema = type_schema( + 'status', + is_encrypted={'type': 'boolean'}, + volume_status={'type': 'string'}, + system_scope={'type': 'boolean'}, + ) + + def _match_(self, volume, is_encrypted, volume_status, system_scope): + if is_encrypted: + if is_encrypted != volume.is_encrypted: + return True + if volume_status: + if volume_status != volume.status: + return True + if system_scope: + if len(volume.attachments) == 0: + return True + else: + if len(volume.attachments) > 0: + return True + return False + + def process(self, resources, event=None): + results = [] + is_encrypted = self.data.get('is_encrypted', None) + volume_status = self.data.get('volume_status', None) + system_scope = self.data.get('system_scope', None) + for volume in resources: + if self._match_(volume, is_encrypted, volume_status, system_scope): + results.append(volume) + return results diff --git a/tools/c7n_openstack/pyproject.toml b/tools/c7n_openstack/pyproject.toml new file mode 100644 index 00000000000..85f93b19dde --- /dev/null +++ b/tools/c7n_openstack/pyproject.toml @@ -0,0 +1,29 @@ +[tool.poetry] +name = "c7n_openstack" +version = "0.1.0" +description = "Cloud Custodian - OpenStack Provider" +readme = "readme.md" +authors = ["Cloud Custodian Project"] +homepage = "https://cloudcustodian.io" +repository = "https://github.com/cloud-custodian/cloud-custodian" +documentation = "https://cloudcustodian.io/docs/" +license = "Apache-2.0" +classifiers = [ + "Topic :: System :: Systems Administration", + "Topic :: System :: Distributed Computing" +] + +[tool.poetry.dependencies] +python = "^3.7" +openstacksdk = "^0.52.0" +cryptography = "^2.9.2" + +[tool.poetry.dev-dependencies] +c7n = {path = "../..", develop = true} +pytest = "~6.0.0" +vcrpy = "^4.0.2" + + +[build-system] +requires = ["poetry>=0.12", "setuptools"] +build-backend = "poetry.masonry.api" \ No newline at end of file diff --git a/tools/c7n_openstack/readme.md b/tools/c7n_openstack/readme.md new file mode 100644 index 00000000000..578a85b72d1 --- /dev/null +++ b/tools/c7n_openstack/readme.md @@ -0,0 +1,115 @@ +# Custodian OpenStack Support + +Work in Progress - Not Ready For Use. + +## Quick Start + +### Installation + +``` +pip install c7n_openstack +``` + +### OpenStack Environment Configration + +C7N will find cloud config for as few as 1 cloud and as many as you want to put in a config file. +It will read environment variables and config files, and it also contains some vendor specific default +values so that you don't have to know extra info to use OpenStack: + +* If you have a config file, you will get the clouds listed in it +* If you have environment variables, you will get a cloud named envvars +* If you have neither, you will get a cloud named defaults with base defaults + +Create a clouds.yml file: + +```yaml +clouds: + demo: + region_name: RegionOne + auth: + username: 'admin' + password: XXXXXXX + project_name: 'admin' + domain_name: 'Default' + auth_url: 'https://montytaylor-sjc.openstack.blueboxgrid.com:5001/v2.0' +``` + +Please note: c7n will look for a file called `clouds.yaml` in the following locations: + +* Current Directory +* ~/.config/openstack +* /etc/openstack + +More information at [https://pypi.org/project/os-client-config](https://pypi.org/project/os-client-config) + +### Create a c7n policy yaml file as follows: + +```yaml +policies: +- name: demo + resource: openstack.flavor + filters: + - type: value + key: vcpus + value: 1 + op: gt +``` + +### Run c7n and report the matched resources: + +```sh +mkdir -p output +custodian run demo.yaml -s output +custodian report demo.yaml -s output --format grid +``` + +## Examples + +filter examples: + +```yaml +policies: +- name: test-flavor + resource: openstack.flavor + filters: + - type: value + key: vcpus + value: 1 + op: gt +- name: test-project + resource: openstack.project + filters: [] +- name: test-server-image + resource: openstack.server + filters: + - type: image + image_name: cirros-0.5.1 +- name: test-user + resource: openstack.user + filters: + - type: role + project_name: demo + role_name: _member_ + system_scope: false +- name: test-server-flavor + resource: openstack.server + filters: + - type: flavor + vcpus: 1 +- name: test-server-age + resource: openstack.server + filters: + - type: age + op: lt + days: 1 +- name: test-server-tags + resource: openstack.server + filters: + - type: tags + tags: + - key: a + value: a + - key: b + value: c + op: any +``` diff --git a/tools/c7n_openstack/requirements.txt b/tools/c7n_openstack/requirements.txt new file mode 100644 index 00000000000..0d049ac38dd --- /dev/null +++ b/tools/c7n_openstack/requirements.txt @@ -0,0 +1,3 @@ +openstacksdk==0.52.0; +python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" +keystoneauth1 <= 3.4.0 and keystoneauth1 > = 3.0.0 \ No newline at end of file diff --git a/tools/c7n_openstack/setup.py b/tools/c7n_openstack/setup.py new file mode 100644 index 00000000000..fc5d75a5ab1 --- /dev/null +++ b/tools/c7n_openstack/setup.py @@ -0,0 +1,36 @@ +# Automatically generated from poetry/pyproject.toml +# flake8: noqa +# -*- coding: utf-8 -*- +from setuptools import setup + +packages = [ + 'c7n_openstack', + 'c7n_openstack.resources' +] + +package_data = {'': ['*']} + +install_requires = \ + ['openstacksdk (>=0.52.0)', + 'c7n (>=0.9.8,<0.10.0)'] + + +setup_kwargs = { + 'name': 'c7n-openstack', + 'version': '0.0.1', + 'description': 'Cloud Custodian - OpenStack Provider', + 'long_description': '# Custodian OpenStack Support', + 'long_description_content_type': 'text/markdown', + 'author': 'Cloud Custodian Project', + 'author_email': None, + 'maintainer': None, + 'maintainer_email': None, + 'url': 'https://cloudcustodian.io', + 'packages': packages, + 'package_data': package_data, + 'install_requires': install_requires, + 'python_requires': '>=3.6,<4.0', +} + + +setup(**setup_kwargs) \ No newline at end of file diff --git a/tools/c7n_openstack/test/data.flight/default.yml b/tools/c7n_openstack/test/data.flight/default.yml new file mode 100644 index 00000000000..6c890a1aeec --- /dev/null +++ b/tools/c7n_openstack/test/data.flight/default.yml @@ -0,0 +1,336 @@ +interactions: + - request: + body: null + headers: + Accept: [application/json] + Content-Type: [application/json] + method: GET + uri: http://keystone:5000/ + response: + body: {string: '{"versions": {"values": [{"id": "v3.14", "status": "stable", "updated": "2020-04-07T00:00:00Z", "links": [{"rel": "self", "href": "http://127.0.0.1:5000/v3/"}], "media-types": [{"base": "application/json", "type": "application/vnd.openstack.identity-v3+j +son"}]}]}}'} + headers: + Content-Type: [application/json] + status: {code: 200, message: OK} + - request: + body: null + headers: + Accept: [application/json] + Content-Type: [application/json] + method: POST + uri: http://keystone:5000/v3/auth/tokens + response: + body: {string: '{"token": {"methods": ["password"], "user": {"domain": {"id": "default", "name": "Default"}, "id": "0ca05c20ee48419bb171707560ad793b", "name": "admin", "password_expires_at": null}, "audit_ids": ["EisDtEpHTcu-_VxIz4jP8w"], "expires_at": "2020-11-26T04:43:09.000000Z", "issued_at": "2020-11-26T03:43:09.000000Z", "project": {"domain": {"id": "default", "name": "Default"}, "id": "3d1d9e8cf44143abbd582e026fa507a3", "name": "admin"}, "is_domain": false, "roles": [{"id": "6135c43502b64aafb105bab98efd8595", "name": "admin"}, {"id": "13df80fdc9064e0482e1485a6294adfd", "name": "reader"}, {"id": "0988d05f27434167b372588bff13f967", "name": "member"}], "catalog": [{"endpoints": [{"id": "0c167064c60b4d31adaac7b9f9e695a4", "interface": "internal", "region_id": "RegionOne", "url": "http://keystone:8080/v1/AUTH_3d1d9e8cf44143abbd582e026fa507a3", "region": "RegionOne"}, {"id": "a72dda2f782e4350a41aad1d6d85ce5a", "interface": "admin", "region_id": "RegionOne", "url": "http://keystone:8080/v1/AUTH_3d1d9e8cf44143abbd582e026fa507a3", "region": "RegionOne"}, {"id": "b18e9aff73af4b57b456b58b800e99bf", "interface": "public", "region_id": "RegionOne", "url": "http://keystone:8080/v1/AUTH_3d1d9e8cf44143abbd582e026fa507a3", "region": "RegionOne"}], "id": "31375fead0d346a68ee168ce9ef6ff48", "type": "object-store", "name": "swift"}, {"endpoints": [{"id": "042b6cf9048c4f63baf576de2d69cd48", "interface": "public", "region_id": "RegionOne", "url": "http://keystone:8042", "region": "RegionOne"}, {"id": "396ae5a7a9cc48fe8baca3d0f2216fe4", "interface": "internal", "region_id": "RegionOne", "url": "http://keystone:8042", "region": "RegionOne"}, {"id": "4824a2fd92ca418dbdf7c3660dbe1d21", "interface": "admin", "region_id": "RegionOne", "url": "http://keystone:8042", "region": "RegionOne"}], "id": "3f40fed5c6274bb9a59dea54628412f5", "type": "alarming", "name": "aodh"}, {"endpoints": [{"id": "119020311c48489f87025fc48eaf581b", "interface": "internal", "region_id": "RegionOne", "url": "http://keystone:8774/v2.1/3d1d9e8cf44143abbd582e026fa507a3", "region": "RegionOne"}, {"id": "2b2c5d0336f340649c73aae455336ac0", "interface": "admin", "region_id": "RegionOne", "url": "http://keystone:8774/v2.1/3d1d9e8cf44143abbd582e026fa507a3", "region": "RegionOne"}, {"id": "7a54f1f96fb2448c8c6311376fd2ec79", "interface": "public", "region_id": "RegionOne", "url": "http://keystone:8774/v2.1/3d1d9e8cf44143abbd582e026fa507a3", "region": "RegionOne"}], "id": "4b27a00dfcf94873a722fbf2b2998642", "type": "compute", "name": "nova"}, {"endpoints": [{"id": "4003b4daea5c4f70af92fd6400d33ace", "interface": "public", "region_id": "RegionOne", "url": "http://keystone:5000", "region": "RegionOne"}, {"id": "938c74331a1b47e8b7aa78ad7c9ee732", "interface": "admin", "region_id": "RegionOne", "url": "http://keystone:5000", "region": "RegionOne"}, {"id": "9ed2d867c48649c4bc6d255d587c2f52", "interface": "internal", "region_id": "RegionOne", "url": "http://keystone:5000", "region": "RegionOne"}], "id": "58b849d76f45497e9db8bba2d300e344", "type": "identity", "name": "keystone"}, {"endpoints": [{"id": "61a72ea2fba54dc1a45803ac3277fc9a", "interface": "admin", "region_id": "RegionOne", "url": "http://keystone:8777", "region": "RegionOne"}, {"id": "f86dcc5035fb4473b1850ebd6fcd221c", "interface": "public", "region_id": "RegionOne", "url": "http://keystone:8777", "region": "RegionOne"}, {"id": "f8e194657e354f52a98808352d81a0a0", "interface": "internal", "region_id": "RegionOne", "url": "http://keystone:8777", "region": "RegionOne"}], "id": "6b48671f1315400ea4cf177d13b6e186", "type": "metering", "name": "ceilometer"}, {"endpoints": [{"id": "6095ce8254bb45b1b8aed529696b0ab3", "interface": "public", "region_id": "RegionOne", "url": "http://keystone:8776/v3/3d1d9e8cf44143abbd582e026fa507a3", "region": "RegionOne"}, {"id": "671156be632b477fa4f075a919375bc2", "interface": "admin", "region_id": "RegionOne", "url": "http://keystone:8776/v3/3d1d9e8cf44143abbd582e026fa507a3", "region": "RegionOne"}, {"id": "85f9742e2e004fbc82a93af08c104dc6", "interface": "internal", "region_id": "RegionOne", "url": "http://keystone:8776/v3/3d1d9e8cf44143abbd582e026fa507a3", "region": "RegionOne"}], "id": "6c534f6b3152422fad61ceaacdfd2d8f", "type": "volumev3", "name": "cinderv3"}, {"endpoints": [{"id": "71a1a6b39c9e4e5a8124b4b5094fd053", "interface": "internal", "region_id": "RegionOne", "url": "http://keystone:8041", "region": "RegionOne"}, {"id": "cc214c3b987848d08d79befabe948863", "interface": "admin", "region_id": "RegionOne", "url": "http://keystone:8041", "region": "RegionOne"}, {"id": "d4edaf8639c84489b7a6d6ea6008b0ae", "interface": "public", "region_id": "RegionOne", "url": "http://keystone:8041", "region": "RegionOne"}], "id": "831cb70d2d244cada780895245c0847c", "type": "metric", "name": "gnocchi"}, {"endpoints": [{"id": "1380f800fe5a4c6785c3f0abbd12e65d", "interface": "public", "region_id": "RegionOne", "url": "http://keystone:9292", "region": "RegionOne"}, {"id": "332f3119d5da4291960abd615faca249", "interface": "internal", "region_id": "RegionOne", "url": "http://keystone:9292", "region": "RegionOne"}, {"id": "a49b45320fc543ae9d890ab46e60c481", "interface": "admin", "region_id": "RegionOne", "url": "http://keystone:9292", "region": "RegionOne"}], "id": "aae882bc60a84408859f0012ff78c584", "type": "image", "name": "glance"}, {"endpoints": [{"id": "4b5088db5d564c9c8c5f3eac11e4aa5d", "interface": "admin", "region_id": "RegionOne", "url": "http://keystone:8776/v2/3d1d9e8cf44143abbd582e026fa507a3", "region": "RegionOne"}, {"id": "63f25449f6764500bf5a7213f23083ec", "interface": "public", "region_id": "RegionOne", "url": "http://keystone:8776/v2/3d1d9e8cf44143abbd582e026fa507a3", "region": "RegionOne"}, {"id": "ac191ab78c6a4d30afe43b3635f7a704", "interface": "internal", "region_id": "RegionOne", "url": "http://keystone:8776/v2/3d1d9e8cf44143abbd582e026fa507a3", "region": "RegionOne"}], "id": "b764ed8b7aa4422b858d8d0624d1040f", "type": "volumev2", "name": "cinderv2"}, {"endpoints": [{"id": "0709743b3a4c4b22884f7d811edb3614", "interface": "public", "region_id": "RegionOne", "url": "http://keystone:9696", "region": "RegionOne"}, {"id": "8d5316efc34b460381e9e61975af3418", "interface": "admin", "region_id": "RegionOne", "url": "http://keystone:9696", "region": "RegionOne"}, {"id": "ca381ed0a9b44224b5cc931e0098f88c", "interface": "internal", "region_id": "RegionOne", "url": "http://keystone:9696", "region": "RegionOne"}], "id": "c9880ff58de14965ab9acd8e3d5dc73e", "type": "network", "name": "neutron"}, {"endpoints": [{"id": "20d58a19e9204b0b983fefa5c2bb02bb", "interface": "admin", "region_id": "RegionOne", "url": "http://keystone:8778/placement", "region": "RegionOne"}, {"id": "aa5f7d1ad8bd489aae40750479ebb46c", "interface": "public", "region_id": "RegionOne", "url": "http://keystone:8778/placement", "region": "RegionOne"}, {"id": "ed99a782cdef4cda908b7e55156862d2", "interface": "internal", "region_id": "RegionOne", "url": "http://keystone:8778/placement", "region": "RegionOne"}], "id": "cea3b185e75e4cb4a9e8f6d6feded397", "type": "placement", "name": "placement"}]}}'} + headers: + X-Subject-Token: "test-token" + Content-Type: [application/json] + status: {code: 200, message: OK} + - request: + body: null + headers: + Accept: [application/json] + Content-Type: [application/json] + method: GET + uri: http://keystone:8774/v2.1/3d1d9e8cf44143abbd582e026fa507a3/servers/detail + response: + body: {string: '{"servers": [{"id": "6adf8322-e7d4-40e4-8f60-66b22eca6798", "name": "c7n-test-1", "status": "ACTIVE", "tenant_id": "3d1d9e8cf44143abbd582e026fa507a3", "user_id": "0ca05c20ee48419bb171707560ad793b", "metadata": {"name": "name", "en": "123", "name1": "test033", "name2": "test033", "name3": "test033", "name4": "test034", "name5": "test035", "name6": "test036"}, "hostId": "665cf485a8bf692d8f0feec5fbeba68343775ea6d40362c39a534b0f", "image": {"id": "d436429c-905a-4fe2-8c76-d7bcd677c879", "links": [{"rel": "bookmark", "href": "http://192.168.193.40:8774/3d1d9e8cf44143abbd582e026fa507a3/images/d436429c-905a-4fe2-8c76-d7bcd677c879"}]}, "flavor": {"vcpus": 1, "ram": 512, "disk": 1, "ephemeral": 0, "swap": 0, "original_name": "m1.tiny", "extra_specs": {}}, "created": "2020-11-19T12:18:10Z", "updated": "2020-11-26T03:41:58Z", "addresses": {"MSKJ_PROD_NETWORK": [{"version": 4, "addr": "192.168.100.100", "OS-EXT-IPS:type": "fixed", "OS-EXT-IPS-MAC:mac_addr": "fa:16:3e:cb:e3:8d"}]}, "accessIPv4": "", "accessIPv6": "", "links": [{"rel": "self", "href": "http://192.168.193.40:8774/v2.1/3d1d9e8cf44143abbd582e026fa507a3/servers/6adf8322-e7d4-40e4-8f60-66b22eca6798"}, {"rel": "bookmark", "href": "http://192.168.193.40:8774/3d1d9e8cf44143abbd582e026fa507a3/servers/6adf8322-e7d4-40e4-8f60-66b22eca6798"}], "OS-DCF:diskConfig": "MANUAL", "progress": 0, "OS-EXT-AZ:availability_zone": "nova", "config_drive": "", "key_name": null, "OS-SRV-USG:launched_at": "2020-11-19T12:18:19.000000", "OS-SRV-USG:terminated_at": null, "OS-EXT-SRV-ATTR:host": "ip-192-168-193-40.cn-northwest-1.compute.internal", "OS-EXT-SRV-ATTR:instance_name": "instance-0000001a", "OS-EXT-SRV-ATTR:hypervisor_hostname": "ip-192-168-193-40.cn-northwest-1.compute.internal", "OS-EXT-SRV-ATTR:reservation_id": "r-3feknjtl", "OS-EXT-SRV-ATTR:launch_index": 0, "OS-EXT-SRV-ATTR:hostname": "c7n-test-1", "OS-EXT-SRV-ATTR:kernel_id": "", "OS-EXT-SRV-ATTR:ramdisk_id": "", "OS-EXT-SRV-ATTR:root_device_name": "/dev/vda", "OS-EXT-SRV-ATTR:user_data": "", "OS-EXT-STS:task_state": null, "OS-EXT-STS:vm_state": "active", "OS-EXT-STS:power_state": 0, "os-extended-volumes:volumes_attached": [], "locked": false, "locked_reason": null, "description": "c7n-test-1", "tags": ["name0=3", "name13333333=35", "name1=33", "name1=35", "name2=2", "name2=333333333"], "trusted_image_certificates": null, "host_status": "UP", "security_groups": [{"name": "MSKJ_BASE_SG"}]}, {"id": "4533efe6-e108-4552-857a-26fe297ca45e", "name": "c7n-test-2", "status": "ACTIVE", "tenant_id": "3d1d9e8cf44143abbd582e026fa507a3", "user_id": "0ca05c20ee48419bb171707560ad793b", "metadata": {}, "hostId": "665cf485a8bf692d8f0feec5fbeba68343775ea6d40362c39a534b0f", "image": {"id": "415f7db5-2d3b-411e-a5f5-80321dbfec0b", "links": [{"rel": "bookmark", "href": "http://192.168.193.40:8774/3d1d9e8cf44143abbd582e026fa507a3/images/415f7db5-2d3b-411e-a5f5-80321dbfec0b"}]}, "flavor": {"vcpus": 1, "ram": 1024, "disk": 20, "ephemeral": 0, "swap": 0, "original_name": "m1.micro", "extra_specs": {}}, "created": "2020-11-05T03:48:57Z", "updated": "2020-11-24T07:19:45Z", "addresses": {"external_network": [{"version": 4, "addr": "192.168.193.77", "OS-EXT-IPS:type": "fixed", "OS-EXT-IPS-MAC:mac_addr": "02:97:2b:98:2e:5c"}]}, "accessIPv4": "", "accessIPv6": "", "links": [{"rel": "self", "href": "http://192.168.193.40:8774/v2.1/3d1d9e8cf44143abbd582e026fa507a3/servers/4533efe6-e108-4552-857a-26fe297ca45e"}, {"rel": "bookmark", "href": "http://192.168.193.40:8774/3d1d9e8cf44143abbd582e026fa507a3/servers/4533efe6-e108-4552-857a-26fe297ca45e"}], "OS-DCF:diskConfig": "MANUAL", "progress": 0, "OS-EXT-AZ:availability_zone": "nova", "config_drive": "", "key_name": "default", "OS-SRV-USG:launched_at": "2020-11-05T03:49:23.000000", "OS-SRV-USG:terminated_at": null, "OS-EXT-SRV-ATTR:host": "ip-192-168-193-40.cn-northwest-1.compute.internal", "OS-EXT-SRV-ATTR:instance_name": "instance-00000018", "OS-EXT-SRV-ATTR:hypervisor_hostname": "ip-192-168-193-40.cn-northwest-1.compute.internal", "OS-EXT-SRV-ATTR:reservation_id": "r-rh305at8", "OS-EXT-SRV-ATTR:launch_index": 0, "OS-EXT-SRV-ATTR:hostname": "int32bit-test-1", "OS-EXT-SRV-ATTR:kernel_id": "", "OS-EXT-SRV-ATTR:ramdisk_id": "", "OS-EXT-SRV-ATTR:root_device_name": "/dev/vda", "OS-EXT-SRV-ATTR:user_data": null, "OS-EXT-STS:task_state": null, "OS-EXT-STS:vm_state": "active", "OS-EXT-STS:power_state": 1, "os-extended-volumes:volumes_attached": [{"id": "2ef1004e-013b-4b77-9e54-b115869a6504", "delete_on_termination": false}], "locked": false, "locked_reason": null, "description": null, "tags": ["a=a", "b=b", "c=c"], "trusted_image_certificates": null, "host_status": "UP", "security_groups": [{"name": "default"}]}]}'} + headers: + Content-Type: [application/json] + status: {code: 200, message: OK} + - request: + body: null + headers: + Accept: [application/json] + Content-Type: [application/json] + method: GET + uri: http://keystone:9696/v2.0/ports.json?device_id=6adf8322-e7d4-40e4-8f60-66b22eca6798 + response: + body: {string: ' {"ports":[{"id":"2940edf7-7dda-4dee-a05e-9c385c4c0b6b","name":"port1","network_id":"2590b53d-2282-4fa5-9251-2b9fbd8e3cec","tenant_id":"3d1d9e8cf44143abbd582e026fa507a3","mac_address":"fa:16:3e:cb:e3:8d","admin_state_up":true,"status":"DOWN","device_id":"6adf8322-e7d4-40e4-8f60-66b22eca6798","device_owner":"compute:nova","fixed_ips":[{"subnet_id":"267a0c4b-fcdd-4b33-9ac6-409cbd8cda3c","ip_address":"192.168.100.100"}],"allowed_address_pairs":[],"extra_dhcp_opts":[],"security_groups":["74e09d96-1ae7-497d-a6d2-ac212277de1b"],"description":"","binding:vnic_type":"normal","binding:profile":{},"binding:host_id":"ip-192-168-193-40.cn-northwest-1.compute.internal","binding:vif_type":"ovs","binding:vif_details":{"port_filter":true},"port_security_enabled":true,"qos_policy_id":null,"qos_network_policy_id":null,"resource_request":null,"tags":["123","blue"],"created_at":"2020-11-19T12:17:57Z","updated_at":"2020-11-26T02:40:10Z","revision_number":33,"project_id":"3d1d9e8cf44143abbd582e026fa507a3"}]}'} + headers: + Content-Type: [application/json] + status: {code: 200, message: OK} + - request: + body: null + headers: + Accept: [application/json] + Content-Type: [application/json] + method: GET + uri: http://keystone:9696/v2.0/ports.json?device_id=4533efe6-e108-4552-857a-26fe297ca45e + response: + body: {string: '{"ports":[{"id":"695c8e70-ae94-4c39-86f0-c15dcaea7dd2","name":"","network_id":"1addba70-cc5e-40aa-a630-6806d68201c6","tenant_id":"3d1d9e8cf44143abbd582e026fa507a3","mac_address":"02:97:2b:98:2e:5c","admin_state_up":true,"status":"ACTIVE","device_id":"4533efe6-e108-4552-857a-26fe297ca45e","device_owner":"compute:nova","fixed_ips":[{"subnet_id":"ee781473-a79c-4a40-a33d-01ae100f0d79","ip_address":"192.168.193.77"}],"allowed_address_pairs":[],"extra_dhcp_opts":[],"security_groups":["bd4cfd7d-be5d-48f0-a85e-48e212c5dde2"],"description":"","binding:vnic_type":"normal","binding:profile":{},"binding:host_id":"ip-192-168-193-40.cn-northwest-1.compute.internal","binding:vif_type":"ovs","binding:vif_details":{"port_filter":true},"port_security_enabled":true,"qos_policy_id":null,"qos_network_policy_id":null,"resource_request":null,"tags":[],"created_at":"2020-11-04T11:54:11Z","updated_at":"2020-11-19T01:04:09Z","revision_number":31,"project_id":"3d1d9e8cf44143abbd582e026fa507a3"}]}'} + headers: + Content-Type: [application/json] + status: {code: 200, message: OK} + - request: + body: null + headers: + Accept: [application/json] + Content-Type: [application/json] + method: GET + uri: http://keystone:9696/v2.0/networks.json + response: + body: {string: '{"networks":[{"id":"1addba70-cc5e-40aa-a630-6806d68201c6","name":"external_network","tenant_id":"3d1d9e8cf44143abbd582e026fa507a3","admin_state_up":true,"mtu":1500,"status":"ACTIVE","subnets":["ee781473-a79c-4a40-a33d-01ae100f0d79"],"shared":false,"availability_zone_hints":[],"availability_zones":[],"ipv4_address_scope":null,"ipv6_address_scope":null,"router:external":true,"description":"","port_security_enabled":true,"qos_policy_id":null,"is_default":false,"tags":[],"created_at":"2020-11-04T10:45:01Z","updated_at":"2020-11-05T03:36:23Z","revision_number":5,"project_id":"3d1d9e8cf44143abbd582e026fa507a3","provider:network_type":"flat","provider:physical_network":"extnet","provider:segmentation_id":null},{"id":"2590b53d-2282-4fa5-9251-2b9fbd8e3cec","name":"MSKJ_PROD_NETWORK","tenant_id":"3d1d9e8cf44143abbd582e026fa507a3","admin_state_up":true,"mtu":1442,"status":"ACTIVE","subnets":["267a0c4b-fcdd-4b33-9ac6-409cbd8cda3c"],"shared":false,"availability_zone_hints":[],"availability_zones":[],"ipv4_address_scope":null,"ipv6_address_scope":null,"router:external":false,"description":"","port_security_enabled":true,"qos_policy_id":null,"tags":["1122","11=2","1122k"],"created_at":"2020-11-19T12:12:28Z","updated_at":"2020-11-26T06:54:09Z","revision_number":5,"project_id":"3d1d9e8cf44143abbd582e026fa507a3","provider:network_type":"geneve","provider:physical_network":null,"provider:segmentation_id":2000}]}' } + headers: + Content-Type: [application/json] + status: {code: 200, message: OK} + - request: + body: null + headers: + Accept: [application/json] + Content-Type: [application/json] + method: GET + uri: http://keystone:9696/v2.0/floatingips.json?port_id=2940edf7-7dda-4dee-a05e-9c385c4c0b6b + response: + body: {string: '{"floatingips": []}'} + headers: + Content-Type: [application/json] + status: {code: 200, message: OK} + - request: + body: null + headers: + Accept: [application/json] + Content-Type: [application/json] + method: GET + uri: http://keystone:9696/v2.0/floatingips.json?port_id=695c8e70-ae94-4c39-86f0-c15dcaea7dd2 + response: + body: {string: '{"floatingips": []}'} + headers: + Content-Type: [application/json] + status: {code: 200, message: OK} + - request: + body: null + headers: + Accept: [application/json] + Content-Type: [application/json] + method: GET + uri: http://keystone:9696/v2.0/subnets.json + response: + body: {string: '{"subnets":[{"id":"267a0c4b-fcdd-4b33-9ac6-409cbd8cda3c","name":"MSKJ_PROD_PRIVATE_SUBNET_1","tenant_id":"3d1d9e8cf44143abbd582e026fa507a3","network_id":"2590b53d-2282-4fa5-9251-2b9fbd8e3cec","ip_version":4,"subnetpool_id":null,"enable_dhcp":true,"ipv6_ra_mode":null,"ipv6_address_mode":null,"gateway_ip":"192.168.100.1","cidr":"192.168.100.0/24","allocation_pools":[{"start":"192.168.100.2","end":"192.168.100.254"}],"host_routes":[],"dns_nameservers":[],"description":"","service_types":[],"tags":[],"created_at":"2020-11-19T12:12:35Z","updated_at":"2020-11-19T12:12:35Z","revision_number":0,"project_id":"3d1d9e8cf44143abbd582e026fa507a3"},{"id":"ee781473-a79c-4a40-a33d-01ae100f0d79","name":"public_subnet","tenant_id":"3d1d9e8cf44143abbd582e026fa507a3","network_id":"1addba70-cc5e-40aa-a630-6806d68201c6","ip_version":4,"subnetpool_id":null,"enable_dhcp":true,"ipv6_ra_mode":null,"ipv6_address_mode":null,"gateway_ip":"192.168.193.1","cidr":"192.168.193.0/25","allocation_pools":[{"start":"192.168.193.77","end":"192.168.193.79"}],"host_routes":[],"dns_nameservers":[],"description":"","service_types":[],"tags":[],"created_at":"2020-11-04T10:46:10Z","updated_at":"2020-11-05T03:36:23Z","revision_number":3,"project_id":"3d1d9e8cf44143abbd582e026fa507a3"}]}'} + headers: + Content-Type: [application/json] + status: {code: 200, message: OK} + - request: + body: null + headers: + Accept: [application/json] + Content-Type: [application/json] + method: GET + uri: http://keystone:8774/v2.1/3d1d9e8cf44143abbd582e026fa507a3/flavors/detail?is_public=None + response: + body: {string: '{"flavors": [{"id": "1", "name": "m1.tiny", "ram": 512, "disk": 1, "swap": 0, "OS-FLV-EXT-DATA:ephemeral": 0, "OS-FLV-DISABLED:disabled": false, "vcpus": 1, "os-flavor-access:is_public": true, "rxtx_factor": 1.0, "links": [{"rel": "self", "href": "http://192.168.193.40:8774/v2.1/3d1d9e8cf44143abbd582e026fa507a3/flavors/1"}, {"rel": "bookmark", "href": "http://192.168.193.40:8774/3d1d9e8cf44143abbd582e026fa507a3/flavors/1"}], "description": "tt", "extra_specs": {}}, {"id": "2", "name": "m1.small", "ram": 2048, "disk": 20, "swap": 0, "OS-FLV-EXT-DATA:ephemeral": 0, "OS-FLV-DISABLED:disabled": false, "vcpus": 1, "os-flavor-access:is_public": true, "rxtx_factor": 1.0, "links": [{"rel": "self", "href": "http://192.168.193.40:8774/v2.1/3d1d9e8cf44143abbd582e026fa507a3/flavors/2"}, {"rel": "bookmark", "href": "http://192.168.193.40:8774/3d1d9e8cf44143abbd582e026fa507a3/flavors/2"}], "description": null, "extra_specs": {}}, {"id": "3", "name": "m1.medium", "ram": 4096, "disk": 40, "swap": 0, "OS-FLV-EXT-DATA:ephemeral": 0, "OS-FLV-DISABLED:disabled": false, "vcpus": 2, "os-flavor-access:is_public": true, "rxtx_factor": 1.0, "links": [{"rel": "self", "href": "http://192.168.193.40:8774/v2.1/3d1d9e8cf44143abbd582e026fa507a3/flavors/3"}, {"rel": "bookmark", "href": "http://192.168.193.40:8774/3d1d9e8cf44143abbd582e026fa507a3/flavors/3"}], "description": null, "extra_specs": {}}, {"id": "4", "name": "m1.large", "ram": 8192, "disk": 80, "swap": 0, "OS-FLV-EXT-DATA:ephemeral": 0, "OS-FLV-DISABLED:disabled": false, "vcpus": 4, "os-flavor-access:is_public": true, "rxtx_factor": 1.0, "links": [{"rel": "self", "href": "http://192.168.193.40:8774/v2.1/3d1d9e8cf44143abbd582e026fa507a3/flavors/4"}, {"rel": "bookmark", "href": "http://192.168.193.40:8774/3d1d9e8cf44143abbd582e026fa507a3/flavors/4"}], "description": null, "extra_specs": {}}, {"id": "5", "name": "m1.xlarge", "ram": 16384, "disk": 160, "swap": 0, "OS-FLV-EXT-DATA:ephemeral": 0, "OS-FLV-DISABLED:disabled": false, "vcpus": 8, "os-flavor-access:is_public": true, "rxtx_factor": 1.0, "links": [{"rel": "self", "href": "http://192.168.193.40:8774/v2.1/3d1d9e8cf44143abbd582e026fa507a3/flavors/5"}, {"rel": "bookmark", "href": "http://192.168.193.40:8774/3d1d9e8cf44143abbd582e026fa507a3/flavors/5"}], "description": null, "extra_specs": {}}, {"id": "2", "name": "m1.small", "ram": 1024, "disk": 20, "swap": 0, "OS-FLV-EXT-DATA:ephemeral": 0, "OS-FLV-DISABLED:disabled": false, "vcpus": 1, "os-flavor-access:is_public": true, "rxtx_factor": 1.0, "links": [{"rel": "self", "href": "http://192.168.193.40:8774/v2.1/3d1d9e8cf44143abbd582e026fa507a3/flavors/2"}, {"rel": "bookmark", "href": "http://192.168.193.40:8774/3d1d9e8cf44143abbd582e026fa507a3/flavors/2"}], "description": null, "extra_specs": {}}]}'} + headers: + Content-Type: [application/json] + status: {code: 200, message: OK} + - request: + body: null + headers: + Accept: [application/json] + Content-Type: [application/json] + method: GET + uri: http://keystone:8774/v2.1/3d1d9e8cf44143abbd582e026fa507a3/flavors/1/os-extra_specs + response: + body: {string: '{"extra_specs":[]}'} + headers: + Content-Type: [application/json] + status: {code: 200, message: OK} + - request: + body: null + headers: + Accept: [application/json] + Content-Type: [application/json] + method: GET + uri: http://keystone:8774/v2.1/3d1d9e8cf44143abbd582e026fa507a3/flavors/2/os-extra_specs + response: + body: {string: '{"extra_specs":[]}'} + headers: + Content-Type: [application/json] + status: {code: 200, message: OK} + - request: + body: null + headers: + Accept: [application/json] + Content-Type: [application/json] + method: GET + uri: http://keystone:8774/v2.1/3d1d9e8cf44143abbd582e026fa507a3/flavors/3/os-extra_specs + response: + body: {string: '{"extra_specs":[]}'} + headers: + Content-Type: [application/json] + status: {code: 200, message: OK} + - request: + body: null + headers: + Accept: [application/json] + Content-Type: [application/json] + method: GET + uri: http://keystone:8774/v2.1/3d1d9e8cf44143abbd582e026fa507a3/flavors/5/os-extra_specs + response: + body: {string: '{"extra_specs":[]}'} + headers: + Content-Type: [application/json] + status: {code: 200, message: OK} + - request: + body: null + headers: + Accept: [application/json] + Content-Type: [application/json] + method: GET + uri: http://keystone:8774/v2.1/3d1d9e8cf44143abbd582e026fa507a3/flavors/4/os-extra_specs + response: + body: {string: '{"extra_specs":[]}'} + headers: + Content-Type: [application/json] + status: {code: 200, message: OK} + - request: + body: null + headers: + Accept: [application/json] + Content-Type: [application/json] + method: GET + uri: http://keystone:5000/v3/projects + response: + body: {string: '{"projects": [{"id": "3d1d9e8cf44143abbd582e026fa507a3", "name": "admin", "domain_id": "default", "description": "Bootstrap project for initializing the cloud.", "enabled": true, "parent_id": "default", "is_domain": false, "tags": [], "options": {}, "links": {"self": "http://127.0.0.1:5000/v3/projects/3d1d9e8cf44143abbd582e026fa507a3"}}, {"id": "57626756800343fd8474e0654f67c89a", "name": "demo", "domain_id": "default", "description": "default tenant", "enabled": true, "parent_id": "default", "is_domain": false, "tags": [], "options": {}, "links": {"self": "http://127.0.0.1:5000/v3/projects/57626756800343fd8474e0654f67c89a"}}, {"id": "cdf7c8b6db234a3695420f45b42e603b", "name": "services", "domain_id": "default", "description": "", "enabled": true, "parent_id": "default", "is_domain": false, "tags": [], "options": {}, "links": {"self": "http://127.0.0.1:5000/v3/projects/cdf7c8b6db234a3695420f45b42e603b"}}], "links": {"next": null, "self": "http://127.0.0.1:5000/v3/projects", "previous": null}}'} + headers: + Content-Type: [application/json] + status: {code: 200, message: OK} + - request: + body: null + headers: + Accept: [application/json] + Content-Type: [application/json] + method: GET + uri: http://keystone:8776/v3/3d1d9e8cf44143abbd582e026fa507a3/volumes/detail + response: + body: {string: '{"volumes": [{"id": "7f64ece8-e8bf-4d0e-b358-8028e9769b42", "status": "available", "size": 1, "availability_zone": "nova", "created_at": "2020-11-27T04:26:03.000000", "updated_at": "2020-11-27T04:26:04.000000", "attachments": [], "name": "c7n-test-1", "description": null, "volume_type": "iscsi", "snapshot_id": null, "source_volid": null, "metadata": {}, "links": [{"rel": "self", "href": "http://192.168.193.40:8776/v3/3d1d9e8cf44143abbd582e026fa507a3/volumes/7f64ece8-e8bf-4d0e-b358-8028e9769b42"}, {"rel": "bookmark", "href": "http://192.168.193.40:8776/3d1d9e8cf44143abbd582e026fa507a3/volumes/7f64ece8-e8bf-4d0e-b358-8028e9769b42"}], "user_id": "0ca05c20ee48419bb171707560ad793b", "bootable": "false", "encrypted": false, "replication_status": null, "consistencygroup_id": null, "multiattach": false, "migration_status": null, "group_id": null, "provider_id": null, "shared_targets": false, "service_uuid": "e3e266a0-510e-43bd-9386-d292672909ca", "os-vol-tenant-attr:tenant_id": "3d1d9e8cf44143abbd582e026fa507a3", "os-vol-mig-status-attr:migstat": null, "os-vol-mig-status-attr:name_id": null, "os-vol-host-attr:host": "ip-192-168-193-40.cn-northwest-1.compute.internal@lvm#lvm"}, {"id": "2ef1004e-013b-4b77-9e54-b115869a6504", "status": "in-use", "size": 1, "availability_zone": "nova", "created_at": "2020-11-24T13:54:37.000000", "updated_at": "2020-11-24T14:01:59.000000", "attachments": [{"id": "2ef1004e-013b-4b77-9e54-b115869a6504", "attachment_id": "56cd49b2-9e50-4084-b08c-060247c7af81", "volume_id": "2ef1004e-013b-4b77-9e54-b115869a6504", "server_id": "4533efe6-e108-4552-857a-26fe297ca45e", "host_name": "ip-192-168-193-40.cn-northwest-1.compute.internal", "device": "/dev/vdb", "attached_at": "2020-11-24T14:01:58.000000"}], "name": "gaoyan-test-volume01", "description": null, "volume_type": "iscsi", "snapshot_id": null, "source_volid": null, "metadata": {}, "links": [{"rel": "self", "href": "http://192.168.193.40:8776/v3/3d1d9e8cf44143abbd582e026fa507a3/volumes/2ef1004e-013b-4b77-9e54-b115869a6504"}, {"rel": "bookmark", "href": "http://192.168.193.40:8776/3d1d9e8cf44143abbd582e026fa507a3/volumes/2ef1004e-013b-4b77-9e54-b115869a6504"}], "user_id": "0ca05c20ee48419bb171707560ad793b", "bootable": "false", "encrypted": false, "replication_status": null, "consistencygroup_id": null, "multiattach": false, "migration_status": null, "group_id": null, "provider_id": null, "shared_targets": false, "service_uuid": "e3e266a0-510e-43bd-9386-d292672909ca", "os-vol-tenant-attr:tenant_id": "3d1d9e8cf44143abbd582e026fa507a3", "os-vol-mig-status-attr:migstat": null, "os-vol-mig-status-attr:name_id": null, "os-vol-host-attr:host": "ip-192-168-193-40.cn-northwest-1.compute.internal@lvm#lvm"}]}'} + headers: + Content-Type: [application/json] + status: {code: 200, message: OK} + - request: + body: null + headers: + Accept: [application/json] + Content-Type: [application/json] + method: DELETE + uri: http://keystone:8776/v3/3d1d9e8cf44143abbd582e026fa507a3/volumes/7f64ece8-e8bf-4d0e-b358-8028e9769b42 + response: + body: {string: ''} + headers: + Content-Type: [test/html] + status: {code: 200, message: OK} +version: 1 + 33 tools/c7n_openstack/test/test_project.py + Viewed + @@ -0,0 +1,33 @@ + # Copyright The Cloud Custodian Authors. + # SPDX-License-Identifier: Apache-2.0 + from common_openstack import OpenStackTest + + +class ProjectTest(OpenStackTest): + + def test_project_query(self): + factory = self.replay_flight_data() + p = self.load_policy({ + 'name': 'all-projects', + 'resource': 'openstack.project'}, + session_factory=factory) + resources = p.run() + self.assertEqual(len(resources), 3) + + def test_project_filter_by_name(self): + factory = self.replay_flight_data() + policy = { + 'name': 'project-demo', + 'resource': 'openstack.project', + 'filters': [ + { + "type": "value", + "key": "name", + "value": "demo", + }, + ], + } + p = self.load_policy(policy, session_factory=factory) + resources = p.run() + self.assertEqual(len(resources), 1) + self.assertEqual(resources[0].name, "demo") + 77 tools/c7n_openstack/test/test_server.py + Viewed + @@ -0,0 +1,77 @@ + # Copyright The Cloud Custodian Authors. + # SPDX-License-Identifier: Apache-2.0 + from common_openstack import OpenStackTest + + +class ServerTest(OpenStackTest): + + def test_server_query(self): + factory = self.replay_flight_data() + p = self.load_policy({ + 'name': 'all-servers', + 'resource': 'openstack.server'}, + session_factory=factory) + resources = p.run() + self.assertEqual(len(resources), 2) + + def test_server_filter_name(self): + factory = self.replay_flight_data() + policy = { + 'name': 'get-server-c7n-test-1', + 'resource': 'openstack.server', + 'filters': [ + { + "type": "value", + "key": "name", + "value": "c7n-test-1", + }, + ], + } + p = self.load_policy(policy, session_factory=factory) + resources = p.run() + self.assertEqual(len(resources), 1) + self.assertEqual(resources[0].name, "c7n-test-1") + + def test_server_filter_flavor(self): + factory = self.replay_flight_data() + policy = { + 'name': 'get-server-c7n-test-1', + 'resource': 'openstack.server', + 'filters': [ + { + "type": "flavor", + "flavor_name": "m1.tiny", + }, + ], + } + p = self.load_policy(policy, session_factory=factory) + resources = p.run() + self.assertEqual(len(resources), 1) + self.assertEqual(resources[0].name, "c7n-test-1") + + def test_server_filter_tags(self): + factory = self.replay_flight_data() + policy = { + 'name': 'get-server-c7n-test-1', + 'resource': 'openstack.server', + 'filters': [ + { + "type": "tags", + "tags": [ + { + "key": "a", + "value": "a", + }, + { + "key": "b", + "value": "b", + }, + ], + "op": "all", + }, + ], + } + p = self.load_policy(policy, session_factory=factory) + resources = p.run() + self.assertEqual(len(resources), 1) + self.assertEqual(resources[0].name, "c7n-test-2") \ No newline at end of file diff --git a/tools/c7n_openstack/test/openstack.py b/tools/c7n_openstack/test/openstack.py new file mode 100644 index 00000000000..c9013d2720d --- /dev/null +++ b/tools/c7n_openstack/test/openstack.py @@ -0,0 +1,66 @@ +# Copyright The Cloud Custodian Authors. +# SPDX-License-Identifier: Apache-2.0 +import logging +import os + +import openstack + +logging.basicConfig(level=logging.INFO, format='%(asctime)s %(filename)s[line:%(lineno)d] %(levelname)s %(message)s', datefmt='%a, %d %b %Y %H:%M:%S') +# 本地测试用例 +def _loadFile_(): + json = dict() + f = open("/opt/fit2cloud/openstack.txt") + lines = f.readlines() + for line in lines: + line = line.strip() + if "openstack.OS_USERNAME" in line: + OS_USERNAME = line[line.rfind('=') + 1:] + json['OS_USERNAME'] = OS_USERNAME + if "openstack.OS_PASSWORD" in line: + OS_PASSWORD = line[line.rfind('=') + 1:] + json['OS_PASSWORD'] = OS_PASSWORD + if "openstack.OS_REGION_NAME" in line: + OS_REGION_NAME = line[line.rfind('=') + 1:] + json['OS_REGION_NAME'] = OS_REGION_NAME + if "openstack.OS_AUTH_URL" in line: + OS_AUTH_URL = line[line.rfind('=') + 1:] + json['OS_AUTH_URL'] = OS_AUTH_URL + if "openstack.OS_PROJECT_NAME" in line: + OS_PROJECT_NAME = line[line.rfind('=') + 1:] + json['OS_PROJECT_NAME'] = OS_PROJECT_NAME + f.close() + print('认证信息: ' + str(json)) + return json + +params = _loadFile_() + +OPENSTACK_CONFIG = { + 'OS_USERNAME': params['OS_USERNAME'], + 'OS_PASSWORD': params['OS_PASSWORD'], + 'OS_REGION_NAME': params['OS_REGION_NAME'], + 'OS_AUTH_URL': params['OS_AUTH_URL'], #http://keystone:5000/v3 + 'OS_PROJECT_NAME': params['OS_PROJECT_NAME'], + 'OS_USER_DOMAIN_NAME': 'Default', + 'OS_PROJECT_DOMAIN_NAME': 'Default', + 'OS_IDENTITY_API_VERSION': '3', + 'OS_CLOUD_NAME': 'c7n-cloud', +} + +DEFAULT_CASSETTE_FILE = "default.yaml" + +def init_openstack_config(): + for k, v in OPENSTACK_CONFIG.items(): + os.environ[k] = v + +def client(): + cloud = openstack.connect(cloud=os.getenv('OS_CLOUD_NAME')) + return cloud + +def list_users(self=None): + print("List Users:") + for user in client().list_users(self): + print(user) + +if __name__ == '__main__': + logging.info("Hello OpenStack OpenApi!") + list_users(None) \ No newline at end of file diff --git a/tools/c7n_tencent/.gitignore b/tools/c7n_tencent/.gitignore index 1e7bb232c6a..fb3bc159d15 100644 --- a/tools/c7n_tencent/.gitignore +++ b/tools/c7n_tencent/.gitignore @@ -2,3 +2,4 @@ *py~ __pycache__ *.egg-info +tencent.py \ No newline at end of file diff --git a/tools/c7n_tencent/__init__.py b/tools/c7n_tencent/__init__.py new file mode 100644 index 00000000000..fbe549677de --- /dev/null +++ b/tools/c7n_tencent/__init__.py @@ -0,0 +1,14 @@ +# Copyright 2017-2018 Capital One Services, LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# \ No newline at end of file diff --git a/tools/c7n_tencent/c7n_tencent/client.py b/tools/c7n_tencent/c7n_tencent/client.py index 3c43cdf5234..f84283118e9 100644 --- a/tools/c7n_tencent/c7n_tencent/client.py +++ b/tools/c7n_tencent/c7n_tencent/client.py @@ -25,8 +25,10 @@ # 导入可选配置类 # 导入对应产品模块的 client models。 from tencentcloud.cvm.v20170312 import cvm_client +from tencentcloud.es.v20180416 import es_client from tencentcloud.mongodb.v20190725 import mongodb_client from tencentcloud.monitor.v20180724 import monitor_client +from tencentcloud.postgres.v20170312 import postgres_client from tencentcloud.redis.v20180412 import redis_client from tencentcloud.vpc.v20170312 import vpc_client @@ -75,14 +77,17 @@ def client(self, service): client = mongodb_client.MongodbClient(cred, os.getenv('TENCENT_DEFAULT_REGION')) elif 'redis_client' in service: client = redis_client.RedisClient(cred, os.getenv('TENCENT_DEFAULT_REGION')) + elif 'postgres_client' in service: + client = postgres_client.PostgresClient(cred, os.getenv("TENCENT_DEFAULT_REGION")) + elif 'es_client' in service: + client = es_client.EsClient(cred, os.getenv('TENCENT_DEFAULT_REGION')) elif 'coss3_client' in service: # 1. 设置用户配置, 包括 secretId,secretKey 以及 Region endpoint = 'cos.' + os.getenv('TENCENT_DEFAULT_REGION') + '.myqcloud.com' config = CosConfig(Region=os.getenv('TENCENT_DEFAULT_REGION'), SecretId=os.getenv('TENCENT_SECRETID'), - SecretKey=os.getenv('TENCENT_SECRETKEY'),Endpoint=endpoint) + SecretKey=os.getenv('TENCENT_SECRETKEY'), Endpoint=endpoint) # 2. 获取客户端对象 client = CosS3Client(config) else: client = cvm_client.CvmClient(cred, os.getenv('TENCENT_DEFAULT_REGION')) return client - diff --git a/tools/c7n_tencent/c7n_tencent/filter_util.py b/tools/c7n_tencent/c7n_tencent/filter_util.py new file mode 100644 index 00000000000..f3413e1a489 --- /dev/null +++ b/tools/c7n_tencent/c7n_tencent/filter_util.py @@ -0,0 +1,196 @@ +import jsonpath +import json +import time + + +def get_value(res_i, value_key): + """ + 获取数据 + @param res_i: + @param value_key: + @return: + """ + json_str = json.dumps(res_i) + u_str = json.loads(json_str) + if str(value_key).startswith("$."): + return jsonpath.jsonpath(u_str, value_key) + return jsonpath.jsonpath(u_str, '$.' + value_key) + + +def like(filter_obj, i): + """ + 判断是否匹配 + @param filter_obj: + @param i: + @return: + """ + cloud_value = get_value(i, filter_obj.schema['properties']['type']['enum'][0]) + if cloud_value: + if len(cloud_value) == 0: + return False + like_value = filter_obj.data.get('like', '') + # 如果不传入数据默认不过滤 + if like_value: + return like_value in cloud_value[0] + return True + + +def eq(filter_obj, i): + """ + 判断是否相等 + @param filter_obj: + @param i: + @return: + """ + cloud_value = get_value(i, filter_obj.schema['properties']['type']['enum'][0]) + if cloud_value: + if len(cloud_value) == 0: + return False + value = filter_obj.data.get('value', '') + # 如果不传入数据默认不过滤 + if value: + return value == cloud_value[0] + return True + + +def time_to_num(time_str, format): + # 2017-12-04 00:00:00 + print(time_str) + timeArray = time.strptime(time_str, format) + return int(time.mktime(timeArray)) + + +def region(filter_obj, i): + """ + 判断是否是在某个范围 + @param filter_obj: + @param i: + @return: + """ + cloud_value = get_value(i, filter_obj.schema['properties']['type']['enum'][0]) + ge = filter_obj.data.get('ge', '') + le = filter_obj.data.get('le', '') + if len(cloud_value) == 0: + return False + cloud_value = cloud_value[0] + if cloud_value and len(str(cloud_value)) > 0 and isinstance(cloud_value, str): + cloud_value = time_to_num(cloud_value, '%Y-%m-%d %H:%M:%S') + # 如果不传入数据默认不过滤 + if cloud_value and (isinstance(cloud_value, int) or isinstance(cloud_value, float)): + if ge and le: + return ge <= cloud_value <= le + elif ge: + return ge <= cloud_value + elif le: + return cloud_value <= le + return True + + +def is_null(filter_obj, i): + """ + 判断变量是否存在 + @param filter_obj: + @param i: + @return: + """ + cloud_value = get_value(i, filter_obj.schema['properties']['type']['enum'][0]) + if cloud_value: + is_empty = filter_obj.data.get('is_empty', '') + if is_empty and len(cloud_value) == 0: + return i + elif not is_empty and len(cloud_value) != 0: + return i + else: + return False + + +def in_like(filter_obj, i): + in_like_value = filter_obj.data.get('in_like', '') + cloud_value = get_value(i, filter_obj.schema['properties']['type']['enum'][0]) + if cloud_value: + if isinstance(cloud_value[0], list): + for index in range(list(cloud_value[0])): + if in_like_value in cloud_value[0][index]: + return i + else: + False + + +def in_eq(filter_obj, i): + in_value = filter_obj.data.get('in', '') + cloud_value = get_value(i, filter_obj.schema['properties']['type']['enum'][0]) + if cloud_value: + if isinstance(cloud_value[0], list): + if list(cloud_value[0]).__contains__(in_value): + return i + else: + False + + +def filter_res(filter_obj, i): + """ + 过滤返回对象 + @param filter_obj: + @param i: + @return: + """ + like_value = filter_obj.data.get('like', '') + value = filter_obj.data.get('value', '') + ge = filter_obj.data.get('ge', '') + le = filter_obj.data.get('le', '') + is_empty = filter_obj.data.get('is_empty', '') + in_like_field = filter_obj.data.get('in_like', '') + in_eq_field = filter_obj.data.get('in', '') + if like_value: + if like(filter_obj, i): + return i + else: + return False + elif value: + if eq(filter_obj, i): + return i + else: + return False + elif ge or le: + if region(filter_obj, i): + return i + else: + return False + elif is_empty: + if is_null(filter_obj, i): + return i + else: + return False + elif in_like_field: + if in_like(filter_obj, i): + return i + else: + return False + elif in_eq_field: + if in_eq(filter_obj, i): + return i + else: + return False + + return i + + +types = { + 'is_empty': {'is_empty': {'type': 'boolean'}}, + 'boolean': {'value': {'type': 'boolean'}, 'is_empty': {'type': 'boolean'}}, + 'number': {'ge': {'type': 'number'}, 'le': {'type': 'number'}, 'is_empty': {'type': 'boolean'}}, + 'string': {'like': {'type': 'string'}, 'value': {'type': 'string'}, 'is_empty': {'type': 'boolean'}}, + 'list_string': {'in': {'type': 'string'}, 'in_like': {'type': {'type': 'string'}}, + 'is_empty': {'type': 'boolean'}}, + 'list_number': {'in': {'type': 'number'}, 'in_like': {'type': 'number'}, 'is_empty': {'type': 'boolean'}}, + 'time': {'ge': {'type': 'number'}, 'le': {'type': 'number'}, 'is_empty': {'type': 'boolean'}} +} + + +def get_schema(type): + """ + 获取类型 + @param type: + @return: + """ + return types.get(type) diff --git a/tools/c7n_tencent/c7n_tencent/filters/filter.py b/tools/c7n_tencent/c7n_tencent/filters/filter.py index f34fc390917..d684734d456 100644 --- a/tools/c7n_tencent/c7n_tencent/filters/filter.py +++ b/tools/c7n_tencent/c7n_tencent/filters/filter.py @@ -1,9 +1,11 @@ import datetime import json +import re from concurrent.futures import as_completed from datetime import timedelta import jmespath +from c7n_tencent import filter_util from dateutil.parser import parse from dateutil.tz import tzutc @@ -23,6 +25,36 @@ def validate(self): def __call__(self, i): return self.get_request(i) + +class TencentEsFilter(Filter): + schema = None + + def get_request(self, i): + return filter_util.filter_res(self, i) + + def validate(self): + keys = ['is_empty', 'value', 'like', 'ge', 'le', 'in', 'in_like'] + hits = [] + for key in self.data: + if keys == 'type': + continue + if keys.__contains__(key): + hits.append(key) + # ge 和 le是可以同时出现的 + if len(hits) > 1: + if len(hits) == 2: + if not list(hits).__contains__('ge') or not list(hits).__contains__('le'): + raise PolicyValidationError( + '过滤类型只能配置一个,ge 和 le 可以同时使用,', self.data) + else: + raise PolicyValidationError( + '过滤类型只能配置一个,ge 和 le 可以同时使用,', self.data) + return self + + def __call__(self, i): + return self.get_request(i) + + class TencentEipFilter(Filter): schema = None @@ -34,6 +66,7 @@ def __call__(self, i): return False return i + class TencentDiskFilter(Filter): schema = None @@ -45,6 +78,7 @@ def __call__(self, i): return False return i + class TencentCdbFilter(Filter): schema = None @@ -59,6 +93,7 @@ def __call__(self, i): return False return i + class TencentClbFilter(Filter): def validate(self): @@ -67,6 +102,7 @@ def validate(self): def __call__(self, i): return self.get_request(i) + class TencentVpcFilter(Filter): def validate(self): @@ -75,6 +111,7 @@ def validate(self): def __call__(self, i): return self.get_request(i) + class TencentAgeFilter(Filter): """Automatically filter resources older than a given date. @@ -106,7 +143,6 @@ def __call__(self, i): op = OPERATORS[self.data.get('op', 'greater-than')] if not self.threshold_date: - days = self.data.get('days', 0) hours = self.data.get('hours', 0) minutes = self.data.get('minutes', 0) @@ -116,6 +152,7 @@ def __call__(self, i): return op(self.threshold_date, v) + class SGPermission(Filter): """Filter for verifying security group ingress and egress permissions @@ -242,7 +279,7 @@ class SGPermission(Filter): perm_attrs = { 'IpProtocol', "Priority", 'Policy'} filter_attrs = { - 'Cidr', 'CidrV6', 'Ports', 'OnlyPorts', + 'Cidr', 'CidrV6', 'Ports', 'OnlyPorts', 'Action', 'SelfReference', 'Description', 'SGReferences'} attrs = perm_attrs.union(filter_attrs) attrs.add('match-operator') @@ -261,7 +298,7 @@ def process(self, resources, event=None): fattrs = list(sorted(self.perm_attrs.intersection(self.data.keys()))) self.ports = 'Ports' in self.data and self.data['Ports'] or () self.only_ports = ( - 'OnlyPorts' in self.data and self.data['OnlyPorts'] or ()) + 'OnlyPorts' in self.data and self.data['OnlyPorts'] or ()) for f in fattrs: fv = self.data.get(f) if isinstance(fv, dict): @@ -275,87 +312,169 @@ def process(self, resources, event=None): return super(SGPermission, self).process(resources, event) - def process_ports(self, perm): - found = None - if self.ip_permissions_type == 'ingress': - poms = perm['IpPermissions']['Ingress'] - else: - poms = perm['IpPermissions']['Egress'] - for ingress in poms: - if ingress['Action'] != 'ACCEPT': - return False - if ingress['Port']: - FromPort = ingress['Port'] + def cidr_process_ports(self, pom, cidr, cidr_key, items): + found = False + accept_drop = pom.get('Action', 'ACCEPT') + action = self.data.get('Action', 'ACCEPT') + if accept_drop == 'ACCEPT': + # 查询ACCEPT 实际ACCEPT + if accept_drop == action: + if pom['Port']: + FromPort = pom['Port'] for port in self.ports: if FromPort == "ALL": - found = True - break + return True else: if ',' in FromPort: strs = FromPort.split(',') for str in strs: if port == int(str): found = True - break + continue elif '-' in FromPort: strs = FromPort.split('-') p1 = int(strs[0]) p2 = int(strs[1]) if port >= p1 and port <= p2: found = True - break + continue else: if port == int(FromPort): found = True - break + continue found = False only_found = False for port in self.only_ports: if port == FromPort: only_found = True if self.only_ports and not only_found: - found = found is None or found and True or False + found = found is False or found and True or False if self.only_ports and only_found: found = False + else: + # 查询DROP 实际ACCEPT + found = False + else: + # 查询DROP 实际DROP + if accept_drop == action: + if pom['Port']: + FromPort = pom['Port'] + for port in self.ports: + if FromPort == "ALL": + return True + else: + if ',' in FromPort: + strs = FromPort.split(',') + for str in strs: + if port == int(str): + return True + elif '-' in FromPort: + strs = FromPort.split('-') + p1 = int(strs[0]) + p2 = int(strs[1]) + if port >= p1 and port <= p2: + return True + else: + if port == int(FromPort): + return True + found = False + only_found = False + for port in self.only_ports: + if port == FromPort: + only_found = True + if self.only_ports and not only_found: + found = found is False or found and True or False + if self.only_ports and only_found: + found = False + else: + # 查询ACCEPT 实际DROP + # 0.0.0.0/0 + CidrBlock = pom.get(cidr, "") + if CidrBlock != self.data.get(cidr_key, ""): + return False + IpProtocol = self.data.get('IpProtocol', "").upper() + if IpProtocol in ["-1", -1]: + IpProtocol = "ALL" + outProtocol = pom.get("Protocol", "").upper() + if outProtocol == "ALL" or IpProtocol == "ALL": + found = True + else: + if IpProtocol != outProtocol: + return False + if pom['Port']: + dropPort = pom['Port'] + for port in self.ports: + if dropPort == "ALL": + return True + else: + if ',' in dropPort: + strs = dropPort.split(',') + for str in strs: + if port == int(str): + self.ports.remove(port) + elif '-' in dropPort: + strs = dropPort.split('-') + p1 = int(strs[0]) + p2 = int(strs[1]) + if port >= p1 and port <= p2: + self.ports.remove(port) + else: + if port == int(dropPort): + self.ports.remove(port) + found = False + only_found = False + for port in self.only_ports: + if port == dropPort: + only_found = True + if self.only_ports and not only_found: + found = found is False or found and True or False + if self.only_ports and only_found: + return False return found - - def _process_cidr(self, cidr_key, cidr_type, SourceCidrIp, perm): - found = None - if not SourceCidrIp: + def _process_cidr(self, cidr_key, cidr_type, cidr, perm): + found = False + if not cidr: return False if self.ip_permissions_type == 'ingress': - items = perm.get('IpPermissions').get('Ingress') + items = perm.get('IpPermissions', {}).get('Ingress', []) else: - items = perm.get('IpPermissions').get('Egress') - for str in items: - SourceCidrIp = str.get(SourceCidrIp) - if SourceCidrIp: - sci = {cidr_type: SourceCidrIp} - match_range = self.data[cidr_key] - if isinstance(match_range, dict): - match_range['key'] = cidr_type - else: - match_range = {cidr_type: match_range} - vf = ValueFilter(match_range, self.manager) - vf.annotate = False - found = vf(sci) - if found: - pass + items = perm.get('IpPermissions', {}).get('Egress', []) + for ip_Permission in items: + # 0.0.0.0/0 + CidrBlock = ip_Permission.get(cidr, "") + if CidrBlock == self.data.get(cidr_key, ""): + found = True + else: + found = False + continue + IpProtocol = self.data.get('IpProtocol', "").upper() + if IpProtocol in ["-1", -1]: + IpProtocol = "ALL" + outProtocol = ip_Permission.get("Protocol", "").upper() + if outProtocol == "ALL" or IpProtocol == "ALL": + found = True + else: + if IpProtocol == outProtocol: + found = True else: found = False + continue + found = self.cidr_process_ports(ip_Permission, cidr, cidr_key, items) + if found: + break return found - def process_cidrs(self, perm, ipv4Cidr, ipv6Cidr): - found_v6 = found_v4 = None + def process_cidrs(self, perm, CidrBlock, Ipv6CidrBlock): + found_v6 = found_v4 = False if 'CidrV6' in self.data: - found_v6 = self._process_cidr('CidrV6', 'CidrIpv6', ipv6Cidr, perm) + found_v6 = self._process_cidr('CidrV6', 'CidrIpv6', Ipv6CidrBlock, perm) if 'Cidr' in self.data: - found_v4 = self._process_cidr('Cidr', 'CidrIp', ipv4Cidr, perm) + found_v4 = self._process_cidr('Cidr', 'CidrIp', CidrBlock, perm) match_op = self.data.get('match-operator', 'and') == 'and' and all or any - cidr_match = [k for k in (found_v6, found_v4) if k is not None] + cidr_match = [k for k in (found_v6, found_v4) if k is not False] if not cidr_match: - return None + return False return match_op(cidr_match) def process_description(self, perm): @@ -391,7 +510,7 @@ def process_self_reference(self, perm, sg_id): def process_sg_references(self, perm, owner_id): sg_refs = self.data.get('SGReferences') if not sg_refs: - return None + return False sg_perm = perm.get('UserIdGroupPairs', []) if not sg_perm: @@ -408,27 +527,34 @@ def process_sg_references(self, perm, owner_id): return False def __call__(self, resource): - result = self.securityGroupAttributeRequst(resource) + perm = self.securityGroupAttributeRequst(resource) matched = [] match_op = self.data.get('match-operator', 'and') == 'and' and all or any - for perm in jmespath.search(self.ip_permissions_key, json.loads(result)): - if perm.get('IpPermissions') is None or len(perm.get('IpPermissions').get(self.direction)) == 0: - continue - perm_matches = {} - perm_matches['ports'] = self.process_ports(perm) - perm_matches['cidrs'] = self.process_self_cidrs(perm) - perm_match_values = list(filter( - lambda x: x is not None, perm_matches.values())) - # account for one python behavior any([]) == False, all([]) == True - if match_op == all and not perm_match_values: - continue + if len(perm.get('IpPermissions', {}).get(self.direction, [])) == 0: + return False + # result = self.securityGroupAttributeRequst(resource) + # matched = [] + # match_op = self.data.get('match-operator', 'and') == 'and' and all or any + # for perm in jmespath.search(self.ip_permissions_key, json.loads(result)): + # if perm.get('IpPermissions') is None or len(perm.get('IpPermissions', {}).get(self.direction, [])) == 0: + # continue + perm_matches = {} + # 将cidrs和ports合并,关联判断 + perm_matches['cidrs'] = self.process_self_cidrs(perm) + return perm_matches['cidrs'] + # perm_match_values = list(filter( + # lambda x: x is not None, perm_matches.values())) + # # account for one python behavior any([]) == False, all([]) == True + # if match_op == all and not perm_match_values: + # return False + # match = match_op(perm_match_values) + # if match: + # matched.append(perm) + # + # if matched: + # resource['Matched%s' % self.ip_permissions_key] = matched + # return True - match = match_op(perm_match_values) - if match: - matched.append(perm) - if matched: - resource['Matched%s' % self.ip_permissions_key] = matched - return True class MetricsFilter(Filter): """Supports metrics filters on resources. @@ -489,7 +615,7 @@ def process(self, resources, event=None): self.statistics = self.data.get('statistics', 'Average') self.model = self.manager.get_model() self.op = OPERATORS[self.data.get('op', 'less-than')] - self.value = self.data['value'] + self.value = self.data.get('value', '') self.namespace = self.model.namespace self.log.debug("Querying metrics for %d", len(resources)) matched = [] @@ -537,15 +663,13 @@ def process_resource_set(self, resource_set): collected_metrics = r.setdefault('c7n_tencent.metrics', {}) key = "%s.%s.%s" % (self.namespace, self.metric, self.statistics) - # print(client.do_action(request)) if key not in collected_metrics: - collected_metrics[key] = json.loads(reponse)["DataPoints"] if len(collected_metrics[key]) == 0: if 'missing-value' not in self.data: continue - collected_metrics[key].append({'timestamp': self.start, self.statistics: self.data['missing-value'], 'c7n_tencent:detail': 'Fill value for missing data'}) - print(collected_metrics[key][0]['Values']) + collected_metrics[key].append({'timestamp': self.start, self.statistics: self.data['missing-value'], + 'c7n_tencent:detail': 'Fill value for missing data'}) if self.data.get('percent-attr', None) != None: rvalue = r[self.data.get('percent-attr')] if self.data.get('attr-multiplier'): @@ -558,6 +682,7 @@ def process_resource_set(self, resource_set): matched.append(r) return matched + SGPermissionSchema = { 'match-operator': {'type': 'string', 'enum': ['or', 'and']}, 'Ports': {'type': 'array', 'items': {'type': 'integer'}}, @@ -565,10 +690,11 @@ def process_resource_set(self, resource_set): 'Policy': {}, 'IpProtocol': { 'oneOf': [ - {'enum': ["-1", -1, 'TCP', 'UDP', 'ICMP', 'ICMPV6']}, + {'enum': ["-1", -1, 'TCP', 'UDP', 'ICMP', 'ICMPV6', 'tcp', 'udp', 'icmp', 'icmpv6']}, {'$ref': '#/definitions/filters/value'} ] }, + 'Action': {'type': 'string', 'enum': ['ACCEPT', 'DROP']}, 'FromPort': {'oneOf': [ {'$ref': '#/definitions/filters/value'}, {'type': 'integer'}]}, diff --git a/tools/c7n_tencent/c7n_tencent/page.py b/tools/c7n_tencent/c7n_tencent/page.py new file mode 100644 index 00000000000..50721140a4e --- /dev/null +++ b/tools/c7n_tencent/c7n_tencent/page.py @@ -0,0 +1,80 @@ +import json +from c7n_tencent.client import Session +from tencentcloud.postgres.v20170312 import postgres_client, models + + +def merge_response(old_response, response_fist_field_name, new_response): + """ + 合并分页返回值 + @param old_response: 合并对象 + @param response_fist_field_name: 需要合并的字段 + @param new_response: 新的对象 + @return: + """ + if not old_response: + return new_response + setattr(old_response, response_fist_field_name, + getattr(old_response, response_fist_field_name) + getattr(new_response, response_fist_field_name)) + return old_response + + +def def_response_len_is_next_fun(old_response, new_response, response_fist_field_name, limit, total_count): + if (len(getattr(old_response, response_fist_field_name)) if old_response else 0 + len( + getattr(new_response, response_fist_field_name)) if new_response else 0) == getattr(new_response, + total_count): + return False + return True + + +def def_is_next_fun(old_response, new_response, response_fist_field_name, limit=20, total_count_field=None): + """ + 默认判断是否有下一页方法 + @param old_response: 合并的response + @param new_response: 新的 response + @param response_fist_field_name: 需要合并的字段 + @param limit: 每页长度 + @param total_count_field: 请求结果中 (总条数)total_count字段名 + @return: + """ + if total_count_field: + if not def_response_len_is_next_fun(old_response, new_response, response_fist_field_name, limit, + total_count_field): + return False + if len(getattr(new_response, response_fist_field_name)) == limit: + return True + return False + + +def page_all(list_func, req, reponse_set_field, total_count_field=None, offset=0, limit=20, old_response=None, + is_next_fun=def_is_next_fun): + """ + 分页查询所有数据 + @param list_func: 查询分页函数 + @param reponse_set_field 返回参数数组字段名称 + @param total_count_field 返回总数名称 + @param req 请求参数 + @param offset: 偏移量 + @param limit: 每页长度 + @param old_response: 合并的response + @param is_next_fun: 判断是否有下一页 + @return: + """ + + params = { + "Offset": offset, + "Limit": limit + } + req.from_json_string(json.dumps(params)) + respose = list_func(req) + old_response = merge_response(old_response, reponse_set_field, respose) + if is_next_fun(old_response, respose, reponse_set_field, limit, total_count_field): + return page_all(list_func, offset + 1, limit, old_response, is_next_fun) + return old_response + + +if __name__ == '__main__': + lsb = Session('AKID65nHX08TO3UZWPPtMchfZEdmrj3A9iHi', 'MXVF2iDZbOzNYVII5nsjtpvBCVZPzbaX', 'ap-shanghai').client( + 'postgres_client') + req = models.DescribeDBInstancesRequest() + res = page_all(lsb.DescribeDBInstances, req, 'DBInstanceSet', 'TotalCount'); + print(res) diff --git a/tools/c7n_tencent/c7n_tencent/query.py b/tools/c7n_tencent/c7n_tencent/query.py index 7c7fd0441f2..2014bcb0fab 100644 --- a/tools/c7n_tencent/c7n_tencent/query.py +++ b/tools/c7n_tencent/c7n_tencent/query.py @@ -41,23 +41,16 @@ def filter(self, resource_manager, **params): params.update(extra_args) if m.service == 'cos': - result = client.listBuckets() + result = client.list_buckets() buckets = [] for b in result.buckets: b.__dict__['F2CId'] = b.__dict__[m.id] buckets.append(b.__dict__) return buckets else: - request = resource_manager.get_request() - if request: - result = request - else: - return None + res = resource_manager.get_request() false = "false" true = "true" - if path is None: - return result - res = jmespath.search(path, eval(result)) for data in res: data['F2CId'] = data[m.id] return res diff --git a/tools/c7n_tencent/c7n_tencent/resources/cdb.py b/tools/c7n_tencent/c7n_tencent/resources/cdb.py index 7b55b25fcdc..c2835adb8bc 100644 --- a/tools/c7n_tencent/c7n_tencent/resources/cdb.py +++ b/tools/c7n_tencent/c7n_tencent/resources/cdb.py @@ -11,8 +11,10 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +import json import logging +import jmespath from tencentcloud.cdb.v20170320 import models from tencentcloud.common.exception.tencent_cloud_sdk_exception import TencentCloudSDKException @@ -34,20 +36,34 @@ class resource_type(TypeInfo): id = 'InstanceId' def get_request(self): + offset = 0 + limit = 20 + res = [] try: - req = models.DescribeDBInstancesRequest() - resp = Session.client(self, service).DescribeDBInstances(req) - # 输出json格式的字符串回包 - # print(resp.to_json_string(indent=2)) - - # 也可以取出单个值。 - # 你可以通过官网接口文档或跳转到response对象的定义处查看返回字段的定义。 - # print(resp.to_json_string()) + while 0 <= offset: + req = models.DescribeDBInstancesRequest() + params = { + "Offset": offset, + "Limit": limit + } + req.from_json_string(json.dumps(params)) + resp = Session.client(self, service).DescribeDBInstances(req) + respose = resp.to_json_string().replace('null', 'None').replace('false', 'False').replace('true', 'True') + result = jmespath.search('Items', eval(respose)) + res = res + result + if len(result) == limit: + offset += limit + else: + return res + # 输出json格式的字符串回包 + # print(resp.to_json_string(indent=2)) + + # 也可以取出单个值。 + # 你可以通过官网接口文档或跳转到response对象的定义处查看返回字段的定义。 + # print(resp.to_json_string()) except TencentCloudSDKException as err: logging.error(err) - return False - # tencent 返回的json里居然不是None,而是java的null,活久见 - return resp.to_json_string().replace('null', 'None') + return res @Cdb.filter_registry.register('Internet') class TencentCdbFilter(TencentCdbFilter): @@ -102,11 +118,11 @@ class InternetAccessCdbFilter(TencentFilter): **{'value': {'type': 'boolean'}}) def get_request(self, i): - if self.data['value']: - if i['WanStatus'] == 1: + if self.data.get('value', ''): + if i.get('WanStatus', '') == 1: return i else: - if i['WanStatus'] != 1: + if i.get('WanStatus', '') != 1: return i return False @@ -130,7 +146,7 @@ class DeviceTypeCdbFilter(TencentFilter): **{'value': {'type': 'string'}}) def get_request(self, i): - if i['DeviceType'] == self.data['value']: + if i.get('DeviceType', '') == self.data.get('value', ''): return i return False @@ -155,12 +171,12 @@ class AvailablezonesCdbFilter(TencentFilter): **{'value': {'type': 'boolean'}}) def get_request(self, i): - if self.data['value']: - if i['DeployMode'] == 1: + if self.data.get('value', ''): + if i.get('DeployMode', '') == 1: return i return False else: - if i['DeployMode'] != 1: + if i.get('DeployMode', '') != 1: return i return False @@ -183,12 +199,12 @@ class NetworkTypeCdbFilter(TencentFilter): **{'value': {'type': 'string'}}) def get_request(self, i): - if self.data['value'] == "vpc": - if i['VpcId']: + if self.data.get('value', '') == "vpc": + if i.get('VpcId', ''): return False return i else: - if i['VpcId']: + if i.get('VpcId', ''): return i return False diff --git a/tools/c7n_tencent/c7n_tencent/resources/clb.py b/tools/c7n_tencent/c7n_tencent/resources/clb.py index 1b7883cf91a..29f9171fa2d 100644 --- a/tools/c7n_tencent/c7n_tencent/resources/clb.py +++ b/tools/c7n_tencent/c7n_tencent/resources/clb.py @@ -11,8 +11,10 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +import json import logging +import jmespath from tencentcloud.clb.v20180317 import models from tencentcloud.common.exception.tencent_cloud_sdk_exception import TencentCloudSDKException @@ -33,23 +35,35 @@ class resource_type(TypeInfo): id = 'LoadBalancerId' def get_request(self): + offset = 0 + limit = 20 + res = [] try: - # 实例化一个cvm实例信息查询请求对象,每个接口都会对应一个request对象。 - req = models.DescribeLoadBalancersRequest() - params = '{}' - req.from_json_string(params) - resp = Session.client(self, service).DescribeLoadBalancers(req) - # 输出json格式的字符串回包 - # print(resp.to_json_string(indent=2)) - - # 也可以取出单个值。 - # 你可以通过官网接口文档或跳转到response对象的定义处查看返回字段的定义。 - # print(resp.to_json_string()) + while 0 <= offset: + # 实例化一个cvm实例信息查询请求对象,每个接口都会对应一个request对象。 + req = models.DescribeLoadBalancersRequest() + params = { + "Offset": offset, + "Limit": limit + } + req.from_json_string(json.dumps(params)) + resp = Session.client(self, service).DescribeLoadBalancers(req) + respose = resp.to_json_string().replace('null', 'None').replace('false', 'False').replace('true', 'True') + result = jmespath.search('LoadBalancerSet', eval(respose)) + res = res + result + if len(result) == limit: + offset += limit + else: + return res + # 输出json格式的字符串回包 + # print(resp.to_json_string(indent=2)) + + # 也可以取出单个值。 + # 你可以通过官网接口文档或跳转到response对象的定义处查看返回字段的定义。 + # print(resp.to_json_string()) except TencentCloudSDKException as err: logging.error(err) - return False - # tencent 返回的json里居然不是None,而是java的null,活久见 - return resp.to_json_string().replace('null', 'None') + return res @Clb.filter_registry.register('unused') @@ -69,7 +83,7 @@ class TencentClbFilter(TencentClbFilter): schema = type_schema('unused') def get_request(self, i): - LoadBalancerId = i['LoadBalancerId'] + LoadBalancerId = i.get('LoadBalancerId', '') # clb 查询clb下是否有监听 self.LoadBalancerId = LoadBalancerId req = models.DescribeTargetsRequest() @@ -103,7 +117,7 @@ class AclsClbFilter(TencentFilter): **{'value': {'type': 'string'}}) def get_request(self, i): - if i['LoadBalancerType'] and i['LoadBalancerType'] == self.data['value']: + if i.get('LoadBalancerType', '') and i.get('LoadBalancerType', '') == self.data.get('value', ''): return False return i @@ -126,8 +140,8 @@ class BandwidthClbFilter(TencentFilter): **{'value': {'type': 'number'}}) def get_request(self, i): - InternetMaxBandwidthOut = i['NetworkAttributes']['InternetMaxBandwidthOut'] - if InternetMaxBandwidthOut and self.data['value'] < InternetMaxBandwidthOut: + InternetMaxBandwidthOut = i.get('NetworkAttributes', {}).get('InternetMaxBandwidthOut', 0) + if InternetMaxBandwidthOut and self.data.get('value', '') < InternetMaxBandwidthOut: return False return i @@ -151,12 +165,12 @@ class NetworkTypeClbFilter(TencentFilter): **{'value': {'type': 'string'}}) def get_request(self, i): - if self.data['value'] == "vpc": - if i['VpcId']: + if self.data.get('value', '') == "vpc": + if i.get('VpcId', ''): return False return i else: - if i['VpcId']: + if i.get('VpcId', ''): return i return False diff --git a/tools/c7n_tencent/c7n_tencent/resources/cos.py b/tools/c7n_tencent/c7n_tencent/resources/cos.py index f6fc901c8c0..cd1433fcd68 100644 --- a/tools/c7n_tencent/c7n_tencent/resources/cos.py +++ b/tools/c7n_tencent/c7n_tencent/resources/cos.py @@ -40,15 +40,20 @@ class resource_type(TypeInfo): id = 'Name' def get_request(self): + resp = [] try: resp = Session.client(self, service).list_buckets() _resp_ = [] - for i in resp['Buckets']['Bucket']: + Buckets = resp.get("Buckets", {}) + if Buckets is None: + Buckets = {} + for i in Buckets.get("Bucket", []): if i['Location'] == regionId: _resp_.append(i) + resp['Buckets'] = Buckets resp['Buckets']['Bucket'] = _resp_ - for obj in resp['Buckets']['Bucket']: - if regionId != obj['Location']: + for obj in Buckets.get("Bucket", []): + if regionId != obj.get('Location', ''): continue objects = list() try: @@ -59,16 +64,14 @@ def get_request(self): continue objects.append(response['Contents']) #响应条目是否被截断,布尔值,例如true或false - if response['IsTruncated'] == 'false': + if response.get('IsTruncated', '') == 'false': continue obj['Objects'] = objects except Exception as e: # 捕获requests抛出的如timeout等客户端错误,转化为客户端错误 logging.error(str(e)) - return json.dumps(resp) except TencentCloudSDKException as err: logging.error(err) - return json.dumps(resp) - return json.dumps(resp) + return eval(json.dumps(Buckets.get('Bucket', []))) @@ -105,14 +108,14 @@ def process_bucket(self, b): response = Session.client(self, service).get_bucket_acl( Bucket=b['Name'] ) - Grant = response.get('AccessControlList').get('Grant') + Grant = response.get('AccessControlList', {}).get('Grant', []) for i in Grant: # 指明授予被授权者的存储桶权限,可选值有 FULL_CONTROL,WRITE,READ,分别对应读写权限、写权限、读权限 - if i.get('Permission') == 'FULL_CONTROL': + if i.get('Permission', '') == 'FULL_CONTROL': b['Permission'] = response return b else: - if self.data['value'] in i.get('Permission'): + if self.data.get('value', '') in i.get('Permission', ''): b['Permission'] = response return b return False @@ -139,7 +142,7 @@ def get_request(self, i): result = Session.client(self, service).get_bucket_referer( Bucket=i['Name'] ) - if self.data['value']: + if self.data.get('value', ''): if result and result.Status == 'Eabled': return False else: @@ -173,7 +176,7 @@ def get_request(self, i): result = Session.client(self, service).get_bucket_encryption( Bucket=i['Name'] ) - if self.data['value']: + if self.data.get('value', ''): if 'Error' in result: return i else: diff --git a/tools/c7n_tencent/c7n_tencent/resources/cvm.py b/tools/c7n_tencent/c7n_tencent/resources/cvm.py index ec31c2dc51f..e32fe2bcea6 100644 --- a/tools/c7n_tencent/c7n_tencent/resources/cvm.py +++ b/tools/c7n_tencent/c7n_tencent/resources/cvm.py @@ -11,9 +11,11 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +import json import logging import operator +import jmespath from tencentcloud.common.exception.tencent_cloud_sdk_exception import TencentCloudSDKException # 导入对应产品模块的client models。 from tencentcloud.cvm.v20170312 import models @@ -39,24 +41,38 @@ class resource_type(TypeInfo): dimension = 'InstanceId' def get_request(self): + offset = 0 + limit = 20 + res = [] try: - # 实例化一个cvm实例信息查询请求对象,每个接口都会对应一个request对象。 - req = models.DescribeInstancesRequest() - # 通过client对象调用DescribeInstances方法发起请求。注意请求方法名与请求对象是对应的。 - # 返回的resp是一个DescribeInstancesResponse类的实例,与请求对象对应。 - resp = Session.client(self, service).DescribeInstances(req) - # 输出json格式的字符串回包 - # print(resp.to_json_string(indent=2)) - - # 也可以取出单个值。 - # 你可以通过官网接口文档或跳转到response对象的定义处查看返回字段的定义。 - # print(resp.InstanceSet) - # print(resp.to_json_string()) + while 0 <= offset: + # 实例化一个cvm实例信息查询请求对象,每个接口都会对应一个request对象。 + req = models.DescribeInstancesRequest() + params = { + "Offset": offset, + "Limit": limit + } + req.from_json_string(json.dumps(params)) + # 通过client对象调用DescribeInstances方法发起请求。注意请求方法名与请求对象是对应的。 + # 返回的resp是一个DescribeInstancesResponse类的实例,与请求对象对应。 + resp = Session.client(self, service).DescribeInstances(req) + respose = resp.to_json_string().replace('null', 'None').replace('false', 'False').replace('true', 'True') + result = jmespath.search('InstanceSet', eval(respose)) + res = res + result + if len(result) == limit: + offset += limit + else: + return res + # 输出json格式的字符串回包 + # print(resp.to_json_string(indent=2)) + + # 也可以取出单个值。 + # 你可以通过官网接口文档或跳转到response对象的定义处查看返回字段的定义。 + # print(resp.InstanceSet) + # print(resp.to_json_string()) except TencentCloudSDKException as err: logging.error(err) - return False - # tencent 返回的json里居然不是None,而是java的null,活久见 - return resp.to_json_string().replace('null', 'None') + return res @Cvm.filter_registry.register('metrics') class CvmMetricsFilter(MetricsFilter): @@ -94,7 +110,7 @@ class CvmAgeFilter(TencentAgeFilter): def get_resource_date(self, i): # '2019-11-20T08:21:02Z' - return i['CreatedTime'] + return i.get('CreatedTime', '2021-08-10T08:21:02Z') @Cvm.action_registry.register('start') class Start(MethodAction): @@ -176,7 +192,7 @@ class PublicIpAddress(TencentFilter): def get_request(self, i): data = i[self.public_ip_address] - if len(data) == 0: + if data is None or len(data) == 0: return False return i @@ -209,6 +225,6 @@ class StopChargingMode(TencentFilter): def get_request(self, i): data = i[self.stop_charging_mode] - if data == self.data['value']: + if data == self.data.get('value', ''): return False return i diff --git a/tools/c7n_tencent/c7n_tencent/resources/dcdb.py b/tools/c7n_tencent/c7n_tencent/resources/dcdb.py index f244268f0b0..d3000fff353 100644 --- a/tools/c7n_tencent/c7n_tencent/resources/dcdb.py +++ b/tools/c7n_tencent/c7n_tencent/resources/dcdb.py @@ -11,8 +11,10 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +import json import logging +import jmespath from tencentcloud.common.exception.tencent_cloud_sdk_exception import TencentCloudSDKException from tencentcloud.dcdb.v20180411 import models @@ -33,20 +35,34 @@ class resource_type(TypeInfo): id = 'InstancesId' def get_request(self): + offset = 0 + limit = 20 + res = [] try: - req = models.DescribeDCDBInstancesRequest() - resp = Session.client(self, service).DescribeDCDBInstances(req) - # 输出json格式的字符串回包 - # print(resp.to_json_string(indent=2)) + while 0 <= offset: + req = models.DescribeDCDBInstancesRequest() + params = { + "Offset": offset, + "Limit": limit + } + req.from_json_string(json.dumps(params)) + resp = Session.client(self, service).DescribeDCDBInstances(req) + respose = resp.to_json_string().replace('null', 'None').replace('false', 'False').replace('true', 'True') + result = jmespath.search('Instances', eval(respose)) + res = res + result + if len(result) == limit: + offset += limit + else: + return res + # 输出json格式的字符串回包 + # print(resp.to_json_string(indent=2)) - # 也可以取出单个值。 - # 你可以通过官网接口文档或跳转到response对象的定义处查看返回字段的定义。 - # print(resp.to_json_string()) + # 也可以取出单个值。 + # 你可以通过官网接口文档或跳转到response对象的定义处查看返回字段的定义。 + # print(resp.to_json_string()) except TencentCloudSDKException as err: logging.error(err) - return False - # tencent 返回的json里居然不是None,而是java的null,活久见 - return resp.to_json_string().replace('null', 'None') + return res @Dcdb.filter_registry.register('Internet') class TencentDcdbFilter(TencentCdbFilter): diff --git a/tools/c7n_tencent/c7n_tencent/resources/disk.py b/tools/c7n_tencent/c7n_tencent/resources/disk.py index a377badd710..ddcf7cc50b6 100644 --- a/tools/c7n_tencent/c7n_tencent/resources/disk.py +++ b/tools/c7n_tencent/c7n_tencent/resources/disk.py @@ -11,8 +11,10 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +import json import logging +import jmespath from tencentcloud.cbs.v20170312 import models from tencentcloud.common.exception.tencent_cloud_sdk_exception import TencentCloudSDKException @@ -34,20 +36,34 @@ class resource_type(TypeInfo): id = 'DiskId' def get_request(self): + offset = 0 + limit = 20 + res = [] try: - req = models.DescribeDisksRequest() - resp = Session.client(self, service).DescribeDisks(req) - # 输出json格式的字符串回包 - # print(resp.to_json_string(indent=2)) - - # 也可以取出单个值。 - # 你可以通过官网接口文档或跳转到response对象的定义处查看返回字段的定义。 - # print(resp.to_json_string()) + while 0 <= offset: + req = models.DescribeDisksRequest() + params = { + "Offset": offset, + "Limit": limit + } + req.from_json_string(json.dumps(params)) + resp = Session.client(self, service).DescribeDisks(req) + respose = resp.to_json_string().replace('null', 'None').replace('false', 'False').replace('true', 'True') + result = jmespath.search('DiskSet', eval(respose)) + res = res + result + if len(result) == limit: + offset += limit + else: + return res + # 输出json格式的字符串回包 + # print(resp.to_json_string(indent=2)) + + # 也可以取出单个值。 + # 你可以通过官网接口文档或跳转到response对象的定义处查看返回字段的定义。 + # print(resp.to_json_string()) except TencentCloudSDKException as err: logging.error(err) - return False - # tencent 返回的json里居然不是None,而是java的null,活久见 - return resp.to_json_string().replace('null', 'None') + return res @Disk.filter_registry.register('unused') class TencentDiskFilter(TencentDiskFilter): @@ -97,7 +113,7 @@ class encrypt(TencentFilter): def get_request(self, i): data = i[self.encrypt] - if data == self.data['value']: + if data == self.data.get('value', ''): return False return i diff --git a/tools/c7n_tencent/c7n_tencent/resources/eip.py b/tools/c7n_tencent/c7n_tencent/resources/eip.py index 8b70649a95e..4f67a6e557a 100644 --- a/tools/c7n_tencent/c7n_tencent/resources/eip.py +++ b/tools/c7n_tencent/c7n_tencent/resources/eip.py @@ -11,8 +11,10 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +import json import logging +import jmespath from tencentcloud.common.exception.tencent_cloud_sdk_exception import TencentCloudSDKException from tencentcloud.vpc.v20170312 import models @@ -34,20 +36,34 @@ class resource_type(TypeInfo): id = 'AddressId' def get_request(self): + offset = 0 + limit = 20 + res = [] try: - req = models.DescribeAddressesRequest() - resp = Session.client(self, service).DescribeAddresses(req) - # 输出json格式的字符串回包 - # print(resp.to_json_string(indent=2)) - - # 也可以取出单个值。 - # 你可以通过官网接口文档或跳转到response对象的定义处查看返回字段的定义。 - # print(resp.to_json_string()) + while 0 <= offset: + req = models.DescribeAddressesRequest() + params = { + "Offset": offset, + "Limit": limit + } + req.from_json_string(json.dumps(params)) + resp = Session.client(self, service).DescribeAddresses(req) + respose = resp.to_json_string().replace('null', 'None').replace('false', 'False').replace('true', 'True') + result = jmespath.search('AddressSet', eval(respose)) + res = res + result + if len(result) == limit: + offset += limit + else: + return res + # 输出json格式的字符串回包 + # print(resp.to_json_string(indent=2)) + + # 也可以取出单个值。 + # 你可以通过官网接口文档或跳转到response对象的定义处查看返回字段的定义。 + # print(resp.to_json_string()) except TencentCloudSDKException as err: logging.error(err) - return False - # tencent 返回的json里居然不是None,而是java的null,活久见 - return resp.to_json_string().replace('null', 'None') + return res @Eip.filter_registry.register('unused') class TencentEipFilter(TencentEipFilter): @@ -87,7 +103,7 @@ class BandwidthEipFilter(TencentFilter): **{'value': {'type': 'number'}}) def get_request(self, i): - if i['Bandwidth'] and self.data['value'] < i['Bandwidth']: + if i.get('Bandwidth', '') and self.data.get('value', '') < i.get('Bandwidth', ''): return False return i diff --git a/tools/c7n_tencent/c7n_tencent/resources/es.py b/tools/c7n_tencent/c7n_tencent/resources/es.py new file mode 100644 index 00000000000..bc82cd394e7 --- /dev/null +++ b/tools/c7n_tencent/c7n_tencent/resources/es.py @@ -0,0 +1,634 @@ +import jmespath +from c7n_tencent import page, filter_util +from c7n_tencent.client import Session +from c7n_tencent.filters.filter import TencentFilter, TencentEsFilter +from c7n_tencent.provider import resources +from c7n_tencent.query import QueryResourceManager, TypeInfo +from tencentcloud.common.exception.tencent_cloud_sdk_exception import TencentCloudSDKException +from tencentcloud.es.v20180416 import es_client, models +import logging + +from c7n.utils import type_schema + +service = 'es_client.es' + + +@resources.register('es') +class Postgres(QueryResourceManager): + class resource_type(TypeInfo): + service = 'postgres_client.postgres' + enum_spec = (None, 'InstanceList', None) + id = 'InstanceId' + + def get_request(self): + listField = self.resource_type.enum_spec[1] + res = [] + try: + req = models.DescribeInstancesRequest() + client = Session.client(self, service) + # 查询到所有分页数据 + resp = page.page_all(client.DescribeInstances, req, listField, + 'TotalCount') + # 将结果转换为字典 + respose = resp.to_json_string().replace('null', 'None').replace('false', 'False').replace('true', 'True') + res = jmespath.search(listField, eval(respose)) + # 也可以取出单个值。 + # 你可以通过官网接口文档或跳转到response对象的定义处查看返回字段的定义。 + except TencentCloudSDKException as err: + logging.error(err) + return res + + +@Postgres.filter_registry.register('InstanceId') +class NetworkTypePostgresFilter(TencentEsFilter): + schema = type_schema( + 'InstanceId', + **filter_util.get_schema('string')) + + +@Postgres.filter_registry.register('InstanceName') +class NetworkTypePostgresFilter(TencentEsFilter): + schema = type_schema( + 'InstanceName', + **filter_util.get_schema('string')) + + +@Postgres.filter_registry.register('Region') +class NetworkTypePostgresFilter(TencentEsFilter): + schema = type_schema( + 'Region', + **filter_util.get_schema('string')) + + +@Postgres.filter_registry.register('Zone') +class NetworkTypePostgresFilter(TencentEsFilter): + schema = type_schema( + 'Zone', + **filter_util.get_schema('string')) + + +@Postgres.filter_registry.register('AppId') +class NetworkTypePostgresFilter(TencentEsFilter): + schema = type_schema( + 'AppId', + **filter_util.get_schema('number')) + + +@Postgres.filter_registry.register('Uin') +class NetworkTypePostgresFilter(TencentEsFilter): + schema = type_schema( + 'Uin', + **filter_util.get_schema('string')) + + +@Postgres.filter_registry.register('VpcUid') +class NetworkTypePostgresFilter(TencentEsFilter): + schema = type_schema( + 'VpcUid', + **filter_util.get_schema('string')) + + +@Postgres.filter_registry.register('SubnetUid') +class NetworkTypePostgresFilter(TencentEsFilter): + schema = type_schema( + 'SubnetUid', + **filter_util.get_schema('string')) + + +@Postgres.filter_registry.register('Status') +class NetworkTypePostgresFilter(TencentEsFilter): + schema = type_schema( + 'Status', + **filter_util.get_schema('number')) + + +@Postgres.filter_registry.register('ChargeType') +class NetworkTypePostgresFilter(TencentEsFilter): + schema = type_schema( + 'ChargeType', + **filter_util.get_schema('string')) + + +@Postgres.filter_registry.register('ChargePeriod') +class NetworkTypePostgresFilter(TencentEsFilter): + schema = type_schema( + 'ChargePeriod', + **filter_util.get_schema('number')) + + +@Postgres.filter_registry.register('RenewFlag') +class NetworkTypePostgresFilter(TencentEsFilter): + schema = type_schema( + 'RenewFlag', + **filter_util.get_schema('string')) + + +@Postgres.filter_registry.register('NodeType') +class NetworkTypePostgresFilter(TencentEsFilter): + schema = type_schema( + 'NodeType', + **filter_util.get_schema('string')) + + +@Postgres.filter_registry.register('NodeNum') +class NetworkTypePostgresFilter(TencentEsFilter): + schema = type_schema( + 'NodeNum', + **filter_util.get_schema('number')) + + +@Postgres.filter_registry.register('CpuNum') +class NetworkTypePostgresFilter(TencentEsFilter): + schema = type_schema( + 'CpuNum', + **filter_util.get_schema('number')) + + +@Postgres.filter_registry.register('MemSize') +class NetworkTypePostgresFilter(TencentEsFilter): + schema = type_schema( + 'MemSize', + **filter_util.get_schema('number')) + + +@Postgres.filter_registry.register('DiskType') +class NetworkTypePostgresFilter(TencentEsFilter): + schema = type_schema( + 'DiskType', + **filter_util.get_schema('string')) + + +@Postgres.filter_registry.register('DiskSize') +class NetworkTypePostgresFilter(TencentEsFilter): + schema = type_schema( + 'DiskSize', + **filter_util.get_schema('number')) + + +@Postgres.filter_registry.register('EsDomain') +class NetworkTypePostgresFilter(TencentEsFilter): + schema = type_schema( + 'EsDomain', + **filter_util.get_schema('string')) + + +@Postgres.filter_registry.register('EsVip') +class NetworkTypePostgresFilter(TencentEsFilter): + schema = type_schema( + 'EsVip', + **filter_util.get_schema('string')) + + +@Postgres.filter_registry.register('EsPort') +class NetworkTypePostgresFilter(TencentEsFilter): + schema = type_schema( + 'EsPort', + **filter_util.get_schema('number')) + + +@Postgres.filter_registry.register('KibanaUrl') +class NetworkTypePostgresFilter(TencentEsFilter): + schema = type_schema( + 'KibanaUrl', + **filter_util.get_schema('string')) + + +@Postgres.filter_registry.register('EsVersion') +class NetworkTypePostgresFilter(TencentEsFilter): + schema = type_schema( + 'EsVersion', + **filter_util.get_schema('string')) + + +# @Postgres.filter_registry.register('EsConfig') +# class NetworkTypePostgresFilter(TencentEsFilter): +# schema = type_schema( +# 'EsConfig', +# **filter_util.get_schema('string')) +# + +@Postgres.filter_registry.register('EsAcl.BlackIpList') +class NetworkTypePostgresFilter(TencentEsFilter): + schema = type_schema( + 'EsAcl.BlackIpList', + **filter_util.get_schema('list_string')) + + +@Postgres.filter_registry.register('EsAcl.WhiteIpList') +class NetworkTypePostgresFilter(TencentEsFilter): + schema = type_schema( + 'EsAcl.WhiteIpList', + **filter_util.get_schema('list_string')) + + +@Postgres.filter_registry.register('CreateTime') +class NetworkTypePostgresFilter(TencentEsFilter): + schema = type_schema( + 'CreateTime', + **filter_util.get_schema('time')) + + +@Postgres.filter_registry.register('UpdateTime') +class NetworkTypePostgresFilter(TencentEsFilter): + schema = type_schema( + 'UpdateTime', + **filter_util.get_schema('time')) + + +@Postgres.filter_registry.register('Deadline') +class NetworkTypePostgresFilter(TencentEsFilter): + schema = type_schema( + 'Deadline', + **filter_util.get_schema('time')) + + +@Postgres.filter_registry.register('InstanceType') +class NetworkTypePostgresFilter(TencentEsFilter): + schema = type_schema( + 'InstanceType', + **filter_util.get_schema('number')) + + +@Postgres.filter_registry.register('IkConfig.MainDict..Key') +class NetworkTypePostgresFilter(TencentEsFilter): + schema = type_schema( + 'IkConfig..MainDict.Key', + **filter_util.get_schema('string')) + + +@Postgres.filter_registry.register('IkConfig.MainDict..Name') +class NetworkTypePostgresFilter(TencentEsFilter): + schema = type_schema( + 'IkConfig..MainDict.Name', + **filter_util.get_schema('string')) + + +@Postgres.filter_registry.register('IkConfig.MainDict..Size') +class NetworkTypePostgresFilter(TencentEsFilter): + schema = type_schema( + 'IkConfig..MainDict.Size', + **filter_util.get_schema('number')) + + +@Postgres.filter_registry.register('IkConfig.Stopwords..Key') +class NetworkTypePostgresFilter(TencentEsFilter): + schema = type_schema( + 'IkConfig..Stopwords.Key', + **filter_util.get_schema('string')) + + +@Postgres.filter_registry.register('IkConfig.Stopwords..Name') +class NetworkTypePostgresFilter(TencentEsFilter): + schema = type_schema( + 'IkConfig..Stopwords.Name', + **filter_util.get_schema('string')) + + +@Postgres.filter_registry.register('IkConfig.Stopwords..Size') +class NetworkTypePostgresFilter(TencentEsFilter): + schema = type_schema( + 'IkConfig.Stopwords..Size', + **filter_util.get_schema('number')) + + +@Postgres.filter_registry.register('IkConfig.QQDict..Key') +class NetworkTypePostgresFilter(TencentEsFilter): + schema = type_schema( + 'IkConfig.QQDict..Key', + **filter_util.get_schema('string')) + + +@Postgres.filter_registry.register('IkConfig.QQDict..Name') +class NetworkTypePostgresFilter(TencentEsFilter): + schema = type_schema( + 'IkConfig.QQDict..Name', + **filter_util.get_schema('string')) + + +@Postgres.filter_registry.register('IkConfig.QQDict..Size') +class NetworkTypePostgresFilter(TencentEsFilter): + schema = type_schema( + 'IkConfig.QQDict..Size', + **filter_util.get_schema('number')) + + +@Postgres.filter_registry.register('IkConfig.Synonym..Key') +class NetworkTypePostgresFilter(TencentEsFilter): + schema = type_schema( + 'IkConfig.Synonym..Key', + **filter_util.get_schema('string')) + + +@Postgres.filter_registry.register('IkConfig.Synonym..Name') +class NetworkTypePostgresFilter(TencentEsFilter): + schema = type_schema( + 'IkConfig.Synonym..Name', + **filter_util.get_schema('string')) + + +@Postgres.filter_registry.register('IkConfig.Synonym..Size') +class NetworkTypePostgresFilter(TencentEsFilter): + schema = type_schema( + 'IkConfig.Synonym..Size', + **filter_util.get_schema('number')) + + +@Postgres.filter_registry.register('IkConfig.UpdateType') +class NetworkTypePostgresFilter(TencentEsFilter): + schema = type_schema( + 'IkConfig.UpdateType', + **filter_util.get_schema('string')) + + +@Postgres.filter_registry.register('MasterNodeInfo.EnableDedicatedMaster') +class NetworkTypePostgresFilter(TencentEsFilter): + schema = type_schema( + 'MasterNodeInfo.UpdateType', + **filter_util.get_schema('boolean')) + + +@Postgres.filter_registry.register('MasterNodeInfo.MasterNodeType') +class NetworkTypePostgresFilter(TencentEsFilter): + schema = type_schema( + 'MasterNodeInfo.MasterNodeType', + **filter_util.get_schema('string')) + + +@Postgres.filter_registry.register('MasterNodeInfo.MasterNodeNum') +class NetworkTypePostgresFilter(TencentEsFilter): + schema = type_schema( + 'MasterNodeInfo.MasterNodeNum', + **filter_util.get_schema('number')) + + +@Postgres.filter_registry.register('MasterNodeInfo.MasterNodeCpuNum') +class NetworkTypePostgresFilter(TencentEsFilter): + schema = type_schema( + 'MasterNodeInfo.MasterNodeCpuNum', + **filter_util.get_schema('number')) + + +@Postgres.filter_registry.register('MasterNodeInfo.MasterNodeMemSize') +class NetworkTypePostgresFilter(TencentEsFilter): + schema = type_schema( + 'MasterNodeInfo.MasterNodeMemSize', + **filter_util.get_schema('number')) + + +@Postgres.filter_registry.register('MasterNodeInfo.MasterNodeDiskSize') +class NetworkTypePostgresFilter(TencentEsFilter): + schema = type_schema( + 'MasterNodeInfo.MasterNodeDiskSize', + **filter_util.get_schema('number')) + + +@Postgres.filter_registry.register('MasterNodeInfo.MasterNodeDiskType') +class NetworkTypePostgresFilter(TencentEsFilter): + schema = type_schema( + 'MasterNodeInfo.MasterNodeDiskType', + **filter_util.get_schema('string')) + + +@Postgres.filter_registry.register('CosBackup.IsAutoBackup') +class NetworkTypePostgresFilter(TencentEsFilter): + schema = type_schema( + 'CosBackup.IsAutoBackup', + **filter_util.get_schema('boolean')) + + +@Postgres.filter_registry.register('CosBackup.BackupTime') +class NetworkTypePostgresFilter(TencentEsFilter): + schema = type_schema( + 'CosBackup.BackupTime', + **filter_util.get_schema('time')) + + +@Postgres.filter_registry.register('AllowCosBackup') +class NetworkTypePostgresFilter(TencentEsFilter): + schema = type_schema( + 'AllowCosBackup', + **filter_util.get_schema('boolean')) + + +@Postgres.filter_registry.register('TagList') +class NetworkTypePostgresFilter(TencentEsFilter): + schema = type_schema( + 'TagList', + **filter_util.get_schema('list_string')) + + +@Postgres.filter_registry.register('LicenseType') +class NetworkTypePostgresFilter(TencentEsFilter): + schema = type_schema( + 'LicenseType', + **filter_util.get_schema('string')) + + +@Postgres.filter_registry.register('EnableHotWarmMode') +class NetworkTypePostgresFilter(TencentEsFilter): + schema = type_schema( + 'EnableHotWarmMode', + **filter_util.get_schema('boolean')) + + +@Postgres.filter_registry.register('WarmNodeType') +class NetworkTypePostgresFilter(TencentEsFilter): + schema = type_schema( + 'WarmNodeType', + **filter_util.get_schema('string')) + + +@Postgres.filter_registry.register('WarmNodeNum') +class NetworkTypePostgresFilter(TencentEsFilter): + schema = type_schema( + 'WarmNodeNum', + **filter_util.get_schema('number')) + + +@Postgres.filter_registry.register('WarmCpuNum') +class NetworkTypePostgresFilter(TencentEsFilter): + schema = type_schema( + 'WarmCpuNum', + **filter_util.get_schema('number')) + + +@Postgres.filter_registry.register('WarmMemSize') +class NetworkTypePostgresFilter(TencentEsFilter): + schema = type_schema( + 'WarmMemSize', + **filter_util.get_schema('number')) + + +@Postgres.filter_registry.register('WarmDiskType') +class NetworkTypePostgresFilter(TencentEsFilter): + schema = type_schema( + 'WarmDiskType', + **filter_util.get_schema('string')) + + +@Postgres.filter_registry.register('WarmDiskSize') +class NetworkTypePostgresFilter(TencentEsFilter): + schema = type_schema( + 'WarmDiskSize', + **filter_util.get_schema('number')) + + +@Postgres.filter_registry.register('NodeInfoList..NodeNum') +class NetworkTypePostgresFilter(TencentEsFilter): + schema = type_schema( + 'NodeInfoList..NodeNum', + **filter_util.get_schema('number')) + + +@Postgres.filter_registry.register('NodeInfoList..NodeType') +class NetworkTypePostgresFilter(TencentEsFilter): + schema = type_schema( + 'NodeInfoList..NodeType', + **filter_util.get_schema('string')) + + +@Postgres.filter_registry.register('NodeInfoList..Type') +class NetworkTypePostgresFilter(TencentEsFilter): + schema = type_schema( + 'NodeInfoList..Type', + **filter_util.get_schema('string')) + + +@Postgres.filter_registry.register('NodeInfoList..DiskType') +class NetworkTypePostgresFilter(TencentEsFilter): + schema = type_schema( + 'NodeInfoList..DiskType', + **filter_util.get_schema('string')) + + +@Postgres.filter_registry.register('NodeInfoList..DiskSize') +class NetworkTypePostgresFilter(TencentEsFilter): + schema = type_schema( + 'NodeInfoList..DiskSize', + **filter_util.get_schema('number')) + + +@Postgres.filter_registry.register('NodeInfoList..LocalDiskInfo') +class NetworkTypePostgresFilter(TencentEsFilter): + schema = type_schema( + 'NodeInfoList..LocalDiskInfo', + **filter_util.get_schema('is_empty')) + + +@Postgres.filter_registry.register('NodeInfoList.LocalDiskInfo..LocalDiskType') +class NetworkTypePostgresFilter(TencentEsFilter): + schema = type_schema( + 'NodeInfoList.LocalDiskInfo..LocalDiskType', + **filter_util.get_schema('string')) + + +@Postgres.filter_registry.register('NodeInfoList.LocalDiskInfo..LocalDiskSize') +class NetworkTypePostgresFilter(TencentEsFilter): + schema = type_schema( + 'NodeInfoList.LocalDiskInfo..LocalDiskSize', + **filter_util.get_schema('number')) + + +@Postgres.filter_registry.register('NodeInfoList.LocalDiskInfo..LocalDiskCount') +class NetworkTypePostgresFilter(TencentEsFilter): + schema = type_schema( + 'NodeInfoList.LocalDiskInfo..LocalDiskCount', + **filter_util.get_schema('number')) + + +@Postgres.filter_registry.register('NodeInfoList..DiskCount') +class NetworkTypePostgresFilter(TencentEsFilter): + schema = type_schema( + 'NodeInfoList..DiskCount', + **filter_util.get_schema('number')) + + +@Postgres.filter_registry.register('NodeInfoList..DiskEncrypt') +class NetworkTypePostgresFilter(TencentEsFilter): + schema = type_schema( + 'NodeInfoList..DiskEncrypt', + **filter_util.get_schema('number')) + + +@Postgres.filter_registry.register('NodeInfoList..DiskEncrypt') +class NetworkTypePostgresFilter(TencentEsFilter): + schema = type_schema( + 'NodeInfoList..DiskEncrypt', + **filter_util.get_schema('number')) + + +@Postgres.filter_registry.register('EsPublicUrl') +class NetworkTypePostgresFilter(TencentEsFilter): + schema = type_schema( + 'EsPublicUrl', + **filter_util.get_schema('string')) + + +@Postgres.filter_registry.register('MultiZoneInfo..Zone') +class NetworkTypePostgresFilter(TencentEsFilter): + schema = type_schema( + 'MultiZoneInfo..Zone', + **filter_util.get_schema('string')) + + +@Postgres.filter_registry.register('MultiZoneInfo..SubnetId') +class NetworkTypePostgresFilter(TencentEsFilter): + schema = type_schema( + 'MultiZoneInfo..SubnetId', + **filter_util.get_schema('string')) + + +@Postgres.filter_registry.register('DeployMode') +class NetworkTypePostgresFilter(TencentEsFilter): + schema = type_schema( + 'DeployMode', + **filter_util.get_schema('number')) + + +@Postgres.filter_registry.register('PublicAccess') +class NetworkTypePostgresFilter(TencentEsFilter): + schema = type_schema( + 'PublicAccess', + **filter_util.get_schema('string')) + + +@Postgres.filter_registry.register('EsPublicAcl.BlackIpList') +class NetworkTypePostgresFilter(TencentEsFilter): + schema = type_schema( + 'EsPublicAcl.BlackIpList', + **filter_util.get_schema('list_string')) + + +@Postgres.filter_registry.register('EsPublicAcl.WhiteIpList') +class NetworkTypePostgresFilter(TencentEsFilter): + schema = type_schema( + 'EsPublicAcl.BlackIpList', + **filter_util.get_schema('list_string')) + + +@Postgres.filter_registry.register('KibanaPrivateUrl') +class NetworkTypePostgresFilter(TencentEsFilter): + schema = type_schema( + 'KibanaPrivateUrl', + **filter_util.get_schema('string')) + + +@Postgres.filter_registry.register('KibanaPublicAccess') +class NetworkTypePostgresFilter(TencentEsFilter): + schema = type_schema( + 'KibanaPublicAccess', + **filter_util.get_schema('string')) + + +@Postgres.filter_registry.register('KibanaPrivateAccess') +class NetworkTypePostgresFilter(TencentEsFilter): + schema = type_schema( + 'KibanaPrivateAccess', + **filter_util.get_schema('string')) + + +@Postgres.filter_registry.register('SecurityType') +class NetworkTypePostgresFilter(TencentEsFilter): + schema = type_schema( + 'SecurityType', + **filter_util.get_schema('number')) diff --git a/tools/c7n_tencent/c7n_tencent/resources/mongodb.py b/tools/c7n_tencent/c7n_tencent/resources/mongodb.py index 7f8d897148b..f4a1a8470d9 100644 --- a/tools/c7n_tencent/c7n_tencent/resources/mongodb.py +++ b/tools/c7n_tencent/c7n_tencent/resources/mongodb.py @@ -11,8 +11,10 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +import json import logging +import jmespath from tencentcloud.common.exception.tencent_cloud_sdk_exception import TencentCloudSDKException from tencentcloud.mongodb.v20190725 import models @@ -33,20 +35,34 @@ class resource_type(TypeInfo): id = 'InstanceId' def get_request(self): + offset = 0 + limit = 20 + res = [] try: - req = models.DescribeDBInstancesRequest() - resp = Session.client(self, service).DescribeDBInstances(req) - # 输出json格式的字符串回包 - # print(resp.to_json_string(indent=2)) + while 0 <= offset: + req = models.DescribeDBInstancesRequest() + params = { + "Offset": offset, + "Limit": limit + } + req.from_json_string(json.dumps(params)) + resp = Session.client(self, service).DescribeDBInstances(req) + respose = resp.to_json_string().replace('null', 'None').replace('false', 'False').replace('true', 'True') + result = jmespath.search('InstanceDetails', eval(respose)) + res = res + result + if len(result) == limit: + offset += limit + else: + return res + # 输出json格式的字符串回包 + # print(resp.to_json_string(indent=2)) - # 也可以取出单个值。 - # 你可以通过官网接口文档或跳转到response对象的定义处查看返回字段的定义。 - # print(resp.to_json_string()) + # 也可以取出单个值。 + # 你可以通过官网接口文档或跳转到response对象的定义处查看返回字段的定义。 + # print(resp.to_json_string()) except TencentCloudSDKException as err: logging.error(err) - return False - # tencent 返回的json里居然不是None,而是java的null,活久见 - return resp.to_json_string().replace('null', 'None') + return res @MongoDB.filter_registry.register('network-type') class NetworkTypeMongoDBFilter(TencentFilter): @@ -67,12 +83,12 @@ class NetworkTypeMongoDBFilter(TencentFilter): **{'value': {'type': 'string'}}) def get_request(self, i): - if self.data['value'] == "vpc": - if i['VpcId']: + if self.data.get('value', '') == "vpc": + if i.get('VpcId', ''): return False return i else: - if i['VpcId']: + if i.get('VpcId', ''): return i return False @@ -97,10 +113,10 @@ class InternetAccessMongoDBFilter(TencentFilter): **{'value': {'type': 'boolean'}}) def get_request(self, i): - if self.data['value']: - if i['NetType'] == 0: + if self.data.get('value', ''): + if i.get('NetType', 0) == 0: return i else: - if i['NetType'] != 0: + if i.get('NetType', 0) != 0: return i return False \ No newline at end of file diff --git a/tools/c7n_tencent/c7n_tencent/resources/postgres.py b/tools/c7n_tencent/c7n_tencent/resources/postgres.py new file mode 100644 index 00000000000..f3c55ed71a7 --- /dev/null +++ b/tools/c7n_tencent/c7n_tencent/resources/postgres.py @@ -0,0 +1,91 @@ +import jmespath +from c7n_tencent import page +from c7n_tencent.query import QueryResourceManager, TypeInfo +from tencentcloud.common.exception.tencent_cloud_sdk_exception import TencentCloudSDKException +import logging +from c7n.utils import type_schema +from c7n_tencent.client import Session +from c7n_tencent.filters.filter import TencentFilter +from c7n_tencent.provider import resources +from tencentcloud.postgres.v20170312 import models + +service = 'postgres_client.postgres' + + +@resources.register('postgres') +class Postgres(QueryResourceManager): + class resource_type(TypeInfo): + service = 'postgres_client.postgres' + enum_spec = (None, 'DBInstanceSet', None) + id = 'DBInstanceId' + + def get_request(self): + res = [] + try: + req = models.DescribeDBInstancesRequest() + client = Session.client(service) + # 查询到所有分页数据 + resp = page.page_all(client.DescribeDBInstances, req, 'DBInstanceSet', + 'TotalCount') + # 将结果转换为字典 + respose = resp.to_json_string().replace('null', 'None').replace('false', 'False').replace('true', 'True') + res = jmespath.search('DBInstanceSet', eval(respose)) + # 也可以取出单个值。 + # 你可以通过官网接口文档或跳转到response对象的定义处查看返回字段的定义。 + except TencentCloudSDKException as err: + logging.error(err) + return res + + +@Postgres.filter_registry.register('network-type') +class NetworkTypePostgresFilter(TencentFilter): + schema = type_schema( + 'network-type', + **{'value': {'type': 'string'}}) + + def get_request(self, i): + if self.data.get('value', '') == "vpc": + if i.get('VpcId', ''): + return False + return i + else: + if i.get('VpcId', ''): + return i + return False + + +def network_is_public(obj): + return obj and obj['NetType'] and obj['NetType'] == 'public' and obj['Status'] and obj['Status'] == 'opened' + + +@Postgres.filter_registry.register('internet-access') +class InternetAccessTypePostgresFilter(TencentFilter): + schema = type_schema( + 'internet-access', + **{'value': {'type': 'boolean'}}) + + """ + Filters + :Example: + .. code-block:: yaml + + policies: + # 检测您账号下postgres实例不允许任意来源公网访问,视为“合规” + - name: tencent-postgres-internet-access + resource: tencent.postgres + filters: + - type: internet-access + value: true + """ + + def get_request(self, i): + if self.data.get('value', ''): + if len(list(filter(network_is_public, i['DBInstanceNetInfo']))) > 0: + return i + else: + return False + else: + if len(list(filter(network_is_public, i['DBInstanceNetInfo']))) == 0: + return i + else: + return False diff --git a/tools/c7n_tencent/c7n_tencent/resources/redis.py b/tools/c7n_tencent/c7n_tencent/resources/redis.py index a1db35cb84b..627d2a4bc5c 100644 --- a/tools/c7n_tencent/c7n_tencent/resources/redis.py +++ b/tools/c7n_tencent/c7n_tencent/resources/redis.py @@ -11,8 +11,10 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +import json import logging +import jmespath from tencentcloud.redis.v20180412 import models from tencentcloud.common.exception.tencent_cloud_sdk_exception import TencentCloudSDKException @@ -33,20 +35,34 @@ class resource_type(TypeInfo): id = 'InstanceId' def get_request(self): + offset = 0 + limit = 20 + res = [] try: - req = models.DescribeInstancesRequest() - resp = Session.client(self, service).DescribeInstances(req) - # 输出json格式的字符串回包 - # print(resp.to_json_string(indent=2)) + while 0 <= offset: + req = models.DescribeInstancesRequest() + params = { + "Offset": offset, + "Limit": limit + } + req.from_json_string(json.dumps(params)) + resp = Session.client(self, service).DescribeInstances(req) + respose = resp.to_json_string().replace('null', 'None').replace('false', 'False').replace('true', 'True') + result = jmespath.search('InstanceSet', eval(respose)) + res = res + result + if len(result) == limit: + offset += limit + else: + return res + # 输出json格式的字符串回包 + # print(resp.to_json_string(indent=2)) - # 也可以取出单个值。 - # 你可以通过官网接口文档或跳转到response对象的定义处查看返回字段的定义。 - # print(resp.to_json_string()) + # 也可以取出单个值。 + # 你可以通过官网接口文档或跳转到response对象的定义处查看返回字段的定义。 + # print(resp.to_json_string()) except TencentCloudSDKException as err: logging.error(err) - return False - # tencent 返回的json里居然不是None,而是java的null,活久见 - return resp.to_json_string().replace('null', 'None') + return res @Redis.filter_registry.register('network-type') class NetworkTypeRedisFilter(TencentFilter): @@ -67,12 +83,12 @@ class NetworkTypeRedisFilter(TencentFilter): **{'value': {'type': 'string'}}) def get_request(self, i): - if self.data['value'] == "vpc": - if i['VpcId']: + if self.data.get('value', '') == "vpc": + if i.get('VpcId', ''): return False return i else: - if i['VpcId']: + if i.get('VpcId', ''): return i return False @@ -97,10 +113,10 @@ class InternetAccessRedisFilter(TencentFilter): **{'value': {'type': 'boolean'}}) def get_request(self, i): - if self.data['value']: - if i['NetType'] == 0: + if self.data.get('value', ''): + if i.get('NetType', '') == 0: return i else: - if i['NetType'] != 0: + if i.get('NetType', '') != 0: return i return False \ No newline at end of file diff --git a/tools/c7n_tencent/c7n_tencent/resources/resource_map.py b/tools/c7n_tencent/c7n_tencent/resources/resource_map.py index fa81f586896..d86b446103f 100644 --- a/tools/c7n_tencent/c7n_tencent/resources/resource_map.py +++ b/tools/c7n_tencent/c7n_tencent/resources/resource_map.py @@ -10,5 +10,6 @@ "tencent.redis": "c7n_tencent.resources.redis.Redis", "tencent.mongodb": "c7n_tencent.resources.mongodb.MongoDB", "tencent.security-group": "c7n_tencent.resources.securitygroup.SecurityGroup", - + "tencent.postgres": "c7n_tencent.resources.postgres.Postgres", + "tencent.es": "c7n_tencent.resources.es.es" } diff --git a/tools/c7n_tencent/c7n_tencent/resources/securitygroup.py b/tools/c7n_tencent/c7n_tencent/resources/securitygroup.py index d2e61b2e139..6eac3685a5f 100644 --- a/tools/c7n_tencent/c7n_tencent/resources/securitygroup.py +++ b/tools/c7n_tencent/c7n_tencent/resources/securitygroup.py @@ -11,6 +11,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +import json import logging import jmespath @@ -36,26 +37,41 @@ class resource_type(TypeInfo): id = 'SecurityGroupId' def get_request(self): + # 为什么设置成字符串,不晓得sd可怎么设计的,全靠猜。 + offset = '0' + limit = '20' + res = [] try: - req = models.DescribeSecurityGroupsRequest() - resp = Session.client(self, service).DescribeSecurityGroups(req) - for res in resp.SecurityGroupSet: - req2 = models.DescribeSecurityGroupPoliciesRequest() - params = '{"SecurityGroupId":"' + res.SecurityGroupId + '"}' - req2.from_json_string(params) - resp2 = Session.client(self, service).DescribeSecurityGroupPolicies(req2) - res.IpPermissions = resp2.SecurityGroupPolicySet - # 输出json格式的字符串回包 - # print(resp.to_json_string(indent=2)) - - # 也可以取出单个值。 - # 你可以通过官网接口文档或跳转到response对象的定义处查看返回字段的定义。 - # print(resp.to_json_string()) + while 0 <= int(offset): + req = models.DescribeSecurityGroupsRequest() + params = { + "Offset": offset, + "Limit": limit + } + req.from_json_string(json.dumps(params)) + resp = Session.client(self, service).DescribeSecurityGroups(req) + for sg in resp.SecurityGroupSet: + req2 = models.DescribeSecurityGroupPoliciesRequest() + params = '{"SecurityGroupId":"' + sg.SecurityGroupId + '"}' + req2.from_json_string(params) + resp2 = Session.client(self, service).DescribeSecurityGroupPolicies(req2) + sg.IpPermissions = resp2.SecurityGroupPolicySet + respose = resp.to_json_string().replace('null', 'None').replace('false', 'False').replace('true', 'True') + result = jmespath.search('SecurityGroupSet', eval(respose)) + res = res + result + if len(result) == int(limit): + offset = str(int(offset) + len(result)) + else: + return res + # 输出json格式的字符串回包 + # print(resp.to_json_string(indent=2)) + + # 也可以取出单个值。 + # 你可以通过官网接口文档或跳转到response对象的定义处查看返回字段的定义。 + # print(resp.to_json_string()) except TencentCloudSDKException as err: logging.error(err) - return False - # tencent 返回的json里居然不是None,而是java的null,活久见 - return resp.to_json_string().replace('null', 'None') + return res @SecurityGroup.action_registry.register('delete') @@ -95,13 +111,15 @@ class IPPermission(SGPermission): filters: - or: - type: ingress - IpProtocol: tcp + IpProtocol: "-1" Ports: [20,21,22,25,80,443,465,1433,1434,3306,3389,4333,5432,5500] Cidr: "0.0.0.0/0" + Action: "ACCEPT" - type: ingress - IpProtocol: tcp + IpProtocol: "-1" Ports: [20,21,22,25,80,443,465,1433,1434,3306,3389,4333,5432,5500] CidrV6: "::/0" + Action: "ACCEPT" """ ip_permissions_key = "SecurityGroupSet" ip_permissions_type = "ingress" @@ -113,23 +131,11 @@ class IPPermission(SGPermission): schema['properties'].update(SGPermissionSchema) def process_self_cidrs(self, perm): - self.process_cidrs(perm, 'CidrBlock', 'Ipv6CidrBlock') + return self.process_cidrs(perm, 'CidrBlock', 'Ipv6CidrBlock') def securityGroupAttributeRequst(self, sg): self.direction = 'Ingress' - req = models.DescribeSecurityGroupsRequest() - params = '{"SecurityGroupId" :"' + sg["SecurityGroupId"] + '"}' - req.from_json_string(params) - resp = Session.client(self, service).DescribeSecurityGroups(req) - for res in resp.SecurityGroupSet: - if sg["SecurityGroupId"] != res.SecurityGroupId: - continue - req2 = models.DescribeSecurityGroupPoliciesRequest() - params = '{"SecurityGroupId":"' + res.SecurityGroupId + '"}' - req2.from_json_string(params) - resp2 = Session.client(self, service).DescribeSecurityGroupPolicies(req2) - res.IpPermissions = resp2.SecurityGroupPolicySet - return resp.to_json_string().replace('null', 'None') + return sg @SecurityGroup.filter_registry.register('egress') class IPPermission(SGPermission): @@ -144,20 +150,10 @@ class IPPermission(SGPermission): def securityGroupAttributeRequst(self, sg): self.direction = 'Egress' - req = models.DescribeSecurityGroupsRequest() - params = '{"SecurityGroupId" :"' + sg["SecurityGroupId"] + '"}' - req.from_json_string(params) - resp = Session.client(self, service).DescribeSecurityGroups(req) - for res in resp.SecurityGroupSet: - req2 = models.DescribeSecurityGroupPoliciesRequest() - params = '{"SecurityGroupId":"' + res.SecurityGroupId + '"}' - req2.from_json_string(params) - resp2 = Session.client(self, service).DescribeSecurityGroupPolicies(req2) - res.IpPermissions = resp2.SecurityGroupPolicySet - return resp.to_json_string().replace('null', 'None') + return sg -def process_self_cidrs(self, perm): - self.process_cidrs(perm, "CidrBlock", "Ipv6CidrBlock") + def process_self_cidrs(self, perm): + return self.process_cidrs(perm, "CidrBlock", "Ipv6CidrBlock") @SecurityGroup.filter_registry.register('source-cidr-ip') @@ -183,6 +179,6 @@ class SourceCidrIp(TencentFilter): def get_request(self, sg): for cidr in jmespath.search(self.ip_permissions_key, sg): - if cidr['CidrBlock'] == self.data['value']: + if cidr['CidrBlock'] == self.data.get('value', ''): return sg return False \ No newline at end of file diff --git a/tools/c7n_tencent/c7n_tencent/resources/vpc.py b/tools/c7n_tencent/c7n_tencent/resources/vpc.py index e7f3cdde356..dc562c83c35 100644 --- a/tools/c7n_tencent/c7n_tencent/resources/vpc.py +++ b/tools/c7n_tencent/c7n_tencent/resources/vpc.py @@ -11,8 +11,10 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +import json import logging +import jmespath from tencentcloud.common.exception.tencent_cloud_sdk_exception import TencentCloudSDKException from tencentcloud.vpc.v20170312 import models @@ -35,22 +37,34 @@ class resource_type(TypeInfo): id = 'VpcId' def get_request(self): + offset = 0 + limit = 20 + res = [] try: - req = models.DescribeVpcsRequest() - params = '{}' - req.from_json_string(params) - resp = Session.client(self, service).DescribeVpcs(req) - # 输出json格式的字符串回包 - # print(resp.to_json_string(indent=2)) + while 0 <= offset: + req = models.DescribeVpcsRequest() + params = { + "Offset": offset, + "Limit": limit + } + req.from_json_string(json.dumps(params)) + resp = Session.client(self, service).DescribeVpcs(req) + respose = resp.to_json_string().replace('null', 'None').replace('false', 'False').replace('true', 'True') + result = jmespath.search('VpcSet', eval(respose)) + res = res + result + if len(result) == limit: + offset += limit + else: + return res + # 输出json格式的字符串回包 + # print(resp.to_json_string(indent=2)) - # 也可以取出单个值。 - # 你可以通过官网接口文档或跳转到response对象的定义处查看返回字段的定义。 - # print(resp.to_json_string()) + # 也可以取出单个值。 + # 你可以通过官网接口文档或跳转到response对象的定义处查看返回字段的定义。 + # print(resp.to_json_string()) except TencentCloudSDKException as err: logging.error(err) - return False - # tencent 返回的json里居然不是None,而是java的null,活久见 - return resp.to_json_string().replace('null', 'None') + return res @Vpc.filter_registry.register('unused') class TencentVpcFilter(TencentVpcFilter): @@ -67,7 +81,7 @@ class TencentVpcFilter(TencentVpcFilter): schema = type_schema('AVAILABLE') def get_request(self, i): - VpcId = i['VpcId'] + VpcId = i.get('VpcId', '') #vpc 查询vpc下是否有ecs资源 cvms = Cvm.get_request(self) cvms_req = eval(cvms.replace('false', 'False'))['InstanceSet'] @@ -80,7 +94,6 @@ def get_request(self, i): clbs_req = eval(clbs.replace('false', 'False'))['LoadBalancerSet'] if clbs_req: for clb in clbs_req: - print(clb['VpcId']) if VpcId == clb['VpcId']: return None return i diff --git a/tools/c7n_tencent/requirements.txt b/tools/c7n_tencent/requirements.txt index 44307b3864a..a444ea984e3 100644 --- a/tools/c7n_tencent/requirements.txt +++ b/tools/c7n_tencent/requirements.txt @@ -1,4 +1,6 @@ pycryptodome==3.9.8 crcmod==1.7 tencentcloud-sdk-python==3.0.234 -cos-python-sdk-v5==1.8.2 \ No newline at end of file +cos-python-sdk-v5==1.8.2 +cryptography==2.9.2 +jsonpath==0.82 \ No newline at end of file diff --git a/tools/c7n_vsphere/.gitignore b/tools/c7n_vsphere/.gitignore new file mode 100644 index 00000000000..1e7bb232c6a --- /dev/null +++ b/tools/c7n_vsphere/.gitignore @@ -0,0 +1,4 @@ +*pyc +*py~ +__pycache__ +*.egg-info diff --git a/tools/c7n_vsphere/c7n_vsphere/__init__.py b/tools/c7n_vsphere/c7n_vsphere/__init__.py new file mode 100644 index 00000000000..cdce4e86dfd --- /dev/null +++ b/tools/c7n_vsphere/c7n_vsphere/__init__.py @@ -0,0 +1,2 @@ +# Copyright The Cloud Custodian Authors. +# SPDX-License-Identifier: Apache-2.0 \ No newline at end of file diff --git a/tools/c7n_vsphere/c7n_vsphere/actions/__init__.py b/tools/c7n_vsphere/c7n_vsphere/actions/__init__.py new file mode 100644 index 00000000000..cdce4e86dfd --- /dev/null +++ b/tools/c7n_vsphere/c7n_vsphere/actions/__init__.py @@ -0,0 +1,2 @@ +# Copyright The Cloud Custodian Authors. +# SPDX-License-Identifier: Apache-2.0 \ No newline at end of file diff --git a/tools/c7n_vsphere/c7n_vsphere/actions/core.py b/tools/c7n_vsphere/c7n_vsphere/actions/core.py new file mode 100644 index 00000000000..6263705fb9b --- /dev/null +++ b/tools/c7n_vsphere/c7n_vsphere/actions/core.py @@ -0,0 +1,117 @@ +# Copyright 2018 Capital One Services, LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from c7n.actions import Action as BaseAction +from c7n.utils import local_session, chunks + + +class Action(BaseAction): + pass + + +class MethodAction(Action): + """Invoke an api call on each resource. + + Quite a number of procedural actions are simply invoking an api + call on a filtered set of resources. The exact handling is mostly + boilerplate at that point following an 80/20 rule. This class is + an encapsulation of the 80%. + """ + + # method we'll be invoking + method_spec = () + + # batch size + chunk_size = 20 + + # implicitly filter resources by state, (attr_name, (valid_enum)) + attr_filter = () + + # error codes that can be safely ignored + ignore_error_codes = () + + permissions = () + method_perm = None + + def validate(self): + if not self.method_spec: + raise NotImplementedError("subclass must define method_spec") + return self + + def filter_resources(self, resources): + rcount = len(resources) + attr_name, valid_enum = self.attr_filter + resources = [r for r in resources if r.get(attr_name) in valid_enum] + if len(resources) != rcount: + self.log.warning( + "policy:%s action:%s implicitly filtered %d resources to %d by attr:%s", + self.manager.ctx.policy.name, + self.type, + rcount, + len(resources), + attr_name, + ) + return resources + + def process(self, resources): + + if self.attr_filter: + resources = self.filter_resources(resources) + model = self.manager.get_model() + session = local_session(self.manager.session_factory) + client = self.get_client(session, model) + for resource_set in chunks(resources, self.chunk_size): + self.process_resource_set(client, model, resource_set) + + def process_resource_set(self, client, model, resources): + result_key = self.method_spec.get('result_key') + annotation_key = self.method_spec.get('annotation_key') + for resource in resources: + requst = self.get_request(resource) + result = self.invoke_api(client, requst) + if result_key and annotation_key: + resource[annotation_key] = result.get(result_key) + + def invoke_api(self, client, requst): + try: + return client.do_action(requst) + except: + raise + + def get_permissions(self): + if self.permissions: + return self.permissions + m = self.manager.resource_type + method = self.method_perm + if not method and 'op' not in self.method_spec: + return () + if not method: + method = self.method_spec['op'] + component = m.component + if '.' in component: + component = component.split('.')[-1] + return ("{}.{}.{}".format( + m.perm_service or m.service, component, method),) + + def get_operation_name(self, model, resource): + return self.method_spec['op'] + + def get_resource_params(self, model, resource): + raise NotImplementedError("subclass responsibility") + + def get_request(self, resource): + raise NotImplementedError("subclass responsibility") + + def get_client(self, session, model): + return session.client(model.service) diff --git a/tools/c7n_vsphere/c7n_vsphere/actions/cscc.py b/tools/c7n_vsphere/c7n_vsphere/actions/cscc.py new file mode 100644 index 00000000000..5160dd67e51 --- /dev/null +++ b/tools/c7n_vsphere/c7n_vsphere/actions/cscc.py @@ -0,0 +1,243 @@ +# Copyright 2018-2019 Capital One Services, LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import datetime +import hashlib +import json +from urllib.parse import urlparse + +from c7n_vsphere.provider import resources as vsphere_resources + +from c7n.exceptions import PolicyExecutionError, PolicyValidationError +from c7n.utils import local_session, type_schema +from .core import MethodAction + + +class PostFinding(MethodAction): + """Post finding for matched resources to Cloud Security Command Center. + + + :Example: + + .. code-block:: yaml + + policies: + - name: vsphere-instances-with-label + resource: vsphere.instance + filters: + - "tag:name": "bad-instance" + actions: + - type: post-finding + org-domain: example.io + category: MEDIUM_INTERNET_SECURITY + + The source for custodian can either be specified inline to the policy, or + custodian can generate one at runtime if it doesn't exist given a org-domain + or org-id. + + Finding updates are not currently supported, due to upstream api issues. + """ + schema = type_schema( + 'post-finding', + **{ + 'source': { + 'type': 'string', + 'description': 'qualified name of source to post to CSCC as'}, + 'org-domain': {'type': 'string'}, + 'org-id': {'type': 'integer'}, + 'category': {'type': 'string'}}) + schema_alias = True + method_spec = {'op': 'create', 'result': 'name', 'annotation_key': 'c7n:Finding'} + + # create throws error if already exists, patch method has bad docs. + ignore_error_codes = (409,) + + CustodianSourceName = 'CloudCustodian' + DefaultCategory = 'Custodian' + Service = 'securitycenter' + ServiceVersion = 'v1beta1' + + _source = None + + # security center permission model is pretty obtuse to correct + permissions = ( + 'securitycenter.findings.list', + 'securitycenter.findings.update', + 'resourcemanager.organizations.get', + 'securitycenter.assetsecuritymarks.update', + 'securitycenter.sources.update', + 'securitycenter.sources.list' + ) + + def validate(self): + if not any([self.data.get(k) for k in ('source', 'org-domain', 'org-id')]): + raise PolicyValidationError( + "policy:%s CSCC post-finding requires one of source, org-domain, org-id" % ( + self.manager.ctx.policy.name)) + + def process(self, resources): + self.initialize_source() + return super(PostFinding, self).process(resources) + + def get_client(self, session, model): + return session.client( + self.Service, self.ServiceVersion, 'organizations.sources.findings') + + def get_resource_params(self, model, resource): + return self.get_finding(resource) + + def initialize_source(self): + # Ideally we'll be given a source, but we'll attempt to auto create it + # if given an org_domain or org_id. + if self._source: + return self._source + elif 'source' in self.data: + self._source = self.data['source'] + return self._source + + session = local_session(self.manager.session_factory) + + # Resolve Organization Id + if 'org-id' in self.data: + org_id = self.data['org-id'] + else: + orgs = session.client('cloudresourcemanager', 'v1', 'organizations') + res = orgs.execute_query( + 'search', {'body': { + 'filter': 'domain:%s' % self.data['org-domain']}}).get( + 'organizations') + if not res: + raise PolicyExecutionError("Could not determine organization id") + org_id = res[0]['name'].rsplit('/', 1)[-1] + + # Resolve Source + client = session.client(self.Service, self.ServiceVersion, 'organizations.sources') + source = None + res = [s for s in + client.execute_query( + 'list', {'parent': 'organizations/{}'.format(org_id)}).get('sources') + if s['displayName'] == self.CustodianSourceName] + if res: + source = res[0]['name'] + + if source is None: + source = client.execute_command( + 'create', + {'parent': 'organizations/{}'.format(org_id), + 'body': { + 'displayName': self.CustodianSourceName, + 'description': 'Cloud Management Rules Engine'}}).get('name') + self.log.info( + "policy:%s resolved cscc source: %s, update policy with this source value", + self.manager.ctx.policy.name, + source) + self._source = source + return self._source + + def get_name(self, r): + """Given an arbitrary resource attempt to resolve back to a qualified name.""" + namer = ResourceNameAdapters[self.manager.resource_type.service] + return namer(r) + + def get_finding(self, resource): + policy = self.manager.ctx.policy + resource_name = self.get_name(resource) + # ideally we could be using shake, but its py3.6+ only + finding_id = hashlib.sha256( + b"%s%s" % ( + policy.name.encode('utf8'), + resource_name.encode('utf8'))).hexdigest()[:32] + + finding = { + 'name': '{}/findings/{}'.format(self._source, finding_id), + 'resourceName': resource_name, + 'state': 'ACTIVE', + 'category': self.data.get('category', self.DefaultCategory), + 'eventTime': datetime.datetime.utcnow().isoformat('T') + 'Z', + 'sourceProperties': { + 'resource_type': self.manager.type, + 'title': policy.data.get('title', policy.name), + 'policy_name': policy.name, + 'policy': json.dumps(policy.data) + } + } + + request = { + 'parent': self._source, + 'findingId': finding_id[:31], + 'body': finding} + return request + + @classmethod + def register_resource(klass, registry, resource_class): + if resource_class.resource_type.service not in ResourceNameAdapters: + return + if 'post-finding' in resource_class.action_registry: + return + resource_class.action_registry.register('post-finding', klass) + + +# CSCC uses its own notion of resource id, if we want our findings on +# a resource to be linked from the asset view we need to post w/ the +# same resource name. If this conceptulization of resource name is +# standard, then we should move these to resource types with +# appropriate hierarchies by service. + + +def name_compute(r): + prefix = urlparse(r['selfLink']).path.strip('/').split('/')[2:][:-1] + return "//compute.googleapis.com/{}/{}".format( + "/".join(prefix), + r['id']) + + +def name_iam(r): + return "//iam.googleapis.com/projects/{}/serviceAccounts/{}".format( + r['projectId'], + r['uniqueId']) + + +def name_resourcemanager(r): + rid = r.get('projectNumber') + if rid is not None: + rtype = 'projects' + else: + rid = r.get('organizationId') + rtype = 'organizations' + return "//cloudresourcemanager.googleapis.com/{}/{}".format( + rtype, rid) + + +def name_container(r): + return "//container.googleapis.com/{}".format( + "/".join(urlparse(r['selfLink']).path.strip('/').split('/')[1:])) + + +def name_storage(r): + return "//storage.googleapis.com/{}".format(r['name']) + + +def name_appengine(r): + return "//appengine.googleapis.com/{}".format(r['name']) + + +ResourceNameAdapters = { + 'appengine': name_appengine, + 'cloudresourcemanager': name_resourcemanager, + 'compute': name_compute, + 'container': name_container, + 'iam': name_iam, + 'storage': name_storage, +} + +vsphere_resources.subscribe(PostFinding.register_resource) diff --git a/tools/c7n_vsphere/c7n_vsphere/client.py b/tools/c7n_vsphere/c7n_vsphere/client.py new file mode 100644 index 00000000000..90d7e7f921c --- /dev/null +++ b/tools/c7n_vsphere/c7n_vsphere/client.py @@ -0,0 +1,39 @@ +# Copyright The Cloud Custodian Authors. +# SPDX-License-Identifier: Apache-2.0 +# Copyright 2017 The Forseti Security Authors. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import logging +import os + +import requests +from vmware.vapi.vsphere.client import create_vsphere_client + +session = requests.session() +session.verify = False +log = logging.getLogger('custodian.vsphere.client') + + +class Session: + def __init__(self, regionId=None): + self.username = os.getenv('VSPHERE_USERNAME') + self.password = os.getenv('VSPHERE_PASSWORD') + self.server = os.getenv('VSPHERE_ENDPOINT') + if not regionId: + regionId = os.getenv('VSPHERE_DEFAULT_REGION') + self.regionId = regionId + + def client(self): + vsphere_client = create_vsphere_client(server=os.getenv('VSPHERE_ENDPOINT'), username=os.getenv('VSPHERE_USERNAME'), password=os.getenv('VSPHERE_PASSWORD'), session=session) + return vsphere_client \ No newline at end of file diff --git a/tools/c7n_vsphere/c7n_vsphere/entry.py b/tools/c7n_vsphere/c7n_vsphere/entry.py new file mode 100644 index 00000000000..8d9dda2359b --- /dev/null +++ b/tools/c7n_vsphere/c7n_vsphere/entry.py @@ -0,0 +1,17 @@ +# Copyright The Cloud Custodian Authors. +# SPDX-License-Identifier: Apache-2.0 + +import logging +from c7n_vsphere.resources import ( + vm, +) + +log = logging.getLogger('custodian.vsphere') + +ALL = [ + vm] + + +def initialize_vsphere(): + """vsphere entry point + """ \ No newline at end of file diff --git a/tools/c7n_vsphere/c7n_vsphere/filters/__init__.py b/tools/c7n_vsphere/c7n_vsphere/filters/__init__.py new file mode 100644 index 00000000000..cdce4e86dfd --- /dev/null +++ b/tools/c7n_vsphere/c7n_vsphere/filters/__init__.py @@ -0,0 +1,2 @@ +# Copyright The Cloud Custodian Authors. +# SPDX-License-Identifier: Apache-2.0 \ No newline at end of file diff --git a/tools/c7n_vsphere/c7n_vsphere/filters/filter.py b/tools/c7n_vsphere/c7n_vsphere/filters/filter.py new file mode 100644 index 00000000000..a985a00ed36 --- /dev/null +++ b/tools/c7n_vsphere/c7n_vsphere/filters/filter.py @@ -0,0 +1,340 @@ +# Copyright The Cloud Custodian Authors. +# SPDX-License-Identifier: Apache-2.0 +import json + +import jmespath + +from c7n.exceptions import PolicyValidationError +from c7n.filters.core import Filter +from c7n.filters.core import ValueFilter + + +class PolicyFilter: + pass + +class Filter(Filter): + + def validate(self): + return self + + def __call__(self, i): + return i + +class vSphereFilter(Filter): + + def validate(self): + return self + + def __call__(self, i): + return self.get_request(i) + +class SGPermission(Filter): + """Filter for verifying security group ingress and egress permissions + + All attributes of a security group permission are available as + value filters. + + If multiple attributes are specified the permission must satisfy + all of them. Note that within an attribute match against a list value + of a permission we default to or. + + If a group has any permissions that match all conditions, then it + matches the filter. + + Permissions that match on the group are annotated onto the group and + can subsequently be used by the remove-permission action. + + We have specialized handling for matching `Ports` in ingress/egress + permission From/To range. The following example matches on ingress + rules which allow for a range that includes all of the given ports. + + .. code-block:: yaml + + - type: ingress + Ports: [22, 443, 80] + + As well for verifying that a rule only allows for a specific set of ports + as in the following example. The delta between this and the previous + example is that if the permission allows for any ports not specified here, + then the rule will match. ie. OnlyPorts is a negative assertion match, + it matches when a permission includes ports outside of the specified set. + + .. code-block:: yaml + + - type: ingress + OnlyPorts: [22] + + For simplifying ipranges handling which is specified as a list on a rule + we provide a `Cidr` key which can be used as a value type filter evaluated + against each of the rules. If any iprange cidr match then the permission + matches. + + .. code-block:: yaml + + - type: ingress + IpProtocol: -1 + FromPort: 445 + + We also have specialized handling for matching self-references in + ingress/egress permissions. The following example matches on ingress + rules which allow traffic its own same security group. + + .. code-block:: yaml + + - type: ingress + SelfReference: True + + As well for assertions that a ingress/egress permission only matches + a given set of ports, *note* OnlyPorts is an inverse match. + + .. code-block:: yaml + + - type: egress + OnlyPorts: [22, 443, 80] + + - type: egress + Cidr: + value_type: cidr + op: in + value: x.y.z + + `Cidr` can match ipv4 rules and `CidrV6` can match ipv6 rules. In + this example we are blocking global inbound connections to SSH or + RDP. + + .. code-block:: yaml + + - type: ingress + Ports: [22, 3389] + Cidr: + value: + - "0.0.0.0/0" + - "::/0" + op: in + + `SGReferences` can be used to filter out SG references in rules. + In this example we want to block ingress rules that reference a SG + that is tagged with `Access: Public`. + + .. code-block:: yaml + + - type: ingress + SGReferences: + key: "tag:Access" + value: "Public" + op: equal + + We can also filter SG references based on the VPC that they are + within. In this example we want to ensure that our outbound rules + that reference SGs are only referencing security groups within a + specified VPC. + + .. code-block:: yaml + + - type: egress + SGReferences: + key: 'VpcId' + value: 'vpc-11a1a1aa' + op: equal + + Likewise, we can also filter SG references by their description. + For example, we can prevent egress rules from referencing any + SGs that have a description of "default - DO NOT USE". + + .. code-block:: yaml + + - type: egress + SGReferences: + key: 'Description' + value: 'default - DO NOT USE' + op: equal + + """ + + perm_attrs = { + 'IpProtocol', "Priority", 'Policy'} + filter_attrs = { + 'Cidr', 'CidrV6', 'Ports', 'OnlyPorts', + 'SelfReference', 'Description', 'SGReferences'} + attrs = perm_attrs.union(filter_attrs) + attrs.add('match-operator') + attrs.add('match-operator') + + def validate(self): + delta = set(self.data.keys()).difference(self.attrs) + delta.remove('type') + if delta: + raise PolicyValidationError("Unknown keys %s on %s" % ( + ", ".join(delta), self.manager.data)) + return self + + def process(self, resources, event=None): + self.vfilters = [] + fattrs = list(sorted(self.perm_attrs.intersection(self.data.keys()))) + self.ports = 'Ports' in self.data and self.data['Ports'] or () + self.only_ports = ( + 'OnlyPorts' in self.data and self.data['OnlyPorts'] or ()) + for f in fattrs: + fv = self.data.get(f) + if isinstance(fv, dict): + fv['key'] = f + else: + fv = {f: fv} + vf = ValueFilter(fv, self.manager) + vf.annotate = False + + self.vfilters.append(vf) + + return super(SGPermission, self).process(resources, event) + + def process_ports(self, perm): + found = None + if perm['remote_ip_prefix'] == '0.0.0.0/0' or perm['remote_ip_prefix'] == '::/0': + return True + if perm['port_range_min'] is None or perm['port_range_max'] is None : + return True + FromPort = int(perm['port_range_min']) + ToPort = int(perm['port_range_max']) + for port in self.ports: + if port >= FromPort and port <= ToPort: + found = True + break + elif FromPort == -1 and ToPort == -1: + found = True + break + else: + found = False + only_found = False + for port in self.only_ports: + if port == FromPort and port == ToPort: + only_found = True + if self.only_ports and not only_found: + found = found is None or found and True or False + if self.only_ports and only_found: + found = False + return found + + + def _process_cidr(self, cidr_key, cidr_type, SourceCidrIp, perm): + found = None + SourceCidrIp = perm.get(SourceCidrIp, "") + if not SourceCidrIp: + return False + SourceCidrIp = {cidr_type: SourceCidrIp} + match_range = self.data[cidr_key] + if isinstance(match_range, dict): + match_range['key'] = cidr_type + else: + match_range = {cidr_type: match_range} + vf = ValueFilter(match_range, self.manager) + vf.annotate = False + found = vf(SourceCidrIp) + if found: + found = True + else: + found = False + return found + + def process_cidrs(self, perm, ipv4Cidr, ipv6Cidr): + found_v6 = found_v4 = None + if 'CidrV6' in self.data: + found_v6 = self._process_cidr('CidrV6', 'CidrIpv6', ipv6Cidr, perm) + if 'Cidr' in self.data: + found_v4 = self._process_cidr('Cidr', 'CidrIp', ipv4Cidr, perm) + match_op = self.data.get('match-operator', 'and') == 'and' and all or any + cidr_match = [k for k in (found_v6, found_v4) if k is not None] + if not cidr_match: + return None + return match_op(cidr_match) + + def process_description(self, perm): + if 'Description' not in self.data: + return None + + d = dict(self.data['Description']) + d['key'] = 'Description' + + vf = ValueFilter(d, self.manager) + vf.annotate = False + + for k in ('Ipv6Ranges', 'IpRanges', 'UserIdGroupPairs', 'PrefixListIds'): + if k not in perm or not perm[k]: + continue + return vf(perm[k][0]) + return False + + def process_self_reference(self, perm, sg_id): + found = None + ref_match = self.data.get('SelfReference') + if ref_match is not None: + found = False + if 'UserIdGroupPairs' in perm and 'SelfReference' in self.data: + self_reference = sg_id in [p['GroupId'] + for p in perm['UserIdGroupPairs']] + if ref_match is False and not self_reference: + found = True + if ref_match is True and self_reference: + found = True + return found + + def process_sg_references(self, perm, owner_id): + sg_refs = self.data.get('SGReferences') + if not sg_refs: + return None + + sg_perm = perm.get('UserIdGroupPairs', []) + if not sg_perm: + return False + + sg_group_ids = [p['GroupId'] for p in sg_perm if p['UserId'] == owner_id] + sg_resources = self.manager.get_resources(sg_group_ids) + vf = ValueFilter(sg_refs, self.manager) + vf.annotate = False + + for sg in sg_resources: + if vf(sg): + return True + return False + + def __call__(self, resource): + result = self.securityGroupAttributeRequst(resource) + matched = [] + match_op = self.data.get('match-operator', 'and') == 'and' and all or any + for perm in jmespath.search(self.ip_permissions_key, result): + perm_matches = {} + perm_matches['ports'] = self.process_ports(perm) + perm_matches['cidrs'] = self.process_self_cidrs(perm) + perm_match_values = list(filter( + lambda x: x is not None, perm_matches.values())) + # account for one python behavior any([]) == False, all([]) == True + if match_op == all and not perm_match_values: + continue + + match = match_op(perm_match_values) + if match: + matched.append(perm) + if matched: + return True + +SGPermissionSchema = { + 'match-operator': {'type': 'string', 'enum': ['or', 'and']}, + 'Ports': {'type': 'array', 'items': {'type': 'integer'}}, + 'OnlyPorts': {'type': 'array', 'items': {'type': 'integer'}}, + 'Policy': {}, + 'IpProtocol': { + 'oneOf': [ + {'enum': ["-1", -1, 'TCP', 'UDP', 'ICMP', 'ICMPV6']}, + {'$ref': '#/definitions/filters/value'} + ] + }, + 'FromPort': {'oneOf': [ + {'$ref': '#/definitions/filters/value'}, + {'type': 'integer'}]}, + 'ToPort': {'oneOf': [ + {'$ref': '#/definitions/filters/value'}, + {'type': 'integer'}]}, + 'IpRanges': {}, + 'Cidr': {}, + 'CidrV6': {}, + 'SGReferences': {} +} \ No newline at end of file diff --git a/tools/c7n_vsphere/c7n_vsphere/filters/labels.py b/tools/c7n_vsphere/c7n_vsphere/filters/labels.py new file mode 100644 index 00000000000..e771c8032b8 --- /dev/null +++ b/tools/c7n_vsphere/c7n_vsphere/filters/labels.py @@ -0,0 +1,119 @@ +# Copyright 2019 Karol Lassak +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from datetime import datetime, timedelta + +from c7n.filters import Filter, FilterValidationError +from c7n.filters.offhours import Time +from c7n.utils import type_schema + +DEFAULT_TAG = "custodian_status" + + +class LabelActionFilter(Filter): + """Filter resources for label specified future action + + Filters resources by a 'custodian_status' label which specifies a future + date for an action. + + The filter parses the label values looking for an 'op@date' + string. The date is parsed and compared to do today's date, the + filter succeeds if today's date is gte to the target date. + + The optional 'skew' parameter provides for incrementing today's + date a number of days into the future. An example use case might + be sending a final notice email a few days before terminating an + instance, or snapshotting a volume prior to deletion. + + The optional 'skew_hours' parameter provides for incrementing the current + time a number of hours into the future. + + Optionally, the 'tz' parameter can get used to specify the timezone + in which to interpret the clock (default value is 'utc') + + :example: + + .. code-block :: yaml + + policies: + - name: vm-stop-marked + resource: vsphere.instance + filters: + - type: marked-for-op + # The default label used is custodian_status + # but that is configurable + label: custodian_status + op: stop + # Another optional label is skew + tz: utc + + + """ + schema = type_schema( + 'marked-for-op', + label={'type': 'string'}, + tz={'type': 'string'}, + skew={'type': 'number', 'minimum': 0}, + skew_hours={'type': 'number', 'minimum': 0}, + op={'type': 'string'}) + + def validate(self): + op = self.data.get('op') + if self.manager and op not in self.manager.action_registry.keys(): + raise FilterValidationError( + "Invalid marked-for-op op:%s in %s" % (op, self.manager.data)) + + tz = Time.get_tz(self.data.get('tz', 'utc')) + if not tz: + raise FilterValidationError( + "Invalid timezone specified '%s' in %s" % ( + self.data.get('tz'), self.manager.data)) + return self + + def process(self, resources, event=None): + self.label = self.data.get('label', DEFAULT_TAG) + self.op = self.data.get('op', 'stop') + self.skew = self.data.get('skew', 0) + self.skew_hours = self.data.get('skew_hours', 0) + self.tz = Time.get_tz(self.data.get('tz', 'utc')) + return super(LabelActionFilter, self).process(resources, event) + + def __call__(self, i): + v = i.get('labels', {}).get(self.label, None) + + if v is None: + return False + if '-' not in v or '_' not in v: + return False + + msg, action, action_date_str = v.rsplit('-', 2) + + if action != self.op: + return False + + try: + action_date = datetime.strptime(action_date_str, '%Y_%m_%d__%H_%M') + except Exception: + self.log.error("could not parse label:%s value:%s on %s" % ( + self.label, v, i['name'])) + return False + + # current_date must match timezones with the parsed date string + if action_date.tzinfo: + action_date = action_date.astimezone(self.tz) + current_date = datetime.now(tz=self.tz) + else: + current_date = datetime.now() + + return current_date >= (action_date - timedelta(days=self.skew, hours=self.skew_hours)) diff --git a/tools/c7n_vsphere/c7n_vsphere/handler.py b/tools/c7n_vsphere/c7n_vsphere/handler.py new file mode 100644 index 00000000000..f2d7bbce95b --- /dev/null +++ b/tools/c7n_vsphere/c7n_vsphere/handler.py @@ -0,0 +1,56 @@ +# Copyright The Cloud Custodian Authors. +# SPDX-License-Identifier: Apache-2.0 + +import json +import logging +import os +import uuid + +# Load resource plugins +from c7n_vsphere.entry import initialize_vsphere + +from c7n.config import Config +from c7n.loader import PolicyLoader + +initialize_vsphere() + +log = logging.getLogger('custodian.vsphere.functions') + +logging.getLogger().setLevel(logging.INFO) + + +def run(event, context=None): + # policies file should always be valid in functions so do loading naively + with open('config.json') as f: + policy_config = json.load(f) + + if not policy_config or not policy_config.get('policies'): + log.error('Invalid policy config') + return False + + # setup execution options + options = Config.empty(**policy_config.pop('execution-options', {})) + options.update( + policy_config['policies'][0].get('mode', {}).get('execution-options', {})) + # if output_dir specified use that, otherwise make a temp directory + if not options.output_dir: + options['output_dir'] = get_tmp_output_dir() + + loader = PolicyLoader(options) + policies = loader.load_data(policy_config, 'config.json', validate=False) + if policies: + for p in policies: + log.info("running policy %s", p.name) + p.validate() + p.push(event, context) + return True + + +def get_tmp_output_dir(): + output_dir = '/tmp/' + str(uuid.uuid4()) + if not os.path.exists(output_dir): + try: + os.mkdir(output_dir) + except OSError as error: + log.warning("Unable to make output directory: {}".format(error)) + return output_dir diff --git a/tools/c7n_vsphere/c7n_vsphere/mu.py b/tools/c7n_vsphere/c7n_vsphere/mu.py new file mode 100644 index 00000000000..cdce4e86dfd --- /dev/null +++ b/tools/c7n_vsphere/c7n_vsphere/mu.py @@ -0,0 +1,2 @@ +# Copyright The Cloud Custodian Authors. +# SPDX-License-Identifier: Apache-2.0 \ No newline at end of file diff --git a/tools/c7n_vsphere/c7n_vsphere/output.py b/tools/c7n_vsphere/c7n_vsphere/output.py new file mode 100644 index 00000000000..cdce4e86dfd --- /dev/null +++ b/tools/c7n_vsphere/c7n_vsphere/output.py @@ -0,0 +1,2 @@ +# Copyright The Cloud Custodian Authors. +# SPDX-License-Identifier: Apache-2.0 \ No newline at end of file diff --git a/tools/c7n_vsphere/c7n_vsphere/policy.py b/tools/c7n_vsphere/c7n_vsphere/policy.py new file mode 100644 index 00000000000..d559235032d --- /dev/null +++ b/tools/c7n_vsphere/c7n_vsphere/policy.py @@ -0,0 +1,182 @@ +# Copyright 2018 Capital One Services, LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import logging + +from c7n_vsphere import mu +from dateutil.tz import tz + +from c7n.exceptions import PolicyValidationError +from c7n.policy import execution, ServerlessExecutionMode, PullMode +from c7n.utils import local_session, type_schema + +DEFAULT_REGION = 'us-central1' + + +class FunctionMode(ServerlessExecutionMode): + + schema = type_schema( + 'vsphere', + **{'execution-options': {'$ref': '#/definitions/basic_dict'}, + 'timeout': {'type': 'string'}, + 'memory-size': {'type': 'integer'}, + 'labels': {'$ref': '#/definitions/string_dict'}, + 'network': {'type': 'string'}, + 'max-instances': {'type': 'integer'}, + 'service-account': {'type': 'string'}, + 'environment': {'$ref': '#/definitions/string_dict'}} + ) + + def __init__(self, policy): + self.policy = policy + self.log = logging.getLogger('custodian.vsphere.funcexec') + self.region = policy.options.regions[0] if len(policy.options.regions) else DEFAULT_REGION + + def run(self): + raise NotImplementedError("subclass responsibility") + + def provision(self): + self.log.info("Provisioning policy function %s", self.policy.name) + manager = mu.CloudFunctionManager(self.policy.session_factory, self.region) + return manager.publish(self._get_function()) + + def deprovision(self): + manager = mu.CloudFunctionManager(self.policy.session_factory, self.region) + return manager.remove(self._get_function()) + + def validate(self): + pass + + def _get_function(self): + raise NotImplementedError("subclass responsibility") + + +@execution.register('vsphere-periodic') +class PeriodicMode(FunctionMode, PullMode): + """Deploy a policy as a Cloud Functions triggered by Cloud Scheduler + at user defined cron interval via Pub/Sub. + + Default region the function is deployed to is ``us-central1``. In + case you want to change that, use the cli ``--region`` flag. + """ + + schema = type_schema( + 'vsphere-periodic', + rinherit=FunctionMode.schema, + required=['schedule'], + **{'trigger-type': {'enum': ['http', 'pubsub']}, + 'tz': {'type': 'string'}, + 'schedule': {'type': 'string'}}) + + def validate(self): + mode = self.policy.data['mode'] + if 'tz' in mode: + error = PolicyValidationError( + "policy:%s vsphere-periodic invalid tz:%s" % ( + self.policy.name, mode['tz'])) + # We can't catch all errors statically, our local tz retrieval + # then the form vsphere is using, ie. not all the same aliases are + # defined. + tzinfo = tz.gettz(mode['tz']) + if tzinfo is None: + raise error + + def _get_function(self): + events = [mu.PeriodicEvent( + local_session(self.policy.session_factory), + self.policy.data['mode'], + self.region + )] + return mu.PolicyFunction(self.policy, events=events) + + def run(self, event, context): + return PullMode.run(self) + + +@execution.register('vsphere-audit') +class ApiAuditMode(FunctionMode): + """Custodian policy execution on vsphere api audit logs events. + + Deploys as a Cloud Function triggered by api calls. This allows + you to apply your policies as soon as an api call occurs. Audit + logs creates an event for every api call that occurs in your vsphere + account. See `vsphere Audit Logs + `_ for more + details. + + Default region the function is deployed to is + ``us-central1``. In case you want to change that, use the cli + ``--region`` flag. + """ + + schema = type_schema( + 'vsphere-audit', + methods={'type': 'array', 'items': {'type': 'string'}}, + required=['methods'], + rinherit=FunctionMode.schema) + + def resolve_resources(self, event): + """Resolve a vsphere resource from its audit trail metadata. + """ + if self.policy.resource_manager.resource_type.get_requires_event: + return [self.policy.resource_manager.get_resource(event)] + resource_info = event.get('resource') + if resource_info is None or 'labels' not in resource_info: + self.policy.log.warning("Could not find resource information in event") + return + # copy resource name, the api doesn't like resource ids, just names. + if 'resourceName' in event['protoPayload']: + resource_info['labels']['resourceName'] = event['protoPayload']['resourceName'] + + resource = self.policy.resource_manager.get_resource(resource_info['labels']) + return [resource] + + def _get_function(self): + events = [mu.ApiSubscriber( + local_session(self.policy.session_factory), + self.policy.data['mode'])] + return mu.PolicyFunction(self.policy, events=events) + + def validate(self): + if not self.policy.resource_manager.resource_type.get: + raise PolicyValidationError( + "Resource:%s does not implement retrieval method" % ( + self.policy.resource_type)) + + def run(self, event, context): + """Execute a vsphere serverless model""" + from c7n.actions import EventAction + + resources = self.resolve_resources(event) + if not resources: + return + + resources = self.policy.resource_manager.filter_resources( + resources, event) + + self.policy.log.info("Filtered resources %d" % len(resources)) + + if not resources: + return + + self.policy.ctx.metrics.put_metric( + 'ResourceCount', len(resources), 'Count', Scope="Policy", + buffer=False) + + for action in self.policy.resource_manager.actions: + if isinstance(action, EventAction): + action.process(resources, event) + else: + action.process(resources) + + return resources diff --git a/tools/c7n_vsphere/c7n_vsphere/provider.py b/tools/c7n_vsphere/c7n_vsphere/provider.py new file mode 100644 index 00000000000..3d686a0a1da --- /dev/null +++ b/tools/c7n_vsphere/c7n_vsphere/provider.py @@ -0,0 +1,34 @@ +# Copyright The Cloud Custodian Authors. +# SPDX-License-Identifier: Apache-2.0 + +from c7n.registry import PluginRegistry +from c7n.provider import Provider, clouds + +from .resources.resource_map import ResourceMap +from .client import Session + +import logging + +log = logging.getLogger('custodian.vsphere') + + +@clouds.register('vsphere') +class vSphere(Provider): + + display_name = 'vsphere' + resource_prefix = 'vsphere' + resources = PluginRegistry('%s.resources' % resource_prefix) + resource_map = ResourceMap + + def initialize(self, options): + return options + + def initialize_policies(self, policy_collection, options): + return policy_collection + + def get_session_factory(self, options): + """Get a credential/session factory for api usage.""" + return Session + + +resources = vSphere.resources \ No newline at end of file diff --git a/tools/c7n_vsphere/c7n_vsphere/query.py b/tools/c7n_vsphere/c7n_vsphere/query.py new file mode 100644 index 00000000000..367427ff2f3 --- /dev/null +++ b/tools/c7n_vsphere/c7n_vsphere/query.py @@ -0,0 +1,360 @@ +# Copyright 2017-2018 Capital One Services, LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import itertools +import json +import logging + +import jmespath + +from c7n.actions import ActionRegistry +from c7n.filters import FilterRegistry +from c7n.manager import ResourceManager +from c7n.query import sources, MaxResourceLimit +from c7n.utils import local_session, chunks + +log = logging.getLogger('c7n_vsphere.query') + + +class ResourceQuery: + def __init__(self, session_factory): + self.session_factory = session_factory + + def filter(self, resource_manager, **params): + result = resource_manager.get_request() + global false, null, true + false = False + null = None + true = True + return eval(result) + + def _invoke_client_enum(self, client, request, params, path): + result = client.do_action_with_exception(request) + return jmespath.search(path, eval(result)) + +@sources.register('describe') +class DescribeSource: + + def __init__(self, manager): + self.manager = manager + self.query = ResourceQuery(manager.session_factory) + + + def get_resources(self, query): + if query is None: + query = {} + return self.query.filter(self.manager, **query) + + def get_permissions(self): + m = self.manager.resource_type + if m.permissions: + return m.permissions + method = m.enum_spec[0] + if method == 'aggregatedList': + method = 'list' + component = m.component + if '.' in component: + component = component.split('.')[-1] + return ("%s.%s.%s" % ( + m.perm_service or m.service, component, method),) + + def augment(self, resources): + return resources + + +@sources.register('inventory') +class AssetInventory: + + permissions = ("cloudasset.assets.searchAllResources", + "cloudasset.assets.exportResource") + + def __init__(self, manager): + self.manager = manager + + def get_resources(self, query): + session = local_session(self.manager.session_factory) + if query is None: + query = {} + if 'scope' not in query: + query['scope'] = 'projects/%s' % session.get_default_project() + if 'assetTypes' not in query: + query['assetTypes'] = [self.manager.resource_type.asset_type] + + search_client = session.client('cloudasset', 'v1p1beta1', 'resources') + resource_client = session.client('cloudasset', 'v1', 'v1') + resources = [] + + results = list(search_client.execute_paged_query('searchAll', query)) + for resource_set in chunks(itertools.chain(*[rs['results'] for rs in results]), 100): + rquery = { + 'parent': query['scope'], + 'contentType': 'RESOURCE', + 'assetNames': [r['name'] for r in resource_set]} + for history_result in resource_client.execute_query( + 'batchGetAssetsHistory', rquery).get('assets', ()): + resource = history_result['asset']['resource']['data'] + resource['c7n:history'] = { + 'window': history_result['window'], + 'ancestors': history_result['asset']['ancestors']} + resources.append(resource) + return resources + + def get_permissions(self): + return self.permissions + + def augment(self, resources): + return resources + + +class QueryMeta(type): + """metaclass to have consistent action/filter registry for new resources.""" + def __new__(cls, name, parents, attrs): + if 'filter_registry' not in attrs: + attrs['filter_registry'] = FilterRegistry( + '%s.filters' % name.lower()) + if 'action_registry' not in attrs: + attrs['action_registry'] = ActionRegistry( + '%s.actions' % name.lower()) + + return super(QueryMeta, cls).__new__(cls, name, parents, attrs) + + +class QueryResourceManager(ResourceManager, metaclass=QueryMeta): + + def __init__(self, data, options): + super(QueryResourceManager, self).__init__(data, options) + self.source = self.get_source(self.source_type) + + + def get_permissions(self): + return self.source.get_permissions() + + def get_source(self, source_type): + return sources.get(source_type)(self) + + def get_client(self): + return local_session(self.session_factory).client( + self.resource_type.service, + self.resource_type.version, + self.resource_type.component) + + def get_model(self): + return self.resource_type + + def get_cache_key(self, query): + return {'source_type': self.source_type, + 'query': query, + 'service': self.resource_type.service, + 'version': self.resource_type.version, + 'component': self.resource_type.component} + + def get_resource(self, resource_info): + + return self.resource_type.get(self.get_client(), resource_info) + + @property + def source_type(self): + return self.data.get('source', 'describe') + + def get_resource_query(self): + if 'query' in self.data: + return {'filter': self.data.get('query')} + + def resources(self, query=None): + q = query or self.get_resource_query() + key = self.get_cache_key(q) + resources = self._fetch_resources(q) + self._cache.save(key, resources) + + resource_count = len(resources) + resources = self.filter_resources(resources) + # Check if we're out of a policies execution limits. + + if self.data == self.ctx.policy.data: + self.check_resource_limit(len(resources), resource_count) + + return resources + + def check_resource_limit(self, selection_count, population_count): + """Check if policy's execution affects more resources then its limit. + """ + p = self.ctx.policy + max_resource_limits = MaxResourceLimit(p, selection_count, population_count) + return max_resource_limits.check_resource_limits() + + def _fetch_resources(self, query): + try: + return self.augment(self.source.get_resources(query)) or [] + except Exception as e: + error = extract_error(e) + if error is None: + raise + elif error == 'accessNotConfigured': + log.warning( + "Resource:%s not available -> Service:%s not enabled on %s", + self.type, + self.resource_type.service, + local_session(self.session_factory).get_default_project()) + return [] + raise + + def augment(self, resources): + return resources + + +class ChildResourceManager(QueryResourceManager): + + def get_resource(self, resource_info): + child_instance = super(ChildResourceManager, self).get_resource(resource_info) + + parent_resource = self.resource_type.parent_spec['resource'] + parent_instance = self.get_resource_manager(parent_resource).get_resource( + self._get_parent_resource_info(child_instance) + ) + + annotation_key = self.resource_type.get_parent_annotation_key() + child_instance[annotation_key] = parent_instance + + return child_instance + + def _fetch_resources(self, query): + if not query: + query = {} + + resources = [] + annotation_key = self.resource_type.get_parent_annotation_key() + parent_query = self.get_parent_resource_query() + parent_resource_manager = self.get_resource_manager( + resource_type=self.resource_type.parent_spec['resource'], + data=({'query': parent_query} if parent_query else {}) + ) + + for parent_instance in parent_resource_manager.resources(): + query.update(self._get_child_enum_args(parent_instance)) + children = super(ChildResourceManager, self)._fetch_resources(query) + + for child_instance in children: + child_instance[annotation_key] = parent_instance + + resources.extend(children) + + return resources + + def _get_parent_resource_info(self, child_instance): + mappings = self.resource_type.parent_spec['parent_get_params'] + return self._extract_fields(child_instance, mappings) + + def _get_child_enum_args(self, parent_instance): + mappings = self.resource_type.parent_spec['child_enum_params'] + return self._extract_fields(parent_instance, mappings) + + def get_parent_resource_query(self): + parent_spec = self.resource_type.parent_spec + enabled = parent_spec['use_child_query'] if 'use_child_query' in parent_spec else False + if enabled and 'query' in self.data: + return self.data.get('query') + + @staticmethod + def _extract_fields(source, mappings): + result = {} + + for mapping in mappings: + result[mapping[1]] = jmespath.search(mapping[0], source) + + return result + + +class TypeMeta(type): + + def __repr__(cls): + return "" % ( + cls.service) + + +class TypeInfo(metaclass=TypeMeta): + + # api client construction information + service = None + version = None + component = None + + +class ChildTypeInfo(TypeInfo): + + parent_spec = None + + @classmethod + def get_parent_annotation_key(cls): + parent_resource = cls.parent_spec['resource'] + return 'c7n:{}'.format(parent_resource) + + +ERROR_REASON = jmespath.compile('error.errors[0].reason') + + +def extract_error(e): + + try: + edata = json.loads(e.content) + except Exception: + return None + return ERROR_REASON.search(edata) + + +class AliyunLocation: + """ + The `_locations` dict is formed by the string keys representing locations taken from + `KMS `_ and + `App Engine `_ and list values containing the string names of the services + the locations are available for. + """ + _locations = {'eur4': ['kms'], + 'global': ['kms'], + 'europe-west4': ['kms'], + 'asia-east2': ['appengine', 'kms'], + 'asia-east1': ['kms'], + 'asia': ['kms'], + 'europe-north1': ['kms'], + 'us-central1': ['kms'], + 'nam4': ['kms'], + 'asia-southeast1': ['kms'], + 'europe': ['kms'], + 'australia-southeast1': ['appengine', 'kms'], + 'us-central': ['appengine'], + 'asia-south1': ['appengine', 'kms'], + 'us-west1': ['kms'], + 'us-west2': ['appengine', 'kms'], + 'asia-northeast2': ['appengine', 'kms'], + 'asia-northeast1': ['appengine', 'kms'], + 'europe-west2': ['appengine', 'kms'], + 'europe-west3': ['appengine', 'kms'], + 'us-east4': ['appengine', 'kms'], + 'europe-west1': ['kms'], + 'europe-west6': ['appengine', 'kms'], + 'us': ['kms'], + 'us-east1': ['appengine', 'kms'], + 'northamerica-northeast1': ['appengine', 'kms'], + 'europe-west': ['appengine'], + 'southamerica-east1': ['appengine', 'kms']} + + @classmethod + def get_service_locations(cls, service): + """ + Returns a list of the locations that have a given service in associated value lists. + + :param service: a string representing the name of a service locations are queried for + """ + return [location for location in AliyunLocation._locations + if service in AliyunLocation._locations[location]] diff --git a/tools/c7n_vsphere/c7n_vsphere/resources/__init__.py b/tools/c7n_vsphere/c7n_vsphere/resources/__init__.py new file mode 100644 index 00000000000..cdce4e86dfd --- /dev/null +++ b/tools/c7n_vsphere/c7n_vsphere/resources/__init__.py @@ -0,0 +1,2 @@ +# Copyright The Cloud Custodian Authors. +# SPDX-License-Identifier: Apache-2.0 \ No newline at end of file diff --git a/tools/c7n_vsphere/c7n_vsphere/resources/cluster.py b/tools/c7n_vsphere/c7n_vsphere/resources/cluster.py new file mode 100644 index 00000000000..66b2d6589fc --- /dev/null +++ b/tools/c7n_vsphere/c7n_vsphere/resources/cluster.py @@ -0,0 +1,67 @@ +# Copyright The Cloud Custodian Authors. +# SPDX-License-Identifier: Apache-2.0 +# +import json + +from c7n.filters import Filter +from c7n.utils import type_schema +from c7n_vsphere.client import Session +from c7n_vsphere.provider import resources +from c7n_vsphere.query import QueryResourceManager, TypeInfo + + +@resources.register('cluster') +class Cluster(QueryResourceManager): + class resource_type(TypeInfo): + enum_spec = ('list', None) + id = 'cluster' + name = 'name' + default_report_fields = ['cluster', 'name'] + + def get_request(self): + client = Session.client(self) + clusters = client.vcenter.Cluster.list() + #Summary(cluster='domain-c33', name='cluster-try', ha_enabled=False, drs_enabled=False) + res = [] + for item in clusters: + cluster = client.vcenter.Cluster.get(item.cluster) + data= { + "F2CId": item.cluster, + "cluster": item.cluster, + "name": item.name, + "ha_enabled": item.ha_enabled, + "drs_enabled": item.drs_enabled, + "resource_pool": cluster.resource_pool + } + res.append(data) + return json.dumps(res) + +@Cluster.filter_registry.register('ha-enabled') +class HaFilter(Filter): + """Filters Clusters based on their ha_enabled + :example: + .. code-block:: yaml + policies: + - name: vsphere-cluster-ha-enabled + resource: vsphere.cluster + filters: + - not: + - type: ha-enabled + value: true + """ + schema = type_schema( + 'ha-enabled', + value={'type': 'boolean'}, + ) + + def process(self, resources, event=None): + results = [] + value = self.data.get('value', None) + for cluster in resources: + matched = True + if value is not None and value != cluster.get('ha_enabled'): + matched = False + if matched: + results.append(cluster) + return results + diff --git a/tools/c7n_vsphere/c7n_vsphere/resources/datacenter.py b/tools/c7n_vsphere/c7n_vsphere/resources/datacenter.py new file mode 100644 index 00000000000..325eeebcbc6 --- /dev/null +++ b/tools/c7n_vsphere/c7n_vsphere/resources/datacenter.py @@ -0,0 +1,78 @@ +# Copyright The Cloud Custodian Authors. +# SPDX-License-Identifier: Apache-2.0 +# +import json + +from c7n.filters import Filter +from c7n.utils import type_schema +from c7n_vsphere.client import Session +from c7n_vsphere.provider import resources +from c7n_vsphere.query import QueryResourceManager, TypeInfo + + +@resources.register('datacenter') +class Datacenter(QueryResourceManager): + class resource_type(TypeInfo): + enum_spec = ('list', None) + id = 'datacenter' + name = 'name' + default_report_fields = ['datacenter', 'name'] + + def get_request(self): + client = Session.client(self) + datacenters = client.vcenter.Datacenter.list() + #Summary(datacenter='datacenter-2', name='Datacenter') + res = [] + for item in datacenters: + #{name : Datacenter, datastore_folder : group-s5, host_folder : group-h4, network_folder : group-n6, vm_folder : group-v3} + datacenter = client.vcenter.Datacenter.get(item.datacenter) + data= { + "F2CId": item.datacenter, + "datacenter": item.datacenter, + "name": item.name, + "datastore_folder": datacenter.datastore_folder, + "host_folder": datacenter.host_folder, + "network_folder": datacenter.network_folder, + "vm_folder": datacenter.vm_folder, + } + res.append(data) + return json.dumps(res) + +@Datacenter.filter_registry.register('system') +class SystemFilter(Filter): + """Filters Datacenters based on their system + :example: + .. code-block:: yaml + policies: + - name: vsphere-datacenter-system + resource: vsphere.datacenter + filters: + - not: + - type: system + host_folder: '' + network_folder: '' + vm_folder: '' + """ + schema = type_schema( + 'system', + host_folder={'type': 'string'}, + network_folder={'type': 'string'}, + vm_folder={'type': 'string'}, + ) + + def process(self, resources, event=None): + results = [] + host_folder = self.data.get('host_folder', None) + network_folder = self.data.get('network_folder', None) + vm_folder = self.data.get('vm_folder', None) + for datacenter in resources: + matched = True + if host_folder == datacenter.get('host_folder'): + matched = False + if network_folder == datacenter.get('network_folder'): + matched = False + if vm_folder == datacenter.get('vm_folder'): + matched = False + if matched: + results.append(datacenter) + return results \ No newline at end of file diff --git a/tools/c7n_vsphere/c7n_vsphere/resources/datastore.py b/tools/c7n_vsphere/c7n_vsphere/resources/datastore.py new file mode 100644 index 00000000000..871860e6637 --- /dev/null +++ b/tools/c7n_vsphere/c7n_vsphere/resources/datastore.py @@ -0,0 +1,80 @@ +# Copyright The Cloud Custodian Authors. +# SPDX-License-Identifier: Apache-2.0 +# +import json + +from c7n.filters import Filter +from c7n.utils import type_schema +from c7n_vsphere.client import Session +from c7n_vsphere.provider import resources +from c7n_vsphere.query import QueryResourceManager, TypeInfo + + +@resources.register('datastore') +class Datastore(QueryResourceManager): + class resource_type(TypeInfo): + enum_spec = ('list', None) + id = 'datastore' + name = 'name' + default_report_fields = ['datastore', 'name'] + + def get_request(self): + client = Session.client(self) + datastores = client.vcenter.Datastore.list() + #Summary(datastore='datastore-1013', name='datastore1', type=Type(string='VMFS'), free_space=610890940416, capacity=991600574464) + res = [] + for item in datastores: + #{name : datastore1, type : VMFS, accessible : True, free_space : 610536521728, multiple_host_access : False, thin_provisioning_supported : True} + datastore = client.vcenter.Datastore.get(item.datastore) + data= { + "F2CId": item.datastore, + "datastore": item.datastore, + "name": item.name, + "type": str(item.type), + "free_space": item.free_space, + "capacity": item.capacity, + "accessible": datastore.accessible, + "multiple_host_access": datastore.multiple_host_access, + "thin_provisioning_supported": datastore.thin_provisioning_supported, + } + res.append(data) + return json.dumps(res) + +@Datastore.filter_registry.register('system') +class SystemFilter(Filter): + """Filters Datastores based on their system + :example: + .. code-block:: yaml + policies: + - name: vsphere-datastore-system + resource: vsphere.datastore + filters: + - not: + - type: system + free_space: 500 + thin_provisioning_supported: true + multiple_host_access: true + """ + schema = type_schema( + 'system', + free_space={'type': 'number'}, + thin_provisioning_supported={'type': 'boolean'}, + multiple_host_access={'type': 'boolean'}, + ) + + def process(self, resources, event=None): + results = [] + free_space = self.data.get('free_space', None) + thin_provisioning_supported = self.data.get('thin_provisioning_supported', None) + multiple_host_access = self.data.get('multiple_host_access', None) + for datastore in resources: + matched = True + if free_space is not None and free_space*1024*1024*1024 > datastore.get('free_space'): + matched = False + if thin_provisioning_supported and thin_provisioning_supported != datastore.get('thin_provisioning_supported'): + matched = False + if multiple_host_access and multiple_host_access != datastore.get('multiple_host_access'): + matched = False + if matched: + results.append(datastore) + return results \ No newline at end of file diff --git a/tools/c7n_vsphere/c7n_vsphere/resources/folder.py b/tools/c7n_vsphere/c7n_vsphere/resources/folder.py new file mode 100644 index 00000000000..004b5850164 --- /dev/null +++ b/tools/c7n_vsphere/c7n_vsphere/resources/folder.py @@ -0,0 +1,31 @@ +# Copyright The Cloud Custodian Authors. +# SPDX-License-Identifier: Apache-2.0 +# +import json + +from c7n_vsphere.query import QueryResourceManager, TypeInfo +from c7n_vsphere.provider import resources +from c7n_vsphere.client import Session + +@resources.register('folder') +class Folder(QueryResourceManager): + class resource_type(TypeInfo): + enum_spec = ('list', None) + id = 'folder' + name = 'name' + default_report_fields = ['folder', 'name'] + + def get_request(self): + client = Session.client(self) + folders = client.vcenter.Folder.list() + #Summary(folder='group-d1', name='Datacenters', type=Type(string='DATACENTER')) + res = [] + for item in folders: + data= { + "F2CId": item.folder, + "folder": item.folder, + "name": item.name, + "type": str(item.type), + } + res.append(data) + return json.dumps(res) \ No newline at end of file diff --git a/tools/c7n_vsphere/c7n_vsphere/resources/host.py b/tools/c7n_vsphere/c7n_vsphere/resources/host.py new file mode 100644 index 00000000000..d9ecc1d02ab --- /dev/null +++ b/tools/c7n_vsphere/c7n_vsphere/resources/host.py @@ -0,0 +1,69 @@ +# Copyright The Cloud Custodian Authors. +# SPDX-License-Identifier: Apache-2.0 +# +import json + +from c7n.filters import Filter +from c7n.utils import type_schema +from c7n_vsphere.client import Session +from c7n_vsphere.provider import resources +from c7n_vsphere.query import QueryResourceManager, TypeInfo + + +@resources.register('host') +class Host(QueryResourceManager): + class resource_type(TypeInfo): + enum_spec = ('list', None) + id = 'host' + name = 'name' + default_report_fields = ['host', 'name'] + + def get_request(self): + client = Session.client(self) + hosts = client.vcenter.Host.list() + #Summary(host='host-10', name='10.1.240.15', connection_state=ConnectionState(string='CONNECTED'), power_state=PowerState(string='POWERED_ON')) + res = [] + for item in hosts: + data= { + "F2CId": item.host, + "host": item.host, + "name": item.name, + "connection_state": str(item.connection_state), + "power_state": str(item.power_state), + } + res.append(data) + return json.dumps(res) + +@Host.filter_registry.register('system') +class SystemFilter(Filter): + """Filters Hosts based on their system + :example: + .. code-block:: yaml + policies: + - name: vsphere-host-system + resource: vsphere.host + filters: + - not: + - type: system + connection_state: CONNECTED + power_state: POWERED_ON + """ + schema = type_schema( + 'system', + connection_state={'type': 'string'}, + power_state={'type': 'string'}, + ) + + def process(self, resources, event=None): + results = [] + connection_state = self.data.get('connection_state', None) + power_state = self.data.get('power_state', None) + for host in resources: + matched = True + if connection_state is not None and connection_state != host.get('connection_state'): + matched = False + if power_state is not None and power_state != host.get('power_state'): + matched = False + if matched: + results.append(host) + return results \ No newline at end of file diff --git a/tools/c7n_vsphere/c7n_vsphere/resources/network.py b/tools/c7n_vsphere/c7n_vsphere/resources/network.py new file mode 100644 index 00000000000..c0273307b38 --- /dev/null +++ b/tools/c7n_vsphere/c7n_vsphere/resources/network.py @@ -0,0 +1,63 @@ +# Copyright The Cloud Custodian Authors. +# SPDX-License-Identifier: Apache-2.0 +# +import json + +from c7n.filters import Filter +from c7n.utils import type_schema +from c7n_vsphere.client import Session +from c7n_vsphere.provider import resources +from c7n_vsphere.query import QueryResourceManager, TypeInfo + + +@resources.register('network') +class Network(QueryResourceManager): + class resource_type(TypeInfo): + enum_spec = ('list', None) + id = 'network' + name = 'name' + default_report_fields = ['network', 'name'] + + def get_request(self): + client = Session.client(self) + networks = client.vcenter.Network.list() + #Summary(network='dvportgroup-1312', name='cluster-try-VSAN-DPortGroup', type=Type(string='DISTRIBUTED_PORTGROUP')) + res = [] + for item in networks: + data= { + "F2CId": item.network, + "network": item.network, + "name": item.name, + "type": str(item.type), + } + res.append(data) + return json.dumps(res) + +@Network.filter_registry.register('system') +class SystemFilter(Filter): + """Filters Networks based on their system + :example: + .. code-block:: yaml + policies: + - name: vsphere-network-system + resource: vsphere.network + filters: + - not: + - type: system + value: DISTRIBUTED_PORTGROUP + """ + schema = type_schema( + 'system', + value={'type': 'string'}, + ) + + def process(self, resources, event=None): + results = [] + value = self.data.get('value', None) + for network in resources: + matched = True + if value is not None and value != network.get('type'): + matched = False + if matched: + results.append(network) + return results \ No newline at end of file diff --git a/tools/c7n_vsphere/c7n_vsphere/resources/resource_map.py b/tools/c7n_vsphere/c7n_vsphere/resources/resource_map.py new file mode 100644 index 00000000000..b92d3f20ec5 --- /dev/null +++ b/tools/c7n_vsphere/c7n_vsphere/resources/resource_map.py @@ -0,0 +1,12 @@ +# Copyright The Cloud Custodian Authors. +# SPDX-License-Identifier: Apache-2.0 +ResourceMap = { + "vsphere.vm": "c7n_vsphere.resources.vm.VM", + "vsphere.cluster": "c7n_vsphere.resources.cluster.Cluster", + "vsphere.datacenter": "c7n_vsphere.resources.datacenter.Datacenter", + "vsphere.datastore": "c7n_vsphere.resources.datastore.Datastore", + "vsphere.folder": "c7n_vsphere.resources.folder.Folder", + "vsphere.host": "c7n_vsphere.resources.host.Host", + "vsphere.network": "c7n_vsphere.resources.network.Network", + "vsphere.resourcepool": "c7n_vsphere.resources.resourcepool.ResourcePool", +} \ No newline at end of file diff --git a/tools/c7n_vsphere/c7n_vsphere/resources/resourcepool.py b/tools/c7n_vsphere/c7n_vsphere/resources/resourcepool.py new file mode 100644 index 00000000000..9c562fefab5 --- /dev/null +++ b/tools/c7n_vsphere/c7n_vsphere/resources/resourcepool.py @@ -0,0 +1,74 @@ +# Copyright The Cloud Custodian Authors. +# SPDX-License-Identifier: Apache-2.0 +# +import json + +from c7n.filters import Filter +from c7n.utils import type_schema +from c7n_vsphere.client import Session +from c7n_vsphere.provider import resources +from c7n_vsphere.query import QueryResourceManager, TypeInfo + + +@resources.register('resourcepool') +class ResourcePool(QueryResourceManager): + class resource_type(TypeInfo): + enum_spec = ('list', None) + id = 'resource_pool' + name = 'name' + default_report_fields = ['resource_pool', 'name'] + + def get_request(self): + client = Session.client(self) + resource_pools = client.vcenter.ResourcePool.list() + #Summary(resource_pool='resgroup-34', name='Resources') + res = [] + for item in resource_pools: + #{name : Resources, resource_pools : set(), cpu_allocation : None, memory_allocation : None} + try: + resource_pool = client.vcenter.ResourcePool.get(item.resource_pool) + except: + resource_pool = None + data= { + "F2CId": item.resource_pool, + "resource_pool": item.resource_pool, + "name": item.name, + "cpu_allocation": str(resource_pool.cpu_allocation) if (resource_pool is not None) else None, + "memory_allocation": str(resource_pool.memory_allocation) if (resource_pool is not None) else None, + } + res.append(data) + return json.dumps(res) + +@ResourcePool.filter_registry.register('system') +class SystemFilter(Filter): + """Filters ResourcePools based on their system + :example: + .. code-block:: yaml + policies: + - name: vsphere-resourcepool-system + resource: vsphere.resourcepool + filters: + - not: + - type: system + cpu_allocation: 0 + memory_allocation: 0 + """ + schema = type_schema( + 'system', + cpu_allocation={'type': 'number'}, + memory_allocation={'type': 'number'}, + ) + + def process(self, resources, event=None): + results = [] + cpu_allocation = self.data.get('cpu_allocation', None) + memory_allocation = self.data.get('memory_allocation', None) + for resourcepool in resources: + matched = True + if cpu_allocation is not None and cpu_allocation != resourcepool.get('cpu_allocation'): + matched = False + if memory_allocation is not None and memory_allocation != resourcepool.get('memory_allocation'): + matched = False + if matched: + results.append(resourcepool) + return results \ No newline at end of file diff --git a/tools/c7n_vsphere/c7n_vsphere/resources/vm.py b/tools/c7n_vsphere/c7n_vsphere/resources/vm.py new file mode 100644 index 00000000000..7e015ae3297 --- /dev/null +++ b/tools/c7n_vsphere/c7n_vsphere/resources/vm.py @@ -0,0 +1,91 @@ +# Copyright The Cloud Custodian Authors. +# SPDX-License-Identifier: Apache-2.0 +# +import json +import logging + +from c7n.filters import Filter +from c7n.utils import type_schema +from c7n_vsphere.client import Session +from c7n_vsphere.provider import resources +from c7n_vsphere.query import QueryResourceManager, TypeInfo + +logging.basicConfig(level=logging.INFO, format='%(asctime)s %(filename)s[line:%(lineno)d] %(levelname)s %(message)s', datefmt='%a, %d %b %Y %H:%M:%S') + +@resources.register('vm') +class VM(QueryResourceManager): + class resource_type(TypeInfo): + enum_spec = ('list', None) + id = 'vm' + name = 'name' + default_report_fields = ['vm', 'name'] + + def get_request(self): + client = Session.client(self) + vms = client.vcenter.VM.list() + #Summary(vm='vm-17', name='QA-PROXY', power_state=State(string='POWERED_ON'), cpu_count=2, memory_size_mib=4096) + #{guest_os : CENTOS_7_64, name : FIT2CLOUD-2.0-TRY2, identity : None, power_state : POWERED_ON, instant_clone_frozen : None, hardware : {version : VMX_14, upgrade_policy : NEVER, upgrade_version : None, upgrade_status : NONE, upgrade_error : None}, boot : {type : BIOS, efi_legacy_boot : None, network_protocol : None, delay : 0, retry : False, retry_delay : 10, enter_setup_mode : False}, boot_devices : [], cpu : {count : 4, cores_per_socket : 1, hot_add_enabled : False, hot_remove_enabled : False}, memory : {size_mib : 16384, hot_add_enabled : False, hot_add_increment_size_mib : None, hot_add_limit_mib : None}, disks : {'2000': Info(label='Hard disk 1', type=HostBusAdapterType(string='SCSI'), ide=None, scsi=ScsiAddressInfo(bus=0, unit=0), sata=None, backing=BackingInfo(type=BackingType(string='VMDK_FILE'), vmdk_file='[Local] FIT2CLOUD-2.0-TRY2/FIT2CLOUD-2.0-TRY2-000001.vmdk'), capacity=107374182400)}, nics : {'4000': Info(label='Network adapter 1', type=EmulationType(string='VMXNET3'), upt_compatibility_enabled=True, mac_type=MacAddressType(string='GENERATED'), mac_address='00:0c:29:24:5d:a3', pci_slot_number=192, wake_on_lan_enabled=False, backing=BackingInfo(type=BackingType(string='STANDARD_PORTGROUP'), network='network-12', network_name='VM Network', host_device=None, distributed_switch_uuid=None, distributed_port=None, connection_cookie=None, opaque_network_type=None, opaque_network_id=None), state=ConnectionState(string='CONNECTED'), start_connected=True, allow_guest_control=True)}, cdroms : {'16000': Info(type=HostBusAdapterType(string='SATA'), label='CD/DVD drive 1', ide=None, sata=SataAddressInfo(bus=0, unit=0), backing=BackingInfo(type=BackingType(string='ISO_FILE'), iso_file='[Local] iso/CentOS-7-x86_64-Minimal-1804.iso', host_device=None, auto_detect=None, device_access_type=None), state=ConnectionState(string='NOT_CONNECTED'), start_connected=True, allow_guest_control=True)}, floppies : {}, parallel_ports : {}, serial_ports : {}, sata_adapters : {'15000': Info(label='SATA controller 0', type=Type(string='AHCI'), bus=0, pci_slot_number=35)}, scsi_adapters : {'1000': Info(label='SCSI controller 0', type=Type(string='PVSCSI'), scsi=ScsiAddressInfo(bus=0, unit=7), pci_slot_number=160, sharing=Sharing(string='NONE'))}} + res = [] + for item in vms: + try: + vm = client.vcenter.VM.get(item.vm) + except: + vm = None + data= { + "F2CId": item.vm, + "vm": item.vm, + "name": item.name, + "power_state": str(item.power_state), + "cpu_count": item.cpu_count, + "memory_size_mib": item.memory_size_mib, + "guest_os": str(vm.guest_os) if (vm is not None) else None, + "identity": str(vm.identity) if (vm is not None) else None, + "instant_clone_frozen": vm.instant_clone_frozen if (vm is not None) else None, + "hardware": str(vm.hardware) if (vm is not None) else None, + "boot": str(vm.boot) if (vm is not None) else None, + "boot_devices": vm.boot_devices if (vm is not None) else None, + "cpu": str(vm.cpu) if (vm is not None) else None, + "memory": str(vm.memory) if (vm is not None) else None, + "disks": str(vm.disks) if (vm is not None) else None, + } + res.append(data) + return json.dumps(res) + +@VM.filter_registry.register('system') +class SystemFilter(Filter): + """Filters VMs based on their system + :example: + .. code-block:: yaml + policies: + - name: vsphere-vm-system + resource: vsphere.vm + filters: + - not: + - type: system + power_state: POWERED_ON + cpu_count: 1 + memory_size_mib: 1024 + """ + schema = type_schema( + 'system', + power_state={'type': 'string'}, + cpu_count={'type': 'number'}, + memory_size_mib={'type': 'number'}, + ) + + def process(self, resources, event=None): + results = [] + power_state = self.data.get('power_state', None) + cpu_count = self.data.get('cpu_count', None) + memory_size_mib = self.data.get('memory_size_mib', None) + for vm in resources: + matched = True + if power_state is not None and power_state != vm.get('power_state'): + matched = False + if cpu_count is not None and cpu_count > vm.get('cpu_count'): + matched = False + if memory_size_mib is not None and memory_size_mib > vm.get('memory_size_mib'): + matched = False + if matched: + results.append(vm) + return results \ No newline at end of file diff --git a/tools/c7n_vsphere/pyproject.toml b/tools/c7n_vsphere/pyproject.toml new file mode 100644 index 00000000000..7d86389a239 --- /dev/null +++ b/tools/c7n_vsphere/pyproject.toml @@ -0,0 +1,29 @@ +[tool.poetry] +name = "c7n_vsphere" +version = "1.0.0" +description = "Cloud Custodian - VMware vSphere Provider" +readme = "readme.md" +authors = ["Cloud Custodian Project"] +homepage = "https://cloudcustodian.io" +repository = "https://github.com/cloud-custodian/cloud-custodian" +documentation = "https://cloudcustodian.io/docs/" +license = "Apache-2.0" +classifiers = [ + "Topic :: System :: Systems Administration", + "Topic :: System :: Distributed Computing" +] + +[tool.poetry.dependencies] +python = "^3.7" +pyvmomi = "^7.0.2" +cryptography = "^2.9.2" + +[tool.poetry.dev-dependencies] +c7n = {path = "../..", develop = true} +pytest = "~6.0.0" +vcrpy = "^4.0.2" + + +[build-system] +requires = ["poetry>=0.12", "setuptools"] +build-backend = "poetry.masonry.api" \ No newline at end of file diff --git a/tools/c7n_vsphere/readme.md b/tools/c7n_vsphere/readme.md new file mode 100644 index 00000000000..deced11c37f --- /dev/null +++ b/tools/c7n_vsphere/readme.md @@ -0,0 +1,21 @@ +# Custodian VMware vSphere Support + +Work in Progress - Not Ready For Use. + +## Quick Start + +### Installation + +``` +pip install c7n_vsphere +``` + +## Examples + +filter examples: + +```yaml +policies: + - name: vsphere-vm + resource: vsphere.vm +``` diff --git a/tools/c7n_vsphere/requirements.txt b/tools/c7n_vsphere/requirements.txt new file mode 100644 index 00000000000..e751a20bdaa --- /dev/null +++ b/tools/c7n_vsphere/requirements.txt @@ -0,0 +1,6 @@ +git+git://github.com/vmware/vsphere-automation-sdk-python.git +pyvmomi==7.0.2 +lxml==4.6.3 +vmware-nsx==17.0.0 +suds-jurko==0.6 +oslo.vmware==3.9.0 \ No newline at end of file diff --git a/tools/c7n_vsphere/setup.py b/tools/c7n_vsphere/setup.py new file mode 100644 index 00000000000..58b2334bd71 --- /dev/null +++ b/tools/c7n_vsphere/setup.py @@ -0,0 +1,36 @@ +# Automatically generated from poetry/pyproject.toml +# flake8: noqa +# -*- coding: utf-8 -*- +from setuptools import setup + +packages = [ + 'c7n_vsphere', + 'c7n_vsphere.resources' +] + +package_data = {'': ['*']} + +install_requires = \ + ['pyvmomi (>=7.0.2)', + 'c7n (>=0.9.8,<0.10.0)'] + + +setup_kwargs = { + 'name': 'c7n-vsphere', + 'version': '1.0.0', + 'description': 'Cloud Custodian - VMware vSphere Provider', + 'long_description': '# Custodian VMware vSphere Support', + 'long_description_content_type': 'text/markdown', + 'author': 'Cloud Custodian Project', + 'author_email': None, + 'maintainer': None, + 'maintainer_email': None, + 'url': 'https://cloudcustodian.io', + 'packages': packages, + 'package_data': package_data, + 'install_requires': install_requires, + 'python_requires': '>=3.6,<4.0', +} + + +setup(**setup_kwargs) \ No newline at end of file diff --git a/tools/c7n_vsphere/test/vsphere.py b/tools/c7n_vsphere/test/vsphere.py new file mode 100644 index 00000000000..de43c360259 --- /dev/null +++ b/tools/c7n_vsphere/test/vsphere.py @@ -0,0 +1,225 @@ +# Copyright The Cloud Custodian Authors. +# SPDX-License-Identifier: Apache-2.0 +import logging + +import requests +import urllib3 +from vmware.vapi.vsphere.client import create_vsphere_client + +from c7n.resources import load_resources + +session = requests.session() + +logging.basicConfig(level=logging.INFO, format='%(asctime)s %(filename)s[line:%(lineno)d] %(levelname)s %(message)s', datefmt='%a, %d %b %Y %H:%M:%S') +# 本地测试用例 +def _loadFile_(): + json = dict() + f = open("/opt/fit2cloud/vsphere.txt") + lines = f.readlines() + for line in lines: + line = line.strip() + if "vsphere.VSPHERE_USERNAME" in line: + VSPHERE_USERNAME = line[line.rfind('=') + 1:] + json['VSPHERE_USERNAME'] = VSPHERE_USERNAME + if "vsphere.VSPHERE_PASSWORD" in line: + VSPHERE_PASSWORD = line[line.rfind('=') + 1:] + json['VSPHERE_PASSWORD'] = VSPHERE_PASSWORD + if "vsphere.VSPHERE_REGION_NAME" in line: + VSPHERE_REGION_NAME = line[line.rfind('=') + 1:] + json['VSPHERE_REGION_NAME'] = VSPHERE_REGION_NAME + if "vsphere.VSPHERE_SERVER" in line: + VSPHERE_SERVER = line[line.rfind('=') + 1:] + json['VSPHERE_SERVER'] = VSPHERE_SERVER + f.close() + print('认证信息: ' + str(json)) + return json + +params = _loadFile_() +load_resources() + +# Disable cert verification for demo purpose. +# This is not recommended in a production environment. +session.verify = False + +# Disable the secure connection warning for demo purpose. +# This is not recommended in a production environment. +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + +# Connect to a vCenter Server using username and password +vsphere_client = create_vsphere_client(server=params['VSPHERE_SERVER'], username=params['VSPHERE_USERNAME'], password=params['VSPHERE_PASSWORD'], session=session) + +def vm_list(): + vms = vsphere_client.vcenter.VM.list() + res = [] + for item in vms: + vm = vsphere_client.vcenter.VM.get(item.vm) + #{guest_os : CENTOS_7_64, name : FIT2CLOUD-2.0-TRY2, identity : None, power_state : POWERED_ON, instant_clone_frozen : None, + # hardware : {version : VMX_14, upgrade_policy : NEVER, upgrade_version : None, upgrade_status : NONE, upgrade_error : None}, + # boot : {type : BIOS, efi_legacy_boot : None, network_protocol : None, delay : 0, retry : False, retry_delay : 10, enter_setup_mode : False}, + # boot_devices : [], cpu : {count : 4, cores_per_socket : 1, hot_add_enabled : False, hot_remove_enabled : False}, + # memory : {size_mib : 16384, hot_add_enabled : False, hot_add_increment_size_mib : None, hot_add_limit_mib : None}, + # disks : {'2000': Info(label='Hard disk 1', type=HostBusAdapterType(string='SCSI'), ide=None, scsi=ScsiAddressInfo(bus=0, unit=0), sata=None, + # backing=BackingInfo(type=BackingType(string='VMDK_FILE'), vmdk_file='[Local] FIT2CLOUD-2.0-TRY2/FIT2CLOUD-2.0-TRY2-000001.vmdk'), capacity=107374182400)}, + # nics : {'4000': Info(label='Network adapter 1', type=EmulationType(string='VMXNET3'), upt_compatibility_enabled=True, + # mac_type=MacAddressType(string='GENERATED'), mac_address='00:0c:29:24:5d:a3', pci_slot_number=192, wake_on_lan_enabled=False, + # backing=BackingInfo(type=BackingType(string='STANDARD_PORTGROUP'), network='network-12', network_name='VM Network', host_device=None, + # distributed_switch_uuid=None, distributed_port=None, connection_cookie=None, opaque_network_type=None, opaque_network_id=None), + # state=ConnectionState(string='CONNECTED'), start_connected=True, allow_guest_control=True)}, cdroms : {'16000': Info(type=HostBusAdapterType(string='SATA'), + # label='CD/DVD drive 1', ide=None, sata=SataAddressInfo(bus=0, unit=0), backing=BackingInfo(type=BackingType(string='ISO_FILE'), + # iso_file='[Local] iso/CentOS-7-x86_64-Minimal-1804.iso', host_device=None, auto_detect=None, device_access_type=None), + # state=ConnectionState(string='NOT_CONNECTED'), start_connected=True, allow_guest_control=True)}, floppies : {}, parallel_ports : {}, serial_ports : {}, + # sata_adapters : {'15000': Info(label='SATA controller 0', type=Type(string='AHCI'), bus=0, pci_slot_number=35)}, + # scsi_adapters : {'1000': Info(label='SCSI controller 0', type=Type(string='PVSCSI'), scsi=ScsiAddressInfo(bus=0, unit=7), pci_slot_number=160, + # sharing=Sharing(string='NONE'))}} + data= { + "F2CId": item.vm, + "vm": item.vm, + "name": item.name, + "power_state": str(item.power_state), + "cpu_count": item.cpu_count, + "memory_size_mib": item.memory_size_mib, + "guest_os": str(vm.guest_os), + "identity": vm.identity, + "instant_clone_frozen": vm.instant_clone_frozen, + "hardware": str(vm.hardware), + "boot": str(vm.boot), + "boot_devices": vm.boot_devices, + "cpu": str(vm.cpu), + "memory": str(vm.memory), + "disks": str(vm.disks), + } + res.append(data) + print(res) + return res + +def cluster_list(): + clusters = vsphere_client.vcenter.Cluster.list() + res = [] + for item in clusters: + cluster = vsphere_client.vcenter.Cluster.get(item.cluster) + data= { + "F2CId": item.cluster, + "cluster": item.cluster, + "name": item.name, + "ha_enabled": item.ha_enabled, + "drs_enabled": item.drs_enabled, + "resource_pool": cluster.resource_pool + } + res.append(data) + print(res) + return res + +def datacenter_list(): + datacenters = vsphere_client.vcenter.Datacenter.list() + res = [] + for item in datacenters: + #{name : Datacenter, datastore_folder : group-s5, host_folder : group-h4, network_folder : group-n6, vm_folder : group-v3} + datacenter = vsphere_client.vcenter.Datacenter.get(item.datacenter) + print(datacenter) + data= { + "F2CId": item.datacenter, + "datacenter": item.datacenter, + "name": item.name, + "datastore_folder": datacenter.datastore_folder, + "host_folder": datacenter.host_folder, + "network_folder": datacenter.network_folder, + "vm_folder": datacenter.vm_folder, + } + res.append(data) + return res + +def datastore_list(): + datastores = vsphere_client.vcenter.Datastore.list() + res = [] + for item in datastores: + #{name : datastore1, type : VMFS, accessible : True, free_space : 610536521728, multiple_host_access : False, thin_provisioning_supported : True} + datastore = vsphere_client.vcenter.Datastore.get(item.datastore) + print(datastore) + data= { + "F2CId": item.datastore, + "datastore": item.datastore, + "name": item.name, + "type": str(item.type), + "free_space": item.free_space, + "capacity": item.capacity, + "accessible": datastore.accessible, + "multiple_host_access": datastore.multiple_host_access, + "thin_provisioning_supported": datastore.thin_provisioning_supported, + } + res.append(data) + return res + +def folder_list(): + folders = vsphere_client.vcenter.Folder.list() + #Summary(folder='group-d1', name='Datacenters', type=Type(string='DATACENTER')) + res = [] + for item in folders: + data= { + "F2CId": item.folder, + "folder": item.folder, + "name": item.name, + "type": str(item.type), + } + res.append(data) + print(res) + return folders + +def host_list(): + hosts = vsphere_client.vcenter.Host.list() + #Summary(host='host-10', name='10.1.240.15', connection_state=ConnectionState(string='CONNECTED'), power_state=PowerState(string='POWERED_ON')) + res = [] + for item in hosts: + data= { + "F2CId": item.host, + "host": item.host, + "name": item.name, + "connection_state": str(item.connection_state), + "power_state": str(item.power_state), + } + res.append(data) + print(res) + return res + +def network_list(): + networks = vsphere_client.vcenter.Network.list() + #Summary(network='dvportgroup-1312', name='cluster-try-VSAN-DPortGroup', type=Type(string='DISTRIBUTED_PORTGROUP')) + res = [] + for item in networks: + data= { + "F2CId": item.network, + "network": item.network, + "name": item.name, + "type": str(item.type), + } + res.append(data) + print(res) + return res + +def resource_pool_list(): + resource_pools = vsphere_client.vcenter.ResourcePool.list() + #Summary(resource_pool='resgroup-34', name='Resources') + res = [] + for item in resource_pools: + #{name : Resources, resource_pools : set(), cpu_allocation : None, memory_allocation : None} + resource_pool = vsphere_client.vcenter.ResourcePool.get(item.resource_pool) + data= { + "F2CId": item.resource_pool, + "resource_pool": item.resource_pool, + "name": item.name, + "cpu_allocation": resource_pool.cpu_allocation, + "memory_allocation": resource_pool.memory_allocation + } + res.append(data) + print(res) + return res + +if __name__ == '__main__': + logging.info("Hello vSphere OpenApi!") + vm_list() + # cluster_list() + # datacenter_list() + # datastore_list() + # folder_list() + # host_list() + # network_list() + # resource_pool_list() \ No newline at end of file diff --git a/tools/dev/dockerpkg.py b/tools/dev/dockerpkg.py index 33d73e9eaee..c1f94714957 100644 --- a/tools/dev/dockerpkg.py +++ b/tools/dev/dockerpkg.py @@ -63,15 +63,19 @@ RUN rm -R tools/c7n_azure/tests_azure ADD tools/c7n_kube /src/tools/c7n_kube RUN rm -R tools/c7n_kube/tests -ADD tools/c7n_aliyun /src/tools/c7n_aliyun +ADD tools/c7n_vsphere /src/tools/c7n_aliyun RUN rm -R tools/c7n_aliyun/test ADD tools/c7n_huawei /src/tools/c7n_huawei RUN rm -R tools/c7n_huawei/test ADD tools/c7n_tencent /src/tools/c7n_tencent RUN rm -R tools/c7n_tencent/test +ADD tools/c7n_vsphere /src/tools/c7n_vsphere +RUN rm -R tools/c7n_vsphere/test +ADD tools/c7n_vsphere /src/tools/c7n_vsphere +RUN rm -R tools/c7n_vsphere/test # Install requested providers -ARG providers="azure gcp kube aliyun huawei tencent" +ARG providers="azure gcp kube aliyun huawei tencent openstack vsphere" RUN . /usr/local/bin/activate && \\ for pkg in $providers; do cd tools/c7n_$pkg && \\ $HOME/.poetry/bin/poetry install && cd ../../; done