diff --git a/.dockerignore b/.dockerignore index a5e40aacfe..fbe8c72d47 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,13 +1,6 @@ -*/.git* -* -!COPYRIGHT -!LICENSE -!assemble/app/build_venv.sh -!conf -!config.json -!go/bin/runstatus -!os -!python -!requirements* -!web/build_static.sh -!wwwroot +**/*.git* +**/.* +data +venv +web +Makefile diff --git a/.flake8 b/.flake8 deleted file mode 100644 index 7e93d382e8..0000000000 --- a/.flake8 +++ /dev/null @@ -1,14 +0,0 @@ -[flake8] - -ignore = - I801, - E501, - W503, - PT004, - -exclude = - .pytest_cache, - venv, - .dockerignore, - .gitignore, - .npmcheckignore, diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000000..ba1be87270 --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,17 @@ +include: + - project: "arenadata/infrastructure/code/ci/gitlab_ci_files" + ref: master + file: "/development/adcm.yml" + + +Linters: + script: + - apk update && apk upgrade && apk add build-base linux-headers openssl libc6-compat openldap-dev python3-dev py3-pip + - pip install -r requirements-venv-2.9.txt + - pip install autoflake black flake8 isort pylint + - black --check license_checker.py python tests + - autoflake --check --quiet -r --remove-all-unused-imports --exclude apps.py,python/ansible/plugins,python/init_db.py,python/task_runner.py,python/backupdb.py,python/job_runner.py,python/drf_docs.py license_checker.py python tests + - isort --check license_checker.py python tests + - python3 license_checker.py --folders python go + - flake8 --max-line-length=120 tests/functional tests/ui_tests + - pylint --rcfile pyproject.toml --recursive y python tests diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 19fbf47779..fae89e7879 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -6,6 +6,9 @@ repos: additional_dependencies: [ "click==8.0.4" ] language_version: python3 files: "^python/" + args: [ + "--config", "pyproject.toml", + ] - repo: https://github.com/pycqa/isort rev: 5.10.1 diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000000..523f7eef72 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,39 @@ +FROM python:3.10-alpine +RUN apk update && \ + apk upgrade && \ + apk add --virtual .build-deps \ + build-base \ + linux-headers && \ + apk add \ + bash \ + openssl \ + libc6-compat \ + openldap-dev \ + git \ + runit \ + nginx \ + openssh-client \ + logrotate +COPY requirements*.txt /adcm/ +RUN pip install --upgrade pip && \ + pip install --no-cache-dir -r /adcm/requirements-venv-default.txt && \ + python -m venv /adcm/venv/2.9 && \ + . /adcm/venv/2.9/bin/activate && \ + pip install --no-cache-dir -r /adcm/requirements-venv-2.9.txt && \ + deactivate && \ + python -m venv /adcm/venv/default && \ + . /adcm/venv/default/bin/activate && \ + pip install --no-cache-dir -r /adcm/requirements-venv-default.txt && \ + deactivate +RUN apk del .build-deps +COPY . /adcm +RUN mkdir -p /adcm/data/log && \ + mkdir -p /usr/share/ansible/plugins/modules && \ + cp -r /adcm/os/* / && \ + cp /adcm/os/etc/crontabs/root /var/spool/cron/crontabs/root && \ + cp -r /adcm/python/ansible/* adcm/venv/default/lib/python3.10/site-packages/ansible/ && \ + cp -r /adcm/python/ansible/* adcm/venv/2.9/lib/python3.10/site-packages/ansible/ && \ + python /adcm/python/manage.py collectstatic --noinput && \ + cp -r /adcm/wwwroot/static/rest_framework/css/* /adcm/wwwroot/static/rest_framework/docs/css/ +EXPOSE 8000 +CMD ["/etc/startup.sh"] diff --git a/Makefile b/Makefile index 0f71ba9e8d..d10e633b55 100644 --- a/Makefile +++ b/Makefile @@ -1,99 +1,63 @@ -# Set number of threads BRANCH_NAME ?= $(shell git rev-parse --abbrev-ref HEAD) - -ADCMBASE_IMAGE ?= hub.arenadata.io/adcm/base -ADCMTEST_IMAGE ?= hub.arenadata.io/adcm/test -ADCMBASE_TAG ?= 20220929145118 APP_IMAGE ?= hub.adsw.io/adcm/adcm APP_TAG ?= $(subst /,_,$(BRANCH_NAME)) - SELENOID_HOST ?= 10.92.2.65 SELENOID_PORT ?= 4444 - -# Default target .PHONY: help -help: ## Shows that help +help: @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' -clean: ## Cleanup. Just a cleanup. - @docker run -i --rm -v $(CURDIR):/code -w /code busybox:latest /bin/sh -c "rm -rf /code/web/node_modules/ /code/web/package-lock.json /code/wwwroot /code/.version /code/go/bin /code/go/pkg /code/go/src/github.com" - -################################################## -# B U I L D -################################################## - -describe: ## Create .version file with output of describe - ./gues_version.sh +describe: + @echo '{"version": "$(shell date '+%Y.%m.%d.%H')","commit_id": "$(shell git log --pretty=format:'%h' -n 1)"}' > config.json + cp config.json web/src/assets/config.json -buildss: ## Build status server - @docker run -i --rm -v $(CURDIR)/go:/code -w /code golang:1.15-alpine3.13 sh -c "apk --update add make git && make && rm -f /code/adcm/go.sum" +buildss: + @docker run -i --rm -v $(CURDIR)/go:/code -w /code golang sh -c "make" -buildjs: ## Build client side js/html/css in directory wwwroot +buildjs: @docker run -i --rm -v $(CURDIR)/wwwroot:/wwwroot -v $(CURDIR)/web:/code -w /code node:16-alpine ./build.sh -build: describe buildss buildjs ## Build final docker image and all depended targets except baseimage. - @docker pull $(ADCMBASE_IMAGE):$(ADCMBASE_TAG) - @docker build --no-cache=true \ - -f assemble/app/Dockerfile \ - -t $(APP_IMAGE):$(APP_TAG) \ - --build-arg ADCMBASE_IMAGE=$(ADCMBASE_IMAGE) --build-arg ADCMBASE_TAG=$(ADCMBASE_TAG) \ - . +build_base: + @docker build . -t $(APP_IMAGE):$(APP_TAG) -################################################## -# T E S T S -################################################## +build: describe buildss buildjs build_base -testpyreqs: ## Install test prereqs into user's pip target dir - pip install --user -r requirements-test.txt +unittests: build_base + docker run -e DJANGO_SETTINGS_MODULE=adcm.settings -i --rm -v $(CURDIR)/data:/adcm/data $(APP_IMAGE):$(APP_TAG) \ + sh -c "pip install --no-cache -r /adcm/requirements.txt && /adcm/python/manage.py test /adcm/python -v 2" -test_image: - docker pull $(ADCMBASE_IMAGE):$(ADCMBASE_TAG) - -unittests: test_image ## Run unittests - docker run -e DJANGO_SETTINGS_MODULE=adcm.settings -i --rm -v $(CURDIR)/python:/adcm/python -v $(CURDIR)/data:/adcm/data -v $(CURDIR)/requirements.txt:/adcm/requirements.txt -w /adcm/ $(ADCMBASE_IMAGE):$(ADCMBASE_TAG) /venv.sh reqs_and_run default /adcm/requirements.txt python python/manage.py test python -v 2 - -pytest: ## Run functional tests - docker pull hub.adsw.io/library/functest:3.8.6.slim.buster-x64 +pytest: + docker pull hub.adsw.io/library/functest:3.10.6.slim.buster-x64 docker run -i --rm --shm-size=4g -v /var/run/docker.sock:/var/run/docker.sock --network=host \ -v $(CURDIR)/:/adcm -w /adcm/ \ -e BUILD_TAG=${BUILD_TAG} -e ADCMPATH=/adcm/ -e PYTHONPATH=${PYTHONPATH}:python/ \ - -e SELENOID_HOST="${SELENOID_HOST}" -e SELENOID_PORT="${SELENOID_PORT}" \ - hub.adsw.io/library/functest:3.8.6.slim.buster-x64 /bin/sh -e \ - ./pytest.sh -m "not full and not extra_rbac and not ldap" \ - --adcm-image='hub.adsw.io/adcm/adcm:$(subst /,_,$(BRANCH_NAME))' + -e SELENOID_HOST="${SELENOID_HOST}" -e SELENOID_PORT="${SELENOID_PORT}" -e ALLURE_TESTPLAN_PATH="${ALLURE_TESTPLAN_PATH}" \ + hub.adsw.io/library/functest:3.10.6.slim.buster-x64 /bin/sh -e \ + ./pytest.sh ${PYTEST_MARK_KEY} ${PYTEST_MARK_VALUE} ${PYTEST_EXPRESSION_KEY} ${PYTEST_EXPRESSION_VALUE} \ + --adcm-image="hub.adsw.io/adcm/adcm:$(subst /,_,$(BRANCH_NAME))" \ -pytest_release: ## Run functional tests on release - docker pull hub.adsw.io/library/functest:3.8.6.slim.buster.firefox-x64 +pytest_release: + docker pull hub.adsw.io/library/functest:3.10.6.slim.buster.firefox-x64 docker run -i --rm --shm-size=4g -v /var/run/docker.sock:/var/run/docker.sock --network=host \ -v $(CURDIR)/:/adcm -v ${LDAP_CONF_FILE}:${LDAP_CONF_FILE} -w /adcm/ \ -e BUILD_TAG=${BUILD_TAG} -e ADCMPATH=/adcm/ -e PYTHONPATH=${PYTHONPATH}:python/ \ - -e SELENOID_HOST="${SELENOID_HOST}" -e SELENOID_PORT="${SELENOID_PORT}" \ - hub.adsw.io/library/functest:3.8.6.slim.buster.firefox-x64 /bin/sh -e \ - ./pytest.sh --adcm-image='hub.adsw.io/adcm/adcm:$(subst /,_,$(BRANCH_NAME))' \ - --ldap-conf ${LDAP_CONF_FILE} + -e SELENOID_HOST="${SELENOID_HOST}" -e SELENOID_PORT="${SELENOID_PORT}" -e ALLURE_TESTPLAN_PATH="${ALLURE_TESTPLAN_PATH}" \ + hub.adsw.io/library/functest:3.10.6.slim.buster.firefox-x64 /bin/sh -e \ + ./pytest.sh --adcm-image="hub.adsw.io/adcm/adcm:$(subst /,_,$(BRANCH_NAME))" --ldap-conf ${LDAP_CONF_FILE} \ + ${PYTEST_MARK_KEY} ${PYTEST_MARK_VALUE} ${PYTEST_EXPRESSION_KEY} ${PYTEST_EXPRESSION_VALUE} + -ng_tests: ## Run Angular tests +ng_tests: docker pull hub.adsw.io/library/functest:3.8.6.slim.buster_node16-x64 docker run -i --rm -v $(CURDIR)/:/adcm -w /adcm/web hub.adsw.io/library/functest:3.8.6.slim.buster_node16-x64 ./ng_test.sh -linters: test_image ## Run linters - docker run -i --rm -e PYTHONPATH="/source/tests" -v $(CURDIR)/:/source -w /source $(ADCMTEST_IMAGE):$(ADCMBASE_TAG) \ - /bin/sh -eol pipefail -c "/linters.sh shellcheck && \ - /venv.sh run default pip install -U -r requirements.txt -r requirements-test.txt && \ - /venv.sh run default pylint --rcfile pyproject.toml --recursive y python && \ - /linters.sh -b ./tests -f ../tests pylint && \ - /linters.sh -f ./tests black && \ - /linters.sh -f ./tests/functional flake8_pytest_style && \ - /linters.sh -f ./tests/ui_tests flake8_pytest_style" - -npm_check: ## Run npm-check +npm_check: docker run -i --rm -v $(CURDIR)/wwwroot:/wwwroot -v $(CURDIR)/web:/code -w /code node:16-alpine ./npm_check.sh -################################################## -# U T I L S -################################################## - -base_shell: ## Just mount a dir to base image and run bash on it over docker run - docker run -e DJANGO_SETTINGS_MODULE=adcm.settings -it --rm -v $(CURDIR)/python:/adcm/python -v $(CURDIR)/data:/adcm/data -w /adcm/ $(ADCMBASE_IMAGE):$(ADCMBASE_TAG) /bin/bash -l +pretty: + black license_checker.py python tests + autoflake -r -i --remove-all-unused-imports --exclude apps.py,python/ansible/plugins,python/init_db.py,python/task_runner.py,python/backupdb.py,python/job_runner.py,python/drf_docs.py license_checker.py python tests + isort license_checker.py python tests + python license_checker.py --fix --folders python go diff --git a/assemble/app/Dockerfile b/assemble/app/Dockerfile deleted file mode 100644 index d660eb9b3e..0000000000 --- a/assemble/app/Dockerfile +++ /dev/null @@ -1,21 +0,0 @@ -ARG ADCMBASE_IMAGE -ARG ADCMBASE_TAG -FROM $ADCMBASE_IMAGE:$ADCMBASE_TAG - -COPY . /adcm/ -COPY assemble/app/build_venv.sh / - -RUN cp -r /adcm/os/* / && rm -rf /adcm/os; /build_venv.sh && rm -f /build_venv.sh && rm -rf /adcm/python/ansible && rmdir /var/log/nginx; - -RUN /venv.sh reqs default /adcm/requirements.txt -RUN /venv.sh reqs 2.9 /adcm/requirements.txt - - -# Secret_key is mandatory for build_static procedure, -# but should not be hardcoded in the image. -# It will be generated on first start. -RUN /venv.sh run default /adcm/web/build_static.sh && rm -f /adcm/web/build_static.sh - -EXPOSE 8000 - -CMD ["/etc/startup.sh"] diff --git a/assemble/app/build_venv.sh b/assemble/app/build_venv.sh deleted file mode 100755 index 4d95297c3e..0000000000 --- a/assemble/app/build_venv.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/usr/bin/env bash - -for d in $(find /adcm/venv -mindepth 1 -maxdepth 1 -type d); do - cp -r /adcm/python/ansible/* "${d}/lib/python3.10/site-packages/ansible/" -done; diff --git a/assemble/base/requirements-base.txt b/assemble/base/requirements-base.txt deleted file mode 100644 index 8c57b1139b..0000000000 --- a/assemble/base/requirements-base.txt +++ /dev/null @@ -1,41 +0,0 @@ -coreapi -django<4.0 -django-cors-headers -django-filter -django-guardian -django-rest-swagger -djangorestframework -drf-flex-fields==0.9.1 -drf-extensions -social-auth-app-django -git+https://github.com/arenadata/django-generate-secret-key.git -virtualenv -# TODO jinja2 and markupsafe should be unpinned after https://arenadata.atlassian.net/browse/ADCM-2089 fixup -Jinja2==2.11.3 -jsonschema -MarkupSafe==1.1.1 -markdown -mitogen -pyyaml -ruyaml -toml -uwsgi -version_utils -ruyaml -yspec -# Custom bundle libs -apache-libcloud -jmespath -lxml -# ansible==2.8.8 -git+https://github.com/arenadata/ansible.git@v2.8.8-p5 -pycryptodome -# YCC bundle libs (yandexcloud requirements) -cryptography>=2.8 -grpcio>=1.38.1 -googleapis-common-protos>=1.53.0 -pyjwt>=1.7.1 -requests>=2.22.0 -six>=1.14.0 ---extra-index-url https://ci.arenadata.io/artifactory/api/pypi/python-packages/simple -adwp-events==0.1.8 diff --git a/assemble/cloud/Dockerfile b/assemble/cloud/Dockerfile deleted file mode 100644 index 296b036e96..0000000000 --- a/assemble/cloud/Dockerfile +++ /dev/null @@ -1,9 +0,0 @@ -ARG ADCM_IMAGE -ARG ADCM_TAG -FROM $ADCM_IMAGE:$ADCM_TAG - -RUN apk add git && pip install adcm-client - -EXPOSE 8000 - -CMD ["/etc/startup.sh"] diff --git a/conf/adcm/config.yaml b/conf/adcm/config.yaml index 245257294b..0858c539ac 100644 --- a/conf/adcm/config.yaml +++ b/conf/adcm/config.yaml @@ -2,7 +2,7 @@ type: adcm name: ADCM - version: 2.2 + version: 2.3 actions: run_ldap_sync: @@ -52,8 +52,6 @@ - name: "yandex_oauth" display_name: "Yandex Auth" type: "group" - ui_options: - invisible: true subs: - name: "client_id" type: string @@ -63,6 +61,8 @@ required: false ui_options: no_confirm: true + ui_options: + invisible: true - name: "job_log" display_name: "Job Log" type: "group" diff --git a/conf/adcm/python_scripts/run_ldap_sync.py b/conf/adcm/python_scripts/run_ldap_sync.py index 345f514666..6347c13c20 100644 --- a/conf/adcm/python_scripts/run_ldap_sync.py +++ b/conf/adcm/python_scripts/run_ldap_sync.py @@ -55,13 +55,26 @@ def _bind(self) -> ldap.ldapobject.LDAPObject: ldap.set_option(ldap.OPT_REFERRALS, 0) conn = ldap.initialize(self.settings["SERVER_URI"]) conn.protocol_version = ldap.VERSION3 - configure_tls(is_tls(self.settings["SERVER_URI"]), os.environ.get(CERT_ENV_KEY, ""), conn) + configure_tls( + is_tls(self.settings["SERVER_URI"]), os.environ.get(CERT_ENV_KEY, ""), conn + ) conn.simple_bind_s(self.settings["BIND_DN"], self.settings["BIND_PASSWORD"]) except ldap.LDAPError as e: - sys.stdout.write(f"Can't connect to {self.settings['SERVER_URI']} with user: {self.settings['BIND_DN']}. Error: {e}\n") + sys.stdout.write(f"Can't connect to {self.settings['SERVER_URI']} " + f"with user: {self.settings['BIND_DN']}. Error: {e}\n") raise return conn + @staticmethod + def _deactivate_extra_users(ldap_usernames: set): + django_usernames = set(User.objects.filter( + type=OriginType.LDAP, is_active=True).values_list("username", flat=True + )) + for username in django_usernames - ldap_usernames: + user = User.objects.get(username__iexact=username) + sys.stdout.write(f"Deactivate user and his session: {user}\n") + user.delete() + def unbind(self) -> None: if self._conn is not None: self.conn.unbind_s() @@ -99,10 +112,11 @@ def sync_groups(self) -> list: def sync_users(self, ldap_groups: list) -> None: """Synchronize LDAP users with user model and delete users which is not found in LDAP""" if not ldap_groups and self._group_search_configured: - sys.stdout.write(f"No groups found. Aborting sync users\n") + sys.stdout.write("No groups found. Aborting sync users\n") + self._deactivate_extra_users(set()) return group_filter = "" - for group_dn, group_attrs in ldap_groups: + for group_dn, _ in ldap_groups: group_filter += f"(memberOf={group_dn})" if group_filter: group_filter = f"(|{group_filter})" @@ -111,13 +125,13 @@ def sync_users(self, ldap_groups: list) -> None: f"{self.settings['USER_FILTER']}" \ f"{group_filter})" ldap_users = self.settings["USER_SEARCH"].execute(self.conn, {"user": "*"}, True) - self._sync_ldap_users(ldap_users) + self._sync_ldap_users(ldap_users, ldap_groups) sys.stdout.write("Users were synchronized\n") def _sync_ldap_groups(self, ldap_groups: list) -> None: new_groups = set() error_names = [] - for cname, ldap_attributes in ldap_groups: + for _, ldap_attributes in ldap_groups: try: name = ldap_attributes[self.settings["GROUP_TYPE"].name_attr][0] except KeyError: @@ -131,12 +145,14 @@ def _sync_ldap_groups(self, ldap_groups: list) -> None: new_groups.add(name) except (IntegrityError, DataError) as e: error_names.append(name) - sys.stdout.write("Error creating group %s: %s\n" % (name, e)) + sys.stdout.write(f"Error creating group {name}: {e}\n") continue else: if created: - sys.stdout.write("Create new group: %s\n" % name) - django_groups = set(Group.objects.filter(type=OriginType.LDAP).values_list("display_name", flat=True)) + sys.stdout.write(f"Create new group: {name}\n") + django_groups = set( + Group.objects.filter(type=OriginType.LDAP).values_list("display_name", flat=True) + ) for groupname in django_groups - new_groups: group = Group.objects.get(name__iexact=f"{groupname} [ldap]") sys.stdout.write(f"Delete this group: {group}\n") @@ -145,7 +161,8 @@ def _sync_ldap_groups(self, ldap_groups: list) -> None: msg = f"{msg} Couldn't synchronize groups: {error_names}" if error_names else f"{msg}" logger.debug(msg) - def _sync_ldap_users(self, ldap_users: list) -> None: + def _sync_ldap_users(self, ldap_users: list, ldap_groups: list) -> None: + ldap_group_names = [group[0].split(",")[0][3:] for group in ldap_groups] ldap_usernames = set() error_names = [] for cname, ldap_attributes in ldap_users: @@ -167,7 +184,7 @@ def _sync_ldap_users(self, ldap_users: list) -> None: user, created = User.objects.get_or_create(**kwargs) except (IntegrityError, DataError) as e: error_names.append(username) - sys.stdout.write("Error creating user %s: %s\n" % (username, e)) + sys.stdout.write(f"Error creating user {username}: {e}\n") continue else: updated = False @@ -175,7 +192,7 @@ def _sync_ldap_users(self, ldap_users: list) -> None: if not hex(int(ldap_attributes["useraccountcontrol"][0])).endswith("2"): user.is_active = True if created: - sys.stdout.write("Create user: %s\n" % username) + sys.stdout.write(f"Create user: {username}\n") user.set_unusable_password() else: for name, attr in defaults.items(): @@ -184,7 +201,7 @@ def _sync_ldap_users(self, ldap_users: list) -> None: setattr(user, name, attr) updated = True if updated: - sys.stdout.write("Updated user: %s\n" % username) + sys.stdout.write(f"Updated user: {username}\n") user.save() ldap_usernames.add(username) @@ -194,19 +211,16 @@ def _sync_ldap_users(self, ldap_users: list) -> None: else: for group in ldap_attributes.get("memberof", []): name = group.split(",")[0][3:] + if not name.lower() in ldap_group_names: + continue try: group = Group.objects.get(name=f"{name} [ldap]", built_in=False, type=OriginType.LDAP) group.user_set.add(user) sys.stdout.write(f"Add user {user} to group {group}\n") - except (IntegrityError, DataError) as e: - sys.stdout.write("Error getting group %s: %s\n" % (name, e)) - - django_usernames = set(User.objects.filter(type=OriginType.LDAP, is_active=True).values_list("username", flat=True)) - for username in django_usernames - ldap_usernames: - user = User.objects.get(username__iexact=username) - sys.stdout.write(f"Deactivate user and his session: {user}\n") - user.delete() + except (IntegrityError, DataError, Group.DoesNotExist) as e: + sys.stdout.write(f"Error getting group {name}: {e}\n") + self._deactivate_extra_users(ldap_usernames) msg = "Sync of users ended successfully." msg = f"{msg} Couldn't synchronize users: {error_names}" if error_names else f"{msg}" logger.debug(msg) diff --git a/go/Makefile b/go/Makefile index a25fa5e89c..817caf5bdc 100644 --- a/go/Makefile +++ b/go/Makefile @@ -13,4 +13,4 @@ lint: cd $(ADCM); golangci-lint run build: - cd $(ADCM); go build -o ../bin/runstatus runstatus.go + cd $(ADCM); CGO_ENABLED=0 go mod tidy && go build -o ../bin/runstatus runstatus.go diff --git a/go/adcm/go.mod b/go/adcm/go.mod index 04bdf7a6f1..f12571e347 100644 --- a/go/adcm/go.mod +++ b/go/adcm/go.mod @@ -1,3 +1,15 @@ +// 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. + module adcm require ( diff --git a/go/adcm/status/api.go b/go/adcm/status/api.go index 01b944706b..9e1032a86f 100644 --- a/go/adcm/status/api.go +++ b/go/adcm/status/api.go @@ -84,7 +84,7 @@ func (api *AdcmApi) getToken() (string, bool) { func (api *AdcmApi) checkAuth(token string) bool { client := api.getClient() - req, _ := http.NewRequest("GET", api.Url+"/stack/", nil) + req, _ := http.NewRequest("GET", api.Url+"/rbac/me/", nil) req.Header.Add("Authorization", "Token "+token) //logg.D.f("checkAuth: client %+v, request %+v", client, req) resp, err := client.Do(req) diff --git a/go/adcm/status/auth.go b/go/adcm/status/auth.go index 4f79da6b78..74c995899c 100644 --- a/go/adcm/status/auth.go +++ b/go/adcm/status/auth.go @@ -17,7 +17,7 @@ import ( "time" ) -func checkADCMToken(hub Hub, token string) bool { +func checkADCMUserToken(hub Hub, token string) bool { checkADCMAuth := func(token string) bool { if hub.AdcmApi.checkAuth(token) { hub.Secrets.adcmTokens[token] = time.Now().Add(hub.Secrets.tokenTimeOut) @@ -32,20 +32,12 @@ func checkADCMToken(hub Hub, token string) bool { return checkADCMAuth(token) } if time.Now().Before(val) { - //logg.D.f("checkADCMToken: get token from cache") return true } else { return checkADCMAuth(token) } } -func checkToken(hub Hub, token string) bool { - if token != hub.Secrets.Token && !checkADCMToken(hub, token) { - return false - } - return true -} - func djangoAuth(r *http.Request, hub Hub) bool { sessionId, err := r.Cookie("sessionid") if err != nil { @@ -55,34 +47,8 @@ func djangoAuth(r *http.Request, hub Hub) bool { return hub.AdcmApi.checkSessionAuth(sessionId.Value) } -func tokenAuth(w http.ResponseWriter, r *http.Request, hub Hub) bool { - if djangoAuth(r, hub) { - return true - } - h, ok := r.Header["Authorization"] - if !ok { - ErrOut4(w, r, "AUTH_ERROR", "no \"Authorization\" header") - return false - } - a := strings.Split(h[0], " ") - if len(a) < 2 { - ErrOut4(w, r, "AUTH_ERROR", "no token") - return false - } - if strings.Title(a[0]) != "Token" { - ErrOut4(w, r, "AUTH_ERROR", "no token") - return false - } - if !checkToken(hub, a[1]) { - ErrOut4(w, r, "AUTH_ERROR", "invalid token") - return false - } - return true -} - func wsTokenAuth(w http.ResponseWriter, r *http.Request, hub Hub) bool { h, ok := r.Header["Sec-Websocket-Protocol"] - //logg.D.f("wsTokenAuth: headers: %+v", r.Header) if !ok { ErrOut4(w, r, "AUTH_ERROR", "no \"Sec-WebSocket-Protocol\" header") return false @@ -95,9 +61,40 @@ func wsTokenAuth(w http.ResponseWriter, r *http.Request, hub Hub) bool { for _, i := range strings.Split(h[0], ",") { token = strings.Trim(i, " ") } - if !checkToken(hub, token) { + if !checkADCMUserToken(hub, token) { ErrOut4(w, r, "AUTH_ERROR", "invalid token") return false } return true } + +func getAuthorizationToken(r *http.Request) string { + h, ok := r.Header["Authorization"] + if !ok { + return "" + } + a := strings.Split(h[0], " ") + if len(a) < 2 { + return "" + } + if strings.Title(a[0]) != "Token" { + return "" + } + return a[1] +} + +// access control + +type authCheckerFunc func(*http.Request, Hub) bool + +func isADCM(r *http.Request, hub Hub) bool { + return getAuthorizationToken(r) == hub.Secrets.ADCMInternalToken +} + +func isStatusChecker(r *http.Request, hub Hub) bool { + return getAuthorizationToken(r) == hub.Secrets.Token +} + +func isADCMUser(r *http.Request, hub Hub) bool { + return djangoAuth(r, hub) || checkADCMUserToken(hub, getAuthorizationToken(r)) +} diff --git a/go/adcm/status/handlers.go b/go/adcm/status/handlers.go index a195debe9e..29e00ecc81 100644 --- a/go/adcm/status/handlers.go +++ b/go/adcm/status/handlers.go @@ -299,78 +299,6 @@ func showHost(h Hub, w http.ResponseWriter, r *http.Request) { jsonOut(w, r, val) } -// listHost - GET method for show list Host entities -// Response format: [{"id": 1, "maintenance_mode": false}, ...] -func listHost(h Hub, w http.ResponseWriter, r *http.Request) { - allow(w, "GET, POST") - result := h.HostStorage.list() - jsonOut(w, r, result) -} - -// createHost - POST method for create Host entities -// Request format: [{"id": 1, "maintenance_mode": false}, ...] -func createHost(h Hub, w http.ResponseWriter, r *http.Request) { - allow(w, "GET, POST") - hosts := make([]Host, 0) - _, err := decodeBody(w, r, &hosts) - if err != nil { - ErrOut4(w, r, "JSON_ERROR", err.Error()) - return - } - ok := h.HostStorage.create(hosts) - code := http.StatusCreated - if !ok { - code = http.StatusBadRequest - } - logg.D.f("createHost: %+v", hosts) - jsonOut3(w, r, "", code) -} - -// retrieveHost - GET method for show Host entity -func retrieveHost(h Hub, w http.ResponseWriter, r *http.Request) { - allow(w, "GET, PUT") - hostId, ok := getPathId(w, r, "hostid") - if !ok { - return - } - _, ok = h.ServiceMap.getHostCluster(hostId) - if !ok { - ErrOut4(w, r, "HOST_NOT_FOUND", "unknown host") - return - } - value, ok := h.HostStorage.retrieve(hostId) - if !ok { - ErrOut4(w, r, "HOST_NOT_FOUND", "unknown host") - return - } - jsonOut(w, r, value) -} - -// updateHost - PUT method for update Host entity -func updateHost(h Hub, w http.ResponseWriter, r *http.Request) { - allow(w, "GET, PUT") - hostId, ok := getPathId(w, r, "hostid") - if !ok { - return - } - _, ok = h.ServiceMap.getHostCluster(hostId) - if !ok { - ErrOut4(w, r, "HOST_NOT_FOUND", "unknown host") - return - } - host := Host{} - _, err := decodeBody(w, r, &host) - if err != nil { - return - } - ok = h.HostStorage.update(hostId, host.MaintenanceMode) - code := http.StatusOK - if !ok { - code = http.StatusBadRequest - } - jsonOut3(w, r, "", code) -} - func setHostComp(h Hub, w http.ResponseWriter, r *http.Request) { allow(w, "GET, POST") hostId, ok1 := getPathId(w, r, "hostid") @@ -422,6 +350,24 @@ func postServiceMap(h Hub, w http.ResponseWriter, r *http.Request) { // h.ServiceStorage.pure() } +func postMMObjects(h Hub, w http.ResponseWriter, r *http.Request) { + allow(w, "POST") + h.MMObjects.mutex.Lock() + defer h.MMObjects.mutex.Unlock() + + var mmData MMObjectsData + if _, err := decodeBody(w, r, &mmData); err != nil { + ErrOut4(w, r, "JSON_ERROR", err.Error()) + return + } + h.MMObjects.data = mmData +} + +func getMMObjects(h Hub, w http.ResponseWriter, r *http.Request) { + allow(w, "GET") + jsonOut(w, r, h.MMObjects.data) +} + // Helpers func getGet(r *http.Request, key string) (string, bool) { diff --git a/go/adcm/status/host.go b/go/adcm/status/host.go deleted file mode 100644 index 334e365579..0000000000 --- a/go/adcm/status/host.go +++ /dev/null @@ -1,135 +0,0 @@ -package status - -type Host struct { - Id int `json:"id"` - MaintenanceMode bool `json:"maintenance_mode"` -} - -type dbHost map[int]Host - -type hostRequest struct { - command string - id int - maintenance_mode bool - hosts []Host -} - -type hostResponse struct { - ok bool - host Host - hosts []Host -} - -type HostStorage struct { - in chan hostRequest - out chan hostResponse - db dbHost - label string -} - -func newHostStorage(db dbHost, label string) *HostStorage { - return &HostStorage{ - in: make(chan hostRequest), - out: make(chan hostResponse), - db: db, - label: label, - } -} - -func (hs *HostStorage) run() { - logg.I.f("start storage %s server", hs.label) - for { - request := <-hs.in - logg.I.f("Storage %s command: %+v", hs.label, request.command) - switch request.command { - case "retrieve": - host, ok := hs.db.retrieve(request.id) - hs.out <- hostResponse{host: host, ok: ok} - case "update": - ok := hs.db.update(request.id, request.maintenance_mode) - hs.out <- hostResponse{ok: ok} - case "list": - hosts := hs.db.list() - hs.out <- hostResponse{hosts: hosts} - case "create": - ok := hs.db.create(request.hosts) - hs.out <- hostResponse{ok: ok} - default: - logg.E.f("Storage %s unknown command: %+v", hs.label, request) - - } - } -} - -func (hs *HostStorage) list() []Host { - request := hostRequest{command: "list"} - hs.in <- request - response := <-hs.out - return response.hosts -} - -func (hs *HostStorage) create(hosts []Host) bool { - request := hostRequest{command: "create", hosts: hosts} - hs.in <- request - response := <-hs.out - return response.ok -} - -func (hs *HostStorage) retrieve(id int) (Host, bool) { - request := hostRequest{command: "retrieve", id: id} - hs.in <- request - response := <-hs.out - return response.host, response.ok -} - -func (hs *HostStorage) update(id int, maintenance_mode bool) bool { - request := hostRequest{command: "update", id: id, maintenance_mode: maintenance_mode} - hs.in <- request - response := <-hs.out - return response.ok -} - -// list - return list Host entities -func (db dbHost) list() []Host { - result := make([]Host, 0) - for _, host := range db { - result = append(result, host) - } - return result -} - -// retrieve - return Host entity, if this exists in db, else return default entity -func (db dbHost) retrieve(id int) (Host, bool) { - value, ok := db[id] - if ok { - return value, true - } - return Host{}, false -} - -// update - update Host entity -func (db dbHost) update(id int, maintenance_mode bool) bool { - host, ok := db[id] - if !ok { - return ok - } - host.MaintenanceMode = maintenance_mode - db[id] = host - return ok -} - -// create - clear db and created Host entities -func (db dbHost) create(hosts []Host) bool { - db.clear() - for _, host := range hosts { - db[host.Id] = host - } - return true -} - -// clear - clear db with Hosts -func (db dbHost) clear() { - for key := range db { - delete(db, key) - } -} diff --git a/go/adcm/status/init.go b/go/adcm/status/init.go index 7cfeb76f22..d4c5f22dd9 100644 --- a/go/adcm/status/init.go +++ b/go/adcm/status/init.go @@ -28,12 +28,12 @@ const componentTimeout = 300 // seconds type Hub struct { HostStatusStorage *Storage HostComponentStorage *Storage - HostStorage *HostStorage ServiceMap *ServiceServer EventWS *wsHub StatusEvent *StatusEvent AdcmApi *AdcmApi Secrets *SecretConfig + MMObjects *MMObjects } func Start(secrets *SecretConfig, logFile string, logLevel string) { @@ -41,6 +41,8 @@ func Start(secrets *SecretConfig, logFile string, logLevel string) { initLog(logFile, logLevel) initSignal() + hub.MMObjects = newMMObjects() + hub.HostComponentStorage = newStorage(dbMap2{}, "HostComponent") go hub.HostComponentStorage.run() hub.HostComponentStorage.setTimeOut(componentTimeout) @@ -49,9 +51,6 @@ func Start(secrets *SecretConfig, logFile string, logLevel string) { go hub.HostStatusStorage.run() hub.HostStatusStorage.setTimeOut(componentTimeout) - hub.HostStorage = newHostStorage(dbHost{}, "Host") - go hub.HostStorage.run() - hub.ServiceMap = newServiceServer() go hub.ServiceMap.run() @@ -85,48 +84,57 @@ func startHTTP(httpPort string, hub Hub) { initWS(hub.EventWS, w, r) }) - router.GET("/api/v1/log/", authWrap(hub, showLogLevel)) - router.POST("/api/v1/log/", authWrap(hub, postLogLevel)) + router.GET("/api/v1/log/", authWrap(hub, showLogLevel, isADCM)) + router.POST("/api/v1/log/", authWrap(hub, postLogLevel, isADCM)) - router.POST("/api/v1/event/", authWrap(hub, postEvent)) + router.POST("/api/v1/event/", authWrap(hub, postEvent, isADCM)) - router.GET("/api/v1/all/", authWrap(hub, showAll)) + router.GET("/api/v1/all/", authWrap(hub, showAll, isADCM, isADCMUser)) - router.GET("/api/v1/host/", authWrap(hub, hostList)) - router.GET("/api/v1/host/:hostid/", authWrap(hub, showHost)) - router.POST("/api/v1/host/:hostid/", authWrap(hub, setHost)) + router.GET("/api/v1/host/", authWrap(hub, hostList, isADCM, isADCMUser)) + router.GET("/api/v1/host/:hostid/", authWrap(hub, showHost, isStatusChecker, isADCM, isADCMUser)) + router.POST("/api/v1/host/:hostid/", authWrap(hub, setHost, isStatusChecker, isADCM)) - router.GET("/api/v1/object/host/", authWrap(hub, listHost)) - router.POST("/api/v1/object/host/", authWrap(hub, createHost)) - router.GET("/api/v1/object/host/:hostid/", authWrap(hub, retrieveHost)) - router.PUT("/api/v1/object/host/:hostid/", authWrap(hub, updateHost)) + router.GET("/api/v1/object/mm/", authWrap(hub, getMMObjects, isADCM)) + router.POST("/api/v1/object/mm/", authWrap(hub, postMMObjects, isADCM)) - router.GET("/api/v1/host/:hostid/component/:compid/", authWrap(hub, showHostComp)) - router.POST("/api/v1/host/:hostid/component/:compid/", authWrap(hub, setHostComp)) + router.GET("/api/v1/host/:hostid/component/:compid/", authWrap(hub, showHostComp, isStatusChecker, isADCM, isADCMUser)) + router.POST("/api/v1/host/:hostid/component/:compid/", authWrap(hub, setHostComp, isStatusChecker, isADCM)) - router.GET("/api/v1/component/:compid/", authWrap(hub, showComp)) + router.GET("/api/v1/component/:compid/", authWrap(hub, showComp, isADCM, isADCMUser)) - router.GET("/api/v1/cluster/", authWrap(hub, clusterList)) - router.GET("/api/v1/cluster/:clusterid/", authWrap(hub, showCluster)) - router.GET("/api/v1/cluster/:clusterid/service/:serviceid/", authWrap(hub, showService)) + router.GET("/api/v1/cluster/", authWrap(hub, clusterList, isADCM, isADCMUser)) + router.GET("/api/v1/cluster/:clusterid/", authWrap(hub, showCluster, isADCM, isADCMUser)) + router.GET("/api/v1/cluster/:clusterid/service/:serviceid/", authWrap(hub, showService, isADCM, isADCMUser)) router.GET( "/api/v1/cluster/:clusterid/service/:serviceid/component/:compid/", - authWrap(hub, showComp), + authWrap(hub, showComp, isADCM, isADCMUser), ) - router.GET("/api/v1/servicemap/", authWrap(hub, showServiceMap)) - router.POST("/api/v1/servicemap/", authWrap(hub, postServiceMap)) - router.POST("/api/v1/servicemap/reload/", authWrap(hub, readConfig)) + router.GET("/api/v1/servicemap/", authWrap(hub, showServiceMap, isADCM)) + router.POST("/api/v1/servicemap/", authWrap(hub, postServiceMap, isADCM)) + router.POST("/api/v1/servicemap/reload/", authWrap(hub, readConfig, isADCM)) log.Fatal(http.ListenAndServe(httpPort, router)) } -func authWrap(hub Hub, f func(h Hub, w http.ResponseWriter, r *http.Request)) http.HandlerFunc { +func authWrap(hub Hub, f func(h Hub, w http.ResponseWriter, r *http.Request), authCheckers ...authCheckerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - if !tokenAuth(w, r, hub) { - return + allowed := false + + for _, checkFunc := range authCheckers { + checkResult := checkFunc(r, hub) + if checkResult { + allowed = true + break + } + } + + if !allowed { + ErrOut4(w, r, "AUTH_ERROR", "forbidden") + } else { + f(hub, w, r) } - f(hub, w, r) } } diff --git a/go/adcm/status/log.go b/go/adcm/status/log.go index 06c4a3d6c7..cead0cd8a1 100644 --- a/go/adcm/status/log.go +++ b/go/adcm/status/log.go @@ -17,7 +17,6 @@ import ( "log" "os" "sync" - "time" ) type logger struct { @@ -65,7 +64,7 @@ func (log *logger) getLogLevel() string { } func (log *logger) rotate() { - log.E.out.Rotate() + log.E.out.ReopenLogFile() } func (log *logger) set(level int) { @@ -104,7 +103,7 @@ func initLog(logFile string, level string) { if logFile == "" { out = newStdoutWriter() } else { - out = newRotateWriter(logFile) + out = newFileWriter(logFile) } logg.level = &logLevel logg.D = newLog(out, &logLevel, DEBUG, "[DEBUG] ") @@ -124,7 +123,7 @@ func newLog(out logWriter, current *int, level int, tag string) logWrapper { type logWriter interface { Write(output []byte) (int, error) - Rotate() + ReopenLogFile() } type stdoutWriter struct { @@ -139,54 +138,35 @@ func (w *stdoutWriter) Write(output []byte) (int, error) { return w.fp.Write(output) } -func (w *stdoutWriter) Rotate() { +func (w *stdoutWriter) ReopenLogFile() { } -// Rotatable Writer +// File Writer -type rotateWriter struct { +type fileWriter struct { lock sync.Mutex filename string fp *os.File } -func newRotateWriter(filename string) *rotateWriter { - w := rotateWriter{filename: filename} - w.Rotate() +func newFileWriter(filename string) *fileWriter { + w := fileWriter{filename: filename} + w.ReopenLogFile() return &w } -func (w *rotateWriter) Write(output []byte) (int, error) { +func (w *fileWriter) Write(output []byte) (int, error) { w.lock.Lock() defer w.lock.Unlock() return w.fp.Write(output) } -func (w *rotateWriter) Rotate() { +func (w *fileWriter) ReopenLogFile() { var err error w.lock.Lock() defer w.lock.Unlock() - // Close existing file if open - if w.fp != nil { - err = w.fp.Close() - w.fp = nil - if err != nil { - log.Fatalf("error closing log file %s: %v", w.filename, err) - } - } - // Rename destination file if it already exists - _, err = os.Stat(w.filename) - if err == nil { - newLog := w.filename + "." + time.Now().Format(time.RFC3339) - err = os.Rename(w.filename, newLog) - if err != nil { - log.Fatalf("error renaming log file %s to %s: %v", w.filename, newLog, err) - } - } - - // Create a file. - w.fp, err = os.Create(w.filename) + w.fp, err = os.OpenFile(w.filename, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) if err != nil { log.Fatalf("error opening log file %s: %v", w.filename, err) } diff --git a/go/adcm/status/secretconfig.go b/go/adcm/status/secretconfig.go index 17e401a30a..21925cb39b 100644 --- a/go/adcm/status/secretconfig.go +++ b/go/adcm/status/secretconfig.go @@ -23,9 +23,10 @@ type SecretConfig struct { User string `json:"user"` Password string `json:"password"` } `json:"adcmuser"` - Token string `json:"token"` - adcmTokens map[string]time.Time - tokenTimeOut time.Duration + Token string `json:"token"` + ADCMInternalToken string `json:"adcm_internal_token"` + adcmTokens map[string]time.Time + tokenTimeOut time.Duration } func ReadSecret(filename *string) *SecretConfig { diff --git a/go/adcm/status/service_map.go b/go/adcm/status/service_map.go index 5a76289881..91ab165627 100644 --- a/go/adcm/status/service_map.go +++ b/go/adcm/status/service_map.go @@ -141,6 +141,20 @@ func (s *ServiceServer) getMap() ServiceMaps { return resp.smap } +func (s *ServiceServer) getServiceIDByComponentID(compId int) (resultServiceID int, found bool) { + resultServiceID = 0 + found = false + for hostIDCompIDKey, clusterService := range s.smap.HostService { + compIdKey, _ := strconv.Atoi(strings.Split(hostIDCompIDKey, ".")[1]) + if compIdKey == compId { + resultServiceID = clusterService.Service + found = true + break + } + } + return +} + func (s *ServiceServer) getHosts(clusterId int) ([]int, bool) { s.in <- ssReq{command: "gethosts", cluster: clusterId} resp := <-s.out diff --git a/go/adcm/status/status.go b/go/adcm/status/status.go index f02757477e..84c4f73107 100644 --- a/go/adcm/status/status.go +++ b/go/adcm/status/status.go @@ -80,13 +80,12 @@ func newEventMsg(status int, objType string, objId int) eventMsg { func getServiceStatus(h Hub, cluster int, service int) (Status, []hostCompStatus) { hc := []hostCompStatus{} hostComp, _ := h.ServiceMap.getServiceHC(cluster, service) - servStatus := Status{} + servStatus := Status{Status: 0} for _, key := range hostComp { spl := strings.Split(key, ".") hostId, _ := strconv.Atoi(spl[0]) compId, _ := strconv.Atoi(spl[1]) - host, ok := h.HostStorage.retrieve(hostId) - if ok && host.MaintenanceMode { + if h.MMObjects.IsHostInMM(hostId) || h.MMObjects.IsComponentInMM(compId) { continue } status, ok := h.HostComponentStorage.get(hostId, compId) @@ -107,10 +106,14 @@ func getComponentStatus(h Hub, compId int) (Status, map[int]Status) { if len(hostList) == 0 { return Status{Status: 32}, hosts } + status := 0 + if h.MMObjects.IsComponentInMM(compId) { + return Status{Status: status}, hosts + } + for _, hostId := range hostList { - host, ok := h.HostStorage.retrieve(hostId) - if ok && host.MaintenanceMode { + if h.MMObjects.IsHostInMM(hostId) { continue } hostStatus, ok := h.HostComponentStorage.get(hostId, compId) @@ -133,8 +136,7 @@ func getClusterHostStatus(h Hub, clusterId int) (int, map[int]Status) { } result := 0 for _, hostId := range hostList { - host, ok := h.HostStorage.retrieve(hostId) - if ok && host.MaintenanceMode { + if h.MMObjects.IsHostInMM(hostId) { continue } status, ok := h.HostStatusStorage.get(ALL, hostId) diff --git a/go/adcm/status/status_event.go b/go/adcm/status/status_event.go index 3fb4e61b41..e70efcf302 100644 --- a/go/adcm/status/status_event.go +++ b/go/adcm/status/status_event.go @@ -1,3 +1,15 @@ +// 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. + package status import ( diff --git a/go/adcm/status/storage.go b/go/adcm/status/storage.go index a0cbfd37a8..ff37acd4c3 100644 --- a/go/adcm/status/storage.go +++ b/go/adcm/status/storage.go @@ -11,7 +11,10 @@ // limitations under the License. package status -import "time" +import ( + "sync" + "time" +) type Status struct { Status int `json:"status"` @@ -63,6 +66,46 @@ type Storage struct { label string } +// maintenance mode objects + +type MMObjectsData struct { + Services []int `json:"services"` + Components []int `json:"components"` + Hosts []int `json:"hosts"` +} + +type MMObjects struct { + data MMObjectsData + mutex sync.Mutex +} + +func newMMObjects() *MMObjects { + return &MMObjects{ + data: MMObjectsData{}, + } +} + +func (mm *MMObjects) IsHostInMM(hostID int) bool { + return intSliceContains(mm.data.Hosts, hostID) +} + +func (mm *MMObjects) IsServiceInMM(serviceID int) bool { + return intSliceContains(mm.data.Services, serviceID) +} + +func (mm *MMObjects) IsComponentInMM(componentID int) bool { + return intSliceContains(mm.data.Components, componentID) +} + +func intSliceContains(a []int, x int) bool { + for _, n := range a { + if x == n { + return true + } + } + return false +} + // Server func newStorage(db dbStorage, label string) *Storage { diff --git a/license_checker.py b/license_checker.py new file mode 100644 index 0000000000..e3f5f59fc0 --- /dev/null +++ b/license_checker.py @@ -0,0 +1,133 @@ +# 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 argparse +import sys +from pathlib import Path +from typing import TextIO + +APACHE_LICENCE_PY = [ + '# Licensed under the Apache License, Version 2.0 (the "License");\n', + '# you may not use this file except in compliance with the License.\n', + '# You may obtain a copy of the License at\n', + '#\n', + '# http://www.apache.org/licenses/LICENSE-2.0\n', + '#\n', + '# Unless required by applicable law or agreed to in writing, software\n', + '# distributed under the License is distributed on an "AS IS" BASIS,\n', + '# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n', + '# See the License for the specific language governing permissions and\n', + '# limitations under the License.\n', + '\n', +] + +APACHE_LICENCE_GO = [ + '// Licensed under the Apache License, Version 2.0 (the "License");\n', + '// you may not use this file except in compliance with the License.\n', + '// You may obtain a copy of the License at\n', + '//\n', + '// http://www.apache.org/licenses/LICENSE-2.0\n', + '//\n', + '// Unless required by applicable law or agreed to in writing, software\n', + '// distributed under the License is distributed on an "AS IS" BASIS,\n', + '// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n', + '// See the License for the specific language governing permissions and\n', + '// limitations under the License.\n', + '\n', +] + + +def check_licence(lines: list, lic: list[str]) -> bool: + """Return False in case of empty licence""" + + if len(lines) < 10: + return False + + if (lines[0] == lic[0] and lines[10] == lic[10]) or (lines[1] == lic[0] and lines[11] == lic[10]): + return True + + return False + + +def check_and_fix_files(fixed: int, skipped: int, fix: bool, root: Path | None = None) -> tuple[int, int]: + for path in root.iterdir(): + lic = None + + if path.is_dir(): + if path.name.startswith("__"): + continue + + fixed, skipped = check_and_fix_files(fixed, skipped, fix, path) + + if path.suffix == ".py": + lic = APACHE_LICENCE_PY + elif path.suffix == ".go" or path.name == "go.mod": + lic = APACHE_LICENCE_GO + + if not lic: + continue + + with open(path, "r+", encoding="utf-8") as f: + lines = f.readlines() + if not check_licence(lines, lic): + sys.stdout.write(f"{path} has no license\n") + + if fix: + update_files(f, lines, lic) + fixed += 1 + else: + skipped += 1 + + return fixed, skipped + + +def update_files(f: TextIO, lines: list, lic: list[str]): + lines.insert(0, "".join(lic)) + f.seek(0) + f.writelines(lines) + + +def main(): + parser = argparse.ArgumentParser(description="Checker for licence existing and fix it if need") + parser.add_argument( + "--fix", + nargs="?", + const=True, + default=False, + help="Flag to fix absent license in file (default will only find it)", + ) + parser.add_argument("--folders", nargs="+", help="Folders to check") + + args = parser.parse_args() + number_of_fixed = number_of_skipped = 0 + + for folder in args.folders: + number_of_fixed, number_of_skipped = check_and_fix_files( + number_of_fixed, number_of_skipped, args.fix, Path(folder) + ) + + if number_of_fixed == number_of_skipped == 0: + sys.stdout.write("Licence is present in all python and go files \n") + sys.exit(0) + + sys.stdout.write( + f"Updating licence skipped in {number_of_skipped} files." f" Licence was updated in {number_of_fixed} files \n" + ) + + if args.fix: + sys.exit(0) + else: + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/os/etc/adcmenv b/os/etc/adcmenv index f080475223..8d7f5bb2bd 100644 --- a/os/etc/adcmenv +++ b/os/etc/adcmenv @@ -22,21 +22,21 @@ adcmsecretfile="${adcmuserconf}/secret_key.txt" initreadyfile="/run/init.done" wsgisocketfile="/run/adcm.sock" -function waitforinit() { +waitforinit() { while [ ! -f "${initreadyfile}" ]; do echo "Waiting for init" sleep 1 done; } -function waitforwsgi() { +waitforwsgi() { while [ ! -S "${wsgisocketfile}" ]; do echo "Waiting for wsgi" sleep 1 done; } -function cleanupwaitstatus() { +cleanupwaitstatus() { rm -f "${initreadyfile}" rm -f "${wsgisocketfile}" } diff --git a/os/etc/crontabs/root b/os/etc/crontabs/root index a503154162..584762e8bc 100644 --- a/os/etc/crontabs/root +++ b/os/etc/crontabs/root @@ -1,22 +1,6 @@ -# 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. - -# do daily/weekly/monthly maintenance -# min hour day month weekday command -*/15 * * * * run-parts /etc/periodic/15min -0 * * * * run-parts /etc/periodic/hourly -0 2 * * * run-parts /etc/periodic/daily -0 3 * * 6 run-parts /etc/periodic/weekly -0 5 1 * * run-parts /etc/periodic/monthly -0 8 */1 * * python /adcm/python/manage.py logrotate --target all +# DO NOT EDIT THIS FILE - edit the master and reinstall. +# (/tmp/crontab.nS0S9F/crontab installed on Wed Oct 5 09:29:23 2022) +# (Cron version -- $Id: crontab.c,v 2.13 1994/01/17 03:20:37 vixie Exp $) +0 8 */1 * * python /adcm/python/manage.py logrotate --target all 0 10 */1 * * python /adcm/python/manage.py clearaudit */1 * * * * python /adcm/python/manage.py run_ldap_sync diff --git a/os/etc/logrotate.d/runstatus b/os/etc/logrotate.d/runstatus index 87c1bed401..94bed0a4f7 100644 --- a/os/etc/logrotate.d/runstatus +++ b/os/etc/logrotate.d/runstatus @@ -1,11 +1,13 @@ /adcm/data/log/status.log { - size 50M - missingok - sharedscripts - compress - delaycompress - rotate 10 - postrotate - killall -USR1 runstatus - endscript + su root root + size 50M + create 0644 + missingok + sharedscripts + compress + delaycompress + rotate 10 + postrotate + killall -USR1 runstatus + endscript } diff --git a/os/etc/startup.sh b/os/etc/startup.sh index 9a96478668..b9872d0219 100755 --- a/os/etc/startup.sh +++ b/os/etc/startup.sh @@ -11,9 +11,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -# shellcheck disable=SC1091 -source /etc/adcmenv - +. /etc/adcmenv cleanupwaitstatus sv_stop() { @@ -22,10 +20,7 @@ sv_stop() { done } -trap "sv_stop; exit" SIGTERM - -trap '' SIGCHLD - -runsvdir -P /etc/service & - +trap "sv_stop; exit" TERM +trap "" CHLD +runsvdir -P /etc/sv & while (true); do wait; done; diff --git a/os/etc/service/crond/run b/os/etc/sv/cron/run similarity index 79% rename from os/etc/service/crond/run rename to os/etc/sv/cron/run index 117479e424..2d33412746 100755 --- a/os/etc/service/crond/run +++ b/os/etc/sv/cron/run @@ -11,11 +11,9 @@ # See the License for the specific language governing permissions and # limitations under the License. -source /etc/adcmenv +. /etc/adcmenv waitforwsgi -echo "Run crond ..." - -source "${adcmroot}/venv/default/bin/activate" -crond -f -l 2 # TODO: do something with cron logs / syslogs +echo "Run cron ..." +crond -f -l 2 diff --git a/os/etc/service/init/run b/os/etc/sv/init/run similarity index 96% rename from os/etc/service/init/run rename to os/etc/sv/init/run index 3ac63dac06..240f682875 100755 --- a/os/etc/service/init/run +++ b/os/etc/sv/init/run @@ -13,7 +13,7 @@ set +x -source /etc/adcmenv +. /etc/adcmenv echo "Application initialisation ..." @@ -31,13 +31,12 @@ chmod 777 "${adcmtmp}" chmod a+t "${adcmtmp}" mkdir -p "${adcmuserconf}" mkdir -p "${adcmlog}/nginx" +rm -r /var/log/nginx [ ! -L "/var/log/nginx" ] && ln -s "${adcmlog}/nginx" /var/log/ ## We got an issue on existing installation with folded log/nginx/nginx/nginx.... [ -L "${adcmlog}/nginx/nginx" ] && rm -f "${adcmlog}/nginx/nginx" -source "${adcmroot}/venv/default/bin/activate" - echo "Root keys generation" key=/root/.ssh/id_rsa [ ! -f ${key} ] && ssh-keygen -f ${key} -t rsa -N "" diff --git a/os/etc/service/nginx/run b/os/etc/sv/nginx/run similarity index 97% rename from os/etc/service/nginx/run rename to os/etc/sv/nginx/run index 7480304297..fd2390c613 100755 --- a/os/etc/service/nginx/run +++ b/os/etc/sv/nginx/run @@ -11,7 +11,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -source /etc/adcmenv +. /etc/adcmenv waitforwsgi diff --git a/os/etc/service/status/run b/os/etc/sv/status/run similarity index 97% rename from os/etc/service/status/run rename to os/etc/sv/status/run index 9f2f35c8ec..0049343ad2 100755 --- a/os/etc/service/status/run +++ b/os/etc/sv/status/run @@ -11,7 +11,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -source /etc/adcmenv +. /etc/adcmenv waitforinit diff --git a/os/etc/service/wsgi/finish b/os/etc/sv/wsgi/finish similarity index 100% rename from os/etc/service/wsgi/finish rename to os/etc/sv/wsgi/finish diff --git a/os/etc/service/wsgi/run b/os/etc/sv/wsgi/run similarity index 76% rename from os/etc/service/wsgi/run rename to os/etc/sv/wsgi/run index 91c6f4e544..bd530432ea 100755 --- a/os/etc/service/wsgi/run +++ b/os/etc/sv/wsgi/run @@ -11,7 +11,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -source /etc/adcmenv +. /etc/adcmenv waitforinit @@ -20,4 +20,5 @@ exec 1>>"${adcmlog}/service_wsgi.out" exec 2>>"${adcmlog}/service_wsgi.err" cd "${adcmroot}/python" -uwsgi --venv "${adcmroot}/venv/default" --socket "${wsgisocketfile}" --pidfile "/run/uwsgi.pid" --module adcm.wsgi --chmod-socket=777 --logger file:logfile="${adcmlog}/wsgi.log",maxsize=2000000 +touch /run/uwsgi.pid +uwsgi --socket "${wsgisocketfile}" --pidfile "/run/uwsgi.pid" --module adcm.wsgi --chmod-socket=777 --logger file:logfile="${adcmlog}/wsgi.log",maxsize=2000000 diff --git a/pyproject.toml b/pyproject.toml index ed1eaa1e7a..9145d33007 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,11 +1,11 @@ [tool.black] -line-length = 100 +line-length = 120 skip-string-normalization = true target-version = ['py310'] include = '\.py$' exclude = ''' ( - \.git + | \.git | \.venv | __pycache__ | data @@ -21,525 +21,19 @@ exclude = ''' profile = "black" src_paths = ["python"] skip_glob = ["python/ansible/plugins"] -skip = ["python/init_db.py", "python/task_runner.py", "python/backupdb.py", "python/job_runner.py", "python/drf_docs.py"] +skip = ["python/init_db.py", "python/task_runner.py", "python/backupdb.py", "python/job_runner.py", + "python/drf_docs.py"] -[tool.pylint.main] -# Analyse import fallback blocks. This can be used to support both Python 2 and 3 -# compatible code, which means that the block might have code that exists only in -# one or another interpreter, leading to false positives when analysed. -analyse-fallback-blocks = false - -# Always return a 0 (non-error) status code, even if lint errors are found. This -# is primarily useful in continuous integration scripts. -# exit-zero = - -# A comma-separated list of package or module names from where C extensions may -# be loaded. Extensions are loading into the active Python interpreter and may -# run arbitrary code. -# extension-pkg-allow-list = - -# A comma-separated list of package or module names from where C extensions may -# be loaded. Extensions are loading into the active Python interpreter and may -# run arbitrary code. (This is an alternative name to extension-pkg-allow-list -# for backward compatibility.) -# extension-pkg-whitelist = - -# Return non-zero exit code if any of these messages/categories are detected, -# even if score is above --fail-under value. Syntax same as enable. Messages -# specified are enabled, while categories only check already-enabled messages. -# fail-on = - -# Specify a score threshold to be exceeded before program exits with error. -fail-under = 10 - -# Interpret the stdin as a python script, whose filename needs to be passed as -# the module_or_package argument. -# from-stdin = - -# Files or directories to be skipped. They should be base names, not paths. -ignore = ["CVS os"] - -# Add files or directories matching the regex patterns to the ignore-list. The -# regex matches against paths and can be in Posix or Windows format. -ignore-paths = ["^tests/.*$"] - -# Files or directories matching the regex patterns are skipped. The regex matches -# against base names, not paths. The default value ignores Emacs file locks -# ignore-patterns = - -# List of module names for which member attributes should not be checked (useful -# for modules/projects where namespaces are manipulated during runtime and thus -# existing member attributes cannot be deduced by static analysis). It supports -# qualified module names, as well as Unix pattern matching. -# ignored-modules = - -# Python code to execute, usually for sys.path manipulation such as -# pygtk.require(). -# init-hook = - -# Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the -# number of processors available to use. -jobs = 1 - -# Control the amount of potential inferred values when inferring a single object. -# This can help the performance when dealing with large functions or complex, -# nested conditions. -limit-inference-results = 100 - -# List of plugins (as comma separated values of python module names) to load, -# usually to register additional checkers. -# load-plugins = - -# Pickle collected data for later comparisons. -persistent = true - -# Minimum Python version to use for version dependent checks. Will default to the -# version used to run pylint. -py-version = "3.10" - -# Discover python modules and packages in the file system subtree. -# recursive = - -# When enabled, pylint would attempt to guess common misconfiguration and emit -# user-friendly hints instead of false-positive error messages. -suggestion-mode = true - -# Allow loading of arbitrary C extensions. Extensions are imported into the -# active Python interpreter and may run arbitrary code. -unsafe-load-any-extension = false - -[tool.pylint.basic] -# Naming style matching correct argument names. -argument-naming-style = "snake_case" - -# Regular expression matching correct argument names. Overrides argument-naming- -# style. If left empty, argument names will be checked with the set naming style. -argument-rgx = "(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$" - -# Naming style matching correct attribute names. -attr-naming-style = "snake_case" - -# Regular expression matching correct attribute names. Overrides attr-naming- -# style. If left empty, attribute names will be checked with the set naming -# style. -attr-rgx = "(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$" - -# Bad variable names which should always be refused, separated by a comma. -bad-names = ["foo", "bar", "baz", "toto", "tutu", "tata"] - -# Bad variable names regexes, separated by a comma. If names match any regex, -# they will always be refused -# bad-names-rgxs = - -# Naming style matching correct class attribute names. -class-attribute-naming-style = "any" - -# Regular expression matching correct class attribute names. Overrides class- -# attribute-naming-style. If left empty, class attribute names will be checked -# with the set naming style. -class-attribute-rgx = "([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$" - -# Naming style matching correct class constant names. -class-const-naming-style = "UPPER_CASE" - -# Regular expression matching correct class constant names. Overrides class- -# const-naming-style. If left empty, class constant names will be checked with -# the set naming style. -# class-const-rgx = - -# Naming style matching correct class names. -class-naming-style = "PascalCase" - -# Regular expression matching correct class names. Overrides class-naming-style. -# If left empty, class names will be checked with the set naming style. -class-rgx = "[A-Z_][a-zA-Z0-9]+$" - -# Naming style matching correct constant names. -const-naming-style = "UPPER_CASE" - -# Regular expression matching correct constant names. Overrides const-naming- -# style. If left empty, constant names will be checked with the set naming style. -const-rgx = "(([A-Z_][A-Z0-9_]*)|(__.*__))$" - -# Minimum line length for functions/classes that require docstrings, shorter ones -# are exempt. -docstring-min-length = -1 - -# Naming style matching correct function names. -function-naming-style = "snake_case" - -# Regular expression matching correct function names. Overrides function-naming- -# style. If left empty, function names will be checked with the set naming style. -function-rgx = "(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$" - -# Good variable names which should always be accepted, separated by a comma. -good-names = ["i", "j", "k", "ex", "Run", "_"] - -# Good variable names regexes, separated by a comma. If names match any regex, -# they will always be accepted -# good-names-rgxs = - -# Include a hint for the correct naming format with invalid-name. -include-naming-hint = false - -# Naming style matching correct inline iteration names. -inlinevar-naming-style = "any" - -# Regular expression matching correct inline iteration names. Overrides -# inlinevar-naming-style. If left empty, inline iteration names will be checked -# with the set naming style. -inlinevar-rgx = "[A-Za-z_][A-Za-z0-9_]*$" - -# Naming style matching correct method names. -method-naming-style = "snake_case" - -# Regular expression matching correct method names. Overrides method-naming- -# style. If left empty, method names will be checked with the set naming style. -method-rgx = "(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$" - -# Naming style matching correct module names. -module-naming-style = "snake_case" - -# Regular expression matching correct module names. Overrides module-naming- -# style. If left empty, module names will be checked with the set naming style. -module-rgx = "(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$" - -# Colon-delimited sets of names that determine each other's naming style when the -# name regexes allow several styles. -# name-group = - -# Regular expression which should only match function or class names that do not -# require a docstring. -no-docstring-rgx = "^_" - -# List of decorators that produce properties, such as abc.abstractproperty. Add -# to this list to register other decorators that produce valid properties. These -# decorators are taken in consideration only for invalid-name. -property-classes = ["abc.abstractproperty"] - -# Regular expression matching correct type variable names. If left empty, type -# variable names will be checked with the set naming style. -# typevar-rgx = - -# Naming style matching correct variable names. -variable-naming-style = "snake_case" - -# Regular expression matching correct variable names. Overrides variable-naming- -# style. If left empty, variable names will be checked with the set naming style. -variable-rgx = "(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$" - -[tool.pylint.classes] -# Warn about protected attribute access inside special methods -# check-protected-access-in-special-methods = - -# List of method names used to declare (i.e. assign) instance attributes. -defining-attr-methods = ["__init__", "__new__", "setUp"] - -# List of member names, which should be excluded from the protected access -# warning. -exclude-protected = ["_asdict", "_fields", "_replace", "_source", "_make"] - -# List of valid names for the first argument in a class method. -valid-classmethod-first-arg = ["cls"] - -# List of valid names for the first argument in a metaclass class method. -valid-metaclass-classmethod-first-arg = ["mcs"] - -[tool.pylint.design] -# List of regular expressions of class ancestor names to ignore when counting -# public methods (see R0903) -# exclude-too-few-public-methods = - -# List of qualified class names to ignore when counting class parents (see R0901) -# ignored-parents = - -# Maximum number of arguments for function / method. -max-args = 7 - -# Maximum number of attributes for a class (see R0902). -max-attributes = 7 - -# Maximum number of boolean expressions in an if statement (see R0916). -max-bool-expr = 5 - -# Maximum number of branch for function / method body. -max-branches = 12 - -# Maximum number of locals for function / method body. -max-locals = 15 - -# Maximum number of parents for a class (see R0901). -max-parents = 7 - -# Maximum number of public methods for a class (see R0904). -max-public-methods = 20 - -# Maximum number of return / yield for function / method body. -max-returns = 10 - -# Maximum number of statements in function / method body. -max-statements = 50 - -# Minimum number of public methods for a class (see R0903). -min-public-methods = 2 - -[tool.pylint.exceptions] -# Exceptions that will emit a warning when caught. -overgeneral-exceptions = ["Exception"] - -[tool.pylint.format] -# Expected format of line ending, e.g. empty (any line ending), LF or CRLF. -# expected-line-ending-format = - -# Regexp for a line that is allowed to be longer than the limit. -ignore-long-lines = "^\\s*(# )??$" - -# Number of spaces of indent required inside a hanging or continued line. -indent-after-paren = 4 - -# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 -# tab). -indent-string = " " - -# Maximum number of characters on a single line. -max-line-length = 100 - -# Maximum number of lines in a module. -max-module-lines = 1000 - -# Allow the body of a class to be on the same line as the declaration if body -# contains single statement. -single-line-class-stmt = false - -# Allow the body of an if to be on the same line as the test if there is no else. -single-line-if-stmt = false - -[tool.pylint.imports] -# List of modules that can be imported at any level, not just the top level one. -# allow-any-import-level = - -# Allow wildcard imports from modules that define __all__. -allow-wildcard-with-all = false - -# Deprecated modules which should not be used, separated by a comma. -deprecated-modules = ["optparse", "tkinter.tix"] - -# Output a graph (.gv or any supported image format) of external dependencies to -# the given file (report RP0402 must not be disabled). -# ext-import-graph = - -# Output a graph (.gv or any supported image format) of all (i.e. internal and -# external) dependencies to the given file (report RP0402 must not be disabled). -# import-graph = - -# Output a graph (.gv or any supported image format) of internal dependencies to -# the given file (report RP0402 must not be disabled). -# int-import-graph = - -# Force import order to recognize a module as part of the standard compatibility -# libraries. -# known-standard-library = - -# Force import order to recognize a module as part of a third party library. -known-third-party = ["enchant"] - -# Couples of modules and preferred modules, separated by a comma. -# preferred-modules = - -[tool.pylint.logging] -# The type of string formatting that logging methods do. `old` means using % -# formatting, `new` is for `{}` formatting. -logging-format-style = "old" - -# Logging modules to check that the string format arguments are in logging -# function parameter format. -logging-modules = ["logging"] +[tool.pylint.master] +ignore = ["migrations"] [tool.pylint."messages control"] -# Only show warnings with the listed confidence levels. Leave empty to show all. -# Valid levels: HIGH, CONTROL_FLOW, INFERENCE, INFERENCE_FAILURE, UNDEFINED. -# confidence = ["HIGH", "CONTROL_FLOW", "INFERENCE", "INFERENCE_FAILURE", "UNDEFINED"] - -# Disable the message, report, category or checker with the given id(s). You can -# either give multiple identifiers separated by comma (,) or put this option -# multiple times (only on the command line, not in the configuration file where -# it should appear only once). You can also use "--disable=all" to disable -# everything first and then re-enable specific checks. For example, if you want -# to run only the similarities checker, you can use "--disable=all -# --enable=similarities". If you want to run only the classes checker, but have -# no Warning level messages displayed, use "--disable=all --enable=classes -# --disable=W". -disable = ["missing-docstring", "invalid-name", "abstract-method", "unused-argument", "no-else-return", "duplicate-code", "fixme", "no-member", "too-few-public-methods", "wrong-import-order", "useless-import-alias", "consider-using-with"] - -# Enable the message, report, category or checker with the given id(s). You can -# either give multiple identifier separated by comma (,) or put this option -# multiple time (only on the command line, not in the configuration file where it -# should appear only once). See also the "--disable" option for examples. -# enable = ["c-extension-no-member"] - -[tool.pylint.miscellaneous] -# List of note tags to take in consideration, separated by a comma. -notes = ["FIXME", "XXX", "TODO"] - -# Regular expression of note tags to take in consideration. -# notes-rgx = - -[tool.pylint.refactoring] -# Maximum number of nested blocks for function / method body -max-nested-blocks = 5 - -# Complete name of functions that never returns. When checking for inconsistent- -# return-statements if a never returning function is called then it will be -# considered as an explicit return statement and no message will be printed. -never-returning-functions = ["sys.exit", "argparse.parse_error"] - -[tool.pylint.reports] -# Python expression which should return a score less than or equal to 10. You -# have access to the variables 'fatal', 'error', 'warning', 'refactor', -# 'convention', and 'info' which contain the number of messages in each category, -# as well as 'statement' which is the total number of statements analyzed. This -# score is used by the global evaluation report (RP0004). -evaluation = "10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)" - -# Template used to display messages. This is a python new-style format string -# used to format the message information. See doc for all details. -# msg-template = - -# Set the output format. Available formats are text, parseable, colorized, json -# and msvs (visual studio). You can also give a reporter class, e.g. -# mypackage.mymodule.MyReporterClass. -output-format = "text" - -# Tells whether to display a full report or only the messages. -reports = false +disable = ["abstract-method", "consider-using-with", "fixme", "import-error", "invalid-name", "missing-docstring", + "missing-timeout", "no-else-return", "no-member", "too-few-public-methods", "too-many-arguments", "unused-argument", + "wrong-import-order"] -# Activate the evaluation score. -score = true +[tool.pylint.format] +max-line-length = 120 [tool.pylint.similarities] -# Comments are removed from the similarity computation -ignore-comments = true - -# Docstrings are removed from the similarity computation -ignore-docstrings = true - -# Imports are removed from the similarity computation -ignore-imports = true - -# Signatures are removed from the similarity computation -ignore-signatures = true - -# Minimum lines number of a similarity. -min-similarity-lines = 5 - -[tool.pylint.spelling] -# Limits count of emitted suggestions for spelling mistakes. -max-spelling-suggestions = 4 - -# Spelling dictionary name. Available dictionaries: none. To make it work, -# install the 'python-enchant' package. -# spelling-dict = - -# List of comma separated words that should be considered directives if they -# appear at the beginning of a comment and should not be checked. -spelling-ignore-comment-directives = "fmt: on,fmt: off,noqa:,noqa,nosec,isort:skip,mypy:" - -# List of comma separated words that should not be checked. -# spelling-ignore-words = - -# A path to a file that contains the private dictionary; one word per line. -# spelling-private-dict-file = - -# Tells whether to store unknown words to the private dictionary (see the -# --spelling-private-dict-file option) instead of raising a message. -spelling-store-unknown-words = false - -[tool.pylint.string] -# This flag controls whether inconsistent-quotes generates a warning when the -# character used as a quote delimiter is used inconsistently within a module. -# check-quote-consistency = - -# This flag controls whether the implicit-str-concat should generate a warning on -# implicit string concatenation in sequences defined over several lines. -# check-str-concat-over-line-jumps = - -[tool.pylint.typecheck] -# List of decorators that produce context managers, such as -# contextlib.contextmanager. Add to this list to register other decorators that -# produce valid context managers. -contextmanager-decorators = ["contextlib.contextmanager"] - -# List of members which are set dynamically and missed by pylint inference -# system, and so shouldn't trigger E1101 when accessed. Python regular -# expressions are accepted. -# generated-members = - -# Tells whether missing members accessed in mixin class should be ignored. A -# class is considered mixin if its name matches the mixin-class-rgx option. -# Tells whether to warn about missing members when the owner of the attribute is -# inferred to be None. -ignore-none = true - -# This flag controls whether pylint should warn about no-member and similar -# checks whenever an opaque object is returned when inferring. The inference can -# return multiple potential results while evaluating a Python object, but some -# branches might not be evaluated, which results in partial inference. In that -# case, it might be useful to still emit no-member and other checks for the rest -# of the inferred objects. -ignore-on-opaque-inference = true - -# List of symbolic message names to ignore for Mixin members. -ignored-checks-for-mixins = ["no-member", "not-async-context-manager", "not-context-manager", "attribute-defined-outside-init"] - -# List of class names for which member attributes should not be checked (useful -# for classes with dynamically set attributes). This supports the use of -# qualified names. -ignored-classes = ["optparse.Values", "thread._local", "_thread._local"] - -# Show a hint with possible names when a member name was not found. The aspect of -# finding the hint is based on edit distance. -missing-member-hint = true - -# The minimum edit distance a name should have in order to be considered a -# similar match for a missing member name. -missing-member-hint-distance = 1 - -# The total number of similar names that should be taken in consideration when -# showing a hint for a missing member. -missing-member-max-choices = 1 - -# Regex pattern to define which classes are considered mixins. -mixin-class-rgx = ".*[Mm]ixin" - -# List of decorators that change the signature of a decorated function. -# signature-mutators = - -[tool.pylint.variables] -# List of additional names supposed to be defined in builtins. Remember that you -# should avoid defining new builtins when possible. -# additional-builtins = - -# Tells whether unused global variables should be treated as a violation. -allow-global-unused-variables = true - -# List of names allowed to shadow builtins -# allowed-redefined-builtins = - -# List of strings which can identify a callback function by name. A callback name -# must start or end with one of those strings. -callbacks = ["cb_", "_cb"] - -# A regular expression matching the name of dummy variables (i.e. expected to not -# be used). -dummy-variables-rgx = "_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_" - -# Argument names that match this expression will be ignored. Default to name with -# leading underscore. -ignored-argument-names = "_.*|^ignored_|^unused_" - -# Tells whether we should check for unused import in __init__ files. -# init-import = - -# List of qualified module names which can have objects that can redefine -# builtins. -redefining-builtins-modules = ["six.moves", "future.builtins"] +min-similarity-lines = 30 diff --git a/python/adcm/auth_backend.py b/python/adcm/auth_backend.py index 8b389aa3b4..13d77c1b39 100644 --- a/python/adcm/auth_backend.py +++ b/python/adcm/auth_backend.py @@ -10,38 +10,16 @@ # See the License for the specific language governing permissions and # limitations under the License. -from django.conf import settings from social_core.backends.google import GoogleOAuth2 -from social_core.backends.oauth import BaseOAuth2 +from social_core.backends.yandex import YandexOAuth2 from adcm.utils import get_google_oauth, get_yandex_oauth -class YandexOAuth2(BaseOAuth2): - name = "yandex-oauth2" - AUTHORIZATION_URL = settings.YANDEX_OAUTH_AUTH_URL - ACCESS_TOKEN_URL = settings.YANDEX_OAUTH_TOKEN_URL - ACCESS_TOKEN_METHOD = "POST" - STATE_PARAMETER = False - REDIRECT_STATE = False - +class CustomYandexOAuth2(YandexOAuth2): def auth_html(self): pass # not necessary - def get_user_details(self, response: dict) -> dict: - return { - "username": response.get("login"), - "email": response.get("emails")[0], - "first_name": response.get("first_name"), - "last_name": response.get("last_name"), - } - - def user_data(self, access_token: str, *args, **kwargs) -> dict: - return self.get_json( - url=settings.YANDEX_OAUTH_USER_DATA_URL, - headers={"Authorization": f"OAuth {access_token}"}, - ) - def get_key_and_secret(self) -> tuple[str, str]: return get_yandex_oauth() diff --git a/python/adcm/permissions.py b/python/adcm/permissions.py index 25c7b5a342..b12d49895b 100644 --- a/python/adcm/permissions.py +++ b/python/adcm/permissions.py @@ -1,4 +1,19 @@ -from rest_framework.permissions import DjangoModelPermissions, DjangoObjectPermissions +# 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 rest_framework.permissions import ( + DjangoModelPermissions, + DjangoObjectPermissions, + IsAuthenticated, +) from audit.utils import audit @@ -13,3 +28,9 @@ class DjangoModelPermissionsAudit(DjangoModelPermissions): @audit def has_permission(self, request, view): return super().has_permission(request, view) + + +class IsAuthenticatedAudit(IsAuthenticated): + @audit + def has_permission(self, request, view): + return super().has_permission(request, view) diff --git a/python/adcm/serializers.py b/python/adcm/serializers.py index 2b81d97b87..e17b4af0b7 100644 --- a/python/adcm/serializers.py +++ b/python/adcm/serializers.py @@ -1,3 +1,14 @@ +# 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 rest_framework.serializers import Serializer diff --git a/python/adcm/settings.py b/python/adcm/settings.py index 9a7f3757f6..a974759055 100644 --- a/python/adcm/settings.py +++ b/python/adcm/settings.py @@ -11,25 +11,65 @@ # limitations under the License. import json +import os import string +import sys from pathlib import Path from django.core.management.utils import get_random_secret_key -BASE_DIR = Path(__file__).absolute().parent.parent.parent +from cm.utils import dict_json_get_or_create, get_adcm_token + +ENCODING_UTF_8 = "utf-8" + +BASE_DIR = os.getenv("ADCM_BASE_DIR") +if BASE_DIR: + BASE_DIR = Path(BASE_DIR) +else: + BASE_DIR = Path(__file__).absolute().parent.parent.parent + CONF_DIR = BASE_DIR / "data" / "conf" -SECRET_KEY_FILE = CONF_DIR / "secret_key.txt" CONFIG_FILE = BASE_DIR / "config.json" +SECRET_KEY_FILE = CONF_DIR / "secret_key.txt" +STACK_DIR = os.getenv("ADCM_STACK_DIR", BASE_DIR) +BUNDLE_DIR = STACK_DIR / "data" / "bundle" +CODE_DIR = BASE_DIR / "python" +DOWNLOAD_DIR = Path(STACK_DIR, "data", "download") RUN_DIR = BASE_DIR / "data" / "run" +FILE_DIR = STACK_DIR / "data" / "file" +LOG_DIR = BASE_DIR / "data" / "log" +LOG_FILE = LOG_DIR / "adcm.log" +SECRETS_FILE = BASE_DIR / "data" / "var" / "secrets.json" +ADCM_TOKEN_FILE = BASE_DIR / "data/var/adcm_token" +PYTHON_SITE_PACKAGES = Path( + sys.exec_prefix, f"lib/python{sys.version_info.major}.{sys.version_info.minor}/site-packages" +) + +ANSIBLE_VAULT_HEADER = "$ANSIBLE_VAULT;1.1;AES256" +DEFAULT_SALT = b'"j\xebi\xc0\xea\x82\xe0\xa8\xba\x9e\x12E>\x11D' + +ADCM_TOKEN = get_adcm_token() +if SECRETS_FILE.is_file(): + with open(SECRETS_FILE, encoding=ENCODING_UTF_8) as f: + data = json.load(f) + STATUS_SECRET_KEY = data["token"] + ANSIBLE_SECRET = data["adcmuser"]["password"] + # workaround to insert `adcm_internal_token` into existing SECRETS_FILE after startup + if data.get("adcm_internal_token") is None: + dict_json_get_or_create(path=SECRETS_FILE, field="adcm_internal_token", value=ADCM_TOKEN) + +else: + STATUS_SECRET_KEY = "" + ANSIBLE_SECRET = "" if SECRET_KEY_FILE.is_file(): - with open(SECRET_KEY_FILE, encoding="utf_8") as f: + with open(SECRET_KEY_FILE, encoding=ENCODING_UTF_8) as f: SECRET_KEY = f.read().strip() else: SECRET_KEY = get_random_secret_key() if CONFIG_FILE.is_file(): - with open(CONFIG_FILE, encoding="utf_8") as f: + with open(CONFIG_FILE, encoding=ENCODING_UTF_8) as f: ADCM_VERSION = json.load(f)["version"] else: ADCM_VERSION = "2019.02.07.00" @@ -52,7 +92,6 @@ "rest_framework.authtoken", "social_django", "guardian", - "adwp_events", "cm.apps.CmConfig", "audit", ] @@ -89,6 +128,7 @@ WSGI_APPLICATION = "adcm.wsgi.application" LOGIN_URL = "/api/v1/auth/login/" +LOGIN_REDIRECT_URL = "/admin/intro/" REST_FRAMEWORK = { "DEFAULT_PERMISSION_CLASSES": ["rest_framework.permissions.IsAuthenticated"], @@ -130,14 +170,10 @@ "django.contrib.auth.backends.ModelBackend", "guardian.backends.ObjectPermissionBackend", "rbac.ldap.CustomLDAPBackend", - "adcm.auth_backend.YandexOAuth2", + "adcm.auth_backend.CustomYandexOAuth2", "adcm.auth_backend.CustomGoogleOAuth2", ) -YANDEX_OAUTH_AUTH_URL = "https://oauth.yandex.ru/authorize" -YANDEX_OAUTH_TOKEN_URL = "https://oauth.yandex.ru/token" -YANDEX_OAUTH_USER_DATA_URL = "https://login.yandex.ru/info?format=json" - LANGUAGE_CODE = "en-us" TIME_ZONE = "UTC" USE_I18N = True @@ -149,9 +185,10 @@ ADWP_EVENT_SERVER = { # path to json file with Event Server secret token - "SECRETS_FILE": BASE_DIR / "data/var/secrets.json", + "SECRETS_FILE": SECRETS_FILE, # URL of Event Server REST API "API_URL": "http://localhost:8020/api/v1", + "SECRET_KEY": ADCM_TOKEN, } LOGGING = { @@ -211,11 +248,6 @@ "django.utils.autoreload": { "level": "INFO", }, - "adwp": { - "handlers": ["adwp_file"], - "level": "DEBUG", - "propagate": True, - }, "django_auth_ldap": { "handlers": ["file"], "level": "DEBUG", @@ -243,3 +275,17 @@ ALLOWED_HOST_FQDN_START_CHARS = LATIN_LETTERS_DIGITS ALLOWED_HOST_FQDN_MID_END_CHARS = f"{ALLOWED_HOST_FQDN_START_CHARS}-." + +ADCM_TURN_ON_MM_ACTION_NAME = "adcm_turn_on_maintenance_mode" +ADCM_TURN_OFF_MM_ACTION_NAME = "adcm_turn_off_maintenance_mode" +ADCM_HOST_TURN_ON_MM_ACTION_NAME = "adcm_host_turn_on_maintenance_mode" +ADCM_HOST_TURN_OFF_MM_ACTION_NAME = "adcm_host_turn_off_maintenance_mode" +ADCM_DELETE_SERVICE_ACTION_NAME = "adcm_delete_service" +ADCM_SERVICE_ACTION_NAMES_SET = { + ADCM_TURN_ON_MM_ACTION_NAME, + ADCM_TURN_OFF_MM_ACTION_NAME, + ADCM_HOST_TURN_ON_MM_ACTION_NAME, + ADCM_HOST_TURN_OFF_MM_ACTION_NAME, + ADCM_DELETE_SERVICE_ACTION_NAME, +} +ADCM_MM_ACTION_FORBIDDEN_PROPS_SET = {"config", "hc_acl", "ui_options"} diff --git a/python/adcm/tests/base.py b/python/adcm/tests/base.py index 2f99dd0943..2117cc2f6c 100644 --- a/python/adcm/tests/base.py +++ b/python/adcm/tests/base.py @@ -17,7 +17,9 @@ from django.test import Client, TestCase from django.urls import reverse from rest_framework.response import Response +from rest_framework.status import HTTP_200_OK, HTTP_201_CREATED +from cm.models import Bundle from rbac.models import Role, User APPLICATION_JSON = "application/json" @@ -43,7 +45,7 @@ def setUp(self) -> None: password="no_rights_user_password", ) - self.client = Client(HTTP_USER_AGENT='Mozilla/5.0') + self.client = Client(HTTP_USER_AGENT="Mozilla/5.0") self.login() self.cluster_admin_role = Role.objects.create( @@ -102,3 +104,21 @@ def another_user_logged_in(self, username: str, password: str): yield self.login() + + def upload_and_load_bundle(self, path: Path) -> Bundle: + with open(path, encoding=settings.ENCODING_UTF_8) as f: + response: Response = self.client.post( + path=reverse("upload-bundle"), + data={"file": f}, + ) + + self.assertEqual(response.status_code, HTTP_201_CREATED) + + response: Response = self.client.post( + path=reverse("load-bundle"), + data={"bundle_file": path.name}, + ) + + self.assertEqual(response.status_code, HTTP_200_OK) + + return Bundle.objects.get(pk=response.data["id"]) diff --git a/python/adcm/utils.py b/python/adcm/utils.py index 0247c95843..0761779815 100644 --- a/python/adcm/utils.py +++ b/python/adcm/utils.py @@ -10,6 +10,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +from cm.adcm_config import ansible_decrypt from cm.models import ADCM, ConfigLog @@ -31,7 +32,7 @@ def str_remove_non_alnum(value: str) -> str: return result -def get_oauth(oauth_key: str) -> tuple[str, str]: +def get_oauth(oauth_key: str) -> tuple[str | None, str | None]: adcm = ADCM.objects.filter().first() if not adcm: return None, None @@ -43,15 +44,16 @@ def get_oauth(oauth_key: str) -> tuple[str, str]: if not config_log.config.get(oauth_key): return None, None - if ( - "client_id" not in config_log.config[oauth_key] - or "secret" not in config_log.config[oauth_key] - ): + if "client_id" not in config_log.config[oauth_key] or "secret" not in config_log.config[oauth_key]: + return None, None + + secret = config_log.config[oauth_key]["secret"] + if not secret: return None, None return ( config_log.config[oauth_key]["client_id"], - config_log.config[oauth_key]["secret"], + ansible_decrypt(secret), ) diff --git a/python/ansible/plugins/action/adcm_add_host.py b/python/ansible/plugins/action/adcm_add_host.py index 3edbffb55a..c1c2d81390 100644 --- a/python/ansible/plugins/action/adcm_add_host.py +++ b/python/ansible/plugins/action/adcm_add_host.py @@ -17,9 +17,9 @@ __metaclass__ = type -ANSIBLE_METADATA = {'metadata_version': '1.1', 'supported_by': 'Arenadata'} +ANSIBLE_METADATA = {"metadata_version": "1.1", "supported_by": "Arenadata"} -DOCUMENTATION = r''' +DOCUMENTATION = r""" --- module: adcm_add_host short_description: add host to ADCM DB @@ -35,19 +35,19 @@ description: - Comment required: no -''' +""" -EXAMPLES = r''' +EXAMPLES = r""" - name: add new host adcm_add_host: fqdn: my.host.org description: "add my host" -''' +""" -RETURN = r''' +RETURN = r""" result: host_id: ID of new created host -''' +""" import sys @@ -57,34 +57,38 @@ sys.path.append('/adcm/python') import adcm.init_django # pylint: disable=unused-import -import cm.api +from cm.api import add_host from cm.ansible_plugin import get_object_id_from_context from cm.errors import AdcmEx from cm.logger import logger +from cm.models import HostProvider, Prototype class ActionModule(ActionBase): - TRANSFERS_FILES = False - _VALID_ARGS = frozenset(('fqdn', 'description')) + _VALID_ARGS = frozenset(("fqdn", "description")) def run(self, tmp=None, task_vars=None): super().run(tmp, task_vars) - msg = 'You can add host only in host provider context' - provider_id = get_object_id_from_context(task_vars, 'provider_id', 'provider', err_msg=msg) - if 'fqdn' not in self._task.args: + provider_pk = get_object_id_from_context( + task_vars, "provider_id", "provider", err_msg="You can add host only in host provider context" + ) + if "fqdn" not in self._task.args: raise AnsibleError("fqdn is mandatory args of adcm_add_host") - fqdn = self._task.args['fqdn'] - desc = '' - if 'description' in self._task.args: - desc = self._task.args['description'] - logger.info('ansible module adcm_add_host: provider %s, fqdn %s', provider_id, fqdn) + fqdn = self._task.args["fqdn"] + desc = "" + if "description" in self._task.args: + desc = self._task.args["description"] + + logger.info("ansible module adcm_add_host: provider %s, fqdn %s", provider_pk, fqdn) try: - host = cm.api.add_provider_host(provider_id, fqdn, desc) + provider = HostProvider.obj.get(pk=provider_pk) + proto = Prototype.objects.get(bundle=provider.prototype.bundle, type="host") + host = add_host(proto=proto, provider=provider, fqdn=fqdn, desc=desc) except AdcmEx as e: - raise AnsibleError(e.code + ":" + e.msg) from e + raise AnsibleError(f"{e.code}:{e.msg}") from e - return {"failed": False, "changed": True, "host_id": host.id} + return {"failed": False, "changed": True, "host_id": host.pk} diff --git a/python/ansible/plugins/action/adcm_add_host_to_cluster.py b/python/ansible/plugins/action/adcm_add_host_to_cluster.py index 9f01da926a..c7de2541c8 100644 --- a/python/ansible/plugins/action/adcm_add_host_to_cluster.py +++ b/python/ansible/plugins/action/adcm_add_host_to_cluster.py @@ -69,17 +69,13 @@ class ActionModule(ActionBase): def run(self, tmp=None, task_vars=None): super().run(tmp, task_vars) msg = 'You can add host only in cluster or service context' - cluster_id = get_object_id_from_context( - task_vars, 'cluster_id', 'cluster', 'service', err_msg=msg - ) + cluster_id = get_object_id_from_context(task_vars, 'cluster_id', 'cluster', 'service', err_msg=msg) fqdn = self._task.args.get('fqdn', None) host_id = self._task.args.get('host_id', None) - logger.info( - 'ansible module: cluster_id %s, fqdn %s, host_id: %s', cluster_id, fqdn, host_id - ) + logger.info('ansible module: cluster_id %s, fqdn %s, host_id: %s', cluster_id, fqdn, host_id) try: - cm.api.add_host_to_cluster_by_id(cluster_id, fqdn, host_id) + cm.api.add_host_to_cluster_by_pk(cluster_id, fqdn, host_id) except AdcmEx as e: raise AnsibleError(e.code + ": " + e.msg) from e diff --git a/python/ansible/plugins/action/adcm_change_maintenance_mode.py b/python/ansible/plugins/action/adcm_change_maintenance_mode.py new file mode 100644 index 0000000000..2098fde848 --- /dev/null +++ b/python/ansible/plugins/action/adcm_change_maintenance_mode.py @@ -0,0 +1,114 @@ +# 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. + +# pylint: disable=wrong-import-position + +DOCUMENTATION = """ +--- +module: adcm_change_maintenance_mode +short_description: Change Host, Service or Component maintenance mode to ON or OFF +description: + - The C(adcm_change_maintenance_mode) module is intended to + change Host, Service or Component maintenance mode to ON or OFF. +options: + type: + description: Entity type. + required: true + choices: + - host + - service + - component + value: + description: Maintenance mode value True or False. + required: True + type: bool +""" + +EXAMPLES = r""" +- name: Change host maintenance mode to True + adcm_change_maintenance_mode: + type: host + value: True + +- name: Change service maintenance mode to False + adcm_change_maintenance_mode: + type: service + value: False +""" + +import sys + +from ansible.errors import AnsibleActionFail + +from ansible.plugins.action import ActionBase + +sys.path.append("/adcm/python") + +import adcm.init_django # pylint: disable=unused-import +from cm.ansible_plugin import get_object_id_from_context +from cm.api import load_mm_objects +from cm.issue import update_hierarchy_issues +from cm.models import ClusterObject, Host, ServiceComponent + + +class ActionModule(ActionBase): + TRANSFERS_FILES = False + _VALID_ARGS = frozenset(["type", "value"]) + + def run(self, tmp=None, task_vars=None): + super().run(tmp, task_vars) + + type_class_map = { + "host": Host, + "service": ClusterObject, + "component": ServiceComponent, + } + type_choices = set(type_class_map.keys()) + + if not self._task.args.get("type"): + raise AnsibleActionFail('"type" option is required') + + if self._task.args.get("value") is None: + raise AnsibleActionFail('"value" option is required') + + if self._task.args["type"] not in type_choices: + raise AnsibleActionFail(f'"type" should be one of {type_choices}') + + if not isinstance(self._task.args["value"], bool): + raise AnsibleActionFail('"value" should be boolean') + + obj_type = self._task.args["type"] + context_type = obj_type + if obj_type == "host": + context_type = "cluster" + + obj_value = "ON" if self._task.args["value"] else "OFF" + obj_pk = get_object_id_from_context( + task_vars, + f'{obj_type}_id', + context_type, + err_msg=f'You can change "{obj_type}" maintenance mode only in {context_type} context', + ) + + obj = type_class_map[obj_type].objects.filter(pk=obj_pk).first() + if not obj: + raise AnsibleActionFail(f'Object of type "{obj_type}" with PK "{obj_pk}" does not exist') + + if obj.maintenance_mode != "CHANGING": + raise AnsibleActionFail('Only "CHANGING" state of object maintenance mode can be changed') + + obj.maintenance_mode = obj_value + obj.save() + update_hierarchy_issues(obj.cluster) + load_mm_objects() + + return {"failed": False, "changed": True} diff --git a/python/ansible/plugins/action/adcm_check.py b/python/ansible/plugins/action/adcm_check.py index d1582f9960..92d4d3e260 100644 --- a/python/ansible/plugins/action/adcm_check.py +++ b/python/ansible/plugins/action/adcm_check.py @@ -131,17 +131,12 @@ def run(self, tmp=None, task_vars=None): old_optional_condition = 'msg' in self._task.args new_optional_condition = 'fail_msg' in self._task.args and 'success_msg' in self._task.args optional_condition = old_optional_condition or new_optional_condition - required_condition = ( - 'title' in self._task.args and 'result' in self._task.args and optional_condition - ) + required_condition = 'title' in self._task.args and 'result' in self._task.args and optional_condition if not required_condition: return { "failed": True, - "msg": ( - "title, result and msg, fail_msg or success" - "_msg are mandatory args of adcm_check" - ), + "msg": ("title, result and msg, fail_msg or success" "_msg are mandatory args of adcm_check"), } title = self._task.args['title'] diff --git a/python/ansible/plugins/action/adcm_config.py b/python/ansible/plugins/action/adcm_config.py index 8fe4c6a423..0042242e2c 100644 --- a/python/ansible/plugins/action/adcm_config.py +++ b/python/ansible/plugins/action/adcm_config.py @@ -127,9 +127,7 @@ def _do_service(self, task_vars, context): return res def _do_host(self, task_vars, context): - res = self._wrap_call( - set_host_config, context['host_id'], self._task.args["key"], self._task.args["value"] - ) + res = self._wrap_call(set_host_config, context['host_id'], self._task.args["key"], self._task.args["value"]) res['value'] = self._task.args["value"] return res diff --git a/python/ansible/plugins/action/adcm_custom_log.py b/python/ansible/plugins/action/adcm_custom_log.py index 9fcf86f262..5365102f89 100644 --- a/python/ansible/plugins/action/adcm_custom_log.py +++ b/python/ansible/plugins/action/adcm_custom_log.py @@ -68,7 +68,7 @@ from ansible.plugins.action import ActionBase -sys.path.append('/adcm/python') +sys.path.append("/adcm/python") import adcm.init_django # pylint: disable=unused-import from cm.errors import AdcmEx from cm.job import log_custom @@ -76,17 +76,17 @@ class ActionModule(ActionBase): - _VALID_ARGS = frozenset(('name', 'format', 'path', 'content')) + _VALID_ARGS = frozenset(("name", "format", "path", "content")) def run(self, tmp=None, task_vars=None): super().run(tmp, task_vars) - if task_vars is not None and 'job' in task_vars or 'id' in task_vars['job']: - job_id = task_vars['job']['id'] + if task_vars is not None and "job" in task_vars or "id" in task_vars["job"]: + job_id = task_vars["job"]["id"] - name = self._task.args.get('name') - log_format = self._task.args.get('format') - path = self._task.args.get('path') - content = self._task.args.get('content') + name = self._task.args.get("name") + log_format = self._task.args.get("format") + path = self._task.args.get("path") + content = self._task.args.get("content") if not name and log_format and (path or content): return { "failed": True, @@ -95,25 +95,21 @@ def run(self, tmp=None, task_vars=None): try: if path is None: - logger.debug( - 'ansible adcm_custom_log: %s, %s, %s, %s', job_id, name, log_format, content - ) + logger.debug("ansible adcm_custom_log: %s, %s, %s, %s", job_id, name, log_format, content) log_custom(job_id, name, log_format, content) else: - logger.debug( - 'ansible adcm_custom_log: %s, %s, %s, %s', job_id, name, log_format, path - ) + logger.debug("ansible adcm_custom_log: %s, %s, %s, %s", job_id, name, log_format, path) slurp_return = self._execute_module( - module_name='slurp', module_args={'src': path}, task_vars=task_vars, tmp=tmp + module_name="slurp", module_args={"src": path}, task_vars=task_vars, tmp=tmp ) + if "failed" in slurp_return and slurp_return["failed"]: + raise AdcmEx("UNKNOWN_ERROR", msg=slurp_return["msg"]) try: - body = base64.standard_b64decode(slurp_return['content']).decode() + body = base64.standard_b64decode(slurp_return["content"]).decode() except Error as error: - raise AdcmEx('UNKNOWN_ERROR', msg='Error b64decode for slurp module') from error + raise AdcmEx("UNKNOWN_ERROR", msg="Error b64decode for slurp module") from error except UnicodeDecodeError as error: - raise AdcmEx( - 'UNKNOWN_ERROR', msg='Error UnicodeDecodeError for slurp module' - ) from error + raise AdcmEx("UNKNOWN_ERROR", msg="Error UnicodeDecodeError for slurp module") from error log_custom(job_id, name, log_format, body) except AdcmEx as e: diff --git a/python/ansible/plugins/action/adcm_delete_host.py b/python/ansible/plugins/action/adcm_delete_host.py index 799a046bf4..d9903c4d05 100644 --- a/python/ansible/plugins/action/adcm_delete_host.py +++ b/python/ansible/plugins/action/adcm_delete_host.py @@ -63,7 +63,7 @@ def run(self, tmp=None, task_vars=None): logger.info('ansible module adcm_delete_host: host #%s', host_id) try: - cm.api.delete_host_by_id(host_id) + cm.api.delete_host_by_pk(host_id) except AdcmEx as e: raise AnsibleError(e.code + ":" + e.msg) from e diff --git a/python/ansible/plugins/action/adcm_delete_service.py b/python/ansible/plugins/action/adcm_delete_service.py index d7647458a5..01cb561c31 100644 --- a/python/ansible/plugins/action/adcm_delete_service.py +++ b/python/ansible/plugins/action/adcm_delete_service.py @@ -72,7 +72,7 @@ def run(self, tmp=None, task_vars=None): service_id = get_object_id_from_context(task_vars, 'service_id', 'service', err_msg=msg) logger.info('ansible module adcm_delete_service: service #%s', service_id) try: - cm.api.delete_service_by_id(service_id) + cm.api.delete_service_by_pk(service_id) except AdcmEx as e: raise AnsibleError(e.code + ":" + e.msg) from e diff --git a/python/ansible/plugins/action/adcm_hc.py b/python/ansible/plugins/action/adcm_hc.py index 384ba96dc9..d3e320ceb5 100644 --- a/python/ansible/plugins/action/adcm_hc.py +++ b/python/ansible/plugins/action/adcm_hc.py @@ -71,9 +71,7 @@ class ActionModule(ActionBase): def run(self, tmp=None, task_vars=None): super().run(tmp, task_vars) msg = 'You can modify hc only in cluster, service or component context' - cluster_id = get_object_id_from_context( - task_vars, 'cluster_id', 'cluster', 'service', 'component', err_msg=msg - ) + cluster_id = get_object_id_from_context(task_vars, 'cluster_id', 'cluster', 'service', 'component', err_msg=msg) job_id = task_vars['job']['id'] ops = self._task.args['operations'] diff --git a/python/ansible/plugins/action/adcm_multi_state_set.py b/python/ansible/plugins/action/adcm_multi_state_set.py index a06b78023f..4ddf916a36 100644 --- a/python/ansible/plugins/action/adcm_multi_state_set.py +++ b/python/ansible/plugins/action/adcm_multi_state_set.py @@ -105,9 +105,7 @@ class ActionModule(ContextActionModule): _MANDATORY_ARGS = ('type', 'state') def _do_cluster(self, task_vars, context): - res = self._wrap_call( - set_cluster_multi_state, context['cluster_id'], self._task.args["state"] - ) + res = self._wrap_call(set_cluster_multi_state, context['cluster_id'], self._task.args["state"]) res['state'] = self._task.args["state"] return res @@ -141,9 +139,7 @@ def _do_host(self, task_vars, context): return res def _do_provider(self, task_vars, context): - res = self._wrap_call( - set_provider_multi_state, context['provider_id'], self._task.args["state"] - ) + res = self._wrap_call(set_provider_multi_state, context['provider_id'], self._task.args["state"]) res['state'] = self._task.args["state"] return res diff --git a/python/ansible/plugins/action/adcm_multi_state_unset.py b/python/ansible/plugins/action/adcm_multi_state_unset.py index b0f4ec6b45..ecef1e8b69 100644 --- a/python/ansible/plugins/action/adcm_multi_state_unset.py +++ b/python/ansible/plugins/action/adcm_multi_state_unset.py @@ -108,9 +108,7 @@ class ActionModule(ContextActionModule): TRANSFERS_FILES = False - _VALID_ARGS = frozenset( - ('type', 'service_name', 'component_name', 'state', 'missing_ok', 'host_id') - ) + _VALID_ARGS = frozenset(('type', 'service_name', 'component_name', 'state', 'missing_ok', 'host_id')) _MANDATORY_ARGS = ('type', 'state') def _do_cluster(self, task_vars, context): diff --git a/python/ansible/plugins/action/adcm_remove_host_from_cluster.py b/python/ansible/plugins/action/adcm_remove_host_from_cluster.py index 75908f60f1..a2c06c54d7 100644 --- a/python/ansible/plugins/action/adcm_remove_host_from_cluster.py +++ b/python/ansible/plugins/action/adcm_remove_host_from_cluster.py @@ -69,17 +69,13 @@ class ActionModule(ActionBase): def run(self, tmp=None, task_vars=None): super().run(tmp, task_vars) msg = 'You can remove host only in cluster or service context' - cluster_id = get_object_id_from_context( - task_vars, 'cluster_id', 'cluster', 'service', err_msg=msg - ) + cluster_id = get_object_id_from_context(task_vars, 'cluster_id', 'cluster', 'service', err_msg=msg) fqdn = self._task.args.get('fqdn', None) host_id = self._task.args.get('host_id', None) - logger.info( - 'ansible module: cluster_id %s, fqdn %s, host_id: %s', cluster_id, fqdn, host_id - ) + logger.info('ansible module: cluster_id %s, fqdn %s, host_id: %s', cluster_id, fqdn, host_id) try: - cm.api.remove_host_from_cluster_by_id(cluster_id, fqdn, host_id) + cm.api.remove_host_from_cluster_by_pk(cluster_id, fqdn, host_id) except AdcmEx as e: raise AnsibleError(e.code + ": " + e.msg) from e diff --git a/python/ansible/plugins/lookup/adcm_config.py b/python/ansible/plugins/lookup/adcm_config.py index d50525cf5f..de26f7be40 100644 --- a/python/ansible/plugins/lookup/adcm_config.py +++ b/python/ansible/plugins/lookup/adcm_config.py @@ -83,13 +83,9 @@ def run(self, terms, variables=None, **kwargs): # pylint: disable=too-many-bran raise AnsibleError('there is no cluster in hostvars') cluster = variables['cluster'] if 'service_name' in kwargs: - res = set_service_config_by_name( - cluster['id'], kwargs['service_name'], terms[1], terms[2] - ) + res = set_service_config_by_name(cluster['id'], kwargs['service_name'], terms[1], terms[2]) elif 'job' in variables and 'service_id' in variables['job']: - res = set_service_config( - cluster['id'], variables['job']['service_id'], terms[1], terms[2] - ) + res = set_service_config(cluster['id'], variables['job']['service_id'], terms[1], terms[2]) else: msg = 'no service_id in job or service_name and service_version in params' raise AnsibleError(msg) diff --git a/python/ansible/plugins/lookup/adcm_state.py b/python/ansible/plugins/lookup/adcm_state.py index add2ac3eb6..f1d9ef4921 100644 --- a/python/ansible/plugins/lookup/adcm_state.py +++ b/python/ansible/plugins/lookup/adcm_state.py @@ -10,7 +10,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -# pylint: disable=wrong-import-position, import-error +# pylint: disable=wrong-import-position from ansible.errors import AnsibleError diff --git a/python/ansible_secret.py b/python/ansible_secret.py index 7dbc4a1162..b8fab8341b 100755 --- a/python/ansible_secret.py +++ b/python/ansible_secret.py @@ -11,7 +11,9 @@ # See the License for the specific language governing permissions and # limitations under the License. -from cm import config +from django.conf import settings -if __name__ == '__main__': - print(config.ANSIBLE_SECRET) +import adcm.init_django # pylint: disable=unused-import + +if __name__ == "__main__": + print(settings.ANSIBLE_SECRET) diff --git a/python/api/action/serializers.py b/python/api/action/serializers.py index a91550b8f9..b4f25f2928 100644 --- a/python/api/action/serializers.py +++ b/python/api/action/serializers.py @@ -10,122 +10,149 @@ # See the License for the specific language governing permissions and # limitations under the License. -# pylint: disable=redefined-builtin - -from rest_framework import serializers from rest_framework.reverse import reverse - -import cm.adcm_config -import cm.job +from rest_framework.serializers import ( + BooleanField, + CharField, + HyperlinkedIdentityField, + HyperlinkedModelSerializer, + IntegerField, + JSONField, + SerializerMethodField, +) + +from adcm.serializers import EmptySerializer from api.config.serializers import ConfigSerializerUI from api.utils import get_api_url_kwargs -from cm.models import PrototypeConfig, SubAction +from cm.adcm_config import get_action_variant, get_prototype_config +from cm.models import Action, PrototypeConfig, SubAction + + +class ActionJobSerializer(HyperlinkedModelSerializer): + class Meta: + model = Action + fields = ( + "name", + "display_name", + "prototype_id", + "prototype_name", + "prototype_type", + "prototype_version", + ) + +class ActionDetailURL(HyperlinkedIdentityField): + def get_url(self, obj, view_name, request, _format): + kwargs = get_api_url_kwargs(self.context.get("object"), request) + kwargs["action_id"] = obj.id -class ActionDetailURL(serializers.HyperlinkedIdentityField): - def get_url(self, obj, view_name, request, format): - kwargs = get_api_url_kwargs(self.context.get('object'), request) - kwargs['action_id'] = obj.id - return reverse(view_name, kwargs=kwargs, request=request, format=format) + return reverse(view_name, kwargs=kwargs, request=request, format=_format) -class HostActionDetailURL(serializers.HyperlinkedIdentityField): - def get_url(self, obj, view_name, request, format): - objects = self.context.get('objects') - if obj.host_action and 'host' in objects: - kwargs = get_api_url_kwargs(objects['host'], request) +class HostActionDetailURL(HyperlinkedIdentityField): + def get_url(self, obj, view_name, request, _format): + objects = self.context.get("objects") + if obj.host_action and "host" in objects: + kwargs = get_api_url_kwargs(objects["host"], request) else: kwargs = get_api_url_kwargs(objects[obj.prototype.type], request) - kwargs['action_id'] = obj.id - return reverse(view_name, kwargs=kwargs, request=request, format=format) - - -class StackActionSerializer(serializers.Serializer): - id = serializers.IntegerField(read_only=True) - prototype_id = serializers.IntegerField() - name = serializers.CharField() - type = serializers.CharField() - display_name = serializers.CharField(required=False) - description = serializers.CharField(required=False) - ui_options = serializers.JSONField(required=False) - button = serializers.CharField(required=False) - script = serializers.CharField() - script_type = serializers.CharField() - state_on_success = serializers.CharField() - state_on_fail = serializers.CharField() - hostcomponentmap = serializers.JSONField(required=False) - allow_to_terminate = serializers.BooleanField(read_only=True) - partial_execution = serializers.BooleanField(read_only=True) - host_action = serializers.BooleanField(read_only=True) - disabling_cause = serializers.CharField(read_only=True) + + kwargs["action_id"] = obj.id + + return reverse(view_name, kwargs=kwargs, request=request, format=_format) + + +class StackActionSerializer(EmptySerializer): + id = IntegerField(read_only=True) + prototype_id = IntegerField() + name = CharField() + type = CharField() + display_name = CharField(required=False) + description = CharField(required=False) + ui_options = JSONField(required=False) + script = CharField() + script_type = CharField() + state_on_success = CharField() + state_on_fail = CharField() + hostcomponentmap = JSONField(required=False) + allow_to_terminate = BooleanField(read_only=True) + partial_execution = BooleanField(read_only=True) + host_action = BooleanField(read_only=True) + start_impossible_reason = SerializerMethodField() + + def get_start_impossible_reason(self, action: Action): + if self.context.get("obj"): + return action.get_start_impossible_reason(self.context["obj"]) + + return None class ActionSerializer(StackActionSerializer): - url = HostActionDetailURL(read_only=True, view_name='object-action-details') + url = HostActionDetailURL(read_only=True, view_name="object-action-details") -class ActionShort(serializers.Serializer): - name = serializers.CharField() - display_name = serializers.CharField(required=False) - button = serializers.CharField(required=False) - config = serializers.SerializerMethodField() - hostcomponentmap = serializers.JSONField(read_only=False) - run = ActionDetailURL(read_only=True, view_name='run-task') +class ActionShort(EmptySerializer): + name = CharField() + display_name = CharField(required=False) + config = SerializerMethodField() + hostcomponentmap = JSONField(read_only=False) + run = ActionDetailURL(read_only=True, view_name="run-task") def get_config(self, obj): context = self.context - context['prototype'] = obj.prototype - _, _, _, attr = cm.adcm_config.get_prototype_config(obj.prototype, obj) - cm.adcm_config.get_action_variant(context.get('object'), obj.config) + context["prototype"] = obj.prototype + _, _, _, attr = get_prototype_config(obj.prototype, obj) + get_action_variant(context.get("object"), obj.config) conf = ConfigSerializerUI(obj.config, many=True, context=context, read_only=True) - return {'attr': attr, 'config': conf.data} + + return {"attr": attr, "config": conf.data} -class SubActionSerializer(serializers.Serializer): - name = serializers.CharField() - display_name = serializers.CharField(required=False) - script = serializers.CharField() - script_type = serializers.CharField() - state_on_fail = serializers.CharField(required=False) - params = serializers.JSONField(required=False) +class SubActionSerializer(EmptySerializer): + name = CharField() + display_name = CharField(required=False) + script = CharField() + script_type = CharField() + state_on_fail = CharField(required=False) + params = JSONField(required=False) class StackActionDetailSerializer(StackActionSerializer): - state_available = serializers.JSONField() - state_unavailable = serializers.JSONField() - multi_state_available = serializers.JSONField() - multi_state_unavailable = serializers.JSONField() - params = serializers.JSONField(required=False) - log_files = serializers.JSONField(required=False) - config = serializers.SerializerMethodField() - subs = serializers.SerializerMethodField() - disabling_cause = serializers.CharField(read_only=True) + state_available = JSONField() + state_unavailable = JSONField() + multi_state_available = JSONField() + multi_state_unavailable = JSONField() + params = JSONField(required=False) + log_files = JSONField(required=False) + config = SerializerMethodField() + subs = SerializerMethodField() + disabling_cause = CharField(read_only=True) def get_config(self, obj): - aconf = PrototypeConfig.objects.filter(prototype=obj.prototype, action=obj).order_by('id') + aconf = PrototypeConfig.objects.filter(prototype=obj.prototype, action=obj).order_by("id") context = self.context - context['prototype'] = obj.prototype + context["prototype"] = obj.prototype conf = ConfigSerializerUI(aconf, many=True, context=context, read_only=True) - _, _, _, attr = cm.adcm_config.get_prototype_config(obj.prototype, obj) - return {'attr': attr, 'config': conf.data} + _, _, _, attr = get_prototype_config(obj.prototype, obj) + return {"attr": attr, "config": conf.data} def get_subs(self, obj): - sub_actions = SubAction.objects.filter(action=obj).order_by('id') + sub_actions = SubAction.objects.filter(action=obj).order_by("id") subs = SubActionSerializer(sub_actions, many=True, context=self.context, read_only=True) + return subs.data class ActionDetailSerializer(StackActionDetailSerializer): - run = HostActionDetailURL(read_only=True, view_name='run-task') + run = HostActionDetailURL(read_only=True, view_name="run-task") class ActionUISerializer(ActionDetailSerializer): def get_config(self, obj): - action_obj = self.context['obj'] - action_conf = PrototypeConfig.objects.filter(prototype=obj.prototype, action=obj).order_by( - 'id' - ) - _, _, _, attr = cm.adcm_config.get_prototype_config(obj.prototype, obj) - cm.adcm_config.get_action_variant(action_obj, action_conf) + action_obj = self.context["obj"] + action_conf = PrototypeConfig.objects.filter(prototype=obj.prototype, action=obj).order_by("id") + _, _, _, attr = get_prototype_config(obj.prototype, obj) + get_action_variant(action_obj, action_conf) conf = ConfigSerializerUI(action_conf, many=True, context=self.context, read_only=True) - return {'attr': attr, 'config': conf.data} + + return {"attr": attr, "config": conf.data} diff --git a/python/api/action/views.py b/python/api/action/views.py index 1b72d25b7c..4b49747fa9 100644 --- a/python/api/action/views.py +++ b/python/api/action/views.py @@ -12,6 +12,7 @@ from itertools import compress +from django.conf import settings from django.contrib.contenttypes.models import ContentType from guardian.mixins import PermissionListMixin from rest_framework.exceptions import PermissionDenied @@ -24,32 +25,21 @@ ActionUISerializer, ) from api.base_view import GenericUIView -from api.job.serializers import RunTaskSerializer -from api.utils import ( - ActionFilter, - AdcmFilterBackend, - create, - filter_actions, - get_object_for_user, - set_disabling_cause, -) +from api.job.serializers import RunTaskRetrieveSerializer +from api.utils import AdcmFilterBackend, create, filter_actions, get_object_for_user from audit.utils import audit from cm.errors import AdcmEx -from cm.models import ( - Action, - Host, - HostComponent, - MaintenanceModeType, - TaskLog, - get_model_by_type, -) +from cm.models import Action, Host, HostComponent, TaskLog, get_model_by_type from rbac.viewsets import DjangoOnlyObjectPermissions +VIEW_ACTION_PERM = "cm.view_action" + def get_object_type_id(**kwargs): - object_type = kwargs.get('object_type') - object_id = kwargs.get(f'{object_type}_id') - action_id = kwargs.get('action_id', None) + object_type = kwargs.get("object_type") + # TODO: this is a temporary patch for `action` endpoint + object_id = kwargs.get(f"{object_type}_id") or kwargs.get(f"{object_type}_pk") + action_id = kwargs.get("action_id", None) return object_type, object_id, action_id @@ -63,34 +53,27 @@ def get_obj(**kwargs): class ActionList(PermissionListMixin, GenericUIView): - queryset = Action.objects.filter(upgrade__isnull=True) + queryset = Action.objects.filter(upgrade__isnull=True).exclude(name__in=settings.ADCM_SERVICE_ACTION_NAMES_SET) serializer_class = ActionSerializer serializer_class_ui = ActionUISerializer - filterset_class = ActionFilter - filterset_fields = ('name', 'button', 'button_is_null') + filterset_fields = ("name",) filter_backends = (AdcmFilterBackend,) - permission_required = ['cm.view_action'] + permission_required = [VIEW_ACTION_PERM] - def _get_host_actions(self, host: Host) -> set: - actions = set( - filter_actions( - host, self.filter_queryset(self.get_queryset().filter(prototype=host.prototype)) - ) - ) + def _get_actions_for_host(self, host: Host) -> set: + actions = set(filter_actions(host, self.filter_queryset(self.get_queryset().filter(prototype=host.prototype)))) hcs = HostComponent.objects.filter(host_id=host.id) if hcs: for hc in hcs: - cluster, _ = get_obj(object_type='cluster', cluster_id=hc.cluster_id) - service, _ = get_obj(object_type='service', service_id=hc.service_id) - component, _ = get_obj(object_type='component', component_id=hc.component_id) + cluster, _ = get_obj(object_type="cluster", cluster_id=hc.cluster_id) + service, _ = get_obj(object_type="service", service_id=hc.service_id) + component, _ = get_obj(object_type="component", component_id=hc.component_id) for connect_obj in [cluster, service, component]: actions.update( filter_actions( connect_obj, self.filter_queryset( - self.get_queryset().filter( - prototype=connect_obj.prototype, host_action=True - ) + self.get_queryset().filter(prototype=connect_obj.prototype, host_action=True) ), ) ) @@ -100,9 +83,7 @@ def _get_host_actions(self, host: Host) -> set: filter_actions( host.cluster, self.filter_queryset( - self.get_queryset().filter( - prototype=host.cluster.prototype, host_action=True - ) + self.get_queryset().filter(prototype=host.cluster.prototype, host_action=True) ), ) ) @@ -110,35 +91,27 @@ def _get_host_actions(self, host: Host) -> set: return actions def get(self, request, *args, **kwargs): # pylint: disable=too-many-locals - """ - List all actions of a specified object - """ - if kwargs['object_type'] == 'host': - host, _ = get_obj(object_type='host', host_id=kwargs['host_id']) - if host.maintenance_mode == MaintenanceModeType.On: - actions = set() - else: - actions = self._get_host_actions(host) + if kwargs["object_type"] == "host": + host, _ = get_obj(object_type="host", host_id=kwargs["host_id"]) + actions = self._get_actions_for_host(host) obj = host - objects = {'host': host} + objects = {"host": host} else: obj, _ = get_obj(**kwargs) actions = filter_actions( obj, - self.filter_queryset( - self.get_queryset().filter(prototype=obj.prototype, host_action=False) - ), + self.filter_queryset(self.get_queryset().filter(prototype=obj.prototype, host_action=False)), ) objects = {obj.prototype.type: obj} # added filter actions by custom perm for run actions - perms = [f'cm.run_action_{a.display_name}' for a in actions] + perms = [f"cm.run_action_{a.display_name}" for a in actions] mask = [request.user.has_perm(perm, obj) for perm in perms] actions = list(compress(actions, mask)) serializer = self.get_serializer( - actions, many=True, context={'request': request, 'objects': objects, 'obj': obj} + actions, many=True, context={"request": request, "objects": objects, "obj": obj} ) return Response(serializer.data) @@ -149,89 +122,58 @@ class ActionDetail(PermissionListMixin, GenericUIView): serializer_class = ActionDetailSerializer serializer_class_ui = ActionUISerializer permission_classes = (DjangoOnlyObjectPermissions,) - permission_required = ['cm.view_action'] + permission_required = [VIEW_ACTION_PERM] def get(self, request, *args, **kwargs): - """ - Show specified action - """ object_type, object_id, action_id = get_object_type_id(**kwargs) model = get_model_by_type(object_type) ct = ContentType.objects.get_for_model(model) - obj = get_object_for_user( - request.user, f'{ct.app_label}.view_{ct.model}', model, id=object_id - ) + obj = get_object_for_user(request.user, f"{ct.app_label}.view_{ct.model}", model, id=object_id) # TODO: we can access not only the actions of this object action = get_object_for_user( request.user, - 'cm.view_action', + VIEW_ACTION_PERM, self.get_queryset(), id=action_id, ) - set_disabling_cause(obj, action) if isinstance(obj, Host) and action.host_action: - objects = {'host': obj} + objects = {"host": obj} else: objects = {action.prototype.type: obj} - serializer = self.get_serializer( - action, context={'request': request, 'objects': objects, 'obj': obj} - ) + + serializer = self.get_serializer(action, context={"request": request, "objects": objects, "obj": obj}) return Response(serializer.data) class RunTask(GenericUIView): queryset = TaskLog.objects.all() - serializer_class = RunTaskSerializer + serializer_class = RunTaskRetrieveSerializer permission_classes = (IsAuthenticated,) def has_action_perm(self, action, obj): user = self.request.user - if user.has_perm('cm.add_task'): + if user.has_perm("cm.add_task"): return True - return user.has_perm(f'cm.run_action_{action.display_name}', obj) + return user.has_perm(f"cm.run_action_{action.display_name}", obj) def check_action_perm(self, action, obj): if not self.has_action_perm(action, obj): raise PermissionDenied() - @staticmethod - def check_disabling_cause(action, obj): - if isinstance(obj, Host) and obj.maintenance_mode == MaintenanceModeType.On: - raise AdcmEx( - 'ACTION_ERROR', - msg='you cannot start an action on a host that is in maintenance mode', - ) - - set_disabling_cause(obj, action) - if action.disabling_cause == 'maintenance_mode': - raise AdcmEx( - 'ACTION_ERROR', - msg='you cannot start the action because at least one host is in maintenance mode', - ) - - if action.disabling_cause == 'no_ldap_settings': - raise AdcmEx( - 'ACTION_ERROR', - msg='you cannot start the action because ldap settings not configured completely', - ) - @audit def post(self, request, *args, **kwargs): - """ - Ran specified action - """ object_type, object_id, action_id = get_object_type_id(**kwargs) model = get_model_by_type(object_type) ct = ContentType.objects.get_for_model(model) - obj = get_object_for_user( - request.user, f'{ct.app_label}.view_{ct.model}', model, id=object_id - ) - action = get_object_for_user(request.user, 'cm.view_action', Action, id=action_id) + obj = get_object_for_user(request.user, f"{ct.app_label}.view_{ct.model}", model, id=object_id) + action = get_object_for_user(request.user, VIEW_ACTION_PERM, Action, id=action_id) + if reason := action.get_start_impossible_reason(obj): + raise AdcmEx("ACTION_ERROR", msg=reason) + self.check_action_perm(action, obj) - self.check_disabling_cause(action, obj) serializer = self.get_serializer(data=request.data) return create(serializer, action=action, task_object=obj) diff --git a/python/api/adcm/serializers.py b/python/api/adcm/serializers.py index 6a73277996..5d61b0ec93 100644 --- a/python/api/adcm/serializers.py +++ b/python/api/adcm/serializers.py @@ -10,37 +10,85 @@ # See the License for the specific language governing permissions and # limitations under the License. -from rest_framework import serializers +from rest_framework.serializers import ( + CharField, + HyperlinkedIdentityField, + HyperlinkedModelSerializer, + SerializerMethodField, +) from api.concern.serializers import ConcernItemSerializer from api.serializers import StringListSerializer -from api.utils import CommonAPIURL, hlink from cm.adcm_config import get_main_info +from cm.models import ADCM -class AdcmSerializer(serializers.Serializer): - id = serializers.IntegerField(read_only=True) - name = serializers.CharField() - prototype_id = serializers.IntegerField() - state = serializers.CharField(read_only=True) - url = hlink('adcm-details', 'id', 'adcm_id') +class ADCMSerializer(HyperlinkedModelSerializer): + class Meta: + model = ADCM + fields = ("id", "name", "state", "prototype_id", "url") + extra_kwargs = {"url": {"lookup_url_kwarg": "adcm_pk"}} -class AdcmDetailSerializer(AdcmSerializer): - prototype_version = serializers.SerializerMethodField() - bundle_id = serializers.IntegerField(read_only=True) - config = CommonAPIURL(view_name='object-config') - action = CommonAPIURL(view_name='object-action') +class ADCMRetrieveSerializer(HyperlinkedModelSerializer): + prototype_version = CharField( + read_only=True, + source="prototype.version", + ) multi_state = StringListSerializer(read_only=True) concerns = ConcernItemSerializer(many=True, read_only=True) - locked = serializers.BooleanField(read_only=True) + action = HyperlinkedIdentityField(view_name="object-action", lookup_url_kwarg="adcm_pk") + config = HyperlinkedIdentityField(view_name="object-config", lookup_url_kwarg="adcm_pk") - def get_prototype_version(self, obj): - return obj.prototype.version + class Meta: + model = ADCM + fields = ( + "id", + "name", + "prototype_id", + "bundle_id", + "state", + "locked", + "prototype_version", + "multi_state", + "concerns", + "action", + "config", + "url", + ) + extra_kwargs = {"url": {"lookup_url_kwarg": "adcm_pk"}} -class AdcmDetailUISerializer(AdcmDetailSerializer): - main_info = serializers.SerializerMethodField() +class ADCMUISerializer(HyperlinkedModelSerializer): + prototype_version = CharField( + read_only=True, + source="prototype.version", + ) + multi_state = StringListSerializer(read_only=True) + concerns = ConcernItemSerializer(many=True, read_only=True) + action = HyperlinkedIdentityField(view_name="object-action", lookup_url_kwarg="adcm_pk") + config = HyperlinkedIdentityField(view_name="object-config", lookup_url_kwarg="adcm_pk") + main_info = SerializerMethodField() + + class Meta: + model = ADCM + fields = ( + "id", + "name", + "prototype_id", + "bundle_id", + "state", + "locked", + "prototype_version", + "multi_state", + "concerns", + "action", + "config", + "main_info", + "url", + ) + extra_kwargs = {"url": {"lookup_url_kwarg": "adcm_pk"}} - def get_main_info(self, obj): + @staticmethod + def get_main_info(obj): return get_main_info(obj) diff --git a/python/api/adcm/urls.py b/python/api/adcm/urls.py index 063c33bdff..df4a6fd046 100644 --- a/python/api/adcm/urls.py +++ b/python/api/adcm/urls.py @@ -11,19 +11,15 @@ # limitations under the License. from django.urls import include, path +from rest_framework.routers import DefaultRouter -from api.adcm.views import AdcmDetail, AdcmList +from api.adcm.views import ADCMViewSet + +router = DefaultRouter() +router.register("", ADCMViewSet) urlpatterns = [ - path("", AdcmList.as_view(), name="adcm"), - path( - "/", - include( - [ - path("", AdcmDetail.as_view(), name="adcm-details"), - path("config/", include("api.config.urls"), {"object_type": "adcm"}), - path("action/", include("api.action.urls"), {"object_type": "adcm"}), - ] - ), - ), + *router.urls, + path(r"/config/", include("api.config.urls"), {"object_type": "adcm"}), + path(r"/action/", include("api.action.urls"), {"object_type": "adcm"}), ] diff --git a/python/api/adcm/views.py b/python/api/adcm/views.py index 3c03900f57..2be5159fb8 100644 --- a/python/api/adcm/views.py +++ b/python/api/adcm/views.py @@ -10,45 +10,29 @@ # See the License for the specific language governing permissions and # limitations under the License. -from rest_framework import permissions -from rest_framework.response import Response +from rest_framework.mixins import ListModelMixin, RetrieveModelMixin from api.adcm.serializers import ( - AdcmDetailSerializer, - AdcmDetailUISerializer, - AdcmSerializer, + ADCMRetrieveSerializer, + ADCMSerializer, + ADCMUISerializer, ) -from api.base_view import DetailView, GenericUIView +from api.base_view import GenericUIViewSet from cm.models import ADCM -class AdcmList(GenericUIView): - """ - get: - List adcm object - """ - - queryset = ADCM.objects.all() - serializer_class = AdcmSerializer - serializer_class_ui = AdcmDetailUISerializer - permission_classes = (permissions.IsAuthenticated,) - - def get(self, request, *args, **kwargs): - obj = self.get_queryset() - serializer = self.get_serializer(obj, many=True) - return Response(serializer.data) - - -class AdcmDetail(DetailView): - """ - get: - Show adcm object - """ - - queryset = ADCM.objects.all() - serializer_class = AdcmDetailSerializer - serializer_class_ui = AdcmDetailUISerializer - permission_classes = (permissions.IsAuthenticated,) - lookup_field = 'id' - lookup_url_kwarg = 'adcm_id' - error_code = 'ADCM_NOT_FOUND' +# pylint:disable-next=too-many-ancestors +class ADCMViewSet(ListModelMixin, RetrieveModelMixin, GenericUIViewSet): + queryset = ADCM.objects.select_related("prototype").all() + serializer_class = ADCMSerializer + lookup_url_kwarg = "adcm_pk" + + def get_serializer_class(self): + + if self.is_for_ui(): + return ADCMUISerializer + + if self.action == "retrieve": + return ADCMRetrieveSerializer + + return super().get_serializer_class() diff --git a/python/api/base_view.py b/python/api/base_view.py index 86720e07d0..b56cd13fad 100644 --- a/python/api/base_view.py +++ b/python/api/base_view.py @@ -13,6 +13,7 @@ # pylint: disable=not-callable, unused-import, too-many-locals import rest_framework.pagination +from django.conf import settings from django.core.exceptions import FieldError, ObjectDoesNotExist from rest_framework import serializers from rest_framework.generics import GenericAPIView @@ -22,7 +23,6 @@ from rest_framework.viewsets import ViewSetMixin from adcm.permissions import DjangoObjectPermissionsAudit -from adcm.settings import REST_FRAMEWORK from api.utils import AdcmFilterBackend, AdcmOrderingFilter, getlist_from_querydict from audit.utils import audit from cm.errors import AdcmEx @@ -37,6 +37,7 @@ def has_permission(self, request, view): else: queryset = self._queryset(view) perms = self.get_required_permissions(request.method, queryset.model) + return request.user.has_perms(perms) return False @@ -58,18 +59,18 @@ class GenericUIView(GenericAPIView): def _is_for_ui(self) -> bool: if not self.request: return False - view = self.request.query_params.get('view', None) - return view == 'interface' + view = self.request.query_params.get("view", None) + return view == "interface" def get_serializer_class(self): if self.request is not None: - if self.request.method == 'POST': + if self.request.method == "POST": if self.serializer_class_post: return self.serializer_class_post - elif self.request.method == 'PUT': + elif self.request.method == "PUT": if self.serializer_class_put: return self.serializer_class_put - elif self.request.method == 'PATCH': + elif self.request.method == "PATCH": if self.serializer_class_patch: return self.serializer_class_patch elif self._is_for_ui(): @@ -79,8 +80,14 @@ def get_serializer_class(self): return super().get_serializer_class() -class GenericUIViewSet(ViewSetMixin, GenericUIView): - """GenericUIView with expanded for ViewSet""" +class GenericUIViewSet(ViewSetMixin, GenericAPIView): + def is_for_ui(self) -> bool: + if not self.request: + return False + + view = self.request.query_params.get("view") + + return view == "interface" class PaginatedView(GenericUIView): @@ -97,8 +104,8 @@ def get_ordering(request, queryset, view): return AdcmOrderingFilter().get_ordering(request, queryset, view) def is_paged(self, request): - limit = self.request.query_params.get('limit', False) - offset = self.request.query_params.get('offset', False) + limit = self.request.query_params.get("limit", False) + offset = self.request.query_params.get("offset", False) return bool(limit or offset) @@ -114,15 +121,15 @@ def get_page(self, obj, request, context=None): if not context: context = {} - context['request'] = request + context["request"] = request count = obj.count() serializer_class = self.get_serializer_class() - if 'fields' in request.query_params or 'distinct' in request.query_params: + if "fields" in request.query_params or "distinct" in request.query_params: serializer_class = None try: - fields = getlist_from_querydict(request.query_params, 'fields') - distinct = int(request.query_params.get('distinct', 0)) + fields = getlist_from_querydict(request.query_params, "fields") + distinct = int(request.query_params.get("distinct", 0)) if fields and distinct: obj = obj.values(*fields).distinct() @@ -130,16 +137,10 @@ def get_page(self, obj, request, context=None): obj = obj.values(*fields) except (FieldError, ValueError): - qp = ','.join( - [ - f'{k}={v}' - for k, v in request.query_params.items() - if k in ['fields', 'distinct'] - ] - ) - msg = f'Bad query params: {qp}' + qp = ",".join([f"{k}={v}" for k, v in request.query_params.items() if k in ["fields", "distinct"]]) + msg = f"Bad query params: {qp}" - raise AdcmEx('BAD_QUERY_PARAMS', msg=msg) from None + raise AdcmEx("BAD_QUERY_PARAMS", msg=msg) from None page = self.paginate_queryset(obj) if self.is_paged(request): @@ -149,16 +150,16 @@ def get_page(self, obj, request, context=None): return self.get_paginated_response(page) - if count <= REST_FRAMEWORK['PAGE_SIZE']: + if count <= settings.REST_FRAMEWORK["PAGE_SIZE"]: if serializer_class is not None: serializer = serializer_class(obj, many=True, context=context) obj = serializer.data return Response(obj) - msg = 'Response is too long, use paginated request' + msg = "Response is too long, use paginated request" - raise AdcmEx('TOO_LONG', msg=msg, args=self.get_paged_link()) + raise AdcmEx("TOO_LONG", msg=msg, args=self.get_paged_link()) def get(self, request, *args, **kwargs): obj = self.filter_queryset(self.get_queryset()) @@ -172,7 +173,7 @@ class DetailView(GenericUIView): extended with selection of serializer class """ - error_code = 'OBJECT_NOT_FOUND' + error_code = "OBJECT_NOT_FOUND" def check_obj(self, kw_req): try: diff --git a/python/api/cluster/serializers.py b/python/api/cluster/serializers.py index 4e0450f710..0dffbd946c 100644 --- a/python/api/cluster/serializers.py +++ b/python/api/cluster/serializers.py @@ -31,21 +31,15 @@ from api.group_config.serializers import GroupConfigsHyperlinkedIdentityField from api.host.serializers import HostSerializer from api.serializers import DoUpgradeSerializer, StringListSerializer -from api.utils import ( - CommonAPIURL, - ObjectURL, - UrlField, - check_obj, - filter_actions, - get_upgradable_func, -) +from api.utils import CommonAPIURL, ObjectURL, UrlField, check_obj, filter_actions from api.validators import StartMidEndValidator from cm.adcm_config import get_main_info from cm.api import add_cluster, add_hc, bind, multi_bind from cm.errors import AdcmEx -from cm.models import Action, Cluster, Host, Prototype, ServiceComponent, Upgrade +from cm.issue import update_hierarchy_issues +from cm.models import Action, Cluster, Host, Prototype, ServiceComponent from cm.status_api import get_cluster_status, get_hc_status -from cm.upgrade import do_upgrade +from cm.upgrade import get_upgrade def get_cluster_id(obj): @@ -82,9 +76,7 @@ class ClusterSerializer(Serializer): description = CharField(help_text="Cluster description", required=False) state = CharField(read_only=True) before_upgrade = JSONField(read_only=True) - url = HyperlinkedIdentityField( - view_name="cluster-details", lookup_field="id", lookup_url_kwarg="cluster_id" - ) + url = HyperlinkedIdentityField(view_name="cluster-details", lookup_field="id", lookup_url_kwarg="cluster_id") @staticmethod def validate_prototype_id(prototype_id): @@ -101,18 +93,51 @@ def update(self, instance, validated_data): instance.name = validated_data.get("name", instance.name) instance.description = validated_data.get("description", instance.description) instance.save() + return instance -class ClusterUISerializer(ClusterSerializer): +class ClusterDetailSerializer(ClusterSerializer): + bundle_id = IntegerField(read_only=True) + edition = CharField(read_only=True) + license = CharField(read_only=True) + action = CommonAPIURL(view_name="object-action") + service = ObjectURL(view_name="service") + host = ObjectURL(view_name="host") + hostcomponent = HyperlinkedIdentityField( + view_name="host-component", lookup_field="id", lookup_url_kwarg="cluster_id" + ) + status = SerializerMethodField() + status_url = HyperlinkedIdentityField(view_name="cluster-status", lookup_field="id", lookup_url_kwarg="cluster_id") + config = CommonAPIURL(view_name="object-config") + serviceprototype = HyperlinkedIdentityField( + view_name="cluster-service-prototype", lookup_field="id", lookup_url_kwarg="cluster_id" + ) + upgrade = HyperlinkedIdentityField(view_name="cluster-upgrade", lookup_field="id", lookup_url_kwarg="cluster_id") + imports = HyperlinkedIdentityField(view_name="cluster-import", lookup_field="id", lookup_url_kwarg="cluster_id") + bind = HyperlinkedIdentityField(view_name="cluster-bind", lookup_field="id", lookup_url_kwarg="cluster_id") + prototype = HyperlinkedIdentityField( + view_name="cluster-prototype-detail", + lookup_field="pk", + lookup_url_kwarg="prototype_pk", + ) + multi_state = StringListSerializer(read_only=True) + concerns = ConcernItemSerializer(many=True, read_only=True) + locked = BooleanField(read_only=True) + group_config = GroupConfigsHyperlinkedIdentityField(view_name="group-config-list") + + @staticmethod + def get_status(obj: Cluster) -> int: + return get_cluster_status(obj) + + +class ClusterUISerializer(ClusterDetailSerializer): action = CommonAPIURL(view_name="object-action") edition = CharField(read_only=True) prototype_version = SerializerMethodField() prototype_name = SerializerMethodField() prototype_display_name = SerializerMethodField() - upgrade = HyperlinkedIdentityField( - view_name="cluster-upgrade", lookup_field="id", lookup_url_kwarg="cluster_id" - ) + upgrade = HyperlinkedIdentityField(view_name="cluster-upgrade", lookup_field="id", lookup_url_kwarg="cluster_id") upgradable = SerializerMethodField() concerns = ConcernItemUISerializer(many=True, read_only=True) locked = BooleanField(read_only=True) @@ -120,7 +145,7 @@ class ClusterUISerializer(ClusterSerializer): @staticmethod def get_upgradable(obj: Cluster) -> bool: - return get_upgradable_func(obj) + return bool(get_upgrade(obj)) @staticmethod def get_prototype_version(obj: Cluster) -> str: @@ -139,48 +164,6 @@ def get_status(obj: Cluster) -> int: return get_cluster_status(obj) -class ClusterDetailSerializer(ClusterSerializer): - bundle_id = IntegerField(read_only=True) - edition = CharField(read_only=True) - license = CharField(read_only=True) - action = CommonAPIURL(view_name="object-action") - service = ObjectURL(view_name="service") - host = ObjectURL(view_name="host") - hostcomponent = HyperlinkedIdentityField( - view_name="host-component", lookup_field="id", lookup_url_kwarg="cluster_id" - ) - status = SerializerMethodField() - status_url = HyperlinkedIdentityField( - view_name="cluster-status", lookup_field="id", lookup_url_kwarg="cluster_id" - ) - config = CommonAPIURL(view_name="object-config") - serviceprototype = HyperlinkedIdentityField( - view_name="cluster-service-prototype", lookup_field="id", lookup_url_kwarg="cluster_id" - ) - upgrade = HyperlinkedIdentityField( - view_name="cluster-upgrade", lookup_field="id", lookup_url_kwarg="cluster_id" - ) - imports = HyperlinkedIdentityField( - view_name="cluster-import", lookup_field="id", lookup_url_kwarg="cluster_id" - ) - bind = HyperlinkedIdentityField( - view_name="cluster-bind", lookup_field="id", lookup_url_kwarg="cluster_id" - ) - prototype = HyperlinkedIdentityField( - view_name="cluster-type-details", - lookup_field="prototype_id", - lookup_url_kwarg="prototype_id", - ) - multi_state = StringListSerializer(read_only=True) - concerns = ConcernItemSerializer(many=True, read_only=True) - locked = BooleanField(read_only=True) - group_config = GroupConfigsHyperlinkedIdentityField(view_name="group-config-list") - - @staticmethod - def get_status(obj: Cluster) -> int: - return get_cluster_status(obj) - - class ClusterUpdateSerializer(EmptySerializer): id = IntegerField(read_only=True) name = CharField( @@ -201,16 +184,14 @@ class ClusterUpdateSerializer(EmptySerializer): description = CharField(required=False, help_text="Cluster description") def update(self, instance, validated_data): - if ( - validated_data.get("name") - and validated_data.get("name") != instance.name - and instance.state != "created" - ): + if validated_data.get("name") and validated_data.get("name") != instance.name and instance.state != "created": raise ValidationError("Name change is available only in the 'created' state") instance.name = validated_data.get("name", instance.name) instance.description = validated_data.get("description", instance.description) instance.save() + update_hierarchy_issues(instance) + return instance @@ -228,11 +209,12 @@ def get_actions(self, obj): self.context["object"] = obj self.context["cluster_id"] = obj.id actions = ActionShort(filter_actions(obj, act_set), many=True, context=self.context) + return actions.data @staticmethod def get_upgradable(obj: Cluster) -> bool: - return get_upgradable_func(obj) + return bool(get_upgrade(obj)) @staticmethod def get_prototype_version(obj: Cluster) -> str: @@ -268,6 +250,7 @@ def to_representation(self, instance): data["monitoring"] = instance.component.prototype.monitoring status = get_hc_status(instance) data["status"] = status + return data @@ -287,9 +270,7 @@ def get_kwargs(self, obj): component_id = IntegerField(read_only=True, help_text="component id") state = CharField(read_only=True, required=False) url = MyUrlField(read_only=True, view_name="host-comp-details") - host_url = HyperlinkedIdentityField( - view_name="host-details", lookup_field="host_id", lookup_url_kwarg="host_id" - ) + host_url = HyperlinkedIdentityField(view_name="host-details", lookup_field="host_id", lookup_url_kwarg="host_id") def to_representation(self, instance): data = super().to_representation(instance) @@ -299,6 +280,7 @@ def to_representation(self, instance): data["service_name"] = instance.service.prototype.name data["service_display_name"] = instance.service.prototype.display_name data["service_version"] = instance.service.prototype.version + return data @@ -309,10 +291,12 @@ class HostComponentUISerializer(EmptySerializer): def get_host(self, obj): hosts = Host.objects.filter(cluster=self.context.get("cluster")) + return HostSerializer(hosts, many=True, context=self.context).data def get_component(self, obj): comps = ServiceComponent.objects.filter(cluster=self.context.get("cluster")) + return HCComponentSerializer(comps, many=True, context=self.context).data @@ -323,17 +307,22 @@ class HostComponentSaveSerializer(EmptySerializer): def validate_hc(hc): if not hc: raise AdcmEx("INVALID_INPUT", "hc field is required") + if not isinstance(hc, list): raise AdcmEx("INVALID_INPUT", "hc field should be a list") + for item in hc: for key in ("component_id", "host_id", "service_id"): if key not in item: msg = '"{}" sub-field is required' + raise AdcmEx("INVALID_INPUT", msg.format(key)) + return hc def create(self, validated_data): hc = validated_data.get("hc") + return add_hc(self.context.get("cluster"), hc) @@ -360,6 +349,7 @@ def get_service_display_name(obj): def get_requires(obj): if not obj.prototype.requires: return None + comp_list = {} def process_requires(req_list): @@ -372,10 +362,13 @@ def process_requires(req_list): ) if _comp == obj.prototype: return + if _comp.name not in comp_list: comp_list[_comp.name] = {"components": {}, "service": _comp.parent} + if _comp.name in comp_list[_comp.name]["components"]: return + comp_list[_comp.name]["components"][_comp.name] = _comp if _comp.requires: process_requires(_comp.requires) @@ -394,8 +387,10 @@ def process_requires(req_list): "display_name": comp.display_name, } ) + if not comp_out: continue + out.append( { "prototype_id": service.id, @@ -404,6 +399,7 @@ def process_requires(req_list): "components": comp_out, } ) + return out @@ -425,24 +421,28 @@ def get_export_cluster_prototype_name(obj): def get_export_service_name(obj): if obj.source_service: return obj.source_service.prototype.name + return None @staticmethod def get_export_service_id(obj): if obj.source_service: return obj.source_service.id + return None @staticmethod def get_import_service_id(obj): if obj.service: return obj.service.id + return None @staticmethod def get_import_service_name(obj): if obj.service: return obj.service.prototype.name + return None @@ -463,6 +463,7 @@ class DoBindSerializer(EmptySerializer): def create(self, validated_data): export_cluster = check_obj(Cluster, validated_data.get("export_cluster_id")) + return bind( validated_data.get("cluster"), None, @@ -478,19 +479,13 @@ def create(self, validated_data): bind_data = validated_data.get("bind") cluster = self.context.get("cluster") service = self.context.get("service") + return multi_bind(cluster, service, bind_data) class DoClusterUpgradeSerializer(DoUpgradeSerializer): hc = JSONField(required=False, default=list) - def create(self, validated_data): - upgrade = check_obj(Upgrade, validated_data.get("upgrade_id"), "UPGRADE_NOT_FOUND") - config = validated_data.get("config", {}) - attr = validated_data.get("attr", {}) - hc = validated_data.get("hc", []) - return do_upgrade(validated_data.get("obj"), upgrade, config, attr, hc) - class ClusterAuditSerializer(ModelSerializer): name = CharField(max_length=80, required=False) diff --git a/python/api/cluster/views.py b/python/api/cluster/views.py index cbb7c81401..07d7312f0f 100644 --- a/python/api/cluster/views.py +++ b/python/api/cluster/views.py @@ -13,8 +13,14 @@ from itertools import chain from guardian.mixins import PermissionListMixin -from rest_framework import permissions, status +from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response +from rest_framework.status import ( + HTTP_200_OK, + HTTP_201_CREATED, + HTTP_204_NO_CONTENT, + HTTP_400_BAD_REQUEST, +) from api.base_view import DetailView, GenericUIView, PaginatedView from api.cluster.serializers import ( @@ -35,9 +41,9 @@ ) from api.serializers import ClusterUpgradeSerializer from api.stack.serializers import ( - BundleServiceUISerializer, + BundleServiceUIPrototypeSerializer, ImportSerializer, - ServiceSerializer, + ServicePrototypeSerializer, ) from api.utils import ( AdcmOrderingFilter, @@ -60,7 +66,7 @@ Upgrade, ) from cm.status_api import make_ui_cluster_status -from cm.upgrade import get_upgrade +from cm.upgrade import do_upgrade, get_upgrade from rbac.viewsets import DjangoOnlyObjectPermissions VIEW_CLUSTER_PERM = "cm.view_cluster" @@ -129,18 +135,16 @@ def delete(self, request, *args, **kwargs): cluster = self.get_object() delete_cluster(cluster) - return Response(status=status.HTTP_204_NO_CONTENT) + return Response(status=HTTP_204_NO_CONTENT) class ClusterBundle(GenericUIView): queryset = Prototype.objects.filter(type="service") - serializer_class = ServiceSerializer - serializer_class_ui = BundleServiceUISerializer + serializer_class = ServicePrototypeSerializer + serializer_class_ui = BundleServiceUIPrototypeSerializer def get(self, request, *args, **kwargs): - cluster = get_object_for_user( - request.user, VIEW_CLUSTER_PERM, Cluster, id=kwargs["cluster_id"] - ) + cluster = get_object_for_user(request.user, VIEW_CLUSTER_PERM, Cluster, id=kwargs["cluster_id"]) check_custom_perm(request.user, "add_service_to", "cluster", cluster) bundle = self.get_queryset().filter(bundle=cluster.prototype.bundle) shared = self.get_queryset().filter(shared=True).exclude(bundle=cluster.prototype.bundle) @@ -155,13 +159,11 @@ class ClusterImport(GenericUIView): queryset = Prototype.objects.all() serializer_class = ImportSerializer serializer_class_post = PostImportSerializer - permission_classes = (permissions.IsAuthenticated,) + permission_classes = (IsAuthenticated,) @staticmethod def get(request, *args, **kwargs): - cluster = get_object_for_user( - request.user, VIEW_CLUSTER_PERM, Cluster, id=kwargs["cluster_id"] - ) + cluster = get_object_for_user(request.user, VIEW_CLUSTER_PERM, Cluster, id=kwargs["cluster_id"]) check_custom_perm(request.user, "view_import_of", "cluster", cluster, "view_clusterbind") res = get_import(cluster) @@ -169,31 +171,25 @@ def get(request, *args, **kwargs): @audit def post(self, request, *args, **kwargs): - cluster = get_object_for_user( - request.user, VIEW_CLUSTER_PERM, Cluster, id=kwargs["cluster_id"] - ) + cluster = get_object_for_user(request.user, VIEW_CLUSTER_PERM, Cluster, id=kwargs["cluster_id"]) check_custom_perm(request.user, "change_import_of", "cluster", cluster) - serializer = self.get_serializer( - data=request.data, context={"request": request, "cluster": cluster} - ) + serializer = self.get_serializer(data=request.data, context={"request": request, "cluster": cluster}) if serializer.is_valid(): res = serializer.create(serializer.validated_data) - return Response(res, status.HTTP_200_OK) + return Response(res, HTTP_200_OK) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + return Response(serializer.errors, status=HTTP_400_BAD_REQUEST) class ClusterBindList(GenericUIView): queryset = ClusterBind.objects.all() serializer_class = ClusterBindSerializer serializer_class_post = DoBindSerializer - permission_classes = (permissions.IsAuthenticated,) + permission_classes = (IsAuthenticated,) def get(self, request, *args, **kwargs): - cluster = get_object_for_user( - request.user, VIEW_CLUSTER_PERM, Cluster, id=kwargs["cluster_id"] - ) + cluster = get_object_for_user(request.user, VIEW_CLUSTER_PERM, Cluster, id=kwargs["cluster_id"]) check_custom_perm(request.user, "view_import_of", "cluster", cluster, "view_clusterbind") obj = self.get_queryset().filter(cluster=cluster, service=None) serializer = self.get_serializer(obj, many=True) @@ -202,9 +198,7 @@ def get(self, request, *args, **kwargs): @audit def post(self, request, *args, **kwargs): - cluster = get_object_for_user( - request.user, VIEW_CLUSTER_PERM, Cluster, id=kwargs["cluster_id"] - ) + cluster = get_object_for_user(request.user, VIEW_CLUSTER_PERM, Cluster, id=kwargs["cluster_id"]) check_custom_perm(request.user, "change_import_of", "cluster", cluster) serializer = self.get_serializer(data=request.data) @@ -214,7 +208,7 @@ def post(self, request, *args, **kwargs): class ClusterBindDetail(GenericUIView): queryset = ClusterBind.objects.all() serializer_class = BindSerializer - permission_classes = (permissions.IsAuthenticated,) + permission_classes = (IsAuthenticated,) @staticmethod def get_obj(kwargs, bind_id): @@ -225,9 +219,7 @@ def get_obj(kwargs, bind_id): return None def get(self, request, *args, **kwargs): - cluster = get_object_for_user( - request.user, VIEW_CLUSTER_PERM, Cluster, id=kwargs["cluster_id"] - ) + cluster = get_object_for_user(request.user, VIEW_CLUSTER_PERM, Cluster, id=kwargs["cluster_id"]) bind = check_obj(ClusterBind, {"cluster": cluster, "id": kwargs["bind_id"]}) check_custom_perm(request.user, "view_import_of", "cluster", cluster, "view_clusterbind") serializer = self.get_serializer(bind) @@ -236,35 +228,29 @@ def get(self, request, *args, **kwargs): @audit def delete(self, request, *args, **kwargs): - cluster = get_object_for_user( - request.user, VIEW_CLUSTER_PERM, Cluster, id=kwargs["cluster_id"] - ) + cluster = get_object_for_user(request.user, VIEW_CLUSTER_PERM, Cluster, id=kwargs["cluster_id"]) bind = check_obj(ClusterBind, {"cluster": cluster, "id": kwargs["bind_id"]}) check_custom_perm(request.user, "change_import_of", "cluster", cluster) unbind(bind) - return Response(status=status.HTTP_204_NO_CONTENT) + return Response(status=HTTP_204_NO_CONTENT) class ClusterUpgrade(GenericUIView): queryset = Upgrade.objects.all() serializer_class = ClusterUpgradeSerializer - permission_classes = (permissions.IsAuthenticated,) + permission_classes = (IsAuthenticated,) def get_ordering(self): order = AdcmOrderingFilter() return order.get_ordering(self.request, self.get_queryset(), self) def get(self, request, *args, **kwargs): - cluster = get_object_for_user( - request.user, VIEW_CLUSTER_PERM, Cluster, id=kwargs["cluster_id"] - ) + cluster = get_object_for_user(request.user, VIEW_CLUSTER_PERM, Cluster, id=kwargs["cluster_id"]) check_custom_perm(request.user, "view_upgrade_of", "cluster", cluster) update_hierarchy_issues(cluster) obj = get_upgrade(cluster, self.get_ordering()) - serializer = self.serializer_class( - obj, many=True, context={"cluster_id": cluster.id, "request": request} - ) + serializer = self.serializer_class(obj, many=True, context={"cluster_id": cluster.id, "request": request}) return Response(serializer.data) @@ -272,19 +258,13 @@ def get(self, request, *args, **kwargs): class ClusterUpgradeDetail(GenericUIView): queryset = Upgrade.objects.all() serializer_class = ClusterUpgradeSerializer - permission_classes = (permissions.IsAuthenticated,) + permission_classes = (IsAuthenticated,) def get(self, request, *args, **kwargs): - cluster = get_object_for_user( - request.user, VIEW_CLUSTER_PERM, Cluster, id=kwargs["cluster_id"] - ) + cluster = get_object_for_user(request.user, VIEW_CLUSTER_PERM, Cluster, id=kwargs["cluster_id"]) check_custom_perm(request.user, "view_upgrade_of", "cluster", cluster) - obj = check_obj( - Upgrade, {"id": kwargs["upgrade_id"], "bundle__name": cluster.prototype.bundle.name} - ) - serializer = self.serializer_class( - obj, context={"cluster_id": cluster.id, "request": request} - ) + obj = check_obj(Upgrade, {"id": kwargs["upgrade_id"], "bundle__name": cluster.prototype.bundle.name}) + serializer = self.serializer_class(obj, context={"cluster_id": cluster.id, "request": request}) return Response(serializer.data) @@ -292,28 +272,38 @@ def get(self, request, *args, **kwargs): class DoClusterUpgrade(GenericUIView): queryset = Upgrade.objects.all() serializer_class = DoClusterUpgradeSerializer - permission_classes = (permissions.IsAuthenticated,) + permission_classes = (IsAuthenticated,) @audit def post(self, request, *args, **kwargs): - cluster = get_object_for_user( - request.user, VIEW_CLUSTER_PERM, Cluster, id=kwargs["cluster_id"] - ) + cluster = get_object_for_user(request.user, VIEW_CLUSTER_PERM, Cluster, id=kwargs["cluster_id"]) check_custom_perm(request.user, "do_upgrade_of", "cluster", cluster) serializer = self.get_serializer(data=request.data) + if not serializer.is_valid(): + return Response(serializer.errors, status=HTTP_400_BAD_REQUEST) - return create(serializer, upgrade_id=int(kwargs["upgrade_id"]), obj=cluster) + upgrade = check_obj( + Upgrade, + kwargs.get("upgrade_id"), + "UPGRADE_NOT_FOUND", + ) + config = serializer.validated_data.get("config", {}) + attr = serializer.validated_data.get("attr", {}) + hc = serializer.validated_data.get("hc", []) + + return Response( + data=do_upgrade(cluster, upgrade, config, attr, hc), + status=HTTP_201_CREATED, + ) class StatusList(GenericUIView): - permission_classes = (permissions.IsAuthenticated,) + permission_classes = (IsAuthenticated,) queryset = HostComponent.objects.all() serializer_class = StatusSerializer def get(self, request, *args, **kwargs): - cluster = get_object_for_user( - request.user, VIEW_CLUSTER_PERM, Cluster, id=kwargs["cluster_id"] - ) + cluster = get_object_for_user(request.user, VIEW_CLUSTER_PERM, Cluster, id=kwargs["cluster_id"]) host_components = self.get_queryset().filter(cluster=cluster) if self._is_for_ui(): return Response(make_ui_cluster_status(cluster, host_components)) @@ -328,26 +318,16 @@ class HostComponentList(GenericUIView): serializer_class = HostComponentSerializer serializer_class_ui = HostComponentUISerializer serializer_class_post = HostComponentSaveSerializer - permission_classes = (permissions.IsAuthenticated,) + permission_classes = (IsAuthenticated,) def get(self, request, *args, **kwargs): - cluster = get_object_for_user( - request.user, VIEW_CLUSTER_PERM, Cluster, id=kwargs["cluster_id"] - ) - check_custom_perm( - request.user, "view_host_components_of", "cluster", cluster, "view_hostcomponent" - ) - hc = ( - self.get_queryset() - .prefetch_related("service", "component", "host") - .filter(cluster=cluster) - ) + cluster = get_object_for_user(request.user, VIEW_CLUSTER_PERM, Cluster, id=kwargs["cluster_id"]) + check_custom_perm(request.user, "view_host_components_of", "cluster", cluster, "view_hostcomponent") + hc = self.get_queryset().prefetch_related("service", "component", "host").filter(cluster=cluster) if self._is_for_ui(): ui_hc = HostComponent() ui_hc.hc = hc - serializer = self.get_serializer( - ui_hc, context={"request": request, "cluster": cluster} - ) + serializer = self.get_serializer(ui_hc, context={"request": request, "cluster": cluster}) else: serializer = self.get_serializer(hc, many=True) @@ -355,9 +335,7 @@ def get(self, request, *args, **kwargs): @audit def post(self, request, *args, **kwargs): - cluster = get_object_for_user( - request.user, VIEW_CLUSTER_PERM, Cluster, id=kwargs["cluster_id"] - ) + cluster = get_object_for_user(request.user, VIEW_CLUSTER_PERM, Cluster, id=kwargs["cluster_id"]) check_custom_perm(request.user, "edit_host_components_of", "cluster", cluster) serializer = self.get_serializer( data=request.data, @@ -368,25 +346,21 @@ def post(self, request, *args, **kwargs): ) if serializer.is_valid(): hc_list = serializer.save() - response_serializer = self.serializer_class( - hc_list, many=True, context={"request": request} - ) + response_serializer = self.serializer_class(hc_list, many=True, context={"request": request}) - return Response(response_serializer.data, status.HTTP_201_CREATED) + return Response(response_serializer.data, HTTP_201_CREATED) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + return Response(serializer.errors, status=HTTP_400_BAD_REQUEST) class HostComponentDetail(GenericUIView): queryset = HostComponent.objects.all() serializer_class = HostComponentSerializer - permission_classes = (permissions.IsAuthenticated,) + permission_classes = (IsAuthenticated,) def get_obj(self, cluster_id, hs_id): cluster = get_object_for_user(self.request.user, VIEW_CLUSTER_PERM, Cluster, id=cluster_id) - check_custom_perm( - self.request.user, "view_host_components_of", "cluster", cluster, "view_hostcomponent" - ) + check_custom_perm(self.request.user, "view_host_components_of", "cluster", cluster, "view_hostcomponent") return check_obj(HostComponent, {"id": hs_id, "cluster": cluster}, "HOSTSERVICE_NOT_FOUND") diff --git a/python/api/component/serializers.py b/python/api/component/serializers.py index b25f6dafba..8bfce3f386 100644 --- a/python/api/component/serializers.py +++ b/python/api/component/serializers.py @@ -15,24 +15,26 @@ from rest_framework.serializers import ( BooleanField, CharField, + ChoiceField, HyperlinkedIdentityField, IntegerField, JSONField, - Serializer, + ModelSerializer, SerializerMethodField, ) +from adcm.serializers import EmptySerializer from api.action.serializers import ActionShort from api.concern.serializers import ConcernItemSerializer, ConcernItemUISerializer from api.group_config.serializers import GroupConfigsHyperlinkedIdentityField from api.serializers import StringListSerializer from api.utils import CommonAPIURL, ObjectURL, filter_actions from cm.adcm_config import get_main_info -from cm.models import Action, ServiceComponent +from cm.models import Action, MaintenanceMode, ServiceComponent from cm.status_api import get_component_status -class ComponentSerializer(Serializer): +class ComponentSerializer(EmptySerializer): id = IntegerField(read_only=True) cluster_id = IntegerField(read_only=True) service_id = IntegerField(read_only=True) @@ -42,6 +44,8 @@ class ComponentSerializer(Serializer): state = CharField(read_only=True) prototype_id = IntegerField(required=True, help_text="id of component prototype") url = ObjectURL(read_only=True, view_name="component-details") + maintenance_mode = CharField(read_only=True) + is_maintenance_mode_available = BooleanField(read_only=True) class ComponentUISerializer(ComponentSerializer): @@ -66,9 +70,9 @@ class ComponentShortSerializer(ComponentSerializer): bound_to = JSONField(read_only=True) bundle_id = IntegerField(read_only=True) prototype = HyperlinkedIdentityField( - view_name="component-type-details", - lookup_field="prototype_id", - lookup_url_kwarg="prototype_id", + view_name="component-prototype-detail", + lookup_field="pk", + lookup_url_kwarg="prototype_pk", ) @@ -82,9 +86,9 @@ class ComponentDetailSerializer(ComponentSerializer): action = CommonAPIURL(read_only=True, view_name="object-action") config = CommonAPIURL(read_only=True, view_name="object-config") prototype = HyperlinkedIdentityField( - view_name="component-type-details", - lookup_field="prototype_id", - lookup_url_kwarg="prototype_id", + view_name="component-prototype-detail", + lookup_field="pk", + lookup_url_kwarg="prototype_pk", ) multi_state = StringListSerializer(read_only=True) concerns = ConcernItemSerializer(many=True, read_only=True) @@ -96,7 +100,7 @@ def get_status(obj: ServiceComponent) -> int: return get_component_status(obj) -class StatusSerializer(Serializer): +class StatusSerializer(EmptySerializer): id = IntegerField(read_only=True) name = CharField(read_only=True) status = SerializerMethodField() @@ -118,6 +122,7 @@ def get_actions(self, obj): self.context["component_id"] = obj.id actions = filter_actions(obj, act_set) acts = ActionShort(actions, many=True, context=self.context) + return acts.data @staticmethod @@ -127,3 +132,17 @@ def get_version(obj: ServiceComponent) -> str: @staticmethod def get_main_info(obj: ServiceComponent) -> str | None: return get_main_info(obj) + + +class ComponentChangeMaintenanceModeSerializer(ModelSerializer): + maintenance_mode = ChoiceField(choices=(MaintenanceMode.ON.value, MaintenanceMode.OFF.value)) + + class Meta: + model = ServiceComponent + fields = ("maintenance_mode",) + + +class ComponentAuditSerializer(ModelSerializer): + class Meta: + model = ServiceComponent + fields = ("maintenance_mode",) diff --git a/python/api/component/urls.py b/python/api/component/urls.py index b000d79c1d..f837ece24c 100644 --- a/python/api/component/urls.py +++ b/python/api/component/urls.py @@ -13,7 +13,12 @@ from django.urls import include, path -from api.component.views import ComponentDetailView, ComponentListView, StatusList +from api.component.views import ( + ComponentDetailView, + ComponentListView, + ComponentMaintenanceModeView, + StatusList, +) urlpatterns = [ path('', ComponentListView.as_view(), name='component'), @@ -22,6 +27,11 @@ include( [ path('', ComponentDetailView.as_view(), name='component-details'), + path( + "maintenance-mode/", + ComponentMaintenanceModeView.as_view(), + name="component-maintenance-mode", + ), path('config/', include('api.config.urls'), {'object_type': 'component'}), path('action/', include('api.action.urls'), {'object_type': 'component'}), path('status/', StatusList.as_view(), name='component-status'), diff --git a/python/api/component/views.py b/python/api/component/views.py index 1e33c1655e..a4afcaa419 100644 --- a/python/api/component/views.py +++ b/python/api/component/views.py @@ -12,17 +12,26 @@ from guardian.mixins import PermissionListMixin from rest_framework import permissions +from rest_framework.request import Request from rest_framework.response import Response +from rest_framework.status import HTTP_200_OK from api.base_view import DetailView, GenericUIView, PaginatedView from api.component.serializers import ( + ComponentChangeMaintenanceModeSerializer, ComponentDetailSerializer, ComponentDetailUISerializer, ComponentSerializer, ComponentUISerializer, StatusSerializer, ) -from api.utils import get_object_for_user +from api.utils import ( + check_custom_perm, + get_maintenance_mode_response, + get_object_for_user, +) +from audit.utils import audit +from cm.api import update_mm_objects from cm.models import Cluster, ClusterObject, HostComponent, ServiceComponent from cm.status_api import make_ui_component_status from rbac.viewsets import DjangoOnlyObjectPermissions @@ -31,15 +40,12 @@ def get_component_queryset(queryset, user, kwargs): if "cluster_id" in kwargs: cluster = get_object_for_user(user, "cm.view_cluster", Cluster, id=kwargs["cluster_id"]) - co = get_object_for_user( - user, "cm.view_clusterobject", ClusterObject, cluster=cluster, id=kwargs["service_id"] - ) + co = get_object_for_user(user, "cm.view_clusterobject", ClusterObject, cluster=cluster, id=kwargs["service_id"]) queryset = queryset.filter(cluster=cluster, service=co) elif "service_id" in kwargs: - co = get_object_for_user( - user, "cm.view_clusterobject", ClusterObject, id=kwargs["service_id"] - ) + co = get_object_for_user(user, "cm.view_clusterobject", ClusterObject, id=kwargs["service_id"]) queryset = queryset.filter(service=co) + return queryset @@ -51,8 +57,9 @@ class ComponentListView(PermissionListMixin, PaginatedView): ordering_fields = ("state", "prototype__display_name", "prototype__version_order") permission_required = ["cm.view_servicecomponent"] - def get_queryset(self): # pylint: disable=arguments-differ - queryset = super().get_queryset() + def get_queryset(self, *args, **kwargs): + queryset = super().get_queryset(*args, **kwargs) + return get_component_queryset(queryset, self.request.user, self.kwargs) @@ -65,27 +72,48 @@ class ComponentDetailView(PermissionListMixin, DetailView): lookup_url_kwarg = "component_id" error_code = ServiceComponent.__error_code__ - def get_queryset(self): # pylint: disable=arguments-differ - queryset = super().get_queryset() + def get_queryset(self, *args, **kwargs): + queryset = super().get_queryset(*args, **kwargs) + return get_component_queryset(queryset, self.request.user, self.kwargs) +class ComponentMaintenanceModeView(GenericUIView): + queryset = ServiceComponent.objects.all() + permission_classes = (DjangoOnlyObjectPermissions,) + serializer_class = ComponentChangeMaintenanceModeSerializer + lookup_field = "id" + lookup_url_kwarg = "component_id" + + @update_mm_objects + @audit + def post(self, request: Request, **kwargs) -> Response: + component = get_object_for_user( + request.user, "cm.view_servicecomponent", ServiceComponent, id=kwargs["component_id"] + ) + # pylint: disable=protected-access + check_custom_perm(request.user, "change_maintenance_mode", component._meta.model_name, component) + serializer = self.get_serializer(instance=component, data=request.data) + serializer.is_valid(raise_exception=True) + + response: Response = get_maintenance_mode_response(obj=component, serializer=serializer) + if response.status_code == HTTP_200_OK: + response.data = serializer.data + + return response + + class StatusList(GenericUIView): queryset = HostComponent.objects.all() permission_classes = (permissions.IsAuthenticated,) serializer_class = StatusSerializer def get(self, request, *args, **kwargs): - """ - Show all components in a specified host - """ queryset = get_component_queryset(ServiceComponent.objects.all(), request.user, kwargs) - component = get_object_for_user( - request.user, "cm.view_servicecomponent", queryset, id=kwargs["component_id"] - ) + component = get_object_for_user(request.user, "cm.view_servicecomponent", queryset, id=kwargs["component_id"]) if self._is_for_ui(): host_components = self.get_queryset().filter(component=component) + return Response(make_ui_component_status(component, host_components)) - else: - serializer = self.get_serializer(component) - return Response(serializer.data) + + return Response(self.get_serializer(component).data) diff --git a/python/api/concern/views.py b/python/api/concern/views.py index 3b4b4b7604..88f8882a52 100644 --- a/python/api/concern/views.py +++ b/python/api/concern/views.py @@ -38,9 +38,7 @@ class ConcernFilter(drf_filters.FilterSet): type = drf_filters.ChoiceFilter(choices=models.ConcernType.choices) cause = drf_filters.ChoiceFilter(choices=models.ConcernCause.choices) object_id = drf_filters.NumberFilter(label='Related object ID', method='_pass') - object_type = drf_filters.ChoiceFilter( - label='Related object type', choices=CHOICES, method='_filter_by_object' - ) + object_type = drf_filters.ChoiceFilter(label='Related object type', choices=CHOICES, method='_filter_by_object') owner_type = drf_filters.ChoiceFilter(choices=CHOICES, method='_filter_by_owner_type') class Meta: diff --git a/python/api/config/views.py b/python/api/config/views.py index 62b30ea8c0..f7f6fc61b6 100644 --- a/python/api/config/views.py +++ b/python/api/config/views.py @@ -64,7 +64,8 @@ def get_obj(object_type, object_id): def get_object_type_id_version(**kwargs): object_type = kwargs.get('object_type') - object_id = kwargs.get(f'{object_type}_id') + # TODO: this is a temporary patch for `config` endpoint + object_id = kwargs.get(f'{object_type}_id') or kwargs.get(f"{object_type}_pk") version = kwargs.get('version') return object_type, object_id, version @@ -107,9 +108,7 @@ class ConfigHistoryView(PermissionListMixin, GenericUIView): def get_queryset(self, *args, **kwargs): if self.request.user.has_perm('cm.view_settings_of_adcm'): - return super().get_queryset(*args, **kwargs) | ConfigLog.objects.filter( - obj_ref__adcm__isnull=False - ) + return super().get_queryset(*args, **kwargs) | ConfigLog.objects.filter(obj_ref__adcm__isnull=False) else: return super().get_queryset(*args, **kwargs).filter(obj_ref__adcm__isnull=True) @@ -141,9 +140,7 @@ class ConfigVersionView(PermissionListMixin, GenericUIView): def get_queryset(self, *args, **kwargs): if self.request.user.has_perm('cm.view_settings_of_adcm'): - return super().get_queryset(*args, **kwargs) | ConfigLog.objects.filter( - obj_ref__adcm__isnull=False - ) + return super().get_queryset(*args, **kwargs) | ConfigLog.objects.filter(obj_ref__adcm__isnull=False) else: return super().get_queryset(*args, **kwargs).filter(obj_ref__adcm__isnull=True) @@ -165,9 +162,7 @@ class ConfigHistoryRestoreView(PermissionListMixin, GenericUIView): def get_queryset(self, *args, **kwargs): if self.request.user.has_perm('cm.view_settings_of_adcm'): - return super().get_queryset(*args, **kwargs) | ConfigLog.objects.filter( - obj_ref__adcm__isnull=False - ) + return super().get_queryset(*args, **kwargs) | ConfigLog.objects.filter(obj_ref__adcm__isnull=False) else: return super().get_queryset(*args, **kwargs).filter(obj_ref__adcm__isnull=True) diff --git a/python/api/config_log/views.py b/python/api/config_log/views.py index d14fbbfbf4..7aac816a9d 100644 --- a/python/api/config_log/views.py +++ b/python/api/config_log/views.py @@ -30,16 +30,20 @@ class ConfigLogViewSet( # pylint: disable=too-many-ancestors queryset = ConfigLog.objects.all() serializer_class = ConfigLogSerializer permission_classes = (DjangoObjectPermissionsAudit,) - serializer_class_ui = UIConfigLogSerializer permission_required = ['cm.view_configlog'] filterset_fields = ('id', 'obj_ref') ordering_fields = ('id',) + def get_serializer_class(self): + + if self.is_for_ui(): + return UIConfigLogSerializer + + return super().get_serializer_class() + def get_queryset(self, *args, **kwargs): if self.request.user.has_perm('cm.view_settings_of_adcm'): - return super().get_queryset(*args, **kwargs) | ConfigLog.objects.filter( - obj_ref__adcm__isnull=False - ) + return super().get_queryset(*args, **kwargs) | ConfigLog.objects.filter(obj_ref__adcm__isnull=False) else: return super().get_queryset(*args, **kwargs).filter(obj_ref__adcm__isnull=True) diff --git a/python/api/docs.py b/python/api/docs.py index 7ea2ae2fbd..739617478f 100644 --- a/python/api/docs.py +++ b/python/api/docs.py @@ -32,9 +32,7 @@ def docs_html(request): def get_context(request, patterns=None): - generator = SchemaGenerator( - title='ArenaData Cluster Manager API', description=intro(), patterns=patterns - ) + generator = SchemaGenerator(title='ArenaData Cluster Manager API', description=intro(), patterns=patterns) data = generator.get_schema(request, True) context = { 'document': data, diff --git a/python/api/group_config/serializers.py b/python/api/group_config/serializers.py index 9fc65e33f4..e9f9d1469e 100644 --- a/python/api/group_config/serializers.py +++ b/python/api/group_config/serializers.py @@ -22,7 +22,6 @@ MultiHyperlinkedRelatedField, UIConfigField, ) -from cm.adcm_config import ui_config from cm.api import update_obj_config from cm.errors import AdcmEx from cm.models import ConfigLog, GroupConfig, Host, ObjectConfig @@ -147,9 +146,7 @@ class GroupConfigHostSerializer(serializers.ModelSerializer): """Serializer for hosts in group config""" id = serializers.PrimaryKeyRelatedField(queryset=Host.objects.all()) - url = MultiHyperlinkedIdentityField( - 'group-config-host-detail', 'parent_lookup_group_config', 'host_id' - ) + url = MultiHyperlinkedIdentityField('group-config-host-detail', 'parent_lookup_group_config', 'host_id') class Meta: model = Host @@ -193,9 +190,7 @@ def validate_id(self, value): class GroupConfigHostCandidateSerializer(GroupConfigHostSerializer): """Serializer for host candidate""" - url = MultiHyperlinkedIdentityField( - 'group-config-host-candidate-detail', 'parent_lookup_group_config', 'host_id' - ) + url = MultiHyperlinkedIdentityField('group-config-host-candidate-detail', 'parent_lookup_group_config', 'host_id') class GroupConfigConfigSerializer(serializers.ModelSerializer): @@ -218,9 +213,7 @@ class GroupConfigConfigSerializer(serializers.ModelSerializer): ) previous_id = serializers.IntegerField(source='previous') history = serializers.SerializerMethodField() - url = MultiHyperlinkedIdentityField( - 'group-config-config-detail', 'parent_lookup_group_config', 'pk' - ) + url = MultiHyperlinkedIdentityField('group-config-config-detail', 'parent_lookup_group_config', 'pk') class Meta: model = ObjectConfig @@ -255,13 +248,10 @@ class Meta: @atomic def create(self, validated_data): object_config = self.context.get('obj_ref') - ui = self.context.get('ui') config = validated_data.get('config') attr = validated_data.get('attr', {}) description = validated_data.get('description', '') cl = update_obj_config(object_config, config, attr, description) - if ui: - cl.config = ui_config(object_config.object.object, cl) return cl diff --git a/python/api/group_config/views.py b/python/api/group_config/views.py index 0e5e691f09..7ddd0c769f 100644 --- a/python/api/group_config/views.py +++ b/python/api/group_config/views.py @@ -55,9 +55,7 @@ def check_config_perm(user, action_type, obj): class GroupConfigFilterSet(FilterSet): - object_type = CharFilter( - field_name='object_type', label='object_type', method='filter_object_type' - ) + object_type = CharFilter(field_name='object_type', label='object_type', method='filter_object_type') @staticmethod def filter_object_type(queryset, name, value): @@ -176,12 +174,16 @@ class GroupConfigConfigLogViewSet( GenericUIViewSet, ): # pylint: disable=too-many-ancestors serializer_class = GroupConfigConfigLogSerializer - serializer_class_ui = UIGroupConfigConfigLogSerializer permission_classes = (DjangoObjectPermissionsAudit,) permission_required = ['view_configlog'] filterset_fields = ('id',) ordering_fields = ('id',) + def get_serializer_class(self): + if self.is_for_ui(): + return UIGroupConfigConfigLogSerializer + return super().get_serializer_class() + def get_queryset(self, *args, **kwargs): kwargs = { 'obj_ref__group_config': self.kwargs.get('parent_lookup_obj_ref__group_config'), @@ -204,7 +206,7 @@ def get_serializer_context(self): obj_ref = ObjectConfig.obj.get(id=obj_ref_id) context.update({'obj_ref': obj_ref}) - context['ui'] = self._is_for_ui() + context["ui"] = self.is_for_ui() return context @@ -213,9 +215,7 @@ def create(self, request, *args, **kwargs): return super().create(request, *args, **kwargs) -class GroupConfigViewSet( - PermissionListMixin, NestedViewSetMixin, ModelViewSet -): # pylint: disable=too-many-ancestors +class GroupConfigViewSet(PermissionListMixin, NestedViewSetMixin, ModelViewSet): # pylint: disable=too-many-ancestors queryset = GroupConfig.objects.all() serializer_class = GroupConfigSerializer filterset_class = GroupConfigFilterSet diff --git a/python/api/host/host_urls.py b/python/api/host/host_urls.py index 08a3acf56b..2c35deddbf 100644 --- a/python/api/host/host_urls.py +++ b/python/api/host/host_urls.py @@ -13,7 +13,7 @@ from django.urls import include, path -from api.host.views import HostDetail, StatusList +from api.host.views import HostDetail, HostMaintenanceModeView, StatusList urlpatterns = [ path( @@ -24,6 +24,11 @@ path("config/", include("api.config.urls"), {"object_type": "host"}), path("action/", include("api.action.urls"), {"object_type": "host"}), path("status/", StatusList.as_view(), name="host-status"), + path( + "maintenance-mode/", + HostMaintenanceModeView.as_view(), + name="host-maintenance-mode", + ), ] ), ), diff --git a/python/api/host/serializers.py b/python/api/host/serializers.py index f1ecad7ab1..30cb1e96f6 100644 --- a/python/api/host/serializers.py +++ b/python/api/host/serializers.py @@ -30,7 +30,7 @@ from cm.adcm_config import get_main_info from cm.api import add_host from cm.issue import update_hierarchy_issues, update_issue_after_deleting -from cm.models import Action, Host, HostProvider, MaintenanceModeType, Prototype +from cm.models import Action, Host, HostProvider, MaintenanceMode, Prototype from cm.status_api import get_host_status @@ -55,7 +55,8 @@ class HostSerializer(EmptySerializer): ) description = CharField(required=False, allow_blank=True) state = CharField(read_only=True) - maintenance_mode = ChoiceField(choices=MaintenanceModeType.choices, read_only=True) + maintenance_mode = ChoiceField(choices=MaintenanceMode.choices, read_only=True) + is_maintenance_mode_available = BooleanField(read_only=True) url = ObjectURL(read_only=True, view_name="host-details") @staticmethod @@ -81,7 +82,7 @@ class HostDetailSerializer(HostSerializer): config = CommonAPIURL(view_name="object-config") action = CommonAPIURL(view_name="object-action") prototype = HyperlinkedIdentityField( - view_name="host-type-details", lookup_field="prototype_id", lookup_url_kwarg="prototype_id" + view_name="host-prototype-detail", lookup_field="pk", lookup_url_kwarg="prototype_pk" ) multi_state = StringListSerializer(read_only=True) concerns = ConcernItemSerializer(many=True, read_only=True) @@ -93,12 +94,7 @@ def get_status(obj): class HostUpdateSerializer(HostDetailSerializer): - maintenance_mode = ChoiceField(choices=MaintenanceModeType.choices) - def update(self, instance, validated_data): - instance.maintenance_mode = validated_data.get( - "maintenance_mode", instance.maintenance_mode - ) instance.description = validated_data.get("description", instance.description) instance.fqdn = validated_data.get("fqdn", instance.fqdn) instance.save() @@ -113,7 +109,6 @@ def update(self, instance, validated_data): class HostAuditSerializer(ModelSerializer): fqdn = CharField(max_length=253) description = CharField(required=False, allow_blank=True) - maintenance_mode = ChoiceField(choices=MaintenanceModeType.choices) class Meta: model = Host @@ -124,6 +119,14 @@ class Meta: ) +class HostChangeMaintenanceModeSerializer(ModelSerializer): + maintenance_mode = ChoiceField(choices=(MaintenanceMode.ON.value, MaintenanceMode.OFF.value)) + + class Meta: + model = Host + fields = ("maintenance_mode",) + + class ClusterHostSerializer(HostSerializer): host_id = IntegerField(source="id") prototype_id = IntegerField(read_only=True) @@ -139,9 +142,7 @@ def create(self, validated_data): provider = check_obj(HostProvider, self.context.get("provider_id")) proto = Prototype.obj.get(bundle=provider.prototype.bundle, type="host") - return add_host( - proto, provider, validated_data.get("fqdn"), validated_data.get("description", "") - ) + return add_host(proto, provider, validated_data.get("fqdn"), validated_data.get("description", "")) class StatusSerializer(EmptySerializer): diff --git a/python/api/host/views.py b/python/api/host/views.py index a2dd021778..80dab71ecd 100644 --- a/python/api/host/views.py +++ b/python/api/host/views.py @@ -14,17 +14,20 @@ from guardian.mixins import PermissionListMixin from guardian.shortcuts import get_objects_for_user from rest_framework.permissions import IsAuthenticated +from rest_framework.request import Request from rest_framework.response import Response from rest_framework.status import ( HTTP_200_OK, HTTP_201_CREATED, HTTP_204_NO_CONTENT, HTTP_400_BAD_REQUEST, + HTTP_409_CONFLICT, ) from api.base_view import DetailView, GenericUIView, PaginatedView from api.host.serializers import ( ClusterHostSerializer, + HostChangeMaintenanceModeSerializer, HostDetailSerializer, HostDetailUISerializer, HostSerializer, @@ -33,13 +36,19 @@ ProvideHostSerializer, StatusSerializer, ) -from api.utils import check_custom_perm, create, get_object_for_user +from api.utils import ( + check_custom_perm, + create, + get_maintenance_mode_response, + get_object_for_user, +) from audit.utils import audit from cm.api import ( add_host_to_cluster, delete_host, load_service_map, remove_host_from_cluster, + update_mm_objects, ) from cm.errors import AdcmEx from cm.models import ( @@ -49,7 +58,7 @@ Host, HostComponent, HostProvider, - MaintenanceModeType, + MaintenanceMode, ServiceComponent, ) from cm.status_api import make_ui_host_status @@ -159,14 +168,10 @@ def post(self, request, *args, **kwargs): ) if serializer.is_valid(): if "provider_id" in kwargs: # List provider hosts - provider = get_object_for_user( - request.user, PROVIDER_VIEW, HostProvider, id=kwargs["provider_id"] - ) + provider = get_object_for_user(request.user, PROVIDER_VIEW, HostProvider, id=kwargs["provider_id"]) else: provider = serializer.validated_data.get("provider_id") - provider = get_object_for_user( - request.user, PROVIDER_VIEW, HostProvider, id=provider.id - ) + provider = get_object_for_user(request.user, PROVIDER_VIEW, HostProvider, id=provider.id) check_custom_perm(request.user, "add_host_to", "hostprovider", provider) @@ -190,9 +195,7 @@ def post(self, request, *args, **kwargs): cluster = None if "cluster_id" in kwargs: - cluster = get_object_for_user( - request.user, CLUSTER_VIEW, Cluster, id=kwargs["cluster_id"] - ) + cluster = get_object_for_user(request.user, CLUSTER_VIEW, Cluster, id=kwargs["cluster_id"]) host = get_object_for_user(request.user, HOST_VIEW, Host, id=validated_data.get("id")) check_custom_perm(request.user, "map_host_to", "cluster", cluster) @@ -222,6 +225,36 @@ class HostDetail(PermissionListMixin, DetailView): lookup_url_kwarg = "host_id" error_code = "HOST_NOT_FOUND" + def _update_host_object( + self, + request, + *args, + partial=True, + **kwargs, + ): + host = self.get_object() + check_custom_perm(request.user, "change", "host", host) + serializer = self.get_serializer( + host, + data=request.data, + context={ + "request": request, + "prototype_id": kwargs.get("prototype_id", None), + "cluster_id": kwargs.get("cluster_id", None), + "provider_id": kwargs.get("provider_id", None), + }, + partial=partial, + ) + + serializer.is_valid(raise_exception=True) + if "fqdn" in request.data and request.data["fqdn"] != host.fqdn and (host.cluster or host.state != "created"): + raise AdcmEx("HOST_UPDATE_ERROR") + + serializer.save(**kwargs) + load_service_map() + + return Response(self.get_serializer(self.get_object()).data, status=HTTP_200_OK) + def get_queryset(self, *args, **kwargs): queryset = super().get_queryset(*args, **kwargs) queryset = get_host_queryset(queryset, self.request.user, self.kwargs) @@ -232,15 +265,11 @@ def get_queryset(self, *args, **kwargs): def delete(self, request, *args, **kwargs): host = self.get_object() if "cluster_id" in kwargs: - # Remove host from cluster - cluster = get_object_for_user( - request.user, CLUSTER_VIEW, Cluster, id=kwargs["cluster_id"] - ) + cluster = get_object_for_user(request.user, CLUSTER_VIEW, Cluster, id=kwargs["cluster_id"]) check_host(host, cluster) check_custom_perm(request.user, "unmap_host_from", "cluster", cluster) remove_host_from_cluster(host) else: - # Delete host (and all corresponding host services:components) check_custom_perm(request.user, "remove", "host", host) delete_host(host) @@ -248,60 +277,40 @@ def delete(self, request, *args, **kwargs): @audit def patch(self, request, *args, **kwargs): - return self.__update_host_object(request, *args, **kwargs) + return self._update_host_object(request, *args, **kwargs) @audit def put(self, request, *args, **kwargs): - return self.__update_host_object(request, partial=False, *args, **kwargs) + return self._update_host_object(request, partial=False, *args, **kwargs) - def __update_host_object( - self, - request, - *args, - partial=True, - **kwargs, - ): - host = self.get_object() - check_custom_perm(request.user, "change", "host", host) - serializer = self.get_serializer( - host, - data=request.data, - context={ - "request": request, - "prototype_id": kwargs.get("prototype_id", None), - "cluster_id": kwargs.get("cluster_id", None), - "provider_id": kwargs.get("provider_id", None), - }, - partial=partial, - ) - serializer.is_valid(raise_exception=True) - if "maintenance_mode" in serializer.validated_data: - self.__check_maintenance_mode_constraint( - host.maintenance_mode, serializer.validated_data.get("maintenance_mode") - ) +class HostMaintenanceModeView(GenericUIView): + queryset = Host.objects.all() + permission_classes = (DjangoOnlyObjectPermissions,) + serializer_class = HostChangeMaintenanceModeSerializer + lookup_field = "id" + lookup_url_kwarg = "host_id" + @update_mm_objects + @audit + def post(self, request: Request, **kwargs) -> Response: + host = get_object_for_user(request.user, HOST_VIEW, Host, id=kwargs["host_id"]) + + check_custom_perm(request.user, "change_maintenance_mode", Host.__name__.lower(), host) + + serializer = self.get_serializer(instance=host, data=request.data) + serializer.is_valid(raise_exception=True) if ( - "fqdn" in request.data - and request.data["fqdn"] != host.fqdn - and (host.cluster or host.state != "created") + serializer.validated_data.get("maintenance_mode") == MaintenanceMode.ON + and not host.is_maintenance_mode_available ): - raise AdcmEx("HOST_UPDATE_ERROR") + return Response(data="MAINTENANCE_MODE_NOT_AVAILABLE", status=HTTP_409_CONFLICT) - serializer.save(**kwargs) - load_service_map() - - return Response(self.get_serializer(self.get_object()).data, status=HTTP_200_OK) + response: Response = get_maintenance_mode_response(obj=host, serializer=serializer) + if response.status_code == HTTP_200_OK: + response.data = serializer.data - @staticmethod - def __check_maintenance_mode_constraint(old_mode, new_mode): - if old_mode == new_mode: - return - if old_mode == MaintenanceModeType.Disabled or new_mode not in ( - MaintenanceModeType.On, - MaintenanceModeType.Off, - ): - raise AdcmEx("MAINTENANCE_MODE_NOT_AVAILABLE") + return response class StatusList(GenericUIView): @@ -313,13 +322,10 @@ def get(self, request, *args, **kwargs): cluster = None host = get_object_for_user(request.user, HOST_VIEW, Host, id=kwargs["host_id"]) if "cluster_id" in kwargs: - cluster = get_object_for_user( - request.user, CLUSTER_VIEW, Cluster, id=kwargs["cluster_id"] - ) + cluster = get_object_for_user(request.user, CLUSTER_VIEW, Cluster, id=kwargs["cluster_id"]) + if "provider_id" in kwargs: - provider = get_object_for_user( - request.user, PROVIDER_VIEW, HostProvider, id=kwargs["provider_id"] - ) + provider = get_object_for_user(request.user, PROVIDER_VIEW, HostProvider, id=kwargs["provider_id"]) host = get_object_for_user( request.user, HOST_VIEW, diff --git a/python/api/job/serializers.py b/python/api/job/serializers.py index 2c9bf7d8fa..769850ae4d 100644 --- a/python/api/job/serializers.py +++ b/python/api/job/serializers.py @@ -11,131 +11,129 @@ # limitations under the License. import json -import os +from pathlib import Path -from rest_framework import serializers +from django.conf import settings from rest_framework.reverse import reverse - -from adcm.serializers import EmptySerializer +from rest_framework.serializers import ( + HyperlinkedIdentityField, + HyperlinkedModelSerializer, + JSONField, + SerializerMethodField, +) + +from api.action.serializers import ActionJobSerializer from api.concern.serializers import ConcernItemSerializer -from api.utils import hlink from cm.ansible_plugin import get_check_log -from cm.config import RUN_DIR, Job from cm.errors import AdcmEx from cm.job import start_task -from cm.models import JobLog, TaskLog +from cm.models import JobLog, JobStatus, LogStorage, TaskLog -def get_job_objects(task: TaskLog) -> list: - objects = [{"type": k, **v} for k, v in task.selector.items()] - return objects +class JobShortSerializer(HyperlinkedModelSerializer): + display_name = SerializerMethodField() + class Meta: + model = JobLog + fields = ("id", "display_name", "status", "start_date", "finish_date", "url") + extra_kwargs = {"url": {"lookup_url_kwarg": "job_pk"}} -def get_job_display_name(self, obj): - if obj.sub_action: - return obj.sub_action.display_name - elif obj.action: - return obj.action.display_name - else: - return None + @staticmethod + def get_display_name(obj: JobLog) -> str | None: + if obj.sub_action: + return obj.sub_action.display_name + elif obj.action: + return obj.action.display_name + else: + return None + + +class TaskSerializer(HyperlinkedModelSerializer): + class Meta: + model = TaskLog + fields = ( + "id", + "pid", + "object_id", + "action_id", + "status", + "start_date", + "finish_date", + "url", + ) + extra_kwargs = {"url": {"lookup_url_kwarg": "task_pk"}} -def get_action_url(self, obj): - if not obj.action_id: - return None +class TaskRetrieveSerializer(HyperlinkedModelSerializer): + action_url = SerializerMethodField() + action = ActionJobSerializer(read_only=True) + objects = SerializerMethodField() + jobs = JobShortSerializer(many=True, source="joblog_set", read_only=True) + terminatable = SerializerMethodField() + object_type = SerializerMethodField() + lock = ConcernItemSerializer(read_only=True) + hc = JSONField(required=False) + restart = HyperlinkedIdentityField(view_name="tasklog-restart", lookup_url_kwarg="task_pk") + cancel = HyperlinkedIdentityField(view_name="tasklog-cancel", lookup_url_kwarg="task_pk") + download = HyperlinkedIdentityField(view_name="tasklog-download", lookup_url_kwarg="task_pk") + + class Meta: + model = TaskLog + fields = ( + *TaskSerializer.Meta.fields, + "selector", + "config", + "attr", + "hosts", + "verbose", + "action_url", + "action", + "objects", + "jobs", + "terminatable", + "object_type", + "lock", + "hc", + "restart", + "cancel", + "download", + ) + read_only_fields = ("object_id", "status", "start_date", "finish_date") + extra_kwargs = {"url": {"lookup_url_kwarg": "task_pk"}} - return reverse( - "action-details", kwargs={"action_id": obj.action_id}, request=self.context["request"] - ) + def get_action_url(self, obj: TaskLog) -> str | None: + if not obj.action_id: + return None + return reverse("action-detail", kwargs={"action_pk": obj.action_id}, request=self.context["request"]) -class DataField(serializers.CharField): - def to_representation(self, value): - return value - - -class JobAction(EmptySerializer): - name = serializers.CharField(read_only=True) - display_name = serializers.CharField(read_only=True) - prototype_id = serializers.IntegerField(read_only=True) - prototype_name = serializers.CharField(read_only=True) - prototype_type = serializers.CharField(read_only=True) - prototype_version = serializers.CharField(read_only=True) - - -class JobShort(EmptySerializer): - id = serializers.IntegerField(read_only=True) - name = serializers.CharField(read_only=True) - display_name = serializers.SerializerMethodField() - status = serializers.CharField(read_only=True) - start_date = serializers.DateTimeField(read_only=True) - finish_date = serializers.DateTimeField(read_only=True) - url = hlink("job-details", "id", "job_id") - - get_display_name = get_job_display_name - - -class TaskListSerializer(EmptySerializer): - id = serializers.IntegerField(read_only=True) - pid = serializers.IntegerField(read_only=True) - object_id = serializers.IntegerField(read_only=True) - action_id = serializers.IntegerField(read_only=True) - status = serializers.CharField(read_only=True) - start_date = serializers.DateTimeField(read_only=True) - finish_date = serializers.DateTimeField(read_only=True) - url = hlink("task-details", "id", "task_id") - - -class TaskSerializer(TaskListSerializer): - selector = serializers.JSONField(read_only=True) - config = serializers.JSONField(required=False) - attr = serializers.JSONField(required=False) - hc = serializers.JSONField(required=False) - hosts = serializers.JSONField(required=False) - verbose = serializers.BooleanField(required=False) - action_url = serializers.SerializerMethodField() - action = serializers.SerializerMethodField() - objects = serializers.SerializerMethodField() - jobs = serializers.SerializerMethodField() - restart = hlink("task-restart", "id", "task_id") - terminatable = serializers.SerializerMethodField() - cancel = hlink("task-cancel", "id", "task_id") - download = hlink("task-download", "id", "task_id") - object_type = serializers.SerializerMethodField() - lock = ConcernItemSerializer(read_only=True) + @staticmethod + def get_objects(obj: TaskLog) -> list: + objects = [{"type": k, **v} for k, v in obj.selector.items()] - get_action_url = get_action_url + return objects - def get_terminatable(self, obj): + @staticmethod + def get_terminatable(obj: TaskLog): if obj.action: allow_to_terminate = obj.action.allow_to_terminate else: allow_to_terminate = False - if allow_to_terminate and obj.status in [Job.CREATED, Job.RUNNING]: + if allow_to_terminate and obj.status in {JobStatus.CREATED, JobStatus.RUNNING}: return True return False - def get_jobs(self, obj): - return JobShort(obj.joblog_set, many=True, context=self.context).data - - def get_action(self, obj): - return JobAction(obj.action, context=self.context).data - - @staticmethod - def get_objects(obj): - return get_job_objects(obj) - @staticmethod - def get_object_type(obj): + def get_object_type(obj: TaskLog): if obj.action: return obj.action.prototype.type return None -class RunTaskSerializer(TaskSerializer): +class RunTaskRetrieveSerializer(TaskRetrieveSerializer): def create(self, validated_data): obj = start_task( validated_data.get("action"), @@ -151,132 +149,158 @@ def create(self, validated_data): return obj -class JobListSerializer(EmptySerializer): - id = serializers.IntegerField(read_only=True) - pid = serializers.IntegerField(read_only=True) - task_id = serializers.IntegerField(read_only=True) - action_id = serializers.IntegerField(read_only=True) - sub_action_id = serializers.IntegerField(read_only=True) - status = serializers.CharField(read_only=True) - start_date = serializers.DateTimeField(read_only=True) - finish_date = serializers.DateTimeField(read_only=True) - url = hlink("job-details", "id", "job_id") - +class JobSerializer(HyperlinkedModelSerializer): + class Meta: + model = JobLog + fields = ( + "id", + "pid", + "task_id", + "action_id", + "sub_action_id", + "status", + "start_date", + "finish_date", + "url", + ) + extra_kwargs = {"url": {"lookup_url_kwarg": "job_pk"}} + + +class JobRetrieveSerializer(HyperlinkedModelSerializer): + action = ActionJobSerializer() + display_name = SerializerMethodField() + objects = SerializerMethodField() + selector = JSONField() + log_dir = SerializerMethodField() + log_files = SerializerMethodField() + action_url = SerializerMethodField() + task_url = HyperlinkedIdentityField( + view_name="tasklog-detail", + lookup_url_kwarg="task_pk", + ) -class JobSerializer(JobListSerializer): - action = serializers.SerializerMethodField() - display_name = serializers.SerializerMethodField() - objects = serializers.SerializerMethodField() - selector = serializers.JSONField(read_only=True) - log_dir = serializers.CharField(read_only=True) - log_files = DataField(read_only=True) - action_url = serializers.SerializerMethodField() - task_url = hlink("task-details", "task_id", "task_id") + class Meta: + model = JobLog + fields = ( + *JobSerializer.Meta.fields, + "action", + "display_name", + "objects", + "selector", + "log_dir", + "log_files", + "action_url", + "task_url", + ) + extra_kwargs = {"url": {"lookup_url_kwarg": "job_pk"}} - get_display_name = get_job_display_name - get_action_url = get_action_url + @staticmethod + def get_objects(obj: JobLog) -> list | None: + objects = [{"type": k, **v} for k, v in obj.task.selector.items()] - def get_action(self, obj): - return JobAction(obj.action, context=self.context).data + return objects @staticmethod - def get_objects(obj): - return get_job_objects(obj.task) + def get_display_name(obj: JobLog) -> str | None: + if obj.sub_action: + return obj.sub_action.display_name + elif obj.action: + return obj.action.display_name + else: + return None + def get_action_url(self, obj: JobLog) -> str | None: + if not obj.action_id: + return None -class LogStorageSerializer(EmptySerializer): - id = serializers.IntegerField(read_only=True) - name = serializers.CharField(read_only=True) - type = serializers.CharField(read_only=True) - format = serializers.CharField(read_only=True) - content = serializers.SerializerMethodField() + return reverse("action-detail", kwargs={"action_pk": obj.action_id}, request=self.context["request"]) + + @staticmethod + def get_log_dir(obj: JobLog) -> str: + return str(Path(settings.RUN_DIR, str(obj.pk))) + + def get_log_files(self, obj: JobLog) -> list[dict[str, str]]: + logs = [] + for log_storage in LogStorage.objects.filter(job=obj): + logs.append( + { + "name": log_storage.name, + "type": log_storage.type, + "format": log_storage.format, + "id": log_storage.pk, + "url": reverse( + "joblog-detail", + kwargs={"job_pk": obj.pk, "log_pk": log_storage.pk}, + request=self.context["request"], + ), + "download_url": reverse( + "joblog-download", + kwargs={"job_pk": obj.pk, "log_pk": log_storage.pk}, + request=self.context["request"], + ), + } + ) + + return logs + + +class LogStorageRetrieveSerializer(HyperlinkedModelSerializer): + content = SerializerMethodField() + + class Meta: + model = LogStorage + fields = ( + "id", + "name", + "type", + "format", + "content", + ) + extra_kwargs = {"url": {"lookup_url_kwarg": "log_pk"}} @staticmethod def _get_ansible_content(obj): - path_file = os.path.join(RUN_DIR, f"{obj.job.id}", f"{obj.name}-{obj.type}.{obj.format}") + path_file = settings.RUN_DIR / f"{obj.job.id}" / f"{obj.name}-{obj.type}.{obj.format}" try: - with open(path_file, "r", encoding="utf_8") as f: + with open(path_file, "r", encoding=settings.ENCODING_UTF_8) as f: content = f.read() except FileNotFoundError as e: msg = f'File "{obj.name}-{obj.type}.{obj.format}" not found' raise AdcmEx("LOG_NOT_FOUND", msg) from e - return content - def get_content(self, obj): - content = obj.body + return content - if obj.type in ["stdout", "stderr"]: - if content is None: - content = self._get_ansible_content(obj) + def get_content(self, obj: LogStorage) -> str: + if obj.type in {"stdout", "stderr"}: + if obj.body is None: + obj.body = self._get_ansible_content(obj) elif obj.type == "check": - if content is None: - content = get_check_log(obj.job_id) - if isinstance(content, str): - content = json.loads(content) + if obj.body is None: + obj.body = get_check_log(obj.job_id) + if isinstance(obj.body, str): + obj.body = json.loads(obj.body) elif obj.type == "custom": - if obj.format == "json" and isinstance(content, str): + if obj.format == "json" and isinstance(obj.body, str): try: - custom_content = json.loads(content) - custom_content = json.dumps(custom_content, indent=4) - content = custom_content + custom_content = json.loads(obj.body) + obj.body = json.dumps(custom_content, indent=4) except json.JSONDecodeError: pass - return content + return obj.body + +class LogStorageSerializer(LogStorageRetrieveSerializer): + url = SerializerMethodField() -class LogStorageListSerializer(LogStorageSerializer): - url = serializers.SerializerMethodField() + class Meta: + model = LogStorage + fields = (*LogStorageRetrieveSerializer.Meta.fields, "url") def get_url(self, obj): return reverse( - "log-storage", - kwargs={"job_id": obj.job_id, "log_id": obj.id}, + "joblog-detail", + kwargs={"job_pk": obj.job_id, "log_pk": obj.id}, request=self.context["request"], ) - - -class LogSerializer(EmptySerializer): - tag = serializers.SerializerMethodField() - level = serializers.SerializerMethodField() - type = serializers.SerializerMethodField() - content = serializers.SerializerMethodField() - - @staticmethod - def get_tag(obj): - if obj.type == "check": - return obj.type - - return obj.name - - @staticmethod - def get_level(obj): - if obj.type == "check": - return "out" - - return obj.type[3:] - - @staticmethod - def get_type(obj): - return obj.format - - @staticmethod - def get_content(obj): - content = obj.body - - if obj.type in ["stdout", "stderr"]: - if content is None: - path_file = os.path.join( - RUN_DIR, f"{obj.job.id}", f"{obj.name}-{obj.type}.{obj.format}" - ) - with open(path_file, "r", encoding="utf_8") as f: - content = f.read() - elif obj.type == "check": - if content is None: - content = get_check_log(obj.job_id) - - if isinstance(content, str): - content = json.loads(content) - - return content diff --git a/python/api/job/task_urls.py b/python/api/job/task_urls.py index d666767954..28058454ae 100644 --- a/python/api/job/task_urls.py +++ b/python/api/job/task_urls.py @@ -10,21 +10,10 @@ # See the License for the specific language governing permissions and # limitations under the License. -from django.urls import include, path +from rest_framework.routers import DefaultRouter -from api.job.views import Task, TaskCancel, TaskDetail, TaskDownload, TaskReStart +from api.job.views import TaskViewSet -urlpatterns = [ - path("", Task.as_view(), name="task"), - path( - "/", - include( - [ - path("", TaskDetail.as_view(), name="task-details"), - path("restart/", TaskReStart.as_view(), name="task-restart"), - path("cancel/", TaskCancel.as_view(), name="task-cancel"), - path("download/", TaskDownload.as_view(), name="task-download"), - ] - ), - ), -] +router = DefaultRouter() +router.register("", TaskViewSet) +urlpatterns = router.urls diff --git a/python/api/job/urls.py b/python/api/job/urls.py index 154cefd1a9..7540940211 100644 --- a/python/api/job/urls.py +++ b/python/api/job/urls.py @@ -10,34 +10,30 @@ # See the License for the specific language governing permissions and # limitations under the License. -from django.urls import include, path +from django.urls import path +from rest_framework.routers import DefaultRouter -from api.job.views import ( - JobDetail, - JobList, - LogFile, - LogStorageListView, - LogStorageView, - download_log_file, -) +from api.job.views import JobViewSet, LogStorageViewSet + +router = DefaultRouter() +router.register("", JobViewSet) -# fmt: off urlpatterns = [ - path('', JobList.as_view(), name='job'), - path('/', include([ - path('', JobDetail.as_view(), name='job-details'), - path('log/', include([ - path('', LogStorageListView.as_view(), name='log-list'), - path('/', include([ - path('', LogStorageView.as_view(), name='log-storage'), - path('download/', download_log_file, name='download-log'), - ])), - path( - '///', - LogFile.as_view(), - name='log-file' - ), - ])), - ])), + *router.urls, + path("/log/", LogStorageViewSet.as_view({"get": "list"}), name="joblog-list"), + path( + "/log//", + LogStorageViewSet.as_view({"get": "retrieve"}), + name="joblog-detail", + ), + path( + "/log//download/", + LogStorageViewSet.as_view({"get": "download"}), + name="joblog-download", + ), + path( + "/log//download////", + LogStorageViewSet.as_view({"get": "logfile"}), + name="joblog-file", + ), ] -# fmt: on diff --git a/python/api/job/views.py b/python/api/job/views.py index a6a9c5a78b..acb66aae3e 100644 --- a/python/api/job/views.py +++ b/python/api/job/views.py @@ -11,84 +11,49 @@ # limitations under the License. import io -import os import re import tarfile from pathlib import Path from django.conf import settings +from django.contrib.contenttypes.models import ContentType from django.http import HttpResponse from guardian.mixins import PermissionListMixin -from rest_framework.permissions import DjangoModelPermissions, IsAuthenticated +from rest_framework.decorators import action +from rest_framework.mixins import ListModelMixin, RetrieveModelMixin +from rest_framework.permissions import DjangoModelPermissions from rest_framework.request import Request from rest_framework.response import Response -from rest_framework.reverse import reverse -from rest_framework.status import HTTP_200_OK, HTTP_404_NOT_FOUND -from rest_framework.views import APIView +from rest_framework.status import HTTP_200_OK from adcm.utils import str_remove_non_alnum -from api.base_view import DetailView, GenericUIView, PaginatedView +from api.base_view import GenericUIViewSet from api.job.serializers import ( - JobListSerializer, + JobRetrieveSerializer, JobSerializer, - LogSerializer, - LogStorageListSerializer, + LogStorageRetrieveSerializer, LogStorageSerializer, - TaskListSerializer, + TaskRetrieveSerializer, TaskSerializer, ) from api.utils import check_custom_perm, get_object_for_user from audit.utils import audit -from cm.config import RUN_DIR -from cm.errors import AdcmEx -from cm.job import cancel_task, get_log, restart_task +from cm.job import cancel_task, restart_task from cm.models import ActionType, JobLog, LogStorage, TaskLog from rbac.viewsets import DjangoOnlyObjectPermissions -VIEW_JOBLOG_PERMISSION = "cm.view_joblog" VIEW_TASKLOG_PERMISSION = "cm.view_tasklog" -def download_log_file(request, job_id, log_id): - job = JobLog.obj.get(id=job_id) - log_storage = LogStorage.obj.get(id=log_id, job=job) - - if log_storage.type in ["stdout", "stderr"]: - filename = f"{job.id}-{log_storage.name}-{log_storage.type}.{log_storage.format}" - else: - filename = f"{job.id}-{log_storage.name}.{log_storage.format}" - - filename = re.sub(r"\s+", "_", filename) - if log_storage.format == "txt": - mime_type = "text/plain" - else: - mime_type = "application/json" - - if log_storage.body is None: - body = "" - length = 0 - else: - body = log_storage.body - length = len(body) - - response = HttpResponse(body) - response["Content-Type"] = mime_type - response["Content-Length"] = length - response["Content-Encoding"] = "UTF-8" - response["Content-Disposition"] = f"attachment; filename={filename}" - - return response - - def get_task_download_archive_name(task: TaskLog) -> str: archive_name = f"{task.pk}.tar.gz" if not task.action: return archive_name - action_display_name = str_remove_non_alnum( - value=task.action.display_name - ) or str_remove_non_alnum(value=task.action.name) + action_display_name = str_remove_non_alnum(value=task.action.display_name) or str_remove_non_alnum( + value=task.action.name + ) if action_display_name: archive_name = f"{action_display_name}_{archive_name}" @@ -130,9 +95,9 @@ def get_task_download_archive_file_handler(task: TaskLog) -> io.BytesIO: jobs = JobLog.objects.filter(task=task) if task.action and task.action.type == ActionType.Job: - task_dir_name_suffix = str_remove_non_alnum( - value=task.action.display_name - ) or str_remove_non_alnum(value=task.action.name) + task_dir_name_suffix = str_remove_non_alnum(value=task.action.display_name) or str_remove_non_alnum( + value=task.action.name + ) else: task_dir_name_suffix = None @@ -142,213 +107,180 @@ def get_task_download_archive_file_handler(task: TaskLog) -> io.BytesIO: if task_dir_name_suffix is None: dir_name_suffix = "" if job.sub_action: - dir_name_suffix = str_remove_non_alnum( - value=job.sub_action.display_name - ) or str_remove_non_alnum(value=job.sub_action.name) + dir_name_suffix = str_remove_non_alnum(value=job.sub_action.display_name) or str_remove_non_alnum( + value=job.sub_action.name + ) else: dir_name_suffix = task_dir_name_suffix directory = Path(settings.RUN_DIR, str(job.pk)) if directory.is_dir(): - files = [ - item for item in Path(settings.RUN_DIR, str(job.pk)).iterdir() if item.is_file() - ] + files = [item for item in Path(settings.RUN_DIR, str(job.pk)).iterdir() if item.is_file()] for log_file in files: - tarinfo = tarfile.TarInfo( - f'{f"{job.pk}-{dir_name_suffix}".strip("-")}/{log_file.name}' - ) + tarinfo = tarfile.TarInfo(f'{f"{job.pk}-{dir_name_suffix}".strip("-")}/{log_file.name}') tarinfo.size = log_file.stat().st_size tar_file.addfile(tarinfo=tarinfo, fileobj=io.BytesIO(log_file.read_bytes())) else: log_storages = LogStorage.objects.filter(job=job, type__in={"stdout", "stderr"}) for log_storage in log_storages: tarinfo = tarfile.TarInfo( - f'{f"{job.pk}-{dir_name_suffix}".strip("-")}' - f'/{log_storage.name}-{log_storage.type}.txt' + f'{f"{job.pk}-{dir_name_suffix}".strip("-")}' f'/{log_storage.name}-{log_storage.type}.txt' ) - body = io.BytesIO(bytes(log_storage.body, "utf-8")) + body = io.BytesIO(bytes(log_storage.body, settings.ENCODING_UTF_8)) tarinfo.size = body.getbuffer().nbytes tar_file.addfile(tarinfo=tarinfo, fileobj=body) return fh -class JobList(PermissionListMixin, PaginatedView): - queryset = JobLog.objects.order_by("-id") - serializer_class = JobListSerializer - serializer_class_ui = JobSerializer +# pylint:disable-next=too-many-ancestors +class JobViewSet(PermissionListMixin, ListModelMixin, RetrieveModelMixin, GenericUIViewSet): + queryset = JobLog.objects.select_related("task", "action").all() + serializer_class = JobSerializer filterset_fields = ("action_id", "task_id", "pid", "status", "start_date", "finish_date") ordering_fields = ("status", "start_date", "finish_date") - permission_classes = (DjangoModelPermissions,) - permission_required = [VIEW_JOBLOG_PERMISSION] + ordering = ["-id"] + permission_required = ["cm.view_joblog"] + lookup_url_kwarg = "job_pk" def get_queryset(self, *args, **kwargs): - if self.request.user.is_superuser: - exclude_pks = [] + queryset = super().get_queryset(*args, **kwargs) + if not self.request.user.is_superuser: + # NOT superuser shouldn't have access to ADCM tasks + queryset = queryset.exclude(task__object_type=ContentType.objects.get(app_label="cm", model="adcm")) + return queryset + + def get_permissions(self): + if self.action == "list": + permission_classes = (DjangoModelPermissions,) else: - exclude_pks = JobLog.get_adcm_jobs_qs().values_list("pk", flat=True) + permission_classes = (DjangoOnlyObjectPermissions,) - return super().get_queryset(*args, **kwargs).exclude(pk__in=exclude_pks) + return [permission() for permission in permission_classes] + def get_serializer_class(self): + if self.is_for_ui() or self.action == "retrieve": + return JobRetrieveSerializer -class JobDetail(PermissionListMixin, GenericUIView): - queryset = JobLog.objects.all() - permission_classes = (DjangoOnlyObjectPermissions,) - permission_required = [VIEW_JOBLOG_PERMISSION] - serializer_class = JobSerializer + return super().get_serializer_class() - def get(self, request, *args, **kwargs): - """ - Show job - """ - job = get_object_for_user(request.user, VIEW_JOBLOG_PERMISSION, JobLog, id=kwargs["job_id"]) - job.log_dir = os.path.join(RUN_DIR, f"{job.id}") - logs = get_log(job) - for lg in logs: - log_id = lg["id"] - lg["url"] = reverse( - "log-storage", kwargs={"job_id": job.id, "log_id": log_id}, request=request - ) - lg["download_url"] = reverse( - "download-log", kwargs={"job_id": job.id, "log_id": log_id}, request=request - ) - - job.log_files = logs - serializer = self.get_serializer(job, data=request.data) - serializer.is_valid() - - return Response(serializer.data) - -class LogStorageListView(PermissionListMixin, PaginatedView): - queryset = LogStorage.objects.all() - permission_required = ["cm.view_logstorage"] - serializer_class = LogStorageListSerializer - filterset_fields = ("name", "type", "format") - ordering_fields = ("id", "name") +# pylint:disable-next=too-many-ancestors +class TaskViewSet(PermissionListMixin, ListModelMixin, RetrieveModelMixin, GenericUIViewSet): + queryset = TaskLog.objects.select_related("action").all() + serializer_class = TaskSerializer + filterset_fields = ("action_id", "pid", "status", "start_date", "finish_date") + ordering_fields = ("status", "start_date", "finish_date") + ordering = ["-id"] + permission_required = [VIEW_TASKLOG_PERMISSION] + lookup_url_kwarg = "task_pk" def get_queryset(self, *args, **kwargs): queryset = super().get_queryset(*args, **kwargs) - if "job_id" not in self.kwargs: - return queryset - - return queryset.filter(job_id=self.kwargs["job_id"]) - + if not self.request.user.is_superuser: + # NOT superuser shouldn't have access to ADCM tasks + queryset = queryset.exclude(object_type=ContentType.objects.get(app_label="cm", model="adcm")) + return queryset -class LogStorageView(PermissionListMixin, GenericUIView): - queryset = LogStorage.objects.all() - permission_classes = (IsAuthenticated,) - permission_required = ["cm.view_logstorage"] - serializer_class = LogStorageSerializer + def get_serializer_class(self): + if self.is_for_ui() or self.action in {"retrieve", "restart", "cancel", "download"}: + return TaskRetrieveSerializer - def get(self, request, *args, **kwargs): - job = get_object_for_user(request.user, VIEW_JOBLOG_PERMISSION, JobLog, id=kwargs["job_id"]) - try: - log_storage = self.get_queryset().get(id=kwargs["log_id"], job=job) - except LogStorage.DoesNotExist as e: - raise AdcmEx( - "LOG_NOT_FOUND", f"log {kwargs['log_id']} not found for job {kwargs['job_id']}" - ) from e + return super().get_serializer_class() - serializer = self.get_serializer(log_storage) + @audit + @action(methods=["put"], detail=True) + def restart(self, request: Request, task_pk: int) -> Response: + task = get_object_for_user(request.user, VIEW_TASKLOG_PERMISSION, TaskLog, id=task_pk) + check_custom_perm(request.user, "change", TaskLog, task) + restart_task(task) - return Response(serializer.data) + return Response(status=HTTP_200_OK) + @audit + @action(methods=["put"], detail=True) + def cancel(self, request: Request, task_pk: int) -> Response: + task = get_object_for_user(request.user, VIEW_TASKLOG_PERMISSION, TaskLog, id=task_pk) + check_custom_perm(request.user, "change", TaskLog, task) + cancel_task(task) -class LogFile(GenericUIView): - permission_classes = (IsAuthenticated,) - queryset = LogStorage.objects.all() - serializer_class = LogSerializer - - def get(self, request, job_id, tag, level, log_type): - """ - Show log file - """ - if tag == "ansible": - _type = f"std{level}" - else: - _type = "check" - tag = "ansible" + return Response(status=HTTP_200_OK) - ls = LogStorage.obj.get(job_id=job_id, name=tag, type=_type, format=log_type) - serializer = self.get_serializer(ls) + @audit + @action(methods=["get"], detail=True) + def download(self, request: Request, task_pk: int) -> Response: + task = get_object_for_user(request.user, VIEW_TASKLOG_PERMISSION, TaskLog, id=task_pk) + response = HttpResponse( + content=get_task_download_archive_file_handler(task=task).getvalue(), + content_type="application/tar+gzip", + ) + response["Content-Disposition"] = f'attachment; filename="{get_task_download_archive_name(task=task)}"' - return Response(serializer.data) + return response -class Task(PermissionListMixin, PaginatedView): - queryset = TaskLog.objects.order_by("-id") - permission_required = [VIEW_TASKLOG_PERMISSION] - serializer_class = TaskListSerializer - serializer_class_ui = TaskSerializer - filterset_fields = ("action_id", "pid", "status", "start_date", "finish_date") - ordering_fields = ("status", "start_date", "finish_date") +# pylint:disable-next=too-many-ancestors +class LogStorageViewSet(PermissionListMixin, ListModelMixin, RetrieveModelMixin, GenericUIViewSet): + queryset = LogStorage.objects.all() + serializer_class = LogStorageSerializer + filterset_fields = ("name", "type", "format") + ordering_fields = ("id", "name") + permission_required = ["cm.view_logstorage"] + lookup_url_kwarg = "log_pk" def get_queryset(self, *args, **kwargs): - if self.request.user.is_superuser: - exclude_pks = [] - else: - exclude_pks = TaskLog.get_adcm_tasks_qs().values_list("pk", flat=True) - - return super().get_queryset(*args, **kwargs).exclude(pk__in=exclude_pks) - - -class TaskDetail(PermissionListMixin, DetailView): - queryset = TaskLog.objects.all() - permission_required = [VIEW_TASKLOG_PERMISSION] - serializer_class = TaskSerializer - lookup_field = "id" - lookup_url_kwarg = "task_id" - error_code = "TASK_NOT_FOUND" - - -class TaskReStart(GenericUIView): - queryset = TaskLog.objects.all() - permission_classes = (IsAuthenticated,) - serializer_class = TaskSerializer - - @audit - def put(self, request, *args, **kwargs): - task = get_object_for_user( - request.user, VIEW_TASKLOG_PERMISSION, TaskLog, id=kwargs["task_id"] - ) - check_custom_perm(request.user, "change", TaskLog, task) - restart_task(task) + queryset = super().get_queryset(*args, **kwargs) + if "job_pk" in self.kwargs: + queryset = queryset.filter(job_id=self.kwargs["job_pk"]) - return Response(status=HTTP_200_OK) + return queryset + def get_serializer_class(self): + if self.is_for_ui() or self.action == "retrieve": + return LogStorageRetrieveSerializer -class TaskCancel(GenericUIView): - queryset = TaskLog.objects.all() - permission_classes = (IsAuthenticated,) - serializer_class = TaskSerializer + return super().get_serializer_class() @audit - def put(self, request, *args, **kwargs): - task = get_object_for_user( - request.user, VIEW_TASKLOG_PERMISSION, TaskLog, id=kwargs["task_id"] - ) - check_custom_perm(request.user, "change", TaskLog, task) - cancel_task(task) - - return Response(status=HTTP_200_OK) + @action(methods=["get"], detail=True) + def download(self, request: Request, job_pk: int, log_pk: int): + # self is necessary for audit + job = JobLog.obj.get(id=job_pk) + log_storage = LogStorage.obj.get(pk=log_pk, job=job) -class TaskDownload(PermissionListMixin, APIView): - permission_required = [VIEW_TASKLOG_PERMISSION] + if log_storage.type in {"stdout", "stderr"}: + filename = f"{job.id}-{log_storage.name}-{log_storage.type}.{log_storage.format}" + else: + filename = f"{job.id}-{log_storage.name}.{log_storage.format}" - @staticmethod - def get(request: Request, task_id: int): # pylint: disable=too-many-locals - task = TaskLog.objects.filter(pk=task_id).first() - if not task: - return Response(status=HTTP_404_NOT_FOUND) + filename = re.sub(r"\s+", "_", filename) + if log_storage.format == "txt": + mime_type = "text/plain" + else: + mime_type = "application/json" - response = HttpResponse( - content=get_task_download_archive_file_handler(task=task).getvalue(), - content_type="application/tar+gzip", - ) - response[ - "Content-Disposition" - ] = f'attachment; filename="{get_task_download_archive_name(task=task)}"' + if log_storage.body is None: + file_path = Path( + settings.RUN_DIR, + f"{job_pk}", + f"{log_storage.name}-{log_storage.type}.{log_storage.format}", + ) + if Path.is_file(file_path): + with open(file_path, "r", encoding=settings.ENCODING_UTF_8) as f: + body = f.read() + length = len(body) + else: + body = "" + length = 0 + else: + body = log_storage.body + length = len(body) + + response = HttpResponse(body) + response["Content-Type"] = mime_type + response["Content-Length"] = length + response["Content-Encoding"] = settings.ENCODING_UTF_8 + response["Content-Disposition"] = f"attachment; filename={filename}" return response diff --git a/python/api/object_config/views.py b/python/api/object_config/views.py index e725575bb5..1c92639923 100644 --- a/python/api/object_config/views.py +++ b/python/api/object_config/views.py @@ -18,9 +18,7 @@ from cm.models import ObjectConfig -class ObjectConfigViewSet( - PermissionListMixin, ReadOnlyModelViewSet -): # pylint: disable=too-many-ancestors +class ObjectConfigViewSet(PermissionListMixin, ReadOnlyModelViewSet): # pylint: disable=too-many-ancestors queryset = ObjectConfig.objects.all() serializer_class = ObjectConfigSerializer permission_classes = (DjangoObjectPermissions,) @@ -28,8 +26,6 @@ class ObjectConfigViewSet( def get_queryset(self, *args, **kwargs): if self.request.user.has_perm('cm.view_settings_of_adcm'): - return super().get_queryset(*args, **kwargs) | ObjectConfig.objects.filter( - adcm__isnull=False - ) + return super().get_queryset(*args, **kwargs) | ObjectConfig.objects.filter(adcm__isnull=False) else: return super().get_queryset(*args, **kwargs).filter(adcm__isnull=True) diff --git a/python/api/provider/serializers.py b/python/api/provider/serializers.py index 826c0912eb..182afb59e8 100644 --- a/python/api/provider/serializers.py +++ b/python/api/provider/serializers.py @@ -25,18 +25,12 @@ from api.concern.serializers import ConcernItemSerializer, ConcernItemUISerializer from api.group_config.serializers import GroupConfigsHyperlinkedIdentityField from api.serializers import DoUpgradeSerializer, StringListSerializer -from api.utils import ( - CommonAPIURL, - ObjectURL, - check_obj, - filter_actions, - get_upgradable_func, -) +from api.utils import CommonAPIURL, ObjectURL, check_obj, filter_actions from cm.adcm_config import get_main_info from cm.api import add_host_provider from cm.errors import AdcmEx from cm.models import Action, HostProvider, Prototype, Upgrade -from cm.upgrade import do_upgrade +from cm.upgrade import do_upgrade, get_upgrade class ProviderSerializer(EmptySerializer): @@ -46,15 +40,11 @@ class ProviderSerializer(EmptySerializer): description = CharField(required=False) state = CharField(read_only=True) before_upgrade = JSONField(read_only=True) - url = HyperlinkedIdentityField( - view_name="provider-details", lookup_field="id", lookup_url_kwarg="provider_id" - ) + url = HyperlinkedIdentityField(view_name="provider-details", lookup_field="id", lookup_url_kwarg="provider_id") @staticmethod def validate_prototype_id(prototype_id): - proto = check_obj( - Prototype, {"id": prototype_id, "type": "provider"}, "PROTOTYPE_NOT_FOUND" - ) + proto = check_obj(Prototype, {"id": prototype_id, "type": "provider"}, "PROTOTYPE_NOT_FOUND") return proto def create(self, validated_data): @@ -73,15 +63,13 @@ class ProviderDetailSerializer(ProviderSerializer): license = CharField(read_only=True) bundle_id = IntegerField(read_only=True) prototype = HyperlinkedIdentityField( - view_name="provider-type-details", - lookup_field="prototype_id", - lookup_url_kwarg="prototype_id", + view_name="provider-prototype-detail", + lookup_field="pk", + lookup_url_kwarg="prototype_pk", ) config = CommonAPIURL(view_name="object-config") action = CommonAPIURL(view_name="object-action") - upgrade = HyperlinkedIdentityField( - view_name="provider-upgrade", lookup_field="id", lookup_url_kwarg="provider_id" - ) + upgrade = HyperlinkedIdentityField(view_name="provider-upgrade", lookup_field="id", lookup_url_kwarg="provider_id") host = ObjectURL(read_only=True, view_name="host") multi_state = StringListSerializer(read_only=True) concerns = ConcernItemSerializer(many=True, read_only=True) @@ -96,15 +84,13 @@ class ProviderUISerializer(ProviderSerializer): prototype_version = SerializerMethodField() prototype_name = SerializerMethodField() prototype_display_name = SerializerMethodField() - upgrade = HyperlinkedIdentityField( - view_name="provider-upgrade", lookup_field="id", lookup_url_kwarg="provider_id" - ) + upgrade = HyperlinkedIdentityField(view_name="provider-upgrade", lookup_field="id", lookup_url_kwarg="provider_id") upgradable = SerializerMethodField() concerns = ConcernItemUISerializer(many=True, read_only=True) @staticmethod def get_upgradable(obj: HostProvider) -> bool: - return get_upgradable_func(obj) + return bool(get_upgrade(obj)) @staticmethod def get_prototype_version(obj: HostProvider) -> str: @@ -137,7 +123,7 @@ def get_actions(self, obj): @staticmethod def get_upgradable(obj: HostProvider) -> bool: - return get_upgradable_func(obj) + return bool(get_upgrade(obj)) @staticmethod def get_prototype_version(obj: HostProvider) -> str: diff --git a/python/api/provider/views.py b/python/api/provider/views.py index d1e90a2017..7288dca60f 100644 --- a/python/api/provider/views.py +++ b/python/api/provider/views.py @@ -101,15 +101,11 @@ def get(self, request, *args, **kwargs): """ List all available upgrades for specified host provider """ - provider = get_object_for_user( - request.user, "cm.view_hostprovider", HostProvider, id=kwargs["provider_id"] - ) + provider = get_object_for_user(request.user, "cm.view_hostprovider", HostProvider, id=kwargs["provider_id"]) check_custom_perm(request.user, "view_upgrade_of", "hostprovider", provider) update_hierarchy_issues(provider) obj = get_upgrade(provider, self.get_ordering()) - serializer = self.serializer_class( - obj, many=True, context={"provider_id": provider.id, "request": request} - ) + serializer = self.serializer_class(obj, many=True, context={"provider_id": provider.id, "request": request}) return Response(serializer.data) @@ -122,16 +118,10 @@ def get(self, request, *args, **kwargs): """ List all available upgrades for specified host provider """ - provider = get_object_for_user( - request.user, "cm.view_hostprovider", HostProvider, id=kwargs["provider_id"] - ) + provider = get_object_for_user(request.user, "cm.view_hostprovider", HostProvider, id=kwargs["provider_id"]) check_custom_perm(request.user, "view_upgrade_of", "hostprovider", provider) - obj = check_obj( - Upgrade, {"id": kwargs["upgrade_id"], "bundle__name": provider.prototype.bundle.name} - ) - serializer = self.serializer_class( - obj, context={"provider_id": provider.id, "request": request} - ) + obj = check_obj(Upgrade, {"id": kwargs["upgrade_id"], "bundle__name": provider.prototype.bundle.name}) + serializer = self.serializer_class(obj, context={"provider_id": provider.id, "request": request}) return Response(serializer.data) @@ -142,9 +132,7 @@ class DoProviderUpgrade(GenericUIView): @audit def post(self, request, *args, **kwargs): - provider = get_object_for_user( - request.user, "cm.view_hostprovider", HostProvider, id=kwargs["provider_id"] - ) + provider = get_object_for_user(request.user, "cm.view_hostprovider", HostProvider, id=kwargs["provider_id"]) check_custom_perm(request.user, "do_upgrade_of", "hostprovider", provider) serializer = self.get_serializer(data=request.data) diff --git a/python/api/serializers.py b/python/api/serializers.py index 37fea47c50..865442b5b2 100644 --- a/python/api/serializers.py +++ b/python/api/serializers.py @@ -42,7 +42,7 @@ class UpgradeSerializer(EmptySerializer): max_strict = BooleanField(required=False) upgradable = BooleanField(required=False) license = CharField(required=False) - license_url = hlink('bundle-license', 'bundle_id', 'bundle_id') + license_url = hlink('bundle-license', 'bundle_id', 'bundle_pk') from_edition = JSONField(required=False) state_available = JSONField(required=False) state_on_success = CharField(required=False) @@ -140,9 +140,7 @@ def get_url(self, obj, view_name, request, _format): # pylint: disable=redefine kwargs = {} for url_arg in self.url_args: if url_arg.startswith(extensions_api_settings.DEFAULT_PARENT_LOOKUP_KWARG_NAME_PREFIX): - parent_name = url_arg.replace( - extensions_api_settings.DEFAULT_PARENT_LOOKUP_KWARG_NAME_PREFIX, '', 1 - ) + parent_name = url_arg.replace(extensions_api_settings.DEFAULT_PARENT_LOOKUP_KWARG_NAME_PREFIX, '', 1) parent = self.context.get(parent_name) kwargs.update({url_arg: parent.id}) else: @@ -164,9 +162,7 @@ def get_url(self, obj, view_name, request, _format): # pylint: disable=redefine kwargs = {} for url_arg in self.url_args: if url_arg.startswith(extensions_api_settings.DEFAULT_PARENT_LOOKUP_KWARG_NAME_PREFIX): - parent_name = url_arg.replace( - extensions_api_settings.DEFAULT_PARENT_LOOKUP_KWARG_NAME_PREFIX, '', 1 - ) + parent_name = url_arg.replace(extensions_api_settings.DEFAULT_PARENT_LOOKUP_KWARG_NAME_PREFIX, '', 1) parent = self.context.get(parent_name) if parent is None: parent = obj diff --git a/python/api/service/serializers.py b/python/api/service/serializers.py index 7d9308d8f9..55250c4f0e 100644 --- a/python/api/service/serializers.py +++ b/python/api/service/serializers.py @@ -17,13 +17,16 @@ from rest_framework.serializers import ( BooleanField, CharField, + ChoiceField, HyperlinkedIdentityField, + HyperlinkedRelatedField, IntegerField, JSONField, - Serializer, + ModelSerializer, SerializerMethodField, ) +from adcm.serializers import EmptySerializer from api.action.serializers import ActionShort from api.cluster.serializers import BindSerializer from api.component.serializers import ComponentUISerializer @@ -34,11 +37,18 @@ from cm.adcm_config import get_main_info from cm.api import add_service_to_cluster, bind, multi_bind from cm.errors import AdcmEx -from cm.models import Action, Cluster, ClusterObject, Prototype, ServiceComponent +from cm.models import ( + Action, + Cluster, + ClusterObject, + MaintenanceMode, + Prototype, + ServiceComponent, +) from cm.status_api import get_service_status -class ServiceSerializer(Serializer): +class ServiceSerializer(EmptySerializer): id = IntegerField(read_only=True) cluster_id = IntegerField(required=True) name = CharField(read_only=True) @@ -46,9 +56,13 @@ class ServiceSerializer(Serializer): state = CharField(read_only=True) prototype_id = IntegerField(required=True, help_text="id of service prototype") url = ObjectURL(read_only=True, view_name="service-details") + maintenance_mode = CharField(read_only=True) + is_maintenance_mode_available = BooleanField(read_only=True) - def validate_prototype_id(self, prototype_id): + @staticmethod + def validate_prototype_id(prototype_id): check_obj(Prototype, {"id": prototype_id, "type": "service"}, "PROTOTYPE_NOT_FOUND") + return prototype_id def create(self, validated_data): @@ -100,12 +114,11 @@ class ServiceDetailSerializer(ServiceSerializer): component = ObjectURL(read_only=True, view_name="component") imports = ObjectURL(read_only=True, view_name="service-import") bind = ObjectURL(read_only=True, view_name="service-bind") - prototype = HyperlinkedIdentityField( - view_name="service-type-details", - lookup_field="prototype_id", - lookup_url_kwarg="prototype_id", + prototype = HyperlinkedRelatedField( + read_only=True, + view_name="service-prototype-detail", + lookup_url_kwarg="prototype_pk", ) - multi_state = StringListSerializer(read_only=True) concerns = ConcernItemSerializer(many=True, read_only=True) locked = BooleanField(read_only=True) @@ -130,10 +143,12 @@ def get_actions(self, obj): self.context["service_id"] = obj.id actions = filter_actions(obj, act_set) acts = ActionShort(actions, many=True, context=self.context) + return acts.data def get_components(self, obj): comps = ServiceComponent.objects.filter(service=obj, cluster=obj.cluster) + return ComponentUISerializer(comps, many=True, context=self.context).data @staticmethod @@ -145,27 +160,29 @@ def get_main_info(obj: ClusterObject) -> str | None: return get_main_info(obj) -class ImportPostSerializer(Serializer): +class ImportPostSerializer(EmptySerializer): bind = JSONField() def create(self, validated_data): binds = validated_data.get("bind") service = self.context.get("service") cluster = self.context.get("cluster") + return multi_bind(cluster, service, binds) -class ServiceBindUrlFiels(HyperlinkedIdentityField): - def get_url(self, obj, view_name, request, format): +class ServiceBindUrlFields(HyperlinkedIdentityField): + def get_url(self, obj, view_name, request, _format): kwargs = {"service_id": obj.service.id, "bind_id": obj.id} - return reverse(view_name, kwargs=kwargs, request=request, format=format) + + return reverse(view_name, kwargs=kwargs, request=request, format=_format) class ServiceBindSerializer(BindSerializer): - url = ServiceBindUrlFiels(read_only=True, view_name="service-bind-details") + url = ServiceBindUrlFields(read_only=True, view_name="service-bind-details") -class ServiceBindPostSerializer(Serializer): +class ServiceBindPostSerializer(EmptySerializer): id = IntegerField(read_only=True) export_cluster_id = IntegerField() export_service_id = IntegerField(required=False) @@ -175,6 +192,7 @@ class ServiceBindPostSerializer(Serializer): def create(self, validated_data): export_cluster = check_obj(Cluster, validated_data.get("export_cluster_id")) + return bind( validated_data.get("cluster"), validated_data.get("service"), @@ -183,10 +201,25 @@ def create(self, validated_data): ) -class StatusSerializer(Serializer): +class StatusSerializer(EmptySerializer): id = IntegerField(read_only=True) name = CharField(read_only=True) status = SerializerMethodField() - def get_status(self, obj): + @staticmethod + def get_status(obj): return get_service_status(obj) + + +class ServiceChangeMaintenanceModeSerializer(ModelSerializer): + maintenance_mode = ChoiceField(choices=(MaintenanceMode.ON.value, MaintenanceMode.OFF.value)) + + class Meta: + model = ClusterObject + fields = ("maintenance_mode",) + + +class ServiceAuditSerializer(ModelSerializer): + class Meta: + model = ClusterObject + fields = ("maintenance_mode",) diff --git a/python/api/service/urls.py b/python/api/service/urls.py index 0ba88a2749..f0b79d143d 100644 --- a/python/api/service/urls.py +++ b/python/api/service/urls.py @@ -10,7 +10,6 @@ # See the License for the specific language governing permissions and # limitations under the License. - from django.urls import include, path from api.service.views import ( @@ -19,34 +18,40 @@ ServiceDetailView, ServiceImportView, ServiceListView, + ServiceMaintenanceModeView, StatusList, ) urlpatterns = [ - path('', ServiceListView.as_view(), name='service'), + path("", ServiceListView.as_view(), name="service"), path( - '/', + "/", include( [ - path('', ServiceDetailView.as_view(), name='service-details'), - path('component/', include('api.component.urls')), - path('import/', ServiceImportView.as_view(), name='service-import'), + path("", ServiceDetailView.as_view(), name="service-details"), + path( + "maintenance-mode/", + ServiceMaintenanceModeView.as_view(), + name="service-maintenance-mode", + ), + path("component/", include("api.component.urls")), + path("import/", ServiceImportView.as_view(), name="service-import"), path( - 'bind/', + "bind/", include( [ - path('', ServiceBindView.as_view(), name='service-bind'), + path("", ServiceBindView.as_view(), name="service-bind"), path( - '/', + "/", ServiceBindDetailView.as_view(), - name='service-bind-details', + name="service-bind-details", ), ] ), ), - path('config/', include('api.config.urls'), {'object_type': 'service'}), - path('action/', include('api.action.urls'), {'object_type': 'service'}), - path('status/', StatusList.as_view(), name='service-status'), + path("config/", include("api.config.urls"), {"object_type": "service"}), + path("action/", include("api.action.urls"), {"object_type": "service"}), + path("status/", StatusList.as_view(), name="service-status"), ] ), ), diff --git a/python/api/service/views.py b/python/api/service/views.py index 147efb5604..6e6319ef4c 100644 --- a/python/api/service/views.py +++ b/python/api/service/views.py @@ -10,8 +10,10 @@ # See the License for the specific language governing permissions and # limitations under the License. +from django.conf import settings from guardian.mixins import PermissionListMixin from rest_framework import permissions +from rest_framework.request import Request from rest_framework.response import Response from rest_framework.status import HTTP_200_OK, HTTP_204_NO_CONTENT, HTTP_400_BAD_REQUEST @@ -22,6 +24,7 @@ ImportPostSerializer, ServiceBindPostSerializer, ServiceBindSerializer, + ServiceChangeMaintenanceModeSerializer, ServiceDetailSerializer, ServiceDetailUISerializer, ServiceSerializer, @@ -29,24 +32,38 @@ StatusSerializer, ) from api.stack.serializers import ImportSerializer -from api.utils import check_custom_perm, check_obj, create, get_object_for_user +from api.utils import ( + check_custom_perm, + check_obj, + create, + get_maintenance_mode_response, + get_object_for_user, +) from audit.utils import audit -from cm.api import delete_service, get_import, unbind +from cm.api import ( + cancel_locking_tasks, + delete_service, + get_import, + unbind, + update_mm_objects, +) from cm.errors import raise_adcm_ex -from cm.models import Cluster, ClusterBind, ClusterObject, HostComponent, Prototype +from cm.job import start_task +from cm.models import ( + Action, + Cluster, + ClusterBind, + ClusterObject, + HostComponent, + JobStatus, + Prototype, + ServiceComponent, + TaskLog, +) from cm.status_api import make_ui_service_status from rbac.viewsets import DjangoOnlyObjectPermissions -def check_service(user, kwargs): - service = get_object_for_user( - user, "cm.view_clusterobject", ClusterObject, id=kwargs["service_id"] - ) - if "cluster_id" in kwargs: - get_object_for_user(user, "cm.view_cluster", Cluster, id=kwargs["cluster_id"]) - return service - - class ServiceListView(PermissionListMixin, PaginatedView): queryset = ClusterObject.objects.all() permission_required = ["cm.view_clusterobject"] @@ -57,37 +74,28 @@ class ServiceListView(PermissionListMixin, PaginatedView): ordering_fields = ("state", "prototype__display_name", "prototype__version_order") def get(self, request, *args, **kwargs): - """ - List all services - """ queryset = self.get_queryset() if "cluster_id" in kwargs: - cluster = get_object_for_user( - request.user, "cm.view_cluster", Cluster, id=kwargs["cluster_id"] - ) + cluster = get_object_for_user(request.user, "cm.view_cluster", Cluster, id=kwargs["cluster_id"]) queryset = queryset.filter(cluster=cluster).select_related("config") + return self.get_page(self.filter_queryset(queryset), request) @audit def post(self, request, *args, **kwargs): - """ - Add service to cluster - """ serializer_class = self.serializer_class if "cluster_id" in kwargs: serializer_class = self.serializer_class_cluster - cluster = get_object_for_user( - request.user, "cm.view_cluster", Cluster, id=kwargs["cluster_id"] - ) + cluster = get_object_for_user(request.user, "cm.view_cluster", Cluster, id=kwargs["cluster_id"]) else: - cluster = get_object_for_user( - request.user, "cm.view_cluster", Cluster, id=request.data["cluster_id"] - ) + cluster = get_object_for_user(request.user, "cm.view_cluster", Cluster, id=request.data["cluster_id"]) + check_custom_perm(request.user, "add_service_to", "cluster", cluster) serializer = serializer_class( data=request.data, context={"request": request, "cluster_id": kwargs.get("cluster_id", None)}, ) + return create(serializer) @@ -103,45 +111,100 @@ class ServiceDetailView(PermissionListMixin, DetailView): def get_queryset(self, *args, **kwargs): queryset = super().get_queryset(*args, **kwargs) if "cluster_id" in self.kwargs: - cluster = get_object_for_user( - self.request.user, "cm.view_cluster", Cluster, id=self.kwargs["cluster_id"] - ) + cluster = get_object_for_user(self.request.user, "cm.view_cluster", Cluster, id=self.kwargs["cluster_id"]) queryset = queryset.filter(cluster=cluster) + return queryset @audit def delete(self, request, *args, **kwargs): - """ - Remove service from cluster - """ - instance = self.get_object() - if instance.state != "created": - raise_adcm_ex("SERVICE_DELETE_ERROR") - delete_service(instance) + instance: ClusterObject = self.get_object() + delete_action = Action.objects.filter( + prototype=instance.prototype, name=settings.ADCM_DELETE_SERVICE_ACTION_NAME + ).first() + host_components_exists = HostComponent.objects.filter(cluster=instance.cluster, service=instance).exists() + + if not delete_action: + if instance.state != "created": + raise_adcm_ex("SERVICE_DELETE_ERROR") + + if host_components_exists: + raise_adcm_ex("SERVICE_CONFLICT", f"Service #{instance.id} has component(s) on host(s)") + + if ClusterBind.objects.filter(source_service=instance).exists(): + raise_adcm_ex("SERVICE_CONFLICT", f"Service #{instance.id} has exports(s)") + + if instance.prototype.required: + raise_adcm_ex("SERVICE_CONFLICT", f"Service #{instance.id} is required") + + if ClusterBind.objects.filter(service=instance).exists(): + raise_adcm_ex("SERVICE_CONFLICT", f"Service #{instance.id} has bind") + + if TaskLog.objects.filter(action=delete_action, status=JobStatus.RUNNING).exists(): + raise_adcm_ex("SERVICE_DELETE_ERROR", "Service is deleting now") + + if any( + service_component.requires_service_name(service_name=instance.name) + for service_component in ServiceComponent.objects.filter(cluster=instance.cluster) + ): + raise_adcm_ex("SERVICE_CONFLICT", "Another service component requires component of this service") + + cancel_locking_tasks(obj=instance, obj_deletion=True) + if delete_action and (host_components_exists or instance.state != "created"): + start_task( + action=delete_action, + obj=instance, + conf={}, + attr={}, + hc=[], + hosts=[], + verbose=False, + ) + else: + delete_service(service=instance) return Response(status=HTTP_204_NO_CONTENT) +class ServiceMaintenanceModeView(GenericUIView): + queryset = ClusterObject.objects.all() + permission_classes = (DjangoOnlyObjectPermissions,) + serializer_class = ServiceChangeMaintenanceModeSerializer + lookup_field = "id" + lookup_url_kwarg = "service_id" + + @update_mm_objects + @audit + def post(self, request: Request, **kwargs) -> Response: + service = get_object_for_user(request.user, "cm.view_clusterobject", ClusterObject, id=kwargs["service_id"]) + check_custom_perm(request.user, "change_maintenance_mode", service.__class__.__name__.lower(), service) + serializer = self.get_serializer(instance=service, data=request.data) + serializer.is_valid(raise_exception=True) + + response: Response = get_maintenance_mode_response(obj=service, serializer=serializer) + if response.status_code == HTTP_200_OK: + response.data = serializer.data + + return response + + class ServiceImportView(GenericUIView): queryset = Prototype.objects.all() serializer_class = ImportSerializer serializer_class_post = ImportPostSerializer permission_classes = (permissions.IsAuthenticated,) - def get(self, request, *args, **kwargs): - """ - List all imports available for specified service - """ - service = check_service(request.user, kwargs) - check_custom_perm( - request.user, "view_import_of", "clusterobject", service, "view_clusterbind" - ) + @staticmethod + def get(request, *args, **kwargs): + service = get_object_for_user(request.user, "cm.view_clusterobject", ClusterObject, id=kwargs["service_id"]) + check_custom_perm(request.user, "view_import_of", "clusterobject", service, "view_clusterbind") cluster = service.cluster + return Response(get_import(cluster, service)) @audit def post(self, request, **kwargs): - service = check_service(request.user, kwargs) + service = get_object_for_user(request.user, "cm.view_clusterobject", ClusterObject, id=kwargs["service_id"]) check_custom_perm(request.user, "change_import_of", "clusterobject", service) cluster = service.cluster serializer = self.get_serializer( @@ -149,6 +212,7 @@ def post(self, request, **kwargs): ) if serializer.is_valid(): return Response(serializer.create(serializer.validated_data), status=HTTP_200_OK) + return Response(serializer.errors, status=HTTP_400_BAD_REQUEST) @@ -159,26 +223,20 @@ class ServiceBindView(GenericUIView): permission_classes = (permissions.IsAuthenticated,) def get(self, request, *args, **kwargs): - """ - List all binds of service - """ - service = check_service(request.user, kwargs) - check_custom_perm( - request.user, "view_import_of", "clusterobject", service, "view_clusterbind" - ) + service = get_object_for_user(request.user, "cm.view_clusterobject", ClusterObject, id=kwargs["service_id"]) + check_custom_perm(request.user, "view_import_of", "clusterobject", service, "view_clusterbind") binds = self.get_queryset().filter(service=service) serializer = self.get_serializer(binds, many=True) + return Response(serializer.data) @audit def post(self, request, **kwargs): - """ - Bind two services - """ - service = check_service(request.user, kwargs) + service = get_object_for_user(request.user, "cm.view_clusterobject", ClusterObject, id=kwargs["service_id"]) check_custom_perm(request.user, "change_import_of", "clusterobject", service) cluster = service.cluster serializer = self.get_serializer(data=request.data) + return create(serializer, cluster=cluster, service=service) @@ -188,29 +246,26 @@ class ServiceBindDetailView(GenericUIView): permission_classes = (permissions.IsAuthenticated,) def get_obj(self, kwargs, bind_id): - service = check_service(self.request.user, kwargs) + service = get_object_for_user( + self.request.user, "cm.view_clusterobject", ClusterObject, id=kwargs["service_id"] + ) cluster = service.cluster + return service, check_obj(ClusterBind, {"cluster": cluster, "id": bind_id}) def get(self, request, *args, **kwargs): - """ - Show specified bind of service - """ service, bind = self.get_obj(kwargs, kwargs["bind_id"]) - check_custom_perm( - request.user, "view_import_of", "clusterobject", service, "view_clusterbind" - ) + check_custom_perm(request.user, "view_import_of", "clusterobject", service, "view_clusterbind") serializer = self.get_serializer(bind) + return Response(serializer.data) @audit def delete(self, request, *args, **kwargs): - """ - Unbind specified bind of service - """ service, bind = self.get_obj(kwargs, kwargs["bind_id"]) check_custom_perm(request.user, "change_import_of", "clusterobject", service) unbind(bind) + return Response(status=HTTP_204_NO_CONTENT) @@ -220,13 +275,10 @@ class StatusList(GenericUIView): serializer_class = StatusSerializer def get(self, request, *args, **kwargs): - """ - Show all hosts and components in a specified cluster - """ - service = check_service(request.user, kwargs) + service = get_object_for_user(request.user, "cm.view_clusterobject", ClusterObject, id=kwargs["service_id"]) if self._is_for_ui(): host_components = self.get_queryset().filter(service=service) + return Response(make_ui_service_status(service, host_components)) - else: - serializer = self.get_serializer(service) - return Response(serializer.data) + + return Response(self.get_serializer(service).data) diff --git a/python/api/stack/root.py b/python/api/stack/root.py index f0b0d84417..40ea19356a 100644 --- a/python/api/stack/root.py +++ b/python/api/stack/root.py @@ -12,20 +12,19 @@ """Stack endpoint root view""" -from rest_framework import permissions, routers +from rest_framework.permissions import AllowAny +from rest_framework.routers import APIRootView -class StackRoot(routers.APIRootView): - """Stack Root""" - - permission_classes = (permissions.AllowAny,) +class StackRoot(APIRootView): + permission_classes = (AllowAny,) api_root_dict = { - 'load': 'load-bundle', - 'upload': 'upload-bundle', - 'bundle': 'bundle', - 'prototype': 'prototype', - 'service': 'service-type', - 'host': 'host-type', - 'provider': 'provider-type', - 'cluster': 'cluster-type', + "load": "load-bundle", + "upload": "upload-bundle", + "bundle": "bundle-list", + "prototype": "prototype-list", + "service": "service-prototype-list", + "host": "host-prototype-list", + "provider": "provider-prototype-list", + "cluster": "cluster-prototype-list", } diff --git a/python/api/stack/serializers.py b/python/api/stack/serializers.py index 2c4753ae34..bd8c9e150f 100644 --- a/python/api/stack/serializers.py +++ b/python/api/stack/serializers.py @@ -13,8 +13,9 @@ from rest_framework.serializers import ( BooleanField, CharField, - DateTimeField, FileField, + HyperlinkedIdentityField, + HyperlinkedModelSerializer, IntegerField, JSONField, ModelSerializer, @@ -25,124 +26,171 @@ from api.action.serializers import StackActionDetailSerializer from api.config.serializers import ConfigSerializer from api.serializers import UpgradeSerializer -from api.utils import hlink -from cm import config from cm.models import Bundle, ClusterObject, Prototype -class LoadBundle(EmptySerializer): - bundle_file = CharField() +class UploadBundleSerializer(EmptySerializer): + file = FileField(help_text="bundle file for upload") -class UploadBundle(EmptySerializer): - file = FileField(help_text='bundle file for upload') +class LoadBundleSerializer(EmptySerializer): + bundle_file = CharField() - def create(self, validated_data): - fd = self.context['request'].data['file'] - fname = f'{config.DOWNLOAD_DIR}/{fd}' - with open(fname, 'wb+') as dest: - for chunk in fd.chunks(): - dest.write(chunk) - return Bundle() +class BundleSerializer(HyperlinkedModelSerializer): + license_url = HyperlinkedIdentityField(view_name="bundle-license", lookup_field="pk", lookup_url_kwarg="bundle_pk") + update = HyperlinkedIdentityField(view_name="bundle-update", lookup_field="pk", lookup_url_kwarg="bundle_pk") + license = SerializerMethodField() -class BundleSerializer(EmptySerializer): - id = IntegerField(read_only=True) - name = CharField(read_only=True) - version = CharField(read_only=True) - edition = CharField(read_only=True) - hash = CharField(read_only=True) - license = CharField(read_only=True) - license_path = CharField(read_only=True) - license_hash = CharField(read_only=True) - description = CharField(required=False) - date = DateTimeField(read_only=True) - url = hlink('bundle-details', 'id', 'bundle_id') - license_url = hlink('bundle-license', 'id', 'bundle_id') - update = hlink('bundle-update', 'id', 'bundle_id') + class Meta: + model = Bundle + fields = ( + "id", + "name", + "version", + "edition", + "license", + "hash", + "description", + "date", + "license_url", + "update", + "url", + ) + read_only_fields = fields + extra_kwargs = {"url": {"lookup_url_kwarg": "bundle_pk"}} def to_representation(self, instance): data = super().to_representation(instance) - proto = Prototype.objects.filter(bundle=instance, name=instance.name) - data['adcm_min_version'] = proto[0].adcm_min_version - data['display_name'] = proto[0].display_name + proto = Prototype.objects.filter(bundle=instance, name=instance.name).first() + data["adcm_min_version"] = proto.adcm_min_version + data["display_name"] = proto.display_name + return data + def get_license(self, obj: Bundle) -> str | None: + proto = Prototype.objects.filter(bundle=obj, name=obj.name).first() + if proto: + return proto.license -class LicenseSerializer(EmptySerializer): - license = CharField(read_only=True) - text = CharField(read_only=True) - accept = hlink('accept-license', 'id', 'bundle_id') + return None -class PrototypeSerializer(EmptySerializer): - bundle_id = IntegerField(read_only=True) - id = IntegerField(read_only=True) - path = CharField(read_only=True) - name = CharField(read_only=True) - display_name = CharField(required=False) - version = CharField(read_only=True) - bundle_edition = SerializerMethodField() - description = CharField(required=False) - type = CharField(read_only=True) - required = BooleanField(read_only=True) - url = hlink('prototype-details', 'id', 'prototype_id') +class PrototypeSerializer(HyperlinkedModelSerializer): + license_url = HyperlinkedIdentityField( + view_name="prototype-license", lookup_field="pk", lookup_url_kwarg="prototype_pk" + ) + bundle_edition = CharField(source="bundle.edition") + class Meta: + model = Prototype + fields = ( + "id", + "bundle_id", + "type", + "path", + "name", + "license", + "license_path", + "license_hash", + "license_url", + "display_name", + "version", + "required", + "description", + "bundle_edition", + "url", + ) + read_only_fields = fields + extra_kwargs = {"url": {"lookup_url_kwarg": "prototype_pk"}} + + +class PrototypeSerializerMixin: @staticmethod - def get_bundle_edition(obj): - return obj.bundle.edition - + def get_constraint(obj: Prototype) -> list[dict]: + if obj.type == "component": + return obj.constraint -def get_constraint(self, obj): - if obj.type == 'component': - return obj.constraint - return [] + return [] + @staticmethod + def get_service_name(obj): + if obj.type == "component": + return obj.parent.name -def get_service_name(self, obj): - if obj.type == 'component': - return obj.parent.name - return '' + return "" + @staticmethod + def get_service_display_name(obj): + if obj.type == "component": + return obj.parent.display_name -def get_service_display_name(self, obj): - if obj.type == 'component': - return obj.parent.display_name - return '' + return "" + @staticmethod + def get_service_id(obj): + if obj.type == "component": + return obj.parent.id -def get_service_id(self, obj): - if obj.type == 'component': - return obj.parent.id - return None + return None -class PrototypeUISerializer(PrototypeSerializer): - parent_id = IntegerField(read_only=True) - version_order = IntegerField(read_only=True) - shared = BooleanField(read_only=True) +class PrototypeUISerializer(PrototypeSerializer, PrototypeSerializerMixin): constraint = SerializerMethodField(read_only=True) - requires = JSONField(read_only=True) - bound_to = JSONField(read_only=True) - adcm_min_version = CharField(read_only=True) - monitoring = CharField(read_only=True) - config_group_customization = BooleanField(read_only=True) - venv = CharField(read_only=True) - allow_maintenance_mode = BooleanField(read_only=True) service_name = SerializerMethodField(read_only=True) service_display_name = SerializerMethodField(read_only=True) service_id = SerializerMethodField(read_only=True) - get_constraint = get_constraint - get_service_name = get_service_name - get_service_display_name = get_service_display_name - get_service_id = get_service_id + class Meta: + model = Prototype + fields = ( + *PrototypeSerializer.Meta.fields, + "parent_id", + "version_order", + "shared", + "constraint", + "requires", + "bound_to", + "adcm_min_version", + "monitoring", + "config_group_customization", + "venv", + "allow_maintenance_mode", + "service_name", + "service_display_name", + "service_id", + ) + read_only_fields = fields + extra_kwargs = {"url": {"lookup_url_kwarg": "prototype_pk"}} + + +class PrototypeDetailSerializer(PrototypeSerializer, PrototypeSerializerMixin): + constraint = SerializerMethodField() + actions = StackActionDetailSerializer(many=True, read_only=True) + config = ConfigSerializer(many=True, read_only=True) + service_name = SerializerMethodField(read_only=True) + service_display_name = SerializerMethodField(read_only=True) + service_id = SerializerMethodField(read_only=True) + + class Meta: + model = Prototype + fields = ( + *PrototypeSerializer.Meta.fields, + "constraint", + "actions", + "config", + "service_name", + "service_display_name", + "service_id", + ) + read_only_fields = fields + extra_kwargs = {"url": {"lookup_url_kwarg": "prototype_pk"}} class PrototypeShort(ModelSerializer): class Meta: model = Prototype - fields = ('name',) + fields = ("name",) class ExportSerializer(EmptySerializer): @@ -161,105 +209,212 @@ class ImportSerializer(EmptySerializer): multibind = BooleanField(read_only=True) -class ComponentTypeSerializer(PrototypeSerializer): - constraint = JSONField(required=False) - requires = JSONField(required=False) - bound_to = JSONField(required=False) - monitoring = CharField(read_only=True) - url = hlink('component-type-details', 'id', 'prototype_id') +class ComponentPrototypeSerializer(PrototypeSerializer): + url = HyperlinkedIdentityField( + view_name="component-prototype-detail", lookup_field="pk", lookup_url_kwarg="prototype_pk" + ) + class Meta: + model = Prototype + fields = ( + *PrototypeSerializer.Meta.fields, + "constraint", + "requires", + "bound_to", + "monitoring", + "url", + ) + read_only_fields = fields + + +class ServicePrototypeSerializer(PrototypeSerializer): + url = HyperlinkedIdentityField( + view_name="service-prototype-detail", lookup_field="pk", lookup_url_kwarg="prototype_pk" + ) -class ServiceSerializer(PrototypeSerializer): - shared = BooleanField(read_only=True) - monitoring = CharField(read_only=True) - url = hlink('service-type-details', 'id', 'prototype_id') + class Meta: + model = Prototype + fields = ( + *PrototypeSerializer.Meta.fields, + "shared", + "monitoring", + "url", + ) + read_only_fields = fields -class ServiceDetailSerializer(ServiceSerializer): +class ServiceDetailPrototypeSerializer(ServicePrototypeSerializer): actions = StackActionDetailSerializer(many=True, read_only=True) - components = ComponentTypeSerializer(many=True, read_only=True) + components = ComponentPrototypeSerializer(many=True, read_only=True) config = ConfigSerializer(many=True, read_only=True) exports = ExportSerializer(many=True, read_only=True) imports = ImportSerializer(many=True, read_only=True) - -class BundleServiceUISerializer(ServiceSerializer): + class Meta: + model = Prototype + fields = ( + *ServicePrototypeSerializer.Meta.fields, + "actions", + "components", + "config", + "exports", + "imports", + ) + read_only_fields = fields + + +class BundleServiceUIPrototypeSerializer(ServicePrototypeSerializer): selected = SerializerMethodField() + class Meta: + model = Prototype + fields = ( + *ServicePrototypeSerializer.Meta.fields, + "selected", + ) + read_only_fields = fields + def get_selected(self, obj): - cluster = self.context.get('cluster') + cluster = self.context.get("cluster") try: ClusterObject.objects.get(cluster=cluster, prototype=obj) + return True except ClusterObject.DoesNotExist: return False -class AdcmTypeSerializer(PrototypeSerializer): - url = hlink('adcm-type-details', 'id', 'prototype_id') - +class ADCMPrototypeSerializer(PrototypeSerializer): + url = HyperlinkedIdentityField( + view_name="adcm-prototype-detail", lookup_field="pk", lookup_url_kwarg="prototype_pk" + ) -class ClusterTypeSerializer(PrototypeSerializer): - license = SerializerMethodField() - url = hlink('cluster-type-details', 'id', 'prototype_id') + class Meta: + model = Prototype + fields = ( + *PrototypeSerializer.Meta.fields, + "url", + ) + read_only_fields = fields - @staticmethod - def get_license(obj): - return obj.bundle.license +class ClusterPrototypeSerializer(PrototypeSerializer): + url = HyperlinkedIdentityField( + view_name="cluster-prototype-detail", lookup_field="pk", lookup_url_kwarg="prototype_pk" + ) -class HostTypeSerializer(PrototypeSerializer): - monitoring = CharField(read_only=True) - url = hlink('host-type-details', 'id', 'prototype_id') + class Meta: + model = Prototype + fields = ( + *PrototypeSerializer.Meta.fields, + "url", + ) + read_only_fields = fields -class ProviderTypeSerializer(PrototypeSerializer): - license = SerializerMethodField() - url = hlink('provider-type-details', 'id', 'prototype_id') +class HostPrototypeSerializer(PrototypeSerializer): + monitoring = CharField(read_only=True) + url = HyperlinkedIdentityField( + view_name="host-prototype-detail", lookup_field="pk", lookup_url_kwarg="prototype_pk" + ) - @staticmethod - def get_license(obj): - return obj.bundle.license + class Meta: + model = Prototype + fields = ( + *PrototypeSerializer.Meta.fields, + "monitoring", + "url", + ) + read_only_fields = fields -class PrototypeDetailSerializer(PrototypeSerializer): - constraint = SerializerMethodField() - actions = StackActionDetailSerializer(many=True, read_only=True) - config = ConfigSerializer(many=True, read_only=True) - service_name = SerializerMethodField(read_only=True) - service_display_name = SerializerMethodField(read_only=True) - service_id = SerializerMethodField(read_only=True) +class ProviderPrototypeSerializer(PrototypeSerializer): + url = HyperlinkedIdentityField( + view_name="provider-prototype-detail", lookup_field="pk", lookup_url_kwarg="prototype_pk" + ) - get_constraint = get_constraint - get_service_name = get_service_name - get_service_display_name = get_service_display_name - get_service_id = get_service_id + class Meta: + model = Prototype + fields = ( + *PrototypeSerializer.Meta.fields, + "url", + ) + read_only_fields = fields -class ProviderTypeDetailSerializer(ProviderTypeSerializer): +class ProviderPrototypeDetailSerializer(ProviderPrototypeSerializer): actions = StackActionDetailSerializer(many=True, read_only=True) config = ConfigSerializer(many=True, read_only=True) upgrade = UpgradeSerializer(many=True, read_only=True) + class Meta: + model = Prototype + fields = ( + *ProviderPrototypeSerializer.Meta.fields, + "actions", + "config", + "upgrade", + ) + read_only_fields = fields + -class HostTypeDetailSerializer(HostTypeSerializer): +class HostPrototypeDetailSerializer(HostPrototypeSerializer): actions = StackActionDetailSerializer(many=True, read_only=True) config = ConfigSerializer(many=True, read_only=True) + class Meta: + model = Prototype + fields = ( + *HostPrototypeSerializer.Meta.fields, + "actions", + "config", + ) + read_only_fields = fields + -class ComponentTypeDetailSerializer(ComponentTypeSerializer): +class ComponentPrototypeDetailSerializer(ComponentPrototypeSerializer): actions = StackActionDetailSerializer(many=True, read_only=True) config = ConfigSerializer(many=True, read_only=True) + class Meta: + model = Prototype + fields = ( + *ComponentPrototypeSerializer.Meta.fields, + "actions", + "config", + ) + read_only_fields = fields + -class AdcmTypeDetailSerializer(AdcmTypeSerializer): +class ADCMPrototypeDetailSerializer(ADCMPrototypeSerializer): actions = StackActionDetailSerializer(many=True, read_only=True) config = ConfigSerializer(many=True, read_only=True) + class Meta: + model = Prototype + fields = ( + *ADCMPrototypeSerializer.Meta.fields, + "actions", + "config", + ) + read_only_fields = fields -class ClusterTypeDetailSerializer(ClusterTypeSerializer): + +class ClusterPrototypeDetailSerializer(ClusterPrototypeSerializer): actions = StackActionDetailSerializer(many=True, read_only=True) config = ConfigSerializer(many=True, read_only=True) upgrade = UpgradeSerializer(many=True, read_only=True) exports = ExportSerializer(many=True, read_only=True) imports = ImportSerializer(many=True, read_only=True) + + class Meta: + model = Prototype + fields = ( + *ADCMPrototypeSerializer.Meta.fields, + "actions", + "config", + "upgrade", + "exports", + "imports", + ) + read_only_fields = fields diff --git a/python/api/stack/urls.py b/python/api/stack/urls.py index 65c858120c..2d977e909a 100644 --- a/python/api/stack/urls.py +++ b/python/api/stack/urls.py @@ -10,64 +10,56 @@ # See the License for the specific language governing permissions and # limitations under the License. -from django.urls import include, path +from django.urls import path +from rest_framework.routers import DefaultRouter -from api.stack import root, views +from api.stack.root import StackRoot +from api.stack.views import ( + ADCMPrototypeViewSet, + BundleViewSet, + ClusterPrototypeViewSet, + ComponentPrototypeViewSet, + HostPrototypeViewSet, + LoadBundleView, + ProtoActionViewSet, + PrototypeViewSet, + ProviderPrototypeViewSet, + ServicePrototypeViewSet, + UploadBundleView, + load_servicemap_view, +) -PROTOTYPE_ID = '/' +router = DefaultRouter() +router.register("bundle", BundleViewSet) +router.register("prototype", PrototypeViewSet) +router.register("action", ProtoActionViewSet) +router.register("service", ServicePrototypeViewSet, basename="service-prototype") +router.register("component", ComponentPrototypeViewSet, basename="component-prototype") +router.register("provider", ProviderPrototypeViewSet, basename="provider-prototype") +router.register("host", HostPrototypeViewSet, basename="host-prototype") +router.register("cluster", ClusterPrototypeViewSet, basename="cluster-prototype") +router.register("adcm", ADCMPrototypeViewSet, basename="adcm-prototype") -# fmt: off urlpatterns = [ - path('', root.StackRoot.as_view(), name='stack'), - path('upload/', views.UploadBundle.as_view(), name='upload-bundle'), - path('load/', views.LoadBundle.as_view({'post': 'create'}), name='load-bundle'), + path("", StackRoot.as_view(), name="stack"), + path("upload/", UploadBundleView.as_view({"post": "create"}), name="upload-bundle"), + path("load/", LoadBundleView.as_view({"post": "create"}), name="load-bundle"), + path("load/servicemap/", load_servicemap_view, name="load-servicemap"), path( - 'load/servicemap/', views.LoadBundle.as_view({'put': 'servicemap'}), name='load-servicemap' + "bundle//update/", + BundleViewSet.as_view({"put": "update_bundle"}), + name="bundle-update", ), path( - 'load/hostmap/', views.LoadBundle.as_view({'put': 'hostmap'}), name='load-hostmap' + "bundle//license/accept/", + BundleViewSet.as_view({"put": "accept_license"}), + name="accept-license", ), - path('bundle/', include([ - path('', views.BundleList.as_view(), name='bundle'), - path('/', include([ - path('', views.BundleDetail.as_view(), name='bundle-details'), - path('update/', views.BundleUpdate.as_view(), name='bundle-update'), - path('license/', views.BundleLicense.as_view(), name='bundle-license'), - path('license/accept/', views.AcceptLicense.as_view(), name='accept-license'), - ])), - ])), - path('action//', views.ProtoActionDetail.as_view(), name='action-details'), - path('prototype/', include([ - path('', views.PrototypeList.as_view(), name='prototype'), - path(PROTOTYPE_ID, views.PrototypeDetail.as_view(), name='prototype-details') - ])), - path('service/', include([ - path('', views.ServiceList.as_view(), name='service-type'), - path(PROTOTYPE_ID, include([ - path('', views.ServiceDetail.as_view(), name='service-type-details'), - path('action/', views.ServiceProtoActionList.as_view(), name='service-actions'), - ])), - ])), - path('component/', include([ - path('', views.ComponentList.as_view(), name='component-type'), - path(PROTOTYPE_ID, views.ComponentTypeDetail.as_view(), name='component-type-details'), - ])), - path('provider/', include([ - path('', views.ProviderTypeList.as_view(), name='provider-type'), - path(PROTOTYPE_ID, views.ProviderTypeDetail.as_view(), name='provider-type-details'), - ])), - path('host/', include([ - path('', views.HostTypeList.as_view(), name='host-type'), - path(PROTOTYPE_ID, views.HostTypeDetail.as_view(), name='host-type-details'), - ])), - path('cluster/', include([ - path('', views.ClusterTypeList.as_view(), name='cluster-type'), - path(PROTOTYPE_ID, views.ClusterTypeDetail.as_view(), name='cluster-type-details'), - ])), - path('adcm/', include([ - path('', views.AdcmTypeList.as_view(), name='adcm-type'), - path(PROTOTYPE_ID, views.AdcmTypeDetail.as_view(), name='adcm-type-details'), - ])), + path( + "prototype//license/accept/", + PrototypeViewSet.as_view({"put": "accept_license"}), + name="accept-license", + ), + *router.urls, # for correct work of root view router urls must be at bottom of urlpatterns ] -# fmt: on diff --git a/python/api/stack/views.py b/python/api/stack/views.py index b1d1626770..a0d9b0925d 100644 --- a/python/api/stack/views.py +++ b/python/api/stack/views.py @@ -10,26 +10,54 @@ # See the License for the specific language governing permissions and # limitations under the License. -from rest_framework import decorators, status +from pathlib import Path + +from django.conf import settings +from django.http import HttpResponse +from django.views.decorators.csrf import csrf_exempt from rest_framework.authentication import SessionAuthentication, TokenAuthentication -from rest_framework.mixins import CreateModelMixin +from rest_framework.decorators import action +from rest_framework.mixins import CreateModelMixin, ListModelMixin, RetrieveModelMixin from rest_framework.parsers import MultiPartParser from rest_framework.permissions import IsAuthenticated +from rest_framework.request import Request from rest_framework.response import Response from rest_framework.reverse import reverse +from rest_framework.status import ( + HTTP_200_OK, + HTTP_201_CREATED, + HTTP_204_NO_CONTENT, + HTTP_400_BAD_REQUEST, + HTTP_405_METHOD_NOT_ALLOWED, +) +from rest_framework.viewsets import ModelViewSet +from adcm.permissions import DjangoObjectPermissionsAudit, IsAuthenticatedAudit from api.action.serializers import StackActionSerializer -from api.base_view import ( - DetailView, - GenericUIView, - GenericUIViewSet, - ModelPermOrReadOnlyForAuth, - PaginatedView, +from api.base_view import GenericUIViewSet, ModelPermOrReadOnlyForAuth +from api.stack.serializers import ( + ADCMPrototypeDetailSerializer, + ADCMPrototypeSerializer, + BundleSerializer, + ClusterPrototypeDetailSerializer, + ClusterPrototypeSerializer, + ComponentPrototypeDetailSerializer, + ComponentPrototypeSerializer, + HostPrototypeDetailSerializer, + HostPrototypeSerializer, + LoadBundleSerializer, + PrototypeDetailSerializer, + PrototypeSerializer, + PrototypeUISerializer, + ProviderPrototypeDetailSerializer, + ProviderPrototypeSerializer, + ServiceDetailPrototypeSerializer, + ServicePrototypeSerializer, + UploadBundleSerializer, ) -from api.stack import filters, serializers from api.utils import check_obj from audit.utils import audit -from cm.api import accept_license, get_license, load_host_map, load_service_map +from cm.api import accept_license, get_license, load_service_map from cm.bundle import delete_bundle, load_bundle, update_bundle from cm.models import ( Action, @@ -42,346 +70,332 @@ ) +@csrf_exempt +def load_servicemap_view(request: Request) -> HttpResponse: + if request.method != "PUT": + return HttpResponse(status=HTTP_405_METHOD_NOT_ALLOWED) + + load_service_map() + + return HttpResponse(status=HTTP_200_OK) + + +class PrototypeRetrieveViewSet(RetrieveModelMixin, GenericUIViewSet): + def get_object(self): + instance = super().get_object() + instance.actions = [] + for adcm_action in Action.objects.filter(prototype__id=instance.id): + adcm_action.config = PrototypeConfig.objects.filter( + prototype__id=instance.id, + action=adcm_action, + ) + instance.actions.append(adcm_action) + + instance.config = PrototypeConfig.objects.filter(prototype=instance, action=None) + instance.imports = PrototypeImport.objects.filter(prototype=instance) + instance.exports = PrototypeExport.objects.filter(prototype=instance) + instance.upgrade = Upgrade.objects.filter(bundle=instance.bundle) + + return instance + + def retrieve(self, request: Request, *args, **kwargs) -> Response: + instance = self.get_object() + serializer = self.get_serializer(instance) + + return Response(serializer.data) + + class CsrfOffSessionAuthentication(SessionAuthentication): def enforce_csrf(self, request): return -class UploadBundle(GenericUIView): +class UploadBundleView(CreateModelMixin, GenericUIViewSet): queryset = Bundle.objects.all() - serializer_class = serializers.UploadBundle + serializer_class = UploadBundleSerializer + permission_classes = (DjangoObjectPermissionsAudit,) authentication_classes = (CsrfOffSessionAuthentication, TokenAuthentication) parser_classes = (MultiPartParser,) @audit - def post(self, request): + def create(self, request: Request, *args, **kwargs) -> Response: serializer = self.get_serializer(data=request.data) - if serializer.is_valid(): - serializer.save() - return Response(status=status.HTTP_201_CREATED) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + if not serializer.is_valid(): + return Response(serializer.errors, status=HTTP_400_BAD_REQUEST) + fd = request.data["file"] + with open(Path(settings.DOWNLOAD_DIR, fd.name), "wb+") as f: + for chunk in fd.chunks(): + f.write(chunk) -class LoadBundle(CreateModelMixin, GenericUIViewSet): - queryset = Prototype.objects.all() - serializer_class = serializers.LoadBundle + return Response(status=HTTP_201_CREATED) - @decorators.action(methods=['put'], detail=False) - def servicemap(self, request): - load_service_map() - return Response(status=status.HTTP_200_OK) - @decorators.action(methods=['put'], detail=False) - def hostmap(self, request): - load_host_map() - return Response(status=status.HTTP_200_OK) +class LoadBundleView(CreateModelMixin, GenericUIViewSet): + queryset = Bundle.objects.all() + serializer_class = LoadBundleSerializer + permission_classes = (DjangoObjectPermissionsAudit,) @audit - def create(self, request, *args, **kwargs): - """ - post: - Load bundle - """ + def create(self, request: Request, *args, **kwargs) -> Response: serializer = self.get_serializer(data=request.data) - if serializer.is_valid(): - bundle = load_bundle(serializer.validated_data.get('bundle_file')) - srl = serializers.BundleSerializer(bundle, context={'request': request}) - return Response(srl.data) - else: - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + if not serializer.is_valid(): + return Response(serializer.errors, status=HTTP_400_BAD_REQUEST) + return Response( + BundleSerializer(load_bundle(serializer.validated_data["bundle_file"]), context={"request": request}).data, + ) -class BundleList(PaginatedView): - """ - get: - List all bundles - """ - queryset = Bundle.objects.exclude(hash='adcm') - serializer_class = serializers.BundleSerializer - permission_classes = (IsAuthenticated,) - filterset_fields = ('name', 'version') - ordering_fields = ('name', 'version_order') +class BundleViewSet(ModelViewSet): # pylint: disable=too-many-ancestors + queryset = Bundle.objects.all() + serializer_class = BundleSerializer + filterset_fields = ("name", "version") + ordering_fields = ("name", "version_order") + lookup_url_kwarg = "bundle_pk" + + def get_permissions(self): + if self.action == "list": + permission_classes = (IsAuthenticated,) + else: + permission_classes = (ModelPermOrReadOnlyForAuth,) + return [permission() for permission in permission_classes] -class BundleDetail(DetailView): - """ - get: - Show bundle + def get_queryset(self): + if self.action == "list": + return Bundle.objects.exclude(hash="adcm") - delete: - Remove bundle - """ - - queryset = Bundle.objects.all() - serializer_class = serializers.BundleSerializer - permission_classes = (ModelPermOrReadOnlyForAuth,) - lookup_field = 'id' - lookup_url_kwarg = 'bundle_id' - error_code = 'BUNDLE_NOT_FOUND' + return super().get_queryset() @audit - def delete(self, request, *args, **kwargs): + def destroy(self, request, *args, **kwargs) -> Response: bundle = self.get_object() delete_bundle(bundle) - return Response(status=status.HTTP_204_NO_CONTENT) - -class BundleUpdate(GenericUIView): - queryset = Bundle.objects.all() - serializer_class = serializers.BundleSerializer + return Response(status=HTTP_204_NO_CONTENT) @audit - def put(self, request, bundle_id): - """ - update bundle - """ - bundle = check_obj(Bundle, bundle_id, 'BUNDLE_NOT_FOUND') + @action(methods=["put"], detail=True) + def update_bundle(self, request, *args, **kwargs) -> Response: + bundle = check_obj(Bundle, kwargs["bundle_pk"], "BUNDLE_NOT_FOUND") update_bundle(bundle) serializer = self.get_serializer(bundle) - return Response(serializer.data) - - -class BundleLicense(GenericUIView): - action = 'retrieve' - queryset = Bundle.objects.all() - serializer_class = serializers.LicenseSerializer - permission_classes = (IsAuthenticated,) - - def get(self, request, bundle_id): - bundle = check_obj(Bundle, bundle_id, 'BUNDLE_NOT_FOUND') - body = get_license(bundle) - url = reverse('accept-license', kwargs={'bundle_id': bundle.id}, request=request) - return Response({'license': bundle.license, 'accept': url, 'text': body}) + return Response(serializer.data) -class AcceptLicense(GenericUIView): - queryset = Bundle.objects.all() - serializer_class = serializers.LicenseSerializer + @staticmethod + @action(methods=["get"], detail=True) + def license(request, *args, **kwargs) -> Response: + bundle = check_obj(Bundle, kwargs["bundle_pk"], "BUNDLE_NOT_FOUND") + proto = Prototype.objects.filter(bundle=bundle, name=bundle.name).first() + body = get_license(proto) + url = reverse(viewname="accept-license", kwargs={"prototype_pk": proto.pk}, request=request) + return Response({"license": proto.license, "accept": url, "text": body}) @audit - def put(self, request, bundle_id): - bundle = check_obj(Bundle, bundle_id, 'BUNDLE_NOT_FOUND') - accept_license(bundle) - return Response(status=status.HTTP_200_OK) + @action(methods=["put"], detail=True) + def accept_license(self, request: Request, *args, **kwargs) -> Response: + # self is necessary for audit + + bundle = check_obj(Bundle, kwargs["bundle_pk"], "BUNDLE_NOT_FOUND") + proto = Prototype.objects.filter(bundle=bundle, name=bundle.name).first() + accept_license(proto) + return Response() -class PrototypeList(PaginatedView): - """ - get: - List all stack prototypes - """ +# pylint:disable-next=too-many-ancestors +class PrototypeViewSet(ListModelMixin, PrototypeRetrieveViewSet): queryset = Prototype.objects.all() - serializer_class = serializers.PrototypeSerializer - serializer_class_ui = serializers.PrototypeUISerializer - filterset_class = filters.PrototypeListFilter - ordering_fields = ('display_name', 'version_order') + serializer_class = PrototypeSerializer + filterset_fields = ("name", "bundle_id") + ordering_fields = ("display_name", "version_order") + lookup_url_kwarg = "prototype_pk" + + def get_permissions(self): + if self.action == "list": + permission_classes = (IsAuthenticated,) + else: + permission_classes = (ModelPermOrReadOnlyForAuth,) + return [permission() for permission in permission_classes] -class ServiceList(PaginatedView): - """ - get: - List all stack services - """ + def get_serializer_class(self): + if self.is_for_ui(): + return PrototypeUISerializer + elif self.action == "retrieve": + return PrototypeDetailSerializer - queryset = Prototype.objects.filter(type='service') - serializer_class = serializers.ServiceSerializer - filterset_fields = ('name', 'bundle_id') - ordering_fields = ('display_name', 'version_order') + return super().get_serializer_class() + @staticmethod + @action(methods=["get"], detail=True) + def license(request: Request, *args, **kwargs) -> Response: + prototype = check_obj(Prototype, kwargs["prototype_pk"], "PROTOTYPE_NOT_FOUND") + body = get_license(prototype) + url = reverse(viewname="accept-license", kwargs={"prototype_pk": prototype.pk}, request=request) -class ServiceDetail(DetailView): - """ - get: - Show stack service - """ + return Response({"license": prototype.license, "accept": url, "text": body}) - queryset = Prototype.objects.filter(type='service') - serializer_class = serializers.ServiceDetailSerializer - lookup_field = 'id' - lookup_url_kwarg = 'prototype_id' - error_code = 'SERVICE_NOT_FOUND' + @audit + @action(methods=["put"], detail=True) + def accept_license(self, request: Request, *args, **kwargs) -> Response: + # self is necessary for audit - def get_object(self): - service = super().get_object() - service.actions = Action.objects.filter(prototype__type='service', prototype__id=service.id) - service.components = Prototype.objects.filter(parent=service, type='component') - service.config = PrototypeConfig.objects.filter(prototype=service, action=None).order_by( - 'id' - ) - service.exports = PrototypeExport.objects.filter(prototype=service) - service.imports = PrototypeImport.objects.filter(prototype=service) - return service + prototype = check_obj(Prototype, kwargs["prototype_pk"], "PROTOTYPE_NOT_FOUND") + accept_license(prototype) + + return Response() -class ProtoActionDetail(GenericUIView): +class ProtoActionViewSet(RetrieveModelMixin, GenericUIViewSet): queryset = Action.objects.all() serializer_class = StackActionSerializer + lookup_url_kwarg = "action_pk" - def get(self, request, action_id): - """ - Show action - """ - obj = check_obj(Action, action_id, 'ACTION_NOT_FOUND') + def retrieve(self, request: Request, *args, **kwargs) -> Response: + obj = check_obj(Action, kwargs["action_pk"], "ACTION_NOT_FOUND") serializer = self.get_serializer(obj) - return Response(serializer.data) - -class ServiceProtoActionList(GenericUIView): - queryset = Action.objects.filter(prototype__type='service') - serializer_class = StackActionSerializer - - def get(self, request, prototype_id): - """ - List all actions of a specified service - """ - obj = self.get_queryset().filter(prototype_id=prototype_id) - serializer = self.get_serializer(obj, many=True) return Response(serializer.data) -class ComponentList(PaginatedView): - """ - get: - List all stack components - """ - - queryset = Prototype.objects.filter(type='component') - serializer_class = serializers.ComponentTypeSerializer - filterset_fields = ('name', 'bundle_id') - ordering_fields = ('display_name', 'version_order') - - -class HostTypeList(PaginatedView): - """ - get: - List all host types - """ +# pylint:disable-next=too-many-ancestors +class ServicePrototypeViewSet(ListModelMixin, RetrieveModelMixin, GenericUIViewSet): + queryset = Prototype.objects.filter(type="service") + serializer_class = ServicePrototypeSerializer + filterset_fields = ("name", "bundle_id") + ordering_fields = ("display_name", "version_order") + lookup_url_kwarg = "prototype_pk" - queryset = Prototype.objects.filter(type='host') - serializer_class = serializers.HostTypeSerializer - filterset_fields = ('name', 'bundle_id') - ordering_fields = ('display_name', 'version_order') + def get_serializer_class(self): + if self.action == "retrieve": + return ServiceDetailPrototypeSerializer + elif self.action == "action": + return StackActionSerializer + return super().get_serializer_class() -class ProviderTypeList(PaginatedView): - """ - get: - List all host providers types - """ + def retrieve(self, request, *args, **kwargs) -> Response: + instance = self.get_object() + instance.actions = Action.objects.filter(prototype__type="service", prototype__pk=instance.pk) + instance.components = Prototype.objects.filter(parent=instance, type="component") + instance.config = PrototypeConfig.objects.filter(prototype=instance, action=None).order_by("id") + instance.exports = PrototypeExport.objects.filter(prototype=instance) + instance.imports = PrototypeImport.objects.filter(prototype=instance) + serializer = self.get_serializer(instance) - queryset = Prototype.objects.filter(type='provider') - serializer_class = serializers.ProviderTypeSerializer - filterset_fields = ('name', 'bundle_id', 'display_name') - ordering_fields = ('display_name', 'version_order') - permission_classes = (IsAuthenticated,) + return Response(serializer.data) + @action(methods=["get"], detail=True) + def actions(self, request: Request, prototype_pk: int) -> Response: + return Response( + StackActionSerializer( + Action.objects.filter(prototype__type="service", prototype_id=prototype_pk), + many=True, + ).data, + ) -class ClusterTypeList(PaginatedView): - """ - get: - List all cluster types - """ - queryset = Prototype.objects.filter(type='cluster') - serializer_class = serializers.ClusterTypeSerializer - filterset_fields = ('name', 'bundle_id', 'display_name') - ordering_fields = ('display_name', 'version_order') +# pylint:disable-next=too-many-ancestors +class ComponentPrototypeViewSet(ListModelMixin, PrototypeRetrieveViewSet): + queryset = Prototype.objects.filter(type="component") + serializer_class = ComponentPrototypeSerializer + filterset_fields = ("name", "bundle_id") + ordering_fields = ("display_name", "version_order") + lookup_url_kwarg = "prototype_pk" + def get_serializer_class(self): + if self.action == "retrieve": + return ComponentPrototypeDetailSerializer -class AdcmTypeList(GenericUIView): - """ - get: - List adcm root object prototypes - """ + return super().get_serializer_class() - queryset = Prototype.objects.filter(type='adcm') - serializer_class = serializers.AdcmTypeSerializer - filterset_fields = ('bundle_id',) - def get(self, request, *args, **kwargs): - obj = self.get_queryset() - serializer = self.get_serializer(obj, many=True) - return Response(serializer.data) +# pylint:disable-next=too-many-ancestors +class ProviderPrototypeViewSet(ListModelMixin, PrototypeRetrieveViewSet): + queryset = Prototype.objects.filter(type="provider") + serializer_class = ProviderPrototypeSerializer + filterset_fields = ("name", "bundle_id") + ordering_fields = ("display_name", "version_order") + permission_classes = (IsAuthenticatedAudit,) + lookup_url_kwarg = "prototype_pk" + def get_serializer_class(self): + if self.action == "retrieve": + return ProviderPrototypeDetailSerializer -class AbstractPrototypeDetail(DetailView): - """Common base class for *PrototypeDetail""" + return super().get_serializer_class() - lookup_field = 'id' - lookup_url_kwarg = 'prototype_id' - error_code = 'PROTOTYPE_NOT_FOUND' - def get_object(self): - obj_type = super().get_object() - act_set = [] - for action in Action.objects.filter(prototype__id=obj_type.id): - action.config = PrototypeConfig.objects.filter(prototype__id=obj_type.id, action=action) - act_set.append(action) - obj_type.actions = act_set - obj_type.config = PrototypeConfig.objects.filter(prototype=obj_type, action=None) - obj_type.imports = PrototypeImport.objects.filter(prototype=obj_type) - obj_type.exports = PrototypeExport.objects.filter(prototype=obj_type) - obj_type.upgrade = Upgrade.objects.filter(bundle=obj_type.bundle) - return obj_type - - -class PrototypeDetail(AbstractPrototypeDetail): - """ - get: - Show prototype - """ +# pylint:disable-next=too-many-ancestors +class HostPrototypeViewSet(ListModelMixin, PrototypeRetrieveViewSet): + queryset = Prototype.objects.filter(type="host") + serializer_class = HostPrototypeSerializer + filterset_fields = ("name", "bundle_id") + ordering_fields = ("display_name", "version_order") + lookup_url_kwarg = "prototype_pk" - queryset = Prototype.objects.all() - serializer_class = serializers.PrototypeDetailSerializer + def get_serializer_class(self): + if self.action == "retrieve": + return HostPrototypeDetailSerializer + return super().get_serializer_class() -class AdcmTypeDetail(AbstractPrototypeDetail): - """ - get: - Show adcm prototype - """ - queryset = Prototype.objects.filter(type='adcm') - serializer_class = serializers.AdcmTypeDetailSerializer +# pylint:disable-next=too-many-ancestors +class ClusterPrototypeViewSet(ListModelMixin, PrototypeRetrieveViewSet): + queryset = Prototype.objects.filter(type="cluster") + serializer_class = ClusterPrototypeSerializer + filterset_fields = ("name", "bundle_id", "display_name") + ordering_fields = ("display_name", "version_order", "version") + lookup_url_kwarg = "prototype_pk" + def get_serializer_class(self): + if self.action == "retrieve": + return ClusterPrototypeDetailSerializer -class ClusterTypeDetail(AbstractPrototypeDetail): - """ - get: - Show cluster prototype - """ + return super().get_serializer_class() - queryset = Prototype.objects.filter(type='cluster') - serializer_class = serializers.ClusterTypeDetailSerializer + def get_queryset(self): + queryset = super().get_queryset() + if self.action != "list": + return queryset + pks = set() + field_names = self.request.query_params.get("fields") + distinct = self.request.query_params.get("distinct") + if field_names and distinct: + for field_name in field_names.split(","): + values_list = queryset.values(field_name, "pk") + if not values_list: + continue -class ComponentTypeDetail(AbstractPrototypeDetail): - """ - get: - Show component prototype - """ + field_value = values_list[0][field_name] + pks.add(values_list[0]["pk"]) + if len(values_list) == 1: + continue - queryset = Prototype.objects.filter(type='component') - serializer_class = serializers.ComponentTypeDetailSerializer + for value in values_list[1:]: + if value[field_name] != field_value: + pks.add(value["pk"]) + if pks: + return queryset.filter(pk__in=pks) -class HostTypeDetail(AbstractPrototypeDetail): - """ - get: - Show host prototype - """ + return queryset - queryset = Prototype.objects.filter(type='host') - serializer_class = serializers.HostTypeDetailSerializer +# pylint:disable-next=too-many-ancestors +class ADCMPrototypeViewSet(ListModelMixin, PrototypeRetrieveViewSet): + queryset = Prototype.objects.filter(type="adcm") + serializer_class = ADCMPrototypeSerializer + filterset_fields = ("bundle_id",) + lookup_url_kwarg = "prototype_pk" -class ProviderTypeDetail(AbstractPrototypeDetail): - """ - get: - Show host provider prototype - """ + def get_serializer_class(self): + if self.action == "retrieve": + return ADCMPrototypeDetailSerializer - queryset = Prototype.objects.filter(type='provider') - serializer_class = serializers.ProviderTypeDetailSerializer + return super().get_serializer_class() diff --git a/python/api/stats/views.py b/python/api/stats/views.py index 23934b1319..1e33af85c5 100644 --- a/python/api/stats/views.py +++ b/python/api/stats/views.py @@ -15,14 +15,13 @@ from rest_framework.response import Response from api.base_view import GenericUIView -from cm import config -from cm.models import JobLog, TaskLog +from cm.models import JobLog, JobStatus, TaskLog class JobStats(PermissionListMixin, GenericUIView): queryset = JobLog.objects.all() permission_classes = (permissions.IsAuthenticated,) - permission_required = ['cm.view_joblog'] + permission_required = ["cm.view_joblog"] def get(self, request, pk): """ @@ -30,9 +29,9 @@ def get(self, request, pk): """ jobs = self.get_queryset().filter(id__gt=pk) data = { - config.Job.FAILED: jobs.filter(status=config.Job.FAILED).count(), - config.Job.SUCCESS: jobs.filter(status=config.Job.SUCCESS).count(), - config.Job.RUNNING: jobs.filter(status=config.Job.RUNNING).count(), + JobStatus.FAILED.value: jobs.filter(status=JobStatus.FAILED).count(), + JobStatus.SUCCESS.value: jobs.filter(status=JobStatus.SUCCESS).count(), + JobStatus.RUNNING.value: jobs.filter(status=JobStatus.RUNNING).count(), } return Response(data) @@ -40,7 +39,7 @@ def get(self, request, pk): class TaskStats(PermissionListMixin, GenericUIView): queryset = TaskLog.objects.all() permission_classes = (permissions.IsAuthenticated,) - permission_required = ['cm.view_tasklog'] + permission_required = ["cm.view_tasklog"] def get(self, request, pk): """ @@ -48,8 +47,8 @@ def get(self, request, pk): """ tasks = self.get_queryset().filter(id__gt=pk) data = { - config.Job.FAILED: tasks.filter(status=config.Job.FAILED).count(), - config.Job.SUCCESS: tasks.filter(status=config.Job.SUCCESS).count(), - config.Job.RUNNING: tasks.filter(status=config.Job.RUNNING).count(), + JobStatus.FAILED.value: tasks.filter(status=JobStatus.FAILED).count(), + JobStatus.SUCCESS.value: tasks.filter(status=JobStatus.SUCCESS).count(), + JobStatus.RUNNING.value: tasks.filter(status=JobStatus.RUNNING).count(), } return Response(data) diff --git a/python/api/tests/__init__.py b/python/api/tests/__init__.py new file mode 100644 index 0000000000..824dd6c8fe --- /dev/null +++ b/python/api/tests/__init__.py @@ -0,0 +1,11 @@ +# 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. diff --git a/python/api/tests/files/bundle_cluster_requires.tar b/python/api/tests/files/bundle_cluster_requires.tar new file mode 100644 index 0000000000..d98ee0d4c3 Binary files /dev/null and b/python/api/tests/files/bundle_cluster_requires.tar differ diff --git a/python/api/tests/files/bundle_issue_component.tar b/python/api/tests/files/bundle_issue_component.tar new file mode 100644 index 0000000000..bf43689fee Binary files /dev/null and b/python/api/tests/files/bundle_issue_component.tar differ diff --git a/python/api/tests/files/bundle_test_cluster_action_with_ui_options.tar b/python/api/tests/files/bundle_test_cluster_action_with_ui_options.tar new file mode 100644 index 0000000000..e6fa304c30 Binary files /dev/null and b/python/api/tests/files/bundle_test_cluster_action_with_ui_options.tar differ diff --git a/python/api/tests/files/bundle_test_cluster_false_host_action.tar b/python/api/tests/files/bundle_test_cluster_false_host_action.tar new file mode 100644 index 0000000000..4c7fb125af Binary files /dev/null and b/python/api/tests/files/bundle_test_cluster_false_host_action.tar differ diff --git a/python/api/tests/files/bundle_test_cluster_host_action_true.tar b/python/api/tests/files/bundle_test_cluster_host_action_true.tar new file mode 100644 index 0000000000..203a7afcaa Binary files /dev/null and b/python/api/tests/files/bundle_test_cluster_host_action_true.tar differ diff --git a/python/api/tests/files/bundle_test_cluster_with_mm.tar b/python/api/tests/files/bundle_test_cluster_with_mm.tar new file mode 100644 index 0000000000..fd7d3ee134 Binary files /dev/null and b/python/api/tests/files/bundle_test_cluster_with_mm.tar differ diff --git a/python/api/tests/files/bundle_test_cluster_wrong_host_action.tar b/python/api/tests/files/bundle_test_cluster_wrong_host_action.tar new file mode 100644 index 0000000000..e795f2ddc1 Binary files /dev/null and b/python/api/tests/files/bundle_test_cluster_wrong_host_action.tar differ diff --git a/python/api/tests/files/bundle_test_provider.tar b/python/api/tests/files/bundle_test_provider.tar new file mode 100644 index 0000000000..3f40a030d4 Binary files /dev/null and b/python/api/tests/files/bundle_test_provider.tar differ diff --git a/python/api/tests/files/bundle_test_provider_concern.tar b/python/api/tests/files/bundle_test_provider_concern.tar new file mode 100644 index 0000000000..e38c747868 Binary files /dev/null and b/python/api/tests/files/bundle_test_provider_concern.tar differ diff --git a/python/api/tests/files/bundle_test_service_with_host_action.tar b/python/api/tests/files/bundle_test_service_with_host_action.tar new file mode 100644 index 0000000000..f839c34bc0 Binary files /dev/null and b/python/api/tests/files/bundle_test_service_with_host_action.tar differ diff --git a/python/api/tests/files/cluster_using_plugin.tar b/python/api/tests/files/cluster_using_plugin.tar new file mode 100644 index 0000000000..3774ec0ca1 Binary files /dev/null and b/python/api/tests/files/cluster_using_plugin.tar differ diff --git a/python/api/tests/files/no-log-files.tar b/python/api/tests/files/no-log-files.tar new file mode 100644 index 0000000000..b11cd15e75 Binary files /dev/null and b/python/api/tests/files/no-log-files.tar differ diff --git a/python/api/tests/files/with_action_dependent_component.tar b/python/api/tests/files/with_action_dependent_component.tar new file mode 100644 index 0000000000..fff3cd154f Binary files /dev/null and b/python/api/tests/files/with_action_dependent_component.tar differ diff --git a/python/api/tests/test_action.py b/python/api/tests/test_action.py new file mode 100644 index 0000000000..e31d3773ed --- /dev/null +++ b/python/api/tests/test_action.py @@ -0,0 +1,73 @@ +# 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 django.conf import settings +from django.urls import reverse +from rest_framework.response import Response + +from adcm.tests.base import BaseTestCase +from cm.models import ( + ADCM, + Action, + ActionType, + Bundle, + ConfigLog, + ObjectConfig, + Prototype, +) + + +class TestPrototypeAPI(BaseTestCase): + def setUp(self) -> None: + super().setUp() + + bundle = Bundle.objects.create() + + config = ObjectConfig.objects.create(current=0, previous=0) + config_log = ConfigLog.objects.create(obj_ref=config, config="{}", attr={"ldap_integration": {"active": False}}) + config.current = config_log.pk + config.save(update_fields=["current"]) + + self.adcm_prototype = Prototype.objects.create(bundle=bundle, type="adcm") + self.adcm = ADCM.objects.create( + prototype=self.adcm_prototype, + name="ADCM", + config=config, + ) + self.action = Action.objects.create( + display_name="test_adcm_action", + prototype=self.adcm_prototype, + type=ActionType.Job, + state_available="any", + ) + + def test_retrieve(self): + response: Response = self.client.get( + reverse("action-detail", kwargs={"action_pk": self.action.pk}), + ) + + self.assertEqual(response.data["id"], self.action.pk) + + def test_list(self): + response: Response = self.client.get( + reverse("object-action", kwargs={"adcm_pk": self.adcm.pk}), + ) + + action = Action.objects.create( + name=settings.ADCM_TURN_ON_MM_ACTION_NAME, + prototype=self.adcm_prototype, + type=ActionType.Job, + state_available="any", + ) + + self.assertEqual(len(response.data), 1) + self.assertNotIn(action.pk, {action_data["id"] for action_data in response.data}) diff --git a/python/api/tests/test_adcm.py b/python/api/tests/test_adcm.py new file mode 100644 index 0000000000..3cdba769df --- /dev/null +++ b/python/api/tests/test_adcm.py @@ -0,0 +1,110 @@ +# 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 django.urls import reverse +from rest_framework.response import Response +from rest_framework.status import HTTP_200_OK + +from adcm.tests.base import BaseTestCase +from cm.models import ADCM +from init_db import init as init_adcm + + +class TestADCM(BaseTestCase): + def setUp(self) -> None: + super().setUp() + init_adcm() + + def test_list(self): + adcm = ADCM.objects.select_related("prototype").last() + test_data = { + "id": adcm.id, + "name": adcm.name, + "prototype_id": adcm.prototype.id, + "state": adcm.state, + "url": f"http://testserver/api/v1/adcm/{adcm.id}/", + } + + response: Response = self.client.get(reverse("adcm-list")) + + self.assertEqual(response.status_code, HTTP_200_OK) + self.assertEqual(response.json()["count"], 1) + self.assertDictEqual(response.json()["results"][0], test_data) + + def test_list_interface(self): + adcm = ADCM.objects.select_related("prototype").last() + test_data = { + "id": adcm.id, + "name": adcm.name, + "prototype_id": adcm.prototype.id, + "state": adcm.state, + "url": f"http://testserver/api/v1/adcm/{adcm.id}/", + "prototype_version": adcm.prototype.version, + "bundle_id": adcm.prototype.bundle_id, + "config": f"http://testserver/api/v1/adcm/{adcm.id}/config/", + "action": f"http://testserver/api/v1/adcm/{adcm.id}/action/", + "multi_state": [], + "concerns": [], + "locked": adcm.locked, + "main_info": None, + } + + response: Response = self.client.get(f"{reverse('adcm-list')}?view=interface") + + self.assertEqual(response.status_code, HTTP_200_OK) + self.assertEqual(response.json()["count"], 1) + self.assertDictEqual(response.json()["results"][0], test_data) + + def test_retrieve(self): + adcm = ADCM.objects.select_related("prototype").last() + test_data = { + "id": adcm.id, + "name": adcm.name, + "prototype_id": adcm.prototype.id, + "state": adcm.state, + "url": f"http://testserver/api/v1/adcm/{adcm.id}/", + "prototype_version": adcm.prototype.version, + "bundle_id": adcm.prototype.bundle_id, + "config": f"http://testserver/api/v1/adcm/{adcm.id}/config/", + "action": f"http://testserver/api/v1/adcm/{adcm.id}/action/", + "multi_state": [], + "concerns": [], + "locked": adcm.locked, + } + + response: Response = self.client.get(reverse("adcm-detail", kwargs={"adcm_pk": adcm.id})) + + self.assertEqual(response.status_code, HTTP_200_OK) + self.assertDictEqual(response.json(), test_data) + + def test_retrieve_interface(self): + adcm = ADCM.objects.select_related("prototype").last() + test_data = { + "id": adcm.id, + "name": adcm.name, + "prototype_id": adcm.prototype.id, + "state": "created", + "url": f"http://testserver/api/v1/adcm/{adcm.id}/", + "prototype_version": adcm.prototype.version, + "bundle_id": adcm.prototype.bundle_id, + "config": f"http://testserver/api/v1/adcm/{adcm.id}/config/", + "action": f"http://testserver/api/v1/adcm/{adcm.id}/action/", + "multi_state": [], + "concerns": [], + "locked": adcm.locked, + "main_info": None, + } + + response: Response = self.client.get(f"{reverse('adcm-detail', kwargs={'adcm_pk': adcm.id})}?view=interface") + + self.assertEqual(response.status_code, HTTP_200_OK) + self.assertDictEqual(response.json(), test_data) diff --git a/python/api/tests/test_adcm_prototype.py b/python/api/tests/test_adcm_prototype.py new file mode 100644 index 0000000000..371f476825 --- /dev/null +++ b/python/api/tests/test_adcm_prototype.py @@ -0,0 +1,67 @@ +# 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 django.urls import reverse +from rest_framework.response import Response + +from adcm.tests.base import BaseTestCase +from cm.models import Action, ActionType, Bundle, Prototype + + +class TestProviderPrototypeAPI(BaseTestCase): + def setUp(self) -> None: + super().setUp() + + self.bundle_1 = Bundle.objects.create(name="test_bundle_1") + self.bundle_2 = Bundle.objects.create(name="test_bundle_2") + + self.prototype_1 = Prototype.objects.create( + bundle=self.bundle_1, + type="adcm", + name="test_prototype_1", + display_name="test_prototype_1", + version_order=1, + ) + self.prototype_2 = Prototype.objects.create( + bundle=self.bundle_2, + type="adcm", + name="test_prototype_2", + display_name="test_prototype_2", + version_order=2, + ) + self.action = Action.objects.create( + display_name="test_adcm_action", + prototype=self.prototype_1, + type=ActionType.Job, + state_available="any", + ) + + def test_list(self): + response: Response = self.client.get(path=reverse("adcm-prototype-list")) + + self.assertEqual(len(response.data["results"]), 2) + + def test_list_filter_bundle_id(self): + response: Response = self.client.get( + reverse("adcm-prototype-list"), + {"bundle_id": self.bundle_1.pk}, + ) + + self.assertEqual(len(response.data["results"]), 1) + self.assertEqual(response.data["results"][0]["id"], self.prototype_1.pk) + + def test_retrieve(self): + response: Response = self.client.get( + reverse("adcm-prototype-detail", kwargs={"prototype_pk": self.prototype_2.pk}), + ) + + self.assertEqual(response.data["id"], self.prototype_2.pk) diff --git a/python/cm/tests/test_api.py b/python/api/tests/test_api.py similarity index 86% rename from python/cm/tests/test_api.py rename to python/api/tests/test_api.py index 1642daec8c..a6fff11f0e 100755 --- a/python/cm/tests/test_api.py +++ b/python/api/tests/test_api.py @@ -11,7 +11,6 @@ # limitations under the License. # pylint: disable=too-many-lines -import os from unittest.mock import patch from uuid import uuid4 @@ -75,7 +74,7 @@ def setUp(self) -> None: self.component = "ZOOKEEPER_SERVER" def load_bundle(self, bundle_name): - with open(os.path.join(self.files_dir, bundle_name), encoding="utf-8") as f: + with open(self.files_dir / bundle_name, encoding=settings.ENCODING_UTF_8) as f: response: Response = self.client.post( path=reverse("upload-bundle"), data={"file": f}, @@ -91,11 +90,11 @@ def load_bundle(self, bundle_name): self.assertEqual(response.status_code, HTTP_200_OK) def get_service_proto_id(self): - response: Response = self.client.get(reverse("service-type")) + response: Response = self.client.get(reverse("service-prototype-list")) self.assertEqual(response.status_code, HTTP_200_OK) - for service in response.json(): + for service in response.json()["results"]: if service["name"] == self.service: return service["id"] @@ -115,36 +114,34 @@ def get_component_id(self, cluster_id, service_id, component_name): raise AssertionError def get_cluster_proto_id(self): - response: Response = self.client.get(reverse("cluster-type")) + response: Response = self.client.get(reverse("cluster-prototype-list")) self.assertEqual(response.status_code, HTTP_200_OK) - for cluster in response.json(): + for cluster in response.json()["results"]: return cluster["bundle_id"], cluster["id"] def get_host_proto_id(self): - response: Response = self.client.get(reverse("host-type")) + response: Response = self.client.get(reverse("host-prototype-list")) self.assertEqual(response.status_code, HTTP_200_OK) - for host in response.json(): + for host in response.json()["results"]: return host["bundle_id"], host["id"] def get_host_provider_proto_id(self): - response: Response = self.client.get(reverse("provider-type")) + response: Response = self.client.get(reverse("provider-prototype-list")) self.assertEqual(response.status_code, HTTP_200_OK) - for provider in response.json(): + for provider in response.json()["results"]: return provider["bundle_id"], provider["id"] def create_host(self, fqdn, name=None): name = name or uuid4().hex ssh_bundle_id, host_proto = self.get_host_proto_id() _, provider_proto = self.get_host_provider_proto_id() - response: Response = self.client.post( - reverse("provider"), {"name": name, "prototype_id": provider_proto} - ) + response: Response = self.client.post(reverse("provider"), {"name": name, "prototype_id": provider_proto}) self.assertEqual(response.status_code, HTTP_201_CREATED) @@ -162,38 +159,30 @@ def create_host(self, fqdn, name=None): def test_access(self): self.client.logout() - api = [reverse("cluster"), reverse("host"), reverse("job"), reverse("task")] + api = [reverse("cluster"), reverse("host"), reverse("tasklog-list")] for url in api: response: Response = self.client.get(url) self.assertEqual(response.status_code, HTTP_401_UNAUTHORIZED) - self.assertEqual( - response.json()["detail"], "Authentication credentials were not provided." - ) + self.assertEqual(response.json()["detail"], "Authentication credentials were not provided.") for url in api: response: Response = self.client.post(url, data={}) self.assertEqual(response.status_code, HTTP_401_UNAUTHORIZED) - self.assertEqual( - response.json()["detail"], "Authentication credentials were not provided." - ) + self.assertEqual(response.json()["detail"], "Authentication credentials were not provided.") for url in api: response: Response = self.client.put(url, {}) self.assertEqual(response.status_code, HTTP_401_UNAUTHORIZED) - self.assertEqual( - response.json()["detail"], "Authentication credentials were not provided." - ) + self.assertEqual(response.json()["detail"], "Authentication credentials were not provided.") for url in api: response: Response = self.client.delete(url) self.assertEqual(response.status_code, HTTP_401_UNAUTHORIZED) - self.assertEqual( - response.json()["detail"], "Authentication credentials were not provided." - ) + self.assertEqual(response.json()["detail"], "Authentication credentials were not provided.") def test_schema(self): response: Response = self.client.get("/api/v1/schema/") @@ -230,23 +219,17 @@ def test_cluster(self): # pylint: disable=too-many-statements self.assertEqual(response.status_code, HTTP_400_BAD_REQUEST) self.assertEqual(response.json()["prototype_id"], ["This field is required."]) - response: Response = self.client.post( - cluster_url, {"name": cluster_name, "prototype_id": ""} - ) + response: Response = self.client.post(cluster_url, {"name": cluster_name, "prototype_id": ""}) self.assertEqual(response.status_code, HTTP_400_BAD_REQUEST) self.assertEqual(response.json()["prototype_id"], ["A valid integer is required."]) - response: Response = self.client.post( - cluster_url, {"name": cluster_name, "prototype_id": "some-string"} - ) + response: Response = self.client.post(cluster_url, {"name": cluster_name, "prototype_id": "some-string"}) self.assertEqual(response.status_code, HTTP_400_BAD_REQUEST) self.assertEqual(response.json()["prototype_id"], ["A valid integer is required."]) - response: Response = self.client.post( - cluster_url, {"name": cluster_name, "prototype_id": 100500} - ) + response: Response = self.client.post(cluster_url, {"name": cluster_name, "prototype_id": 100500}) self.assertEqual(response.status_code, HTTP_404_NOT_FOUND) self.assertEqual(response.json()["code"], "PROTOTYPE_NOT_FOUND") @@ -260,9 +243,7 @@ def test_cluster(self): # pylint: disable=too-many-statements self.assertEqual(response.status_code, HTTP_400_BAD_REQUEST) self.assertEqual(response.json()["description"], ["This field may not be blank."]) - response: Response = self.client.post( - cluster_url, {"name": cluster_name, "prototype_id": proto_id} - ) + response: Response = self.client.post(cluster_url, {"name": cluster_name, "prototype_id": proto_id}) self.assertEqual(response.status_code, HTTP_201_CREATED) @@ -274,9 +255,7 @@ def test_cluster(self): # pylint: disable=too-many-statements self.assertEqual(response.status_code, HTTP_200_OK) self.assertEqual(response.json()["name"], cluster_name) - response: Response = self.client.post( - cluster_url, {"name": cluster_name, "prototype_id": proto_id} - ) + response: Response = self.client.post(cluster_url, {"name": cluster_name, "prototype_id": proto_id}) self.assertEqual(response.status_code, HTTP_409_CONFLICT) self.assertEqual(response.json()["code"], "CLUSTER_CONFLICT") @@ -295,9 +274,7 @@ def test_cluster(self): # pylint: disable=too-many-statements self.assertEqual(response.status_code, HTTP_404_NOT_FOUND) self.assertEqual(response.json()["code"], "CLUSTER_NOT_FOUND") - response: Response = self.client.delete( - reverse("bundle-details", kwargs={"bundle_id": bundle_id}) - ) + response: Response = self.client.delete(reverse("bundle-detail", kwargs={"bundle_pk": bundle_id})) self.assertEqual(response.status_code, HTTP_204_NO_CONTENT) @@ -317,9 +294,7 @@ def test_cluster_patching(self): patched_name = "patched-cluster" - response: Response = self.client.patch( - first_cluster_url, {"name": patched_name}, content_type=APPLICATION_JSON - ) + response: Response = self.client.patch(first_cluster_url, {"name": patched_name}, content_type=APPLICATION_JSON) self.assertEqual(response.status_code, HTTP_200_OK) self.assertEqual(response.json()["name"], patched_name) @@ -356,9 +331,7 @@ def test_cluster_patching(self): self.assertEqual(response.status_code, HTTP_204_NO_CONTENT) - response: Response = self.client.delete( - reverse("bundle-details", kwargs={"bundle_id": bundle_id}) - ) + response: Response = self.client.delete(reverse("bundle-detail", kwargs={"bundle_pk": bundle_id})) self.assertEqual(response.status_code, HTTP_204_NO_CONTENT) @@ -371,9 +344,7 @@ def test_cluster_host(self): adh_bundle_id, cluster_proto = self.get_cluster_proto_id() - response: Response = self.client.post( - cluster_url, {"name": self.cluster, "prototype_id": cluster_proto} - ) + response: Response = self.client.post(cluster_url, {"name": self.cluster, "prototype_id": cluster_proto}) cluster_id = response.json()["id"] this_cluster_host_url = reverse("host", kwargs={"cluster_id": cluster_id}) @@ -395,9 +366,7 @@ def test_cluster_host(self): self.assertEqual(response.json()["id"], host_id) self.assertEqual(response.json()["cluster_id"], cluster_id) - response: Response = self.client.post( - cluster_url, {"name": "qwe", "prototype_id": cluster_proto} - ) + response: Response = self.client.post(cluster_url, {"name": "qwe", "prototype_id": cluster_proto}) cluster_id2 = response.json()["id"] second_cluster_host_url = reverse("host", kwargs={"cluster_id": cluster_id2}) @@ -423,15 +392,11 @@ def test_cluster_host(self): self.client.delete(reverse("cluster-details", kwargs={"cluster_id": cluster_id})) self.client.delete(reverse("cluster-details", kwargs={"cluster_id": cluster_id2})) self.client.delete(reverse("host-details", kwargs={"host_id": host_id})) - response: Response = self.client.delete( - reverse("bundle-details", kwargs={"bundle_id": adh_bundle_id}) - ) + response: Response = self.client.delete(reverse("bundle-detail", kwargs={"bundle_pk": adh_bundle_id})) self.assertEqual(response.status_code, HTTP_204_NO_CONTENT) - response: Response = self.client.delete( - reverse("bundle-details", kwargs={"bundle_id": ssh_bundle_id}) - ) + response: Response = self.client.delete(reverse("bundle-detail", kwargs={"bundle_pk": ssh_bundle_id})) self.assertEqual(response.status_code, HTTP_409_CONFLICT) self.assertEqual(response.json()["code"], "BUNDLE_CONFLICT") @@ -439,8 +404,8 @@ def test_cluster_host(self): def test_service(self): self.load_bundle(self.bundle_adh_name) service_id = self.get_service_proto_id() - service_url = reverse("service-type") - this_service_url = reverse("service-type-details", kwargs={"prototype_id": service_id}) + service_url = reverse("service-prototype-list") + this_service_url = reverse("service-prototype-detail", kwargs={"prototype_pk": service_id}) response: Response = self.client.post(service_url, {}) @@ -464,9 +429,7 @@ def test_service(self): bundle_id = response.json()["bundle_id"] - response: Response = self.client.delete( - reverse("bundle-details", kwargs={"bundle_id": bundle_id}) - ) + response: Response = self.client.delete(reverse("bundle-detail", kwargs={"bundle_pk": bundle_id})) self.assertEqual(response.status_code, HTTP_204_NO_CONTENT) @@ -478,9 +441,7 @@ def test_cluster_service(self): cluster = "test-cluster" cluster_url = reverse("cluster") - response: Response = self.client.post( - cluster_url, {"name": cluster, "prototype_id": cluster_proto_id} - ) + response: Response = self.client.post(cluster_url, {"name": cluster, "prototype_id": cluster_proto_id}) self.assertEqual(response.status_code, HTTP_201_CREATED) @@ -533,15 +494,11 @@ def test_cluster_service(self): self.assertEqual(response.status_code, HTTP_204_NO_CONTENT) - response: Response = self.client.delete( - reverse("cluster-details", kwargs={"cluster_id": cluster_id}) - ) + response: Response = self.client.delete(reverse("cluster-details", kwargs={"cluster_id": cluster_id})) self.assertEqual(response.status_code, HTTP_204_NO_CONTENT) - response: Response = self.client.delete( - reverse("bundle-details", kwargs={"bundle_id": bundle_id}) - ) + response: Response = self.client.delete(reverse("bundle-detail", kwargs={"bundle_pk": bundle_id})) self.assertEqual(response.status_code, HTTP_204_NO_CONTENT) @@ -552,9 +509,7 @@ def test_hostcomponent(self): # pylint: disable=too-many-statements,too-many-lo adh_bundle_id, cluster_proto = self.get_cluster_proto_id() ssh_bundle_id, _, host_id = self.create_host(self.host) service_proto_id = self.get_service_proto_id() - response: Response = self.client.post( - reverse("cluster"), {"name": self.cluster, "prototype_id": cluster_proto} - ) + response: Response = self.client.post(reverse("cluster"), {"name": self.cluster, "prototype_id": cluster_proto}) cluster_id = response.json()["id"] response: Response = self.client.post( @@ -626,9 +581,7 @@ def test_hostcomponent(self): # pylint: disable=too-many-statements,too-many-lo self.assertEqual(response.status_code, HTTP_400_BAD_REQUEST) self.assertEqual(response.json()["code"], "INVALID_INPUT") - response: Response = self.client.post( - hc_url, {"hc": [{"host_id": host_id}]}, content_type=APPLICATION_JSON - ) + response: Response = self.client.post(hc_url, {"hc": [{"host_id": host_id}]}, content_type=APPLICATION_JSON) self.assertEqual(response.status_code, HTTP_400_BAD_REQUEST) self.assertEqual(response.json()["code"], "INVALID_INPUT") @@ -713,15 +666,11 @@ def test_hostcomponent(self): # pylint: disable=too-many-statements,too-many-lo self.client.delete(reverse("cluster-details", kwargs={"cluster_id": cluster_id})) self.client.delete(reverse("cluster-details", kwargs={"cluster_id": cluster_id2})) self.client.delete(reverse("host-details", kwargs={"host_id": host_id})) - response: Response = self.client.delete( - reverse("bundle-details", kwargs={"bundle_id": adh_bundle_id}) - ) + response: Response = self.client.delete(reverse("bundle-detail", kwargs={"bundle_pk": adh_bundle_id})) self.assertEqual(response.status_code, HTTP_204_NO_CONTENT) - response: Response = self.client.delete( - reverse("bundle-details", kwargs={"bundle_id": ssh_bundle_id}) - ) + response: Response = self.client.delete(reverse("bundle-detail", kwargs={"bundle_pk": ssh_bundle_id})) self.assertEqual(response.status_code, HTTP_409_CONFLICT) self.assertEqual(response.json()["code"], "BUNDLE_CONFLICT") @@ -730,9 +679,7 @@ def test_config(self): # pylint: disable=too-many-statements self.load_bundle(self.bundle_adh_name) adh_bundle_id, proto_id = self.get_cluster_proto_id() service_proto_id = self.get_service_proto_id() - response: Response = self.client.post( - reverse("cluster"), {"name": self.cluster, "prototype_id": proto_id} - ) + response: Response = self.client.post(reverse("cluster"), {"name": self.cluster, "prototype_id": proto_id}) cluster_id = response.json()["id"] response: Response = self.client.get(reverse("service", kwargs={"cluster_id": cluster_id})) @@ -756,9 +703,7 @@ def test_config(self): # pylint: disable=too-many-statements service_id = response.json()["id"] - zurl = reverse( - "service-details", kwargs={"cluster_id": cluster_id, "service_id": service_id} - ) + zurl = reverse("service-details", kwargs={"cluster_id": cluster_id, "service_id": service_id}) response: Response = self.client.get(zurl) self.assertEqual(response.status_code, HTTP_200_OK) @@ -798,9 +743,7 @@ def test_config(self): # pylint: disable=too-many-statements config["zoo.cfg"]["autopurge.purgeInterval"] = 42 config["zoo.cfg"]["port"] = 80 - response: Response = self.client.post( - config_history_url, {"config": config}, content_type=APPLICATION_JSON - ) + response: Response = self.client.post(config_history_url, {"config": config}, content_type=APPLICATION_JSON) self.assertEqual(response.status_code, HTTP_201_CREATED) @@ -879,10 +822,10 @@ def test_config(self): # pylint: disable=too-many-statements self.assertEqual(len(response.json()), 2) self.client.delete(reverse("cluster-details", kwargs={"cluster_id": cluster_id})) - self.client.delete(reverse("bundle-details", kwargs={"bundle_id": adh_bundle_id})) + self.client.delete(reverse("bundle-detail", kwargs={"bundle_pk": adh_bundle_id})) -class TestApi2(BaseTestCase): +class TestAPI2(BaseTestCase): def setUp(self): gen_adcm() self.bundle = Bundle.objects.create( @@ -891,9 +834,6 @@ def setUp(self): "version": "2.5", "version_order": 4, "edition": "community", - "license": "absent", - "license_path": None, - "license_hash": None, "hash": "2232f33c6259d44c23046fce4382f16c450f8ba5", "description": "", "date": timezone.now(), @@ -907,6 +847,9 @@ def setUp(self): "name": "ADB", "display_name": "ADB", "version": "2.5", + "license": "absent", + "license_path": None, + "license_hash": None, "version_order": 11, "required": False, "shared": False, @@ -928,12 +871,10 @@ def setUp(self): ) @patch("cm.api.load_service_map") - @patch("cm.issue.update_hierarchy_issues") - @patch("cm.status_api.post_event") + @patch("cm.api.update_hierarchy_issues") + @patch("cm.api.post_event") def test_save_hc(self, mock_post_event, mock_update_issues, mock_load_service_map): - cluster_object = ClusterObject.objects.create( - prototype=self.prototype, cluster=self.cluster - ) + cluster_object = ClusterObject.objects.create(prototype=self.prototype, cluster=self.cluster) host = Host.objects.create(prototype=self.prototype, cluster=self.cluster) component = Prototype.objects.create( parent=self.prototype, type="component", bundle_id=self.bundle.id, name="node" @@ -951,15 +892,13 @@ def test_save_hc(self, mock_post_event, mock_update_issues, mock_load_service_ma self.assertListEqual(hc_list, [HostComponent.objects.first()]) - mock_post_event.assert_called_once_with( - "change_hostcomponentmap", "cluster", self.cluster.id - ) + mock_post_event.assert_called_once_with("change_hostcomponentmap", "cluster", self.cluster.id) mock_update_issues.assert_called() mock_load_service_map.assert_called_once() @patch("cm.api.ctx") @patch("cm.api.load_service_map") - @patch("cm.issue.update_hierarchy_issues") + @patch("cm.api.update_hierarchy_issues") def test_save_hc__big_update__locked_hierarchy(self, mock_issue, mock_load, ctx): """ Update bigger HC map - move `component_2` from `host_2` to `host_3` @@ -1011,7 +950,7 @@ def test_save_hc__big_update__locked_hierarchy(self, mock_issue, mock_load, ctx) self.assertTrue(host_3.locked) @patch("cm.api.load_service_map") - @patch("cm.issue.update_hierarchy_issues") + @patch("cm.api.update_hierarchy_issues") def test_save_hc__big_update__unlocked_hierarchy(self, mock_update, mock_load): """ Update bigger HC map - move `component_2` from `host_2` to `host_3` diff --git a/python/api/tests/test_bundle.py b/python/api/tests/test_bundle.py new file mode 100644 index 0000000000..8366dedf12 --- /dev/null +++ b/python/api/tests/test_bundle.py @@ -0,0 +1,254 @@ +# 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 pathlib import Path +from unittest.mock import patch + +from django.conf import settings +from django.urls import reverse +from rest_framework.response import Response +from rest_framework.status import HTTP_200_OK, HTTP_201_CREATED, HTTP_409_CONFLICT + +from adcm.tests.base import BaseTestCase +from cm.bundle import get_hash +from cm.models import Bundle, Prototype + + +class TestBundle(BaseTestCase): + # pylint: disable=too-many-public-methods + + def setUp(self) -> None: + super().setUp() + + self.bundle_1 = Bundle.objects.create( + name="test_bundle_1", + version="123", + version_order=1, + ) + self.bundle_2 = Bundle.objects.create(name="test_bundle_2", version="456", version_order=2) + Prototype.objects.create( + bundle=self.bundle_1, name=self.bundle_1.name, license_path="some_path", license="unaccepted" + ) + Prototype.objects.create(bundle=self.bundle_2, name=self.bundle_2.name) + + def tearDown(self) -> None: + Path(settings.DOWNLOAD_DIR, self.test_bundle_filename).unlink(missing_ok=True) + + def upload_bundle(self, bundle_path: Path = None): + if bundle_path is None: + bundle_path = self.test_bundle_path + + with open(bundle_path, encoding=settings.ENCODING_UTF_8) as f: + return self.client.post( + path=reverse("upload-bundle"), + data={"file": f}, + ) + + def test_upload_bundle(self) -> None: + response: Response = self.upload_bundle() + + self.assertEqual(response.status_code, HTTP_201_CREATED) + self.assertTrue(Path(settings.DOWNLOAD_DIR, self.test_bundle_filename).exists()) + + def test_load_bundle(self): + self.upload_bundle() + + response: Response = self.client.post( + path=reverse("load-bundle"), + data={"bundle_file": self.test_bundle_filename}, + ) + + self.assertEqual(response.status_code, HTTP_200_OK) + self.assertEqual(response.data["hash"], get_hash(self.test_bundle_path)) + + def test_load_bundle_wrong_cluster_mm_action_no_host_action_prop_fail(self): + bundle_filename = "bundle_test_cluster_wrong_host_action.tar" + + self.upload_bundle( + Path( + settings.BASE_DIR, + "python/api/tests/files", + bundle_filename, + ) + ) + + response: Response = self.client.post( + path=reverse("load-bundle"), + data={"bundle_file": bundle_filename}, + ) + + self.assertEqual(response.status_code, HTTP_409_CONFLICT) + self.assertEqual(response.data["code"], "INVALID_OBJECT_DEFINITION") + + def test_load_bundle_wrong_cluster_mm_action_false_host_action_prop_fail(self): + bundle_filename = "bundle_test_cluster_false_host_action.tar" + + self.upload_bundle( + Path( + settings.BASE_DIR, + "python/api/tests/files", + bundle_filename, + ) + ) + + response: Response = self.client.post( + path=reverse("load-bundle"), + data={"bundle_file": bundle_filename}, + ) + + self.assertEqual(response.status_code, HTTP_409_CONFLICT) + self.assertEqual(response.data["code"], "INVALID_OBJECT_DEFINITION") + + def test_load_bundle_cluster_mm_action_host_action_true_success(self): + bundle_filename = "bundle_test_cluster_host_action_true.tar" + + self.upload_bundle( + Path( + settings.BASE_DIR, + "python/api/tests/files", + bundle_filename, + ) + ) + + response: Response = self.client.post( + path=reverse("load-bundle"), + data={"bundle_file": bundle_filename}, + ) + + self.assertEqual(response.status_code, HTTP_200_OK) + + def test_load_bundle_service_with_host_mm_action_fail(self): + bundle_filename = "bundle_test_service_with_host_action.tar" + + self.upload_bundle( + Path( + settings.BASE_DIR, + "python/api/tests/files", + bundle_filename, + ) + ) + + response: Response = self.client.post( + path=reverse("load-bundle"), + data={"bundle_file": bundle_filename}, + ) + + self.assertEqual(response.status_code, HTTP_409_CONFLICT) + self.assertEqual(response.data["code"], "INVALID_OBJECT_DEFINITION") + + def test_load_bundle_cluster_with_host_mm_has_ui_options_fail(self): + bundle_filename = "bundle_test_cluster_action_with_ui_options.tar" + + self.upload_bundle( + Path( + settings.BASE_DIR, + "python/api/tests/files", + bundle_filename, + ) + ) + + response: Response = self.client.post( + path=reverse("load-bundle"), + data={"bundle_file": bundle_filename}, + ) + + self.assertEqual(response.status_code, HTTP_409_CONFLICT) + self.assertEqual(response.data["code"], "INVALID_OBJECT_DEFINITION") + + def test_load_servicemap(self): + with patch("api.stack.views.load_service_map"): + response: Response = self.client.put( + path=reverse("load-servicemap"), + ) + + self.assertEqual(response.status_code, HTTP_200_OK) + + def test_list(self): + response: Response = self.client.get(path=reverse("bundle-list")) + + self.assertEqual(len(response.data["results"]), 2) + + def test_list_filter_name(self): + response: Response = self.client.get(reverse("bundle-list"), {"name": self.bundle_1.name}) + + self.assertEqual(response.data["results"][0]["id"], self.bundle_1.pk) + + def test_list_filter_version(self): + response: Response = self.client.get(reverse("bundle-list"), {"version": self.bundle_1.version}) + + self.assertEqual(response.data["results"][0]["id"], self.bundle_1.pk) + + def test_list_ordering_name(self): + response: Response = self.client.get(reverse("bundle-list"), {"ordering": "name"}) + + self.assertEqual(len(response.data["results"]), 2) + self.assertEqual(response.data["results"][0]["id"], self.bundle_1.pk) + self.assertEqual(response.data["results"][1]["id"], self.bundle_2.pk) + + def test_list_ordering_name_reverse(self): + response: Response = self.client.get(reverse("bundle-list"), {"ordering": "-name"}) + + self.assertEqual(len(response.data["results"]), 2) + self.assertEqual(response.data["results"][0]["id"], self.bundle_2.pk) + self.assertEqual(response.data["results"][1]["id"], self.bundle_1.pk) + + def test_list_ordering_version_order(self): + response: Response = self.client.get(reverse("bundle-list"), {"ordering": "version_order"}) + + self.assertEqual(len(response.data["results"]), 2) + self.assertEqual(response.data["results"][0]["id"], self.bundle_1.pk) + self.assertEqual(response.data["results"][1]["id"], self.bundle_2.pk) + + def test_list_ordering_version_order_reverse(self): + response: Response = self.client.get(reverse("bundle-list"), {"ordering": "-version_order"}) + + self.assertEqual(len(response.data["results"]), 2) + self.assertEqual(response.data["results"][0]["id"], self.bundle_2.pk) + self.assertEqual(response.data["results"][1]["id"], self.bundle_1.pk) + + def test_retrieve(self): + response: Response = self.client.get( + path=reverse("bundle-detail", kwargs={"bundle_pk": self.bundle_2.pk}), + ) + + self.assertEqual(response.data["id"], self.bundle_2.pk) + + def test_delete(self): + with patch("cm.bundle.shutil.rmtree"): + self.client.delete( + path=reverse("bundle-detail", kwargs={"bundle_pk": self.bundle_2.pk}), + ) + + self.assertFalse(Bundle.objects.filter(pk=self.bundle_2.pk)) + + def test_update(self): + with patch("api.stack.views.update_bundle"): + response: Response = self.client.put( + path=reverse("bundle-update", kwargs={"bundle_pk": self.bundle_1.pk}), + ) + + self.assertEqual(response.status_code, HTTP_200_OK) + + def test_license(self): + with patch("api.stack.views.get_license", return_value="license body"): + response: Response = self.client.get( + path=reverse("bundle-license", kwargs={"bundle_pk": self.bundle_1.pk}), + ) + + self.assertEqual(response.status_code, HTTP_200_OK) + + def test_accept_license(self): + response: Response = self.client.put( + path=reverse("accept-license", kwargs={"bundle_pk": self.bundle_1.pk}), + ) + + self.assertEqual(response.status_code, HTTP_200_OK) diff --git a/python/api/tests/test_cluster.py b/python/api/tests/test_cluster.py new file mode 100644 index 0000000000..bc38ca738b --- /dev/null +++ b/python/api/tests/test_cluster.py @@ -0,0 +1,48 @@ +# 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 django.urls import reverse +from rest_framework.response import Response +from rest_framework.status import HTTP_200_OK + +from adcm.tests.base import BaseTestCase +from cm.models import Action, ActionType, Bundle, Cluster, Prototype, Upgrade + + +class TestClusterAPI(BaseTestCase): + def setUp(self) -> None: + super().setUp() + + self.bundle = Bundle.objects.create(name="test_cluster_prototype") + self.cluster_prototype = Prototype.objects.create( + bundle=self.bundle, + version=2, + name="test_cluster_prototype", + ) + self.cluster = Cluster.objects.create(prototype=self.cluster_prototype) + + def test_upgrade(self): + Upgrade.objects.create( + bundle=self.bundle, + min_version=1, + max_version=3, + action=Action.objects.create( + prototype=self.cluster_prototype, + type=ActionType.Job, + state_available="any", + ), + ) + response: Response = self.client.get( + path=reverse("cluster-upgrade", kwargs={"cluster_id": self.cluster.pk}), + ) + + self.assertEqual(response.status_code, HTTP_200_OK) diff --git a/python/api/tests/test_cluster_prototype.py b/python/api/tests/test_cluster_prototype.py new file mode 100644 index 0000000000..7a277d083b --- /dev/null +++ b/python/api/tests/test_cluster_prototype.py @@ -0,0 +1,147 @@ +# 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 django.urls import reverse +from rest_framework.response import Response + +from adcm.tests.base import BaseTestCase +from cm.models import Action, ActionType, Bundle, Prototype + + +class TestClusterPrototypeAPI(BaseTestCase): + def setUp(self) -> None: + super().setUp() + + self.bundle_1 = Bundle.objects.create(name="test_bundle_1") + self.bundle_2 = Bundle.objects.create(name="test_bundle_2") + + self.prototype_1 = Prototype.objects.create( + bundle=self.bundle_1, + type="cluster", + name="test_prototype_1", + display_name="test_prototype_1", + version_order=1, + version=1, + ) + self.prototype_2 = Prototype.objects.create( + bundle=self.bundle_2, + type="cluster", + name="test_prototype_2", + display_name="test_prototype_2", + version_order=2, + version=2, + ) + self.action = Action.objects.create( + display_name="test_adcm_action", + prototype=self.prototype_1, + type=ActionType.Job, + state_available="any", + ) + + def test_list(self): + response: Response = self.client.get(path=reverse("cluster-prototype-list")) + + self.assertEqual(len(response.data["results"]), 2) + + def test_list_filter_name(self): + response: Response = self.client.get(reverse("cluster-prototype-list"), {"name": "test_prototype_2"}) + + self.assertEqual(len(response.data["results"]), 1) + self.assertEqual(response.data["results"][0]["id"], self.prototype_2.pk) + + def test_list_filter_bundle_id(self): + response: Response = self.client.get( + reverse("cluster-prototype-list"), + {"bundle_id": self.bundle_1.pk}, + ) + + self.assertEqual(len(response.data["results"]), 1) + self.assertEqual(response.data["results"][0]["id"], self.prototype_1.pk) + + def test_list_filter_display_name(self): + response: Response = self.client.get( + reverse("cluster-prototype-list"), + {"display_name": "test_prototype_2"}, + ) + + self.assertEqual(len(response.data["results"]), 1) + self.assertEqual(response.data["results"][0]["id"], self.prototype_2.pk) + + def test_list_ordering_display_name(self): + response: Response = self.client.get(reverse("cluster-prototype-list"), {"ordering": "display_name"}) + + self.assertEqual(len(response.data["results"]), 2) + self.assertEqual(response.data["results"][0]["id"], self.prototype_1.pk) + self.assertEqual(response.data["results"][1]["id"], self.prototype_2.pk) + + def test_list_ordering_display_name_reverse(self): + response: Response = self.client.get(reverse("cluster-prototype-list"), {"ordering": "-display_name"}) + + self.assertEqual(len(response.data["results"]), 2) + self.assertEqual(response.data["results"][0]["id"], self.prototype_2.pk) + self.assertEqual(response.data["results"][1]["id"], self.prototype_1.pk) + + def test_list_ordering_version_order(self): + response: Response = self.client.get(reverse("cluster-prototype-list"), {"ordering": "version_order"}) + + self.assertEqual(len(response.data["results"]), 2) + self.assertEqual(response.data["results"][0]["id"], self.prototype_1.pk) + self.assertEqual(response.data["results"][1]["id"], self.prototype_2.pk) + + def test_list_ordering_version_order_reverse(self): + response: Response = self.client.get(reverse("cluster-prototype-list"), {"ordering": "-version_order"}) + + self.assertEqual(len(response.data["results"]), 2) + self.assertEqual(response.data["results"][0]["id"], self.prototype_2.pk) + self.assertEqual(response.data["results"][1]["id"], self.prototype_1.pk) + + def test_list_ordering_version(self): + response: Response = self.client.get(reverse("cluster-prototype-list"), {"ordering": "version"}) + + self.assertEqual(len(response.data["results"]), 2) + self.assertEqual(response.data["results"][0]["id"], self.prototype_1.pk) + self.assertEqual(response.data["results"][1]["id"], self.prototype_2.pk) + + def test_list_ordering_version_reverse(self): + response: Response = self.client.get(reverse("cluster-prototype-list"), {"ordering": "-version"}) + + self.assertEqual(len(response.data["results"]), 2) + self.assertEqual(response.data["results"][0]["id"], self.prototype_2.pk) + self.assertEqual(response.data["results"][1]["id"], self.prototype_1.pk) + + def test_retrieve(self): + response: Response = self.client.get( + reverse("cluster-prototype-detail", kwargs={"prototype_pk": self.prototype_2.pk}), + ) + + self.assertEqual(response.data["id"], self.prototype_2.pk) + + def test_display_name_distinct_two_objs(self): + self.prototype_1.display_name = "test_prototype" + self.prototype_2.display_name = "test_prototype" + self.prototype_1.save(update_fields=["display_name"]) + self.prototype_2.save(update_fields=["display_name"]) + + response: Response = self.client.get( + reverse("cluster-prototype-list"), + {"fields": "display_name", "distinct": 1}, + ) + + self.assertEqual(len(response.data["results"]), 1) + + def test_display_name_distinct_one_obj(self): + response: Response = self.client.get( + reverse("cluster-prototype-list"), + {"fields": "display_name", "distinct": 1}, + ) + + self.assertEqual(len(response.data["results"]), 2) diff --git a/python/api/tests/test_component.py b/python/api/tests/test_component.py new file mode 100644 index 0000000000..4c77b2d3a1 --- /dev/null +++ b/python/api/tests/test_component.py @@ -0,0 +1,231 @@ +# 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 pathlib import Path +from unittest.mock import patch + +from django.conf import settings +from django.urls import reverse +from rest_framework.response import Response +from rest_framework.status import HTTP_200_OK, HTTP_400_BAD_REQUEST, HTTP_409_CONFLICT + +from adcm.tests.base import BaseTestCase +from cm.models import ( + Action, + Bundle, + Cluster, + ClusterObject, + Host, + HostComponent, + MaintenanceMode, + Prototype, + ServiceComponent, +) + + +class TestComponentAPI(BaseTestCase): + def setUp(self) -> None: + super().setUp() + + bundle = Bundle.objects.create() + cluster_prototype = Prototype.objects.create(bundle=bundle, type="cluster") + self.cluster = Cluster.objects.create(prototype=cluster_prototype, name="test_cluster") + service_prototype = Prototype.objects.create( + bundle=bundle, + type="service", + display_name="test_service", + ) + self.service = ClusterObject.objects.create(prototype=service_prototype, cluster=self.cluster) + self.component = ServiceComponent.objects.create( + prototype=Prototype.objects.create( + bundle=bundle, + type="component", + display_name="test_component", + ), + cluster=self.cluster, + service=self.service, + ) + self.host = Host.objects.create( + fqdn="test-host", prototype=Prototype.objects.create(bundle=bundle, type="host") + ) + + def test_change_maintenance_mode_wrong_name_fail(self): + response: Response = self.client.post( + path=reverse("component-maintenance-mode", kwargs={"component_id": self.component.pk}), + data={"maintenance_mode": "wrong"}, + ) + + self.assertEqual(response.status_code, HTTP_400_BAD_REQUEST) + self.assertIn("maintenance_mode", response.data) + + def test_change_maintenance_mode_on_no_action_success(self): + response: Response = self.client.post( + path=reverse("component-maintenance-mode", kwargs={"component_id": self.component.pk}), + data={"maintenance_mode": MaintenanceMode.ON}, + ) + + self.component.refresh_from_db() + + self.assertEqual(response.status_code, HTTP_200_OK) + self.assertEqual(response.data["maintenance_mode"], MaintenanceMode.ON) + self.assertEqual(self.component.maintenance_mode, MaintenanceMode.ON) + + def test_change_maintenance_mode_on_no_service_issue_success(self): + bundle = self.upload_and_load_bundle( + path=Path( + settings.BASE_DIR, + "python/api/tests/files/bundle_issue_component.tar", + ), + ) + + cluster_prototype = Prototype.objects.get(bundle=bundle, type="cluster") + cluster_response: Response = self.client.post( + path=reverse("cluster"), + data={"name": "test-cluster", "prototype_id": cluster_prototype.pk}, + ) + cluster = Cluster.objects.get(pk=cluster_response.data["id"]) + + service_prototype = Prototype.objects.get(bundle=bundle, type="service") + service_response: Response = self.client.post( + path=reverse("service", kwargs={"cluster_id": cluster.pk}), + data={"prototype_id": service_prototype.pk}, + ) + service = ClusterObject.objects.get(pk=service_response.data["id"]) + + component_1 = ServiceComponent.objects.get(service=service, prototype__name="first_component") + component_2 = ServiceComponent.objects.get(service=service, prototype__name="second_component") + + self.assertTrue(service.concerns.exists()) + self.assertTrue(component_2.concerns.exists()) + self.assertFalse(component_1.concerns.exists()) + + response: Response = self.client.post( + path=reverse("component-maintenance-mode", kwargs={"component_id": component_2.pk}), + data={"maintenance_mode": MaintenanceMode.ON}, + ) + + component_2.refresh_from_db() + service.refresh_from_db() + + self.assertEqual(response.status_code, HTTP_200_OK) + self.assertEqual(response.data["maintenance_mode"], MaintenanceMode.ON) + self.assertEqual(component_2.maintenance_mode, MaintenanceMode.ON) + self.assertFalse(service.concerns.exists()) + + def test_change_maintenance_mode_on_with_action_success(self): + HostComponent.objects.create( + cluster=self.cluster, host=self.host, service=self.service, component=self.component + ) + action = Action.objects.create(prototype=self.component.prototype, name=settings.ADCM_TURN_ON_MM_ACTION_NAME) + + with patch("api.utils.start_task") as start_task_mock: + response: Response = self.client.post( + path=reverse("component-maintenance-mode", kwargs={"component_id": self.component.pk}), + data={"maintenance_mode": MaintenanceMode.ON}, + ) + + self.component.refresh_from_db() + + self.assertEqual(response.status_code, HTTP_200_OK) + self.assertEqual(response.data["maintenance_mode"], MaintenanceMode.CHANGING) + self.assertEqual(self.component.maintenance_mode, MaintenanceMode.CHANGING) + start_task_mock.assert_called_once_with( + action=action, obj=self.component, conf={}, attr={}, hc=[], hosts=[], verbose=False + ) + + def test_change_maintenance_mode_on_from_on_with_action_fail(self): + self.component.maintenance_mode = MaintenanceMode.ON + self.component.save() + + with patch("api.utils.start_task") as start_task_mock: + response: Response = self.client.post( + path=reverse("component-maintenance-mode", kwargs={"component_id": self.component.pk}), + data={"maintenance_mode": MaintenanceMode.ON}, + ) + + self.component.refresh_from_db() + + self.assertEqual(response.status_code, HTTP_409_CONFLICT) + self.assertEqual(self.component.maintenance_mode, MaintenanceMode.ON) + start_task_mock.assert_not_called() + + def test_change_maintenance_mode_off_no_action_success(self): + self.component.maintenance_mode = MaintenanceMode.ON + self.component.save() + + response: Response = self.client.post( + path=reverse("component-maintenance-mode", kwargs={"component_id": self.component.pk}), + data={"maintenance_mode": MaintenanceMode.OFF}, + ) + + self.component.refresh_from_db() + + self.assertEqual(response.status_code, HTTP_200_OK) + self.assertEqual(response.data["maintenance_mode"], MaintenanceMode.OFF) + self.assertEqual(self.component.maintenance_mode, MaintenanceMode.OFF) + + def test_change_maintenance_mode_off_with_action_success(self): + self.component.maintenance_mode = MaintenanceMode.ON + self.component.save() + HostComponent.objects.create( + cluster=self.cluster, host=self.host, service=self.service, component=self.component + ) + action = Action.objects.create(prototype=self.component.prototype, name=settings.ADCM_TURN_OFF_MM_ACTION_NAME) + + with patch("api.utils.start_task") as start_task_mock: + response: Response = self.client.post( + path=reverse("component-maintenance-mode", kwargs={"component_id": self.component.pk}), + data={"maintenance_mode": MaintenanceMode.OFF}, + ) + + self.component.refresh_from_db() + + self.assertEqual(response.status_code, HTTP_200_OK) + self.assertEqual(response.data["maintenance_mode"], MaintenanceMode.CHANGING) + self.assertEqual(self.component.maintenance_mode, MaintenanceMode.CHANGING) + start_task_mock.assert_called_once_with( + action=action, obj=self.component, conf={}, attr={}, hc=[], hosts=[], verbose=False + ) + + def test_change_maintenance_mode_off_to_off_with_action_fail(self): + self.component.maintenance_mode = MaintenanceMode.OFF + self.component.save() + + with patch("api.utils.start_task") as start_task_mock: + response: Response = self.client.post( + path=reverse("component-maintenance-mode", kwargs={"component_id": self.component.pk}), + data={"maintenance_mode": MaintenanceMode.OFF}, + ) + + self.component.refresh_from_db() + + self.assertEqual(response.status_code, HTTP_409_CONFLICT) + self.assertEqual(self.component.maintenance_mode, MaintenanceMode.OFF) + start_task_mock.assert_not_called() + + def test_change_maintenance_mode_changing_now_fail(self): + self.component.maintenance_mode = MaintenanceMode.CHANGING + self.component.save() + + response: Response = self.client.post( + path=reverse("component-maintenance-mode", kwargs={"component_id": self.component.pk}), + data={"maintenance_mode": MaintenanceMode.ON}, + ) + + self.assertEqual(response.status_code, HTTP_409_CONFLICT) + + response: Response = self.client.post( + path=reverse("component-maintenance-mode", kwargs={"component_id": self.component.pk}), + data={"maintenance_mode": MaintenanceMode.OFF}, + ) + + self.assertEqual(response.status_code, HTTP_409_CONFLICT) diff --git a/python/api/tests/test_component_prototype.py b/python/api/tests/test_component_prototype.py new file mode 100644 index 0000000000..f62db17d0d --- /dev/null +++ b/python/api/tests/test_component_prototype.py @@ -0,0 +1,101 @@ +# 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 django.urls import reverse +from rest_framework.response import Response + +from adcm.tests.base import BaseTestCase +from cm.models import Action, ActionType, Bundle, Prototype + + +class TestComponentPrototypeAPI(BaseTestCase): + def setUp(self) -> None: + super().setUp() + + self.bundle_1 = Bundle.objects.create(name="test_bundle_1") + self.bundle_2 = Bundle.objects.create(name="test_bundle_2") + + self.prototype_1 = Prototype.objects.create( + bundle=self.bundle_1, + type="component", + name="test_prototype_1", + display_name="test_prototype_1", + version_order=1, + ) + self.prototype_2 = Prototype.objects.create( + bundle=self.bundle_2, + type="component", + name="test_prototype_2", + display_name="test_prototype_2", + version_order=2, + ) + self.action = Action.objects.create( + display_name="test_adcm_action", + prototype=self.prototype_1, + type=ActionType.Job, + state_available="any", + ) + + def test_list(self): + response: Response = self.client.get(path=reverse("component-prototype-list")) + + self.assertEqual(len(response.data["results"]), 2) + + def test_list_filter_name(self): + response: Response = self.client.get(reverse("component-prototype-list"), {"name": "test_prototype_2"}) + + self.assertEqual(len(response.data["results"]), 1) + self.assertEqual(response.data["results"][0]["id"], self.prototype_2.pk) + + def test_list_filter_bundle_id(self): + response: Response = self.client.get( + reverse("component-prototype-list"), + {"bundle_id": self.bundle_1.pk}, + ) + + self.assertEqual(len(response.data["results"]), 1) + self.assertEqual(response.data["results"][0]["id"], self.prototype_1.pk) + + def test_list_ordering_display_name(self): + response: Response = self.client.get(reverse("component-prototype-list"), {"ordering": "display_name"}) + + self.assertEqual(len(response.data["results"]), 2) + self.assertEqual(response.data["results"][0]["id"], self.prototype_1.pk) + self.assertEqual(response.data["results"][1]["id"], self.prototype_2.pk) + + def test_list_ordering_display_name_reverse(self): + response: Response = self.client.get(reverse("component-prototype-list"), {"ordering": "-display_name"}) + + self.assertEqual(len(response.data["results"]), 2) + self.assertEqual(response.data["results"][0]["id"], self.prototype_2.pk) + self.assertEqual(response.data["results"][1]["id"], self.prototype_1.pk) + + def test_list_ordering_version_order(self): + response: Response = self.client.get(reverse("component-prototype-list"), {"ordering": "version_order"}) + + self.assertEqual(len(response.data["results"]), 2) + self.assertEqual(response.data["results"][0]["id"], self.prototype_1.pk) + self.assertEqual(response.data["results"][1]["id"], self.prototype_2.pk) + + def test_list_ordering_version_order_reverse(self): + response: Response = self.client.get(reverse("component-prototype-list"), {"ordering": "-version_order"}) + + self.assertEqual(len(response.data["results"]), 2) + self.assertEqual(response.data["results"][0]["id"], self.prototype_2.pk) + self.assertEqual(response.data["results"][1]["id"], self.prototype_1.pk) + + def test_retrieve(self): + response: Response = self.client.get( + reverse("component-prototype-detail", kwargs={"prototype_pk": self.prototype_2.pk}), + ) + + self.assertEqual(response.data["id"], self.prototype_2.pk) diff --git a/python/api/tests/test_host.py b/python/api/tests/test_host.py new file mode 100644 index 0000000000..880e8c28fe --- /dev/null +++ b/python/api/tests/test_host.py @@ -0,0 +1,330 @@ +# 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 pathlib import Path +from unittest.mock import patch + +from django.conf import settings +from django.urls import reverse +from rest_framework.response import Response +from rest_framework.status import HTTP_200_OK, HTTP_400_BAD_REQUEST, HTTP_409_CONFLICT + +from adcm.tests.base import APPLICATION_JSON, BaseTestCase +from cm.models import ( + Action, + ActionType, + Bundle, + Cluster, + ClusterObject, + Host, + HostProvider, + MaintenanceMode, + Prototype, + ServiceComponent, +) + + +class TestHostAPI(BaseTestCase): + def setUp(self) -> None: + super().setUp() + + self.bundle = Bundle.objects.create() + self.cluster_prototype = Prototype.objects.create( + bundle=self.bundle, type="cluster", allow_maintenance_mode=True + ) + cluster = Cluster.objects.create(name="test_cluster", prototype=self.cluster_prototype) + + self.provider_prototype = Prototype.objects.create(bundle=self.bundle, type="provider") + self.host_provider = HostProvider.objects.create(name="test_provider_2", prototype=self.provider_prototype) + + self.host_prototype = Prototype.objects.create(bundle=self.bundle, type="host") + self.host = Host.objects.create( + fqdn="test_host_fqdn", + prototype=self.host_prototype, + cluster=cluster, + provider=self.host_provider, + ) + + def test_change_mm_wrong_name_fail(self): + response: Response = self.client.post( + path=reverse("host-maintenance-mode", kwargs={"host_id": self.host.pk}), + data={"maintenance_mode": "wrong"}, + ) + + self.assertEqual(response.status_code, HTTP_400_BAD_REQUEST) + self.assertIn("maintenance_mode", response.data) + + def test_change_mm_to_changing_fail(self): + response: Response = self.client.post( + path=reverse("host-maintenance-mode", kwargs={"host_id": self.host.pk}), + data={"maintenance_mode": MaintenanceMode.CHANGING}, + ) + + self.assertEqual(response.status_code, HTTP_400_BAD_REQUEST) + + def test_change_mm_on_no_action_success(self): + response: Response = self.client.post( + path=reverse("host-maintenance-mode", kwargs={"host_id": self.host.pk}), + data={"maintenance_mode": MaintenanceMode.ON}, + ) + + self.host.refresh_from_db() + + self.assertEqual(response.status_code, HTTP_200_OK) + self.assertEqual(response.data["maintenance_mode"], MaintenanceMode.ON) + self.assertEqual(self.host.maintenance_mode, MaintenanceMode.ON) + + def test_change_mm_on_with_action_success(self): + action = Action.objects.create( + prototype=self.host.cluster.prototype, + name=settings.ADCM_HOST_TURN_ON_MM_ACTION_NAME, + type=ActionType.Job, + state_available="any", + ) + + with patch("api.utils.start_task") as start_task_mock: + response: Response = self.client.post( + path=reverse("host-maintenance-mode", kwargs={"host_id": self.host.pk}), + data={"maintenance_mode": MaintenanceMode.ON}, + ) + + self.host.refresh_from_db() + + self.assertEqual(response.status_code, HTTP_200_OK) + self.assertEqual(response.data["maintenance_mode"], MaintenanceMode.CHANGING) + self.assertEqual(self.host.maintenance_mode, MaintenanceMode.CHANGING) + start_task_mock.assert_called_once_with( + action=action, obj=self.host, conf={}, attr={}, hc=[], hosts=[], verbose=False + ) + + def test_change_mm_on_from_on_with_action_fail(self): + self.host.maintenance_mode = MaintenanceMode.ON + self.host.save(update_fields=["maintenance_mode"]) + + with patch("api.utils.start_task") as start_task_mock: + response: Response = self.client.post( + path=reverse("host-maintenance-mode", kwargs={"host_id": self.host.pk}), + data={"maintenance_mode": MaintenanceMode.ON}, + ) + + self.host.refresh_from_db() + + self.assertEqual(response.status_code, HTTP_409_CONFLICT) + self.assertEqual(self.host.maintenance_mode, MaintenanceMode.ON) + start_task_mock.assert_not_called() + + def test_change_mm_off_no_action_success(self): + self.host.maintenance_mode = MaintenanceMode.ON + self.host.save(update_fields=["maintenance_mode"]) + + response: Response = self.client.post( + path=reverse("host-maintenance-mode", kwargs={"host_id": self.host.pk}), + data={"maintenance_mode": MaintenanceMode.OFF}, + ) + + self.host.refresh_from_db() + + self.assertEqual(response.status_code, HTTP_200_OK) + self.assertEqual(response.data["maintenance_mode"], MaintenanceMode.OFF) + self.assertEqual(self.host.maintenance_mode, MaintenanceMode.OFF) + + def test_change_mm_off_with_action_success(self): + self.host.maintenance_mode = MaintenanceMode.ON + self.host.save(update_fields=["maintenance_mode"]) + action = Action.objects.create( + prototype=self.host.cluster.prototype, name=settings.ADCM_HOST_TURN_OFF_MM_ACTION_NAME + ) + + with patch("api.utils.start_task") as start_task_mock: + response: Response = self.client.post( + path=reverse("host-maintenance-mode", kwargs={"host_id": self.host.pk}), + data={"maintenance_mode": MaintenanceMode.OFF}, + ) + + self.host.refresh_from_db() + + self.assertEqual(response.status_code, HTTP_200_OK) + self.assertEqual(response.data["maintenance_mode"], MaintenanceMode.CHANGING) + self.assertEqual(self.host.maintenance_mode, MaintenanceMode.CHANGING) + start_task_mock.assert_called_once_with( + action=action, obj=self.host, conf={}, attr={}, hc=[], hosts=[], verbose=False + ) + + def test_change_mm_off_to_off_with_action_fail(self): + self.host.maintenance_mode = MaintenanceMode.OFF + self.host.save(update_fields=["maintenance_mode"]) + + with patch("api.utils.start_task") as start_task_mock: + response: Response = self.client.post( + path=reverse("host-maintenance-mode", kwargs={"host_id": self.host.pk}), + data={"maintenance_mode": MaintenanceMode.OFF}, + ) + + self.host.refresh_from_db() + + self.assertEqual(response.status_code, HTTP_409_CONFLICT) + self.assertEqual(self.host.maintenance_mode, MaintenanceMode.OFF) + start_task_mock.assert_not_called() + + def test_change_mm_changing_now_fail(self): + self.host.maintenance_mode = MaintenanceMode.CHANGING + self.host.save(update_fields=["maintenance_mode"]) + + response: Response = self.client.post( + path=reverse("host-maintenance-mode", kwargs={"host_id": self.host.pk}), + data={"maintenance_mode": MaintenanceMode.ON}, + ) + + self.assertEqual(response.status_code, HTTP_409_CONFLICT) + + response: Response = self.client.post( + path=reverse("host-maintenance-mode", kwargs={"host_id": self.host.pk}), + data={"maintenance_mode": MaintenanceMode.OFF}, + ) + + self.assertEqual(response.status_code, HTTP_409_CONFLICT) + + def test_cluster_clear_issue_success(self): + provider_bundle = self.upload_and_load_bundle( + path=Path( + settings.BASE_DIR, + "python/api/tests/files/bundle_test_provider_concern.tar", + ), + ) + + cluster_bundle = self.upload_and_load_bundle( + path=Path( + settings.BASE_DIR, + "python/api/tests/files/bundle_test_cluster_with_mm.tar", + ), + ) + + provider_prototype = Prototype.objects.get(bundle=provider_bundle, type="provider") + provider_response: Response = self.client.post( + path=reverse("provider"), + data={"name": "test_provider", "prototype_id": provider_prototype.pk}, + ) + provider = HostProvider.objects.get(pk=provider_response.data["id"]) + + host_response: Response = self.client.post( + path=reverse("host", kwargs={"provider_id": provider.pk}), + data={"fqdn": "test-host"}, + ) + host = Host.objects.get(pk=host_response.data["id"]) + + self.assertTrue(host.concerns.exists()) + + cluster_prototype = Prototype.objects.get(bundle_id=cluster_bundle.pk, type="cluster") + cluster_response: Response = self.client.post( + path=reverse("cluster"), + data={"name": "test-cluster", "prototype_id": cluster_prototype.pk}, + ) + cluster = Cluster.objects.get(pk=cluster_response.data["id"]) + + service_prototype = Prototype.objects.get(name="test_service", type="service") + service_response: Response = self.client.post( + path=reverse("service", kwargs={"cluster_id": cluster.pk}), + data={"prototype_id": service_prototype.pk}, + ) + service = ClusterObject.objects.get(pk=service_response.data["id"]) + + component = ServiceComponent.objects.get(service=service, prototype__name="first_component") + + self.assertFalse(cluster.concerns.exists()) + + self.client.post( + path=reverse("host", kwargs={"cluster_id": cluster.pk}), + data={"host_id": host.pk}, + ) + + self.client.post( + path=reverse("host-component", kwargs={"cluster_id": cluster.pk}), + data={"hc": [{"service_id": service.pk, "component_id": component.pk, "host_id": host.pk}]}, + content_type=APPLICATION_JSON, + ) + + self.assertTrue(cluster.concerns.exists()) + + self.client.post( + path=reverse("host-maintenance-mode", kwargs={"host_id": host.pk}), + data={"maintenance_mode": MaintenanceMode.ON}, + ) + + self.assertFalse(cluster.concerns.exists()) + + def test_mm_constraint_by_no_cluster_fail(self): + self.host.cluster = None + self.host.save(update_fields=["cluster"]) + + response: Response = self.client.post( + path=reverse("host-maintenance-mode", kwargs={"host_id": self.host.pk}), + data={"maintenance_mode": MaintenanceMode.ON}, + ) + + self.assertEqual(response.status_code, HTTP_409_CONFLICT) + + def test_mm_constraint_by_cluster_without_mm_fail(self): + self.cluster_prototype.allow_maintenance_mode = False + self.cluster_prototype.save(update_fields=["allow_maintenance_mode"]) + + response: Response = self.client.post( + path=reverse("host-maintenance-mode", kwargs={"host_id": self.host.pk}), + data={"maintenance_mode": MaintenanceMode.ON}, + ) + + self.assertEqual(response.status_code, HTTP_409_CONFLICT) + + def test_change_maintenance_mode_on_with_action_via_bundle_success(self): + bundle = self.upload_and_load_bundle( + path=Path( + settings.BASE_DIR, + "python/api/tests/files/cluster_using_plugin.tar", + ), + ) + action = Action.objects.get(name=settings.ADCM_HOST_TURN_ON_MM_ACTION_NAME) + + cluster_prototype = Prototype.objects.get(bundle_id=bundle.pk, type="cluster") + cluster_response: Response = self.client.post( + path=reverse("cluster"), + data={"name": "test-cluster", "prototype_id": cluster_prototype.pk}, + ) + cluster = Cluster.objects.get(pk=cluster_response.data["id"]) + + self.client.post( + path=reverse("provider"), + data={"name": "test_provider", "prototype_id": self.provider_prototype.pk}, + ) + host_response: Response = self.client.post( + path=reverse("host", kwargs={"provider_id": self.host_provider.pk}), + data={"fqdn": "test-host"}, + ) + host = Host.objects.get(pk=host_response.data["id"]) + + self.client.post( + path=reverse("host", kwargs={"cluster_id": cluster.pk}), + data={"host_id": host.pk}, + ) + + with patch("api.utils.start_task") as start_task_mock: + response: Response = self.client.post( + path=reverse("host-maintenance-mode", kwargs={"host_id": host.pk}), + data={"maintenance_mode": MaintenanceMode.ON}, + ) + + host.refresh_from_db() + + self.assertEqual(response.status_code, HTTP_200_OK) + self.assertEqual(response.data["maintenance_mode"], MaintenanceMode.CHANGING) + self.assertEqual(host.maintenance_mode, MaintenanceMode.CHANGING) + start_task_mock.assert_called_once_with( + action=action, obj=host, conf={}, attr={}, hc=[], hosts=[], verbose=False + ) diff --git a/python/api/tests/test_host_prototype.py b/python/api/tests/test_host_prototype.py new file mode 100644 index 0000000000..f5704423ac --- /dev/null +++ b/python/api/tests/test_host_prototype.py @@ -0,0 +1,101 @@ +# 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 django.urls import reverse +from rest_framework.response import Response + +from adcm.tests.base import BaseTestCase +from cm.models import Action, ActionType, Bundle, Prototype + + +class TestComponentPrototypeAPI(BaseTestCase): + def setUp(self) -> None: + super().setUp() + + self.bundle_1 = Bundle.objects.create(name="test_bundle_1") + self.bundle_2 = Bundle.objects.create(name="test_bundle_2") + + self.prototype_1 = Prototype.objects.create( + bundle=self.bundle_1, + type="host", + name="test_prototype_1", + display_name="test_prototype_1", + version_order=1, + ) + self.prototype_2 = Prototype.objects.create( + bundle=self.bundle_2, + type="host", + name="test_prototype_2", + display_name="test_prototype_2", + version_order=2, + ) + self.action = Action.objects.create( + display_name="test_adcm_action", + prototype=self.prototype_1, + type=ActionType.Job, + state_available="any", + ) + + def test_list(self): + response: Response = self.client.get(path=reverse("host-prototype-list")) + + self.assertEqual(len(response.data["results"]), 2) + + def test_list_filter_name(self): + response: Response = self.client.get(reverse("host-prototype-list"), {"name": "test_prototype_2"}) + + self.assertEqual(len(response.data["results"]), 1) + self.assertEqual(response.data["results"][0]["id"], self.prototype_2.pk) + + def test_list_filter_bundle_id(self): + response: Response = self.client.get( + reverse("host-prototype-list"), + {"bundle_id": self.bundle_1.pk}, + ) + + self.assertEqual(len(response.data["results"]), 1) + self.assertEqual(response.data["results"][0]["id"], self.prototype_1.pk) + + def test_list_ordering_display_name(self): + response: Response = self.client.get(reverse("host-prototype-list"), {"ordering": "display_name"}) + + self.assertEqual(len(response.data["results"]), 2) + self.assertEqual(response.data["results"][0]["id"], self.prototype_1.pk) + self.assertEqual(response.data["results"][1]["id"], self.prototype_2.pk) + + def test_list_ordering_display_name_reverse(self): + response: Response = self.client.get(reverse("host-prototype-list"), {"ordering": "-display_name"}) + + self.assertEqual(len(response.data["results"]), 2) + self.assertEqual(response.data["results"][0]["id"], self.prototype_2.pk) + self.assertEqual(response.data["results"][1]["id"], self.prototype_1.pk) + + def test_list_ordering_version_order(self): + response: Response = self.client.get(reverse("host-prototype-list"), {"ordering": "version_order"}) + + self.assertEqual(len(response.data["results"]), 2) + self.assertEqual(response.data["results"][0]["id"], self.prototype_1.pk) + self.assertEqual(response.data["results"][1]["id"], self.prototype_2.pk) + + def test_list_ordering_version_order_reverse(self): + response: Response = self.client.get(reverse("host-prototype-list"), {"ordering": "-version_order"}) + + self.assertEqual(len(response.data["results"]), 2) + self.assertEqual(response.data["results"][0]["id"], self.prototype_2.pk) + self.assertEqual(response.data["results"][1]["id"], self.prototype_1.pk) + + def test_retrieve(self): + response: Response = self.client.get( + reverse("host-prototype-detail", kwargs={"prototype_pk": self.prototype_2.pk}), + ) + + self.assertEqual(response.data["id"], self.prototype_2.pk) diff --git a/python/api/tests/test_job.py b/python/api/tests/test_job.py new file mode 100644 index 0000000000..87e70e2b4c --- /dev/null +++ b/python/api/tests/test_job.py @@ -0,0 +1,236 @@ +# 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 pathlib import Path +from unittest.mock import patch +from zoneinfo import ZoneInfo + +from django.conf import settings +from django.contrib.contenttypes.models import ContentType +from django.urls import reverse +from rest_framework.response import Response +from rest_framework.status import HTTP_201_CREATED + +from adcm.tests.base import BaseTestCase +from cm.models import ( + ADCM, + Action, + ActionType, + Bundle, + Cluster, + JobLog, + Prototype, + TaskLog, +) +from rbac.models import Policy, Role +from rbac.upgrade.role import init_roles + + +class TestJobAPI(BaseTestCase): + def setUp(self) -> None: + super().setUp() + + bundle = Bundle.objects.create() + self.adcm_prototype = Prototype.objects.create(bundle=bundle, type="adcm") + self.adcm = ADCM.objects.create( + prototype=self.adcm_prototype, + name="ADCM", + ) + self.action = Action.objects.create( + display_name="test_adcm_action", + prototype=self.adcm_prototype, + type=ActionType.Job, + state_available="any", + ) + self.task = TaskLog.objects.create( + object_id=self.adcm.pk, + object_type=ContentType.objects.get(app_label="cm", model="adcm"), + start_date=datetime.now(tz=ZoneInfo("UTC")), + finish_date=datetime.now(tz=ZoneInfo("UTC")), + action=self.action, + ) + self.job_1 = JobLog.objects.create( + status="created", + start_date=datetime.now(tz=ZoneInfo("UTC")), + finish_date=datetime.now(tz=ZoneInfo("UTC")) + timedelta(days=1), + ) + self.job_2 = JobLog.objects.create( + status="failed", + start_date=datetime.now(tz=ZoneInfo("UTC")) + timedelta(days=1), + finish_date=datetime.now(tz=ZoneInfo("UTC")) + timedelta(days=2), + action=self.action, + task=self.task, + pid=self.job_1.pid + 1, + ) + + def test_list(self): + response: Response = self.client.get(path=reverse("joblog-list")) + + self.assertEqual(len(response.data["results"]), 2) + + def test_list_filter_action_id(self): + response: Response = self.client.get(reverse("joblog-list"), {"action_id": self.action.pk}) + + self.assertEqual(len(response.data["results"]), 1) + self.assertEqual(response.data["results"][0]["id"], self.job_2.pk) + + def test_list_filter_task_id(self): + response: Response = self.client.get(reverse("joblog-list"), {"task_id": self.task.pk}) + + self.assertEqual(len(response.data["results"]), 1) + self.assertEqual(response.data["results"][0]["id"], self.job_2.pk) + + def test_list_filter_pid(self): + response: Response = self.client.get(reverse("joblog-list"), {"pid": self.job_1.pid}) + + self.assertEqual(len(response.data["results"]), 1) + self.assertEqual(response.data["results"][0]["pid"], self.job_1.pid) + + def test_list_filter_status(self): + response: Response = self.client.get(reverse("joblog-list"), {"status": self.job_1.status}) + + self.assertEqual(len(response.data["results"]), 1) + self.assertEqual(response.data["results"][0]["status"], self.job_1.status) + + def test_list_filter_start_date(self): + response: Response = self.client.get( + reverse("joblog-list"), + {"start_date": self.job_1.start_date.isoformat()}, + ) + + self.assertEqual(len(response.data["results"]), 1) + self.assertEqual(response.data["results"][0]["id"], self.job_1.pk) + + def test_list_filter_finish_date(self): + response: Response = self.client.get( + reverse("joblog-list"), + {"finish_date": self.job_2.finish_date.isoformat()}, + ) + + self.assertEqual(len(response.data["results"]), 1) + self.assertEqual(response.data["results"][0]["id"], self.job_2.pk) + + def test_list_ordering_status(self): + response: Response = self.client.get(reverse("joblog-list"), {"ordering": "status"}) + + self.assertEqual(len(response.data["results"]), 2) + self.assertEqual(response.data["results"][0]["id"], self.job_1.pk) + self.assertEqual(response.data["results"][1]["id"], self.job_2.pk) + + def test_list_ordering_status_reverse(self): + response: Response = self.client.get(reverse("joblog-list"), {"ordering": "-status"}) + + self.assertEqual(len(response.data["results"]), 2) + self.assertEqual(response.data["results"][0]["id"], self.job_2.pk) + self.assertEqual(response.data["results"][1]["id"], self.job_1.pk) + + def test_list_ordering_start_date(self): + response: Response = self.client.get(reverse("joblog-list"), {"ordering": "start_date"}) + + self.assertEqual(len(response.data["results"]), 2) + self.assertEqual(response.data["results"][0]["id"], self.job_1.pk) + self.assertEqual(response.data["results"][1]["id"], self.job_2.pk) + + def test_list_ordering_start_date_reverse(self): + response: Response = self.client.get(reverse("joblog-list"), {"ordering": "-start_date"}) + + self.assertEqual(len(response.data["results"]), 2) + self.assertEqual(response.data["results"][0]["id"], self.job_2.pk) + self.assertEqual(response.data["results"][1]["id"], self.job_1.pk) + + def test_list_ordering_finish_date(self): + response: Response = self.client.get(reverse("joblog-list"), {"ordering": "finish_date"}) + + self.assertEqual(len(response.data["results"]), 2) + self.assertEqual(response.data["results"][0]["id"], self.job_1.pk) + self.assertEqual(response.data["results"][1]["id"], self.job_2.pk) + + def test_list_ordering_finish_date_reverse(self): + response: Response = self.client.get(reverse("joblog-list"), {"ordering": "-finish_date"}) + + self.assertEqual(len(response.data["results"]), 2) + self.assertEqual(response.data["results"][0]["id"], self.job_2.pk) + self.assertEqual(response.data["results"][1]["id"], self.job_1.pk) + + def test_retrieve(self): + response: Response = self.client.get( + reverse("joblog-detail", kwargs={"job_pk": self.job_2.pk}), + ) + + self.assertEqual(response.data["id"], self.job_2.pk) + + def test_log_files(self): + bundle = self.upload_and_load_bundle( + path=Path( + settings.BASE_DIR, + "python/api/tests/files/no-log-files.tar", + ), + ) + + action = Action.objects.get(name="adcm_check") + cluster_prototype = Prototype.objects.get(bundle=bundle, type="cluster") + cluster = Cluster.objects.create(name="test_cluster", prototype=cluster_prototype) + + with patch("cm.job.run_task"): + response: Response = self.client.post( + path=reverse("run-task", kwargs={"cluster_id": cluster.pk, "action_id": action.pk}) + ) + + self.assertEqual(response.status_code, HTTP_201_CREATED) + + job = JobLog.objects.get(action=action) + + response: Response = self.client.get( + reverse("joblog-detail", kwargs={"job_pk": job.pk}), + ) + + self.assertEqual(len(response.data["log_files"]), 2) + + def test_task_permissions(self): + bundle = self.upload_and_load_bundle( + path=Path( + settings.BASE_DIR, + "python/api/tests/files/no-log-files.tar", + ), + ) + + action = Action.objects.get(name="adcm_check") + cluster_prototype = Prototype.objects.get(bundle=bundle, type="cluster") + cluster = Cluster.objects.create(name="test_cluster", prototype=cluster_prototype) + + init_roles() + role = Role.objects.get(name="Cluster Administrator") + policy = Policy.objects.create(name="test_policy", role=role) + policy.user.add(self.no_rights_user) + policy.add_object(cluster) + policy.apply() + + with self.no_rights_user_logged_in: + with patch("cm.job.run_task"): + response: Response = self.client.post( + path=reverse("run-task", kwargs={"cluster_id": cluster.pk, "action_id": action.pk}) + ) + + response: Response = self.client.get(reverse("joblog-list")) + + self.assertIn( + JobLog.objects.get(action=action).pk, + {job_data["id"] for job_data in response.data["results"]}, + ) + + response: Response = self.client.get(reverse("tasklog-list")) + + self.assertIn( + TaskLog.objects.get(action=action).pk, + {job_data["id"] for job_data in response.data["results"]}, + ) diff --git a/python/api/tests/test_log_storage.py b/python/api/tests/test_log_storage.py new file mode 100644 index 0000000000..bdec8e2634 --- /dev/null +++ b/python/api/tests/test_log_storage.py @@ -0,0 +1,132 @@ +# 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 zoneinfo import ZoneInfo + +from django.urls import reverse +from rest_framework.response import Response +from rest_framework.status import HTTP_200_OK + +from adcm.tests.base import BaseTestCase +from cm.models import JobLog, LogStorage + + +class TestTaskAPI(BaseTestCase): + def setUp(self) -> None: + super().setUp() + + self.job = JobLog.objects.create( + status="created", + start_date=datetime.now(tz=ZoneInfo("UTC")), + finish_date=datetime.now(tz=ZoneInfo("UTC")) + timedelta(days=1), + ) + self.log_storage_1 = LogStorage.objects.create( + name="log_storage_1", + job=self.job, + type="custom", + format="txt", + ) + self.log_storage_2 = LogStorage.objects.create( + name="log_storage_2", + job=self.job, + type="check", + format="json", + ) + + def test_list(self): + response: Response = self.client.get( + path=reverse("joblog-list", kwargs={"job_pk": self.job.pk}), + ) + + self.assertEqual(len(response.data["results"]), 2) + + def test_list_filter_name(self): + response: Response = self.client.get( + reverse("joblog-list", kwargs={"job_pk": self.job.pk}), + {"name": self.log_storage_1.name}, + ) + + self.assertEqual(len(response.data["results"]), 1) + self.assertEqual(response.data["results"][0]["id"], self.log_storage_1.pk) + + def test_list_filter_type(self): + response: Response = self.client.get( + reverse("joblog-list", kwargs={"job_pk": self.job.pk}), + {"type": self.log_storage_2.type}, + ) + + self.assertEqual(len(response.data["results"]), 1) + self.assertEqual(response.data["results"][0]["id"], self.log_storage_2.pk) + + def test_list_filter_format(self): + response: Response = self.client.get( + reverse("joblog-list", kwargs={"job_pk": self.job.pk}), + {"format": self.log_storage_2.format}, + ) + + self.assertEqual(len(response.data["results"]), 1) + self.assertEqual(response.data["results"][0]["id"], self.log_storage_2.pk) + + def test_list_ordering_id(self): + response: Response = self.client.get( + reverse("joblog-list", kwargs={"job_pk": self.job.pk}), + {"ordering": "id"}, + ) + + self.assertEqual(len(response.data["results"]), 2) + self.assertEqual(response.data["results"][0]["id"], self.log_storage_1.pk) + self.assertEqual(response.data["results"][1]["id"], self.log_storage_2.pk) + + def test_list_ordering_id_reverse(self): + response: Response = self.client.get( + reverse("joblog-list", kwargs={"job_pk": self.job.pk}), + {"ordering": "-id"}, + ) + + self.assertEqual(len(response.data["results"]), 2) + self.assertEqual(response.data["results"][0]["id"], self.log_storage_2.pk) + self.assertEqual(response.data["results"][1]["id"], self.log_storage_1.pk) + + def test_list_ordering_name(self): + response: Response = self.client.get( + reverse("joblog-list", kwargs={"job_pk": self.job.pk}), + {"ordering": "name"}, + ) + + self.assertEqual(len(response.data["results"]), 2) + self.assertEqual(response.data["results"][0]["id"], self.log_storage_1.pk) + self.assertEqual(response.data["results"][1]["id"], self.log_storage_2.pk) + + def test_list_ordering_name_reverse(self): + response: Response = self.client.get( + reverse("joblog-list", kwargs={"job_pk": self.job.pk}), + {"ordering": "-name"}, + ) + + self.assertEqual(len(response.data["results"]), 2) + self.assertEqual(response.data["results"][0]["id"], self.log_storage_2.pk) + self.assertEqual(response.data["results"][1]["id"], self.log_storage_1.pk) + + def test_retrieve(self): + response: Response = self.client.get( + reverse("joblog-detail", kwargs={"job_pk": self.job.pk, "log_pk": self.log_storage_1.pk}), + ) + + self.assertEqual(response.data["id"], self.log_storage_1.pk) + + def test_download(self): + response: Response = self.client.get( + reverse("joblog-download", kwargs={"job_pk": self.job.pk, "log_pk": self.log_storage_1.pk}), + ) + + self.assertEqual(response.status_code, HTTP_200_OK) diff --git a/python/api/tests/test_prototype.py b/python/api/tests/test_prototype.py new file mode 100644 index 0000000000..97100aa737 --- /dev/null +++ b/python/api/tests/test_prototype.py @@ -0,0 +1,94 @@ +# 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 django.urls import reverse +from rest_framework.response import Response + +from adcm.tests.base import BaseTestCase +from cm.models import Bundle, Prototype + + +class TestPrototypeAPI(BaseTestCase): + def setUp(self) -> None: + super().setUp() + + self.bundle_1 = Bundle.objects.create(name="test_bundle_1") + self.bundle_2 = Bundle.objects.create(name="test_bundle_2") + self.prototype_1 = Prototype.objects.create( + bundle=self.bundle_1, + type="adcm", + name="test_prototype_1", + display_name="test_prototype_1", + version_order=1, + ) + self.prototype_2 = Prototype.objects.create( + bundle=self.bundle_2, + type="cluster", + name="test_prototype_2", + display_name="test_prototype_2", + version_order=2, + ) + + def test_list(self): + response: Response = self.client.get(path=reverse("prototype-list")) + + self.assertEqual(len(response.data["results"]), 2) + + def test_list_filter_name(self): + response: Response = self.client.get(reverse("prototype-list"), {"name": "test_prototype_2"}) + + self.assertEqual(len(response.data["results"]), 1) + self.assertEqual(response.data["results"][0]["id"], self.prototype_2.pk) + + def test_list_filter_bundle_id(self): + response: Response = self.client.get( + reverse("prototype-list"), + {"bundle_id": self.bundle_1.pk}, + ) + + self.assertEqual(len(response.data["results"]), 1) + self.assertEqual(response.data["results"][0]["id"], self.prototype_1.pk) + + def test_list_ordering_display_name(self): + response: Response = self.client.get(reverse("prototype-list"), {"ordering": "display_name"}) + + self.assertEqual(len(response.data["results"]), 2) + self.assertEqual(response.data["results"][0]["id"], self.prototype_1.pk) + self.assertEqual(response.data["results"][1]["id"], self.prototype_2.pk) + + def test_list_ordering_display_name_reverse(self): + response: Response = self.client.get(reverse("prototype-list"), {"ordering": "-display_name"}) + + self.assertEqual(len(response.data["results"]), 2) + self.assertEqual(response.data["results"][0]["id"], self.prototype_2.pk) + self.assertEqual(response.data["results"][1]["id"], self.prototype_1.pk) + + def test_list_ordering_version_order(self): + response: Response = self.client.get(reverse("prototype-list"), {"ordering": "version_order"}) + + self.assertEqual(len(response.data["results"]), 2) + self.assertEqual(response.data["results"][0]["id"], self.prototype_1.pk) + self.assertEqual(response.data["results"][1]["id"], self.prototype_2.pk) + + def test_list_ordering_version_order_reverse(self): + response: Response = self.client.get(reverse("prototype-list"), {"ordering": "-version_order"}) + + self.assertEqual(len(response.data["results"]), 2) + self.assertEqual(response.data["results"][0]["id"], self.prototype_2.pk) + self.assertEqual(response.data["results"][1]["id"], self.prototype_1.pk) + + def test_retrieve(self): + response: Response = self.client.get( + reverse("prototype-detail", kwargs={"prototype_pk": self.prototype_2.pk}), + ) + + self.assertEqual(response.data["id"], self.prototype_2.pk) diff --git a/python/api/tests/test_provider_prototype.py b/python/api/tests/test_provider_prototype.py new file mode 100644 index 0000000000..e090f0ed70 --- /dev/null +++ b/python/api/tests/test_provider_prototype.py @@ -0,0 +1,101 @@ +# 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 django.urls import reverse +from rest_framework.response import Response + +from adcm.tests.base import BaseTestCase +from cm.models import Action, ActionType, Bundle, Prototype + + +class TestProviderPrototypeAPI(BaseTestCase): + def setUp(self) -> None: + super().setUp() + + self.bundle_1 = Bundle.objects.create(name="test_bundle_1") + self.bundle_2 = Bundle.objects.create(name="test_bundle_2") + + self.prototype_1 = Prototype.objects.create( + bundle=self.bundle_1, + type="provider", + name="test_prototype_1", + display_name="test_prototype_1", + version_order=1, + ) + self.prototype_2 = Prototype.objects.create( + bundle=self.bundle_2, + type="provider", + name="test_prototype_2", + display_name="test_prototype_2", + version_order=2, + ) + self.action = Action.objects.create( + display_name="test_adcm_action", + prototype=self.prototype_1, + type=ActionType.Job, + state_available="any", + ) + + def test_list(self): + response: Response = self.client.get(path=reverse("provider-prototype-list")) + + self.assertEqual(len(response.data["results"]), 2) + + def test_list_filter_name(self): + response: Response = self.client.get(reverse("provider-prototype-list"), {"name": "test_prototype_2"}) + + self.assertEqual(len(response.data["results"]), 1) + self.assertEqual(response.data["results"][0]["id"], self.prototype_2.pk) + + def test_list_filter_bundle_id(self): + response: Response = self.client.get( + reverse("provider-prototype-list"), + {"bundle_id": self.bundle_1.pk}, + ) + + self.assertEqual(len(response.data["results"]), 1) + self.assertEqual(response.data["results"][0]["id"], self.prototype_1.pk) + + def test_list_ordering_display_name(self): + response: Response = self.client.get(reverse("provider-prototype-list"), {"ordering": "display_name"}) + + self.assertEqual(len(response.data["results"]), 2) + self.assertEqual(response.data["results"][0]["id"], self.prototype_1.pk) + self.assertEqual(response.data["results"][1]["id"], self.prototype_2.pk) + + def test_list_ordering_display_name_reverse(self): + response: Response = self.client.get(reverse("provider-prototype-list"), {"ordering": "-display_name"}) + + self.assertEqual(len(response.data["results"]), 2) + self.assertEqual(response.data["results"][0]["id"], self.prototype_2.pk) + self.assertEqual(response.data["results"][1]["id"], self.prototype_1.pk) + + def test_list_ordering_version_order(self): + response: Response = self.client.get(reverse("provider-prototype-list"), {"ordering": "version_order"}) + + self.assertEqual(len(response.data["results"]), 2) + self.assertEqual(response.data["results"][0]["id"], self.prototype_1.pk) + self.assertEqual(response.data["results"][1]["id"], self.prototype_2.pk) + + def test_list_ordering_version_order_reverse(self): + response: Response = self.client.get(reverse("provider-prototype-list"), {"ordering": "-version_order"}) + + self.assertEqual(len(response.data["results"]), 2) + self.assertEqual(response.data["results"][0]["id"], self.prototype_2.pk) + self.assertEqual(response.data["results"][1]["id"], self.prototype_1.pk) + + def test_retrieve(self): + response: Response = self.client.get( + reverse("provider-prototype-detail", kwargs={"prototype_pk": self.prototype_2.pk}), + ) + + self.assertEqual(response.data["id"], self.prototype_2.pk) diff --git a/python/api/tests/test_service.py b/python/api/tests/test_service.py new file mode 100644 index 0000000000..a1a4fed461 --- /dev/null +++ b/python/api/tests/test_service.py @@ -0,0 +1,414 @@ +# 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 pathlib import Path +from unittest.mock import patch + +from django.conf import settings +from django.urls import reverse +from rest_framework.response import Response +from rest_framework.status import ( + HTTP_200_OK, + HTTP_204_NO_CONTENT, + HTTP_400_BAD_REQUEST, + HTTP_409_CONFLICT, +) + +from adcm.tests.base import APPLICATION_JSON, BaseTestCase +from cm.models import ( + Action, + Bundle, + Cluster, + ClusterBind, + ClusterObject, + Host, + HostComponent, + HostProvider, + MaintenanceMode, + Prototype, + ServiceComponent, +) + + +class TestServiceAPI(BaseTestCase): + def setUp(self) -> None: + super().setUp() + + self.bundle = Bundle.objects.create() + self.cluster_prototype = Prototype.objects.create(bundle=self.bundle, type="cluster") + self.cluster = Cluster.objects.create(prototype=self.cluster_prototype, name="test_cluster") + self.service_prototype = Prototype.objects.create( + bundle=self.bundle, + type="service", + display_name="test_service", + ) + self.service = ClusterObject.objects.create(prototype=self.service_prototype, cluster=self.cluster) + self.component = ServiceComponent.objects.create( + prototype=Prototype.objects.create( + bundle=self.bundle, + type="component", + display_name="test_component", + ), + cluster=self.cluster, + service=self.service, + ) + + def get_host(self, bundle_path: str): + provider_bundle = self.upload_and_load_bundle( + path=Path(settings.BASE_DIR, bundle_path), + ) + provider_prototype = Prototype.objects.get(bundle=provider_bundle, type="provider") + provider_response: Response = self.client.post( + path=reverse("provider"), + data={"name": "test_provider", "prototype_id": provider_prototype.pk}, + ) + provider = HostProvider.objects.get(pk=provider_response.data["id"]) + + host_response: Response = self.client.post( + path=reverse("host", kwargs={"provider_id": provider.pk}), + data={"fqdn": "test-host"}, + ) + + return Host.objects.get(pk=host_response.data["id"]) + + def get_cluster(self, bundle_path: str): + cluster_bundle = self.upload_and_load_bundle(path=Path(settings.BASE_DIR, bundle_path)) + cluster_prototype = Prototype.objects.get(bundle_id=cluster_bundle.pk, type="cluster") + cluster_response: Response = self.client.post( + path=reverse("cluster"), + data={"name": "test-cluster", "prototype_id": cluster_prototype.pk}, + ) + + return Cluster.objects.get(pk=cluster_response.data["id"]) + + def test_change_maintenance_mode_wrong_name_fail(self): + response: Response = self.client.post( + path=reverse("service-maintenance-mode", kwargs={"service_id": self.service.pk}), + data={"maintenance_mode": "wrong"}, + ) + + self.assertEqual(response.status_code, HTTP_400_BAD_REQUEST) + self.assertIn("maintenance_mode", response.data) + + def test_change_maintenance_mode_on_no_action_success(self): + response: Response = self.client.post( + path=reverse("service-maintenance-mode", kwargs={"service_id": self.service.pk}), + data={"maintenance_mode": MaintenanceMode.ON}, + ) + + self.service.refresh_from_db() + + self.assertEqual(response.status_code, HTTP_200_OK) + self.assertEqual(response.data["maintenance_mode"], MaintenanceMode.ON) + self.assertEqual(self.service.maintenance_mode, MaintenanceMode.ON) + + def test_change_maintenance_mode_on_with_action_success(self): + HostComponent.objects.create( + cluster=self.cluster, + host=self.get_host(bundle_path="python/api/tests/files/bundle_test_provider.tar"), + service=self.service, + component=self.component, + ) + action = Action.objects.create(prototype=self.service.prototype, name=settings.ADCM_TURN_ON_MM_ACTION_NAME) + + with patch("api.utils.start_task") as start_task_mock: + response: Response = self.client.post( + path=reverse("service-maintenance-mode", kwargs={"service_id": self.service.pk}), + data={"maintenance_mode": MaintenanceMode.ON}, + ) + + self.service.refresh_from_db() + + self.assertEqual(response.status_code, HTTP_200_OK) + self.assertEqual(response.data["maintenance_mode"], MaintenanceMode.CHANGING) + self.assertEqual(self.service.maintenance_mode, MaintenanceMode.CHANGING) + start_task_mock.assert_called_once_with( + action=action, obj=self.service, conf={}, attr={}, hc=[], hosts=[], verbose=False + ) + + def test_change_maintenance_mode_on_from_on_with_action_fail(self): + self.service.maintenance_mode = MaintenanceMode.ON + self.service.save() + + with patch("api.utils.start_task") as start_task_mock: + response: Response = self.client.post( + path=reverse("service-maintenance-mode", kwargs={"service_id": self.service.pk}), + data={"maintenance_mode": MaintenanceMode.ON}, + ) + + self.service.refresh_from_db() + + self.assertEqual(response.status_code, HTTP_409_CONFLICT) + self.assertEqual(self.service.maintenance_mode, MaintenanceMode.ON) + start_task_mock.assert_not_called() + + def test_change_maintenance_mode_off_no_action_success(self): + self.service.maintenance_mode = MaintenanceMode.ON + self.service.save() + + response: Response = self.client.post( + path=reverse("service-maintenance-mode", kwargs={"service_id": self.service.pk}), + data={"maintenance_mode": MaintenanceMode.OFF}, + ) + + self.service.refresh_from_db() + + self.assertEqual(response.status_code, HTTP_200_OK) + self.assertEqual(response.data["maintenance_mode"], MaintenanceMode.OFF) + self.assertEqual(self.service.maintenance_mode, MaintenanceMode.OFF) + + def test_change_maintenance_mode_off_with_action_success(self): + self.service.maintenance_mode = MaintenanceMode.ON + self.service.save() + HostComponent.objects.create( + cluster=self.cluster, + host=self.get_host(bundle_path="python/api/tests/files/bundle_test_provider.tar"), + service=self.service, + component=self.component, + ) + action = Action.objects.create(prototype=self.service.prototype, name=settings.ADCM_TURN_OFF_MM_ACTION_NAME) + + with patch("api.utils.start_task") as start_task_mock: + response: Response = self.client.post( + path=reverse("service-maintenance-mode", kwargs={"service_id": self.service.pk}), + data={"maintenance_mode": MaintenanceMode.OFF}, + ) + + self.service.refresh_from_db() + + self.assertEqual(response.status_code, HTTP_200_OK) + self.assertEqual(response.data["maintenance_mode"], MaintenanceMode.CHANGING) + self.assertEqual(self.service.maintenance_mode, MaintenanceMode.CHANGING) + start_task_mock.assert_called_once_with( + action=action, obj=self.service, conf={}, attr={}, hc=[], hosts=[], verbose=False + ) + + def test_change_maintenance_mode_off_to_off_with_action_fail(self): + self.service.maintenance_mode = MaintenanceMode.OFF + self.service.save() + + with patch("api.utils.start_task") as start_task_mock: + response: Response = self.client.post( + path=reverse("service-maintenance-mode", kwargs={"service_id": self.service.pk}), + data={"maintenance_mode": MaintenanceMode.OFF}, + ) + + self.service.refresh_from_db() + + self.assertEqual(response.status_code, HTTP_409_CONFLICT) + self.assertEqual(self.service.maintenance_mode, MaintenanceMode.OFF) + start_task_mock.assert_not_called() + + def test_change_maintenance_mode_changing_now_fail(self): + self.service.maintenance_mode = MaintenanceMode.CHANGING + self.service.save() + + response: Response = self.client.post( + path=reverse("service-maintenance-mode", kwargs={"service_id": self.service.pk}), + data={"maintenance_mode": MaintenanceMode.ON}, + ) + + self.assertEqual(response.status_code, HTTP_409_CONFLICT) + + response: Response = self.client.post( + path=reverse("service-maintenance-mode", kwargs={"service_id": self.service.pk}), + data={"maintenance_mode": MaintenanceMode.OFF}, + ) + + self.assertEqual(response.status_code, HTTP_409_CONFLICT) + + def test_delete_without_action(self): + response: Response = self.client.delete(path=reverse("service-details", kwargs={"service_id": self.service.pk})) + + self.assertEqual(response.status_code, HTTP_204_NO_CONTENT) + + def test_delete_with_action(self): + action = Action.objects.create(prototype=self.service.prototype, name=settings.ADCM_DELETE_SERVICE_ACTION_NAME) + + with patch("api.service.views.delete_service"), patch("api.service.views.start_task") as start_task_mock: + response: Response = self.client.delete( + path=reverse("service-details", kwargs={"service_id": self.service.pk}) + ) + + self.assertEqual(response.status_code, HTTP_204_NO_CONTENT) + start_task_mock.assert_not_called() + + host = Host.objects.create( + fqdn="test-fqdn", + prototype=Prototype.objects.create(bundle=self.bundle, type="host"), + provider=HostProvider.objects.create( + name="test_provider", + prototype=Prototype.objects.create(bundle=self.bundle, type="provider"), + ), + ) + service_component = ServiceComponent.objects.create( + prototype=Prototype.objects.create( + bundle=self.bundle, + type="component", + ), + cluster=self.cluster, + service=self.service, + ) + HostComponent.objects.create( + cluster=self.cluster, + host=host, + service=self.service, + component=service_component, + ) + + with patch("api.service.views.delete_service"), patch("api.service.views.start_task") as start_task_mock: + response: Response = self.client.delete( + path=reverse("service-details", kwargs={"service_id": self.service.pk}) + ) + + self.assertEqual(response.status_code, HTTP_204_NO_CONTENT) + start_task_mock.assert_called_once_with( + action=action, obj=self.service, conf={}, attr={}, hc=[], hosts=[], verbose=False + ) + + def test_delete_with_action_not_created_state(self): + action = Action.objects.create(prototype=self.service.prototype, name=settings.ADCM_DELETE_SERVICE_ACTION_NAME) + self.service.state = "not created" + self.service.save(update_fields=["state"]) + + with patch("api.service.views.delete_service"), patch("api.service.views.start_task") as start_task_mock: + response: Response = self.client.delete( + path=reverse("service-details", kwargs={"service_id": self.service.pk}) + ) + + self.assertEqual(response.status_code, HTTP_204_NO_CONTENT) + start_task_mock.assert_called_once_with( + action=action, obj=self.service, conf={}, attr={}, hc=[], hosts=[], verbose=False + ) + + def test_delete_service_with_requires_fail(self): + host = self.get_host(bundle_path="python/api/tests/files/bundle_test_provider_concern.tar") + cluster = self.get_cluster(bundle_path="python/api/tests/files/bundle_cluster_requires.tar") + self.client.post( + path=reverse("host", kwargs={"cluster_id": cluster.pk}), + data={"host_id": host.pk}, + ) + + service_1_prototype = Prototype.objects.get(name="service_1", type="service") + service_1_response: Response = self.client.post( + path=reverse("service", kwargs={"cluster_id": cluster.pk}), + data={"prototype_id": service_1_prototype.pk}, + ) + service_1 = ClusterObject.objects.get(pk=service_1_response.data["id"]) + + service_2_prototype = Prototype.objects.get(name="service_2", type="service") + service_2_response: Response = self.client.post( + path=reverse("service", kwargs={"cluster_id": cluster.pk}), + data={"prototype_id": service_2_prototype.pk}, + ) + service_2 = ClusterObject.objects.get(pk=service_2_response.data["id"]) + + component_2_1 = ServiceComponent.objects.get(service=service_2, prototype__name="component_1") + component_1_1 = ServiceComponent.objects.get(service=service_1, prototype__name="component_1") + + self.client.post( + path=reverse("host-component", kwargs={"cluster_id": cluster.pk}), + data={ + "hc": [ + {"service_id": service_2.pk, "component_id": component_2_1.pk, "host_id": host.pk}, + {"service_id": service_1.pk, "component_id": component_1_1.pk, "host_id": host.pk}, + ] + }, + content_type=APPLICATION_JSON, + ) + + response: Response = self.client.delete(path=reverse("service-details", kwargs={"service_id": service_1.pk})) + + self.assertEqual(response.status_code, HTTP_409_CONFLICT) + + def test_delete_required_fail(self): + self.service.prototype.required = True + self.service.prototype.save(update_fields=["required"]) + + with patch("api.service.views.delete_service"): + response: Response = self.client.delete( + path=reverse("service-details", kwargs={"service_id": self.service.pk}) + ) + + self.assertEqual(response.status_code, HTTP_409_CONFLICT) + + def test_delete_bind_fail(self): + cluster_2 = Cluster.objects.create(prototype=self.cluster_prototype, name="test_cluster_2") + service_2 = ClusterObject.objects.create(prototype=self.service_prototype, cluster=cluster_2) + ClusterBind.objects.create( + cluster=self.cluster, service=self.service, source_cluster=cluster_2, source_service=service_2 + ) + + with patch("api.service.views.delete_service"): + response: Response = self.client.delete( + path=reverse("service-details", kwargs={"service_id": self.service.pk}) + ) + + self.assertEqual(response.status_code, HTTP_409_CONFLICT) + + def test_delete_with_dependent_component_fail(self): + host = self.get_host(bundle_path="python/api/tests/files/bundle_test_provider.tar") + cluster = self.get_cluster(bundle_path="python/api/tests/files/with_action_dependent_component.tar") + self.client.post( + path=reverse("host", kwargs={"cluster_id": cluster.pk}), + data={"host_id": host.pk}, + ) + + service_with_component_prototype = Prototype.objects.get(name="with_component", type="service") + service_with_component_response: Response = self.client.post( + path=reverse("service", kwargs={"cluster_id": cluster.pk}), + data={"prototype_id": service_with_component_prototype.pk}, + ) + service_with_component = ClusterObject.objects.get(pk=service_with_component_response.data["id"]) + + service_with_dependent_component_prototype = Prototype.objects.get( + name="with_dependent_component", type="service" + ) + service_with_dependent_component_response: Response = self.client.post( + path=reverse("service", kwargs={"cluster_id": cluster.pk}), + data={"prototype_id": service_with_dependent_component_prototype.pk}, + ) + service_with_dependent_component = ClusterObject.objects.get( + pk=service_with_dependent_component_response.data["id"] + ) + + component = ServiceComponent.objects.get(service=service_with_component) + component_with_dependent_component = ServiceComponent.objects.get(service=service_with_dependent_component) + + self.client.post( + path=reverse("host-component", kwargs={"cluster_id": cluster.pk}), + data={ + "hc": [ + {"service_id": service_with_component.pk, "component_id": component.pk, "host_id": host.pk}, + { + "service_id": service_with_dependent_component.pk, + "component_id": component_with_dependent_component.pk, + "host_id": host.pk, + }, + ] + }, + content_type=APPLICATION_JSON, + ) + + response: Response = self.client.delete( + path=reverse("service-details", kwargs={"service_id": service_with_component.pk}) + ) + + self.assertEqual(response.status_code, HTTP_409_CONFLICT) + + HostComponent.objects.all().delete() + + response: Response = self.client.delete( + path=reverse("service-details", kwargs={"service_id": service_with_dependent_component.pk}) + ) + + self.assertEqual(response.status_code, HTTP_204_NO_CONTENT) diff --git a/python/api/tests/test_service_prototype.py b/python/api/tests/test_service_prototype.py new file mode 100644 index 0000000000..4f2ab8c04d --- /dev/null +++ b/python/api/tests/test_service_prototype.py @@ -0,0 +1,108 @@ +# 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 django.urls import reverse +from rest_framework.response import Response + +from adcm.tests.base import BaseTestCase +from cm.models import Action, ActionType, Bundle, Prototype + + +class TestServicePrototypeAPI(BaseTestCase): + def setUp(self) -> None: + super().setUp() + + self.bundle_1 = Bundle.objects.create(name="test_bundle_1") + self.bundle_2 = Bundle.objects.create(name="test_bundle_2") + + self.prototype_1 = Prototype.objects.create( + bundle=self.bundle_1, + type="service", + name="test_prototype_1", + display_name="test_prototype_1", + version_order=1, + ) + self.prototype_2 = Prototype.objects.create( + bundle=self.bundle_2, + type="service", + name="test_prototype_2", + display_name="test_prototype_2", + version_order=2, + ) + self.action = Action.objects.create( + display_name="test_adcm_action", + prototype=self.prototype_1, + type=ActionType.Job, + state_available="any", + ) + + def test_list(self): + response: Response = self.client.get(path=reverse("service-prototype-list")) + + self.assertEqual(len(response.data["results"]), 2) + + def test_list_filter_name(self): + response: Response = self.client.get(reverse("service-prototype-list"), {"name": "test_prototype_2"}) + + self.assertEqual(len(response.data["results"]), 1) + self.assertEqual(response.data["results"][0]["id"], self.prototype_2.pk) + + def test_list_filter_bundle_id(self): + response: Response = self.client.get( + reverse("service-prototype-list"), + {"bundle_id": self.bundle_1.pk}, + ) + + self.assertEqual(len(response.data["results"]), 1) + self.assertEqual(response.data["results"][0]["id"], self.prototype_1.pk) + + def test_list_ordering_display_name(self): + response: Response = self.client.get(reverse("service-prototype-list"), {"ordering": "display_name"}) + + self.assertEqual(len(response.data["results"]), 2) + self.assertEqual(response.data["results"][0]["id"], self.prototype_1.pk) + self.assertEqual(response.data["results"][1]["id"], self.prototype_2.pk) + + def test_list_ordering_display_name_reverse(self): + response: Response = self.client.get(reverse("service-prototype-list"), {"ordering": "-display_name"}) + + self.assertEqual(len(response.data["results"]), 2) + self.assertEqual(response.data["results"][0]["id"], self.prototype_2.pk) + self.assertEqual(response.data["results"][1]["id"], self.prototype_1.pk) + + def test_list_ordering_version_order(self): + response: Response = self.client.get(reverse("service-prototype-list"), {"ordering": "version_order"}) + + self.assertEqual(len(response.data["results"]), 2) + self.assertEqual(response.data["results"][0]["id"], self.prototype_1.pk) + self.assertEqual(response.data["results"][1]["id"], self.prototype_2.pk) + + def test_list_ordering_version_order_reverse(self): + response: Response = self.client.get(reverse("service-prototype-list"), {"ordering": "-version_order"}) + + self.assertEqual(len(response.data["results"]), 2) + self.assertEqual(response.data["results"][0]["id"], self.prototype_2.pk) + self.assertEqual(response.data["results"][1]["id"], self.prototype_1.pk) + + def test_retrieve(self): + response: Response = self.client.get( + reverse("service-prototype-detail", kwargs={"prototype_pk": self.prototype_2.pk}), + ) + + self.assertEqual(response.data["id"], self.prototype_2.pk) + + def test_action(self): + response: Response = self.client.get( + reverse("service-prototype-actions", kwargs={"prototype_pk": self.prototype_1.pk}), + ) + + self.assertEqual(response.data[0]["id"], self.action.pk) diff --git a/python/api/tests/test_task.py b/python/api/tests/test_task.py new file mode 100644 index 0000000000..d90c595d1d --- /dev/null +++ b/python/api/tests/test_task.py @@ -0,0 +1,209 @@ +# 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 unittest.mock import patch +from zoneinfo import ZoneInfo + +from django.contrib.contenttypes.models import ContentType +from django.urls import reverse +from rest_framework.response import Response +from rest_framework.status import HTTP_200_OK, HTTP_201_CREATED + +from adcm.tests.base import BaseTestCase +from cm.models import ( + ADCM, + Action, + ActionType, + Bundle, + Cluster, + JobLog, + Prototype, + TaskLog, +) + + +class TestTaskAPI(BaseTestCase): + def setUp(self) -> None: + super().setUp() + + bundle = Bundle.objects.create() + self.cluster = Cluster.objects.create( + name="test_cluster", + prototype=Prototype.objects.create(bundle=bundle, type="cluster"), + ) + self.adcm_prototype = Prototype.objects.create(bundle=bundle, type="adcm") + self.adcm = ADCM.objects.create( + prototype=self.adcm_prototype, + name="ADCM", + ) + self.action = Action.objects.create( + display_name="test_adcm_action", + prototype=self.adcm_prototype, + type=ActionType.Job, + state_available="any", + ) + adcm_object_type = ContentType.objects.get(app_label="cm", model="adcm") + self.task_1 = TaskLog.objects.create( + object_id=self.adcm.pk, + object_type=adcm_object_type, + start_date=datetime.now(tz=ZoneInfo("UTC")), + finish_date=datetime.now(tz=ZoneInfo("UTC")) + timedelta(days=1), + status="created", + ) + self.task_2 = TaskLog.objects.create( + object_id=self.adcm.pk, + object_type=adcm_object_type, + start_date=datetime.now(tz=ZoneInfo("UTC")) + timedelta(days=1), + finish_date=datetime.now(tz=ZoneInfo("UTC")) + timedelta(days=2), + action=self.action, + status="failed", + pid=self.task_1.pid + 1, + ) + JobLog.objects.create( + status="created", + start_date=datetime.now(tz=ZoneInfo("UTC")), + finish_date=datetime.now(tz=ZoneInfo("UTC")) + timedelta(days=1), + task=self.task_2, + ) + + def test_list(self): + response: Response = self.client.get(path=reverse("tasklog-list")) + + self.assertEqual(len(response.data["results"]), 2) + + def test_list_filter_action_id(self): + response: Response = self.client.get(reverse("tasklog-list"), {"action_id": self.action.pk}) + + self.assertEqual(len(response.data["results"]), 1) + self.assertEqual(response.data["results"][0]["id"], self.task_2.pk) + + def test_list_filter_pid(self): + response: Response = self.client.get(reverse("tasklog-list"), {"pid": self.task_1.pid}) + + self.assertEqual(len(response.data["results"]), 1) + self.assertEqual(response.data["results"][0]["pid"], self.task_1.pid) + + def test_list_filter_status(self): + response: Response = self.client.get( + reverse("tasklog-list"), + {"status": self.task_1.status}, + ) + + self.assertEqual(len(response.data["results"]), 1) + self.assertEqual(response.data["results"][0]["status"], self.task_1.status) + + def test_list_filter_start_date(self): + response: Response = self.client.get( + reverse("tasklog-list"), + {"start_date": self.task_1.start_date.isoformat()}, + ) + + self.assertEqual(len(response.data["results"]), 1) + self.assertEqual(response.data["results"][0]["id"], self.task_1.pk) + + def test_list_filter_finish_date(self): + response: Response = self.client.get( + reverse("tasklog-list"), + {"finish_date": self.task_2.finish_date.isoformat()}, + ) + + self.assertEqual(len(response.data["results"]), 1) + self.assertEqual(response.data["results"][0]["id"], self.task_2.pk) + + def test_list_ordering_status(self): + response: Response = self.client.get(reverse("tasklog-list"), {"ordering": "status"}) + + self.assertEqual(len(response.data["results"]), 2) + self.assertEqual(response.data["results"][0]["id"], self.task_1.pk) + self.assertEqual(response.data["results"][1]["id"], self.task_2.pk) + + def test_list_ordering_status_reverse(self): + response: Response = self.client.get(reverse("tasklog-list"), {"ordering": "-status"}) + + self.assertEqual(len(response.data["results"]), 2) + self.assertEqual(response.data["results"][0]["id"], self.task_2.pk) + self.assertEqual(response.data["results"][1]["id"], self.task_1.pk) + + def test_list_ordering_start_date(self): + response: Response = self.client.get(reverse("tasklog-list"), {"ordering": "start_date"}) + + self.assertEqual(len(response.data["results"]), 2) + self.assertEqual(response.data["results"][0]["id"], self.task_1.pk) + self.assertEqual(response.data["results"][1]["id"], self.task_2.pk) + + def test_list_ordering_start_date_reverse(self): + response: Response = self.client.get(reverse("tasklog-list"), {"ordering": "-start_date"}) + + self.assertEqual(len(response.data["results"]), 2) + self.assertEqual(response.data["results"][0]["id"], self.task_2.pk) + self.assertEqual(response.data["results"][1]["id"], self.task_1.pk) + + def test_list_ordering_finish_date(self): + response: Response = self.client.get(reverse("tasklog-list"), {"ordering": "finish_date"}) + + self.assertEqual(len(response.data["results"]), 2) + self.assertEqual(response.data["results"][0]["id"], self.task_1.pk) + self.assertEqual(response.data["results"][1]["id"], self.task_2.pk) + + def test_list_ordering_finish_date_reverse(self): + response: Response = self.client.get(reverse("tasklog-list"), {"ordering": "-finish_date"}) + + self.assertEqual(len(response.data["results"]), 2) + self.assertEqual(response.data["results"][0]["id"], self.task_2.pk) + self.assertEqual(response.data["results"][1]["id"], self.task_1.pk) + + def test_retrieve(self): + response: Response = self.client.get( + reverse("tasklog-detail", kwargs={"task_pk": self.task_2.pk}), + ) + + self.assertEqual(response.data["id"], self.task_2.pk) + self.assertSetEqual( + set(response.data["jobs"][0].keys()), + {"display_name", "finish_date", "id", "start_date", "status", "url"}, + ) + + def test_restart(self): + with patch("api.job.views.restart_task"): + response: Response = self.client.put( + reverse("tasklog-restart", kwargs={"task_pk": self.task_1.pk}), + ) + + self.assertEqual(response.status_code, HTTP_200_OK) + + def test_cancel(self): + with patch("api.job.views.cancel_task"): + response: Response = self.client.put( + reverse("tasklog-cancel", kwargs={"task_pk": self.task_1.pk}), + ) + + self.assertEqual(response.status_code, HTTP_200_OK) + + def test_download(self): + with patch("api.job.views.get_task_download_archive_file_handler"): + response: Response = self.client.get( + reverse("tasklog-download", kwargs={"task_pk": self.task_1.pk}), + ) + + self.assertEqual(response.status_code, HTTP_200_OK) + + def test_run(self): + with patch("api.action.views.create", return_value=Response(status=HTTP_201_CREATED)): + response: Response = self.client.post( + reverse( + "run-task", + kwargs={"cluster_id": self.cluster.pk, "action_id": self.action.pk}, + ) + ) + + self.assertEqual(response.status_code, HTTP_201_CREATED) diff --git a/python/api/urls.py b/python/api/urls.py index d95c5818bd..616afc216e 100644 --- a/python/api/urls.py +++ b/python/api/urls.py @@ -19,9 +19,7 @@ register_converter(views.NameConverter, 'name') swagger_view = get_swagger_view(title='ArenaData Chapel API') -schema_view = get_schema_view( - title='ArenaData Chapel API', patterns=[path('api/v1/', include('api.urls'))] -) +schema_view = get_schema_view(title='ArenaData Chapel API', patterns=[path('api/v1/', include('api.urls'))]) urlpatterns = [ path('', views.APIRoot.as_view()), diff --git a/python/api/utils.py b/python/api/utils.py index 5a85be47b2..81f83189fe 100644 --- a/python/api/utils.py +++ b/python/api/utils.py @@ -12,6 +12,7 @@ from typing import List +from django.conf import settings from django.core.exceptions import ObjectDoesNotExist from django.http.request import QueryDict from django_filters import rest_framework as drf_filters @@ -20,23 +21,65 @@ from rest_framework.filters import OrderingFilter from rest_framework.response import Response from rest_framework.reverse import reverse -from rest_framework.serializers import HyperlinkedIdentityField -from rest_framework.status import HTTP_200_OK, HTTP_201_CREATED, HTTP_400_BAD_REQUEST +from rest_framework.serializers import HyperlinkedIdentityField, Serializer +from rest_framework.status import ( + HTTP_200_OK, + HTTP_201_CREATED, + HTTP_400_BAD_REQUEST, + HTTP_409_CONFLICT, +) +from cm.api import load_mm_objects from cm.errors import AdcmEx +from cm.issue import update_hierarchy_issues, update_issue_after_deleting +from cm.job import start_task from cm.models import ( Action, ADCMEntity, - Cluster, + ClusterObject, ConcernType, - ConfigLog, Host, HostComponent, - HostProvider, - MaintenanceModeType, + MaintenanceMode, + Prototype, PrototypeConfig, + ServiceComponent, ) -from cm.upgrade import get_upgrade + + +def _change_mm_via_action( + prototype: Prototype, + action_name: str, + obj: Host | ClusterObject | ServiceComponent, + serializer: Serializer, +) -> Serializer: + action = Action.objects.filter(prototype=prototype, name=action_name).first() + if action: + start_task( + action=action, + obj=obj, + conf={}, + attr={}, + hc=[], + hosts=[], + verbose=False, + ) + serializer.validated_data["maintenance_mode"] = MaintenanceMode.CHANGING + + return serializer + + +def _update_mm_hierarchy_issues(obj: Host | ClusterObject | ServiceComponent) -> None: + if isinstance(obj, Host): + update_hierarchy_issues(obj.provider) + + hosts = (host_component.host for host_component in HostComponent.objects.filter(cluster=obj.cluster)) + for host in hosts: + update_hierarchy_issues(host.provider) + + update_hierarchy_issues(obj.cluster) + update_issue_after_deleting() + load_mm_objects() def get_object_for_user(user, perms, klass, **kwargs): @@ -66,9 +109,7 @@ def check_obj(model, req, error=None): def hlink(view, lookup, lookup_url): - return HyperlinkedIdentityField( - view_name=view, lookup_field=lookup, lookup_url_kwarg=lookup_url - ) + return HyperlinkedIdentityField(view_name=view, lookup_field=lookup, lookup_url_kwarg=lookup_url) def check_custom_perm(user, action_type, model, obj, second_perm=None): @@ -98,41 +139,6 @@ def update(serializer, **kwargs): return save(serializer, HTTP_200_OK, **kwargs) -def set_disabling_cause(obj: ADCMEntity, action: Action) -> None: - action.disabling_cause = None - if obj.prototype.type == "adcm": - current_configlog = ConfigLog.objects.get(obj_ref=obj.config, id=obj.config.current) - if not current_configlog.attr["ldap_integration"]["active"]: - action.disabling_cause = "no_ldap_settings" - - if obj.prototype.type == "cluster": - mm = Host.objects.filter(cluster=obj, maintenance_mode=MaintenanceModeType.On).exists() - if not action.allow_in_maintenance_mode and mm: - action.disabling_cause = "maintenance_mode" - elif obj.prototype.type == "service": - mm = HostComponent.objects.filter( - service=obj, cluster=obj.cluster, host__maintenance_mode=MaintenanceModeType.On - ).exists() - if not action.allow_in_maintenance_mode and mm: - action.disabling_cause = "maintenance_mode" - elif obj.prototype.type == "component": - mm = HostComponent.objects.filter( - component=obj, - cluster=obj.cluster, - service=obj.service, - host__maintenance_mode=MaintenanceModeType.On, - ).exists() - if not action.allow_in_maintenance_mode and mm: - action.disabling_cause = "maintenance_mode" - elif obj.prototype.type == "host": - mm = HostComponent.objects.filter( - component_id__in=HostComponent.objects.filter(host=obj).values_list("component_id"), - host__maintenance_mode=MaintenanceModeType.On, - ).exists() - if action.host_action and not action.allow_in_maintenance_mode and mm: - action.disabling_cause = "maintenance_mode" - - def filter_actions(obj: ADCMEntity, actions_set: List[Action]): """Filter out actions that are not allowed to run on object at that moment""" if obj.concerns.filter(type=ConcernType.Lock).exists(): @@ -142,23 +148,18 @@ def filter_actions(obj: ADCMEntity, actions_set: List[Action]): for action in actions_set: if action.allowed(obj): allowed.append(action) - action.config = PrototypeConfig.objects.filter( - prototype=action.prototype, action=action - ).order_by("id") - set_disabling_cause(obj, action) + action.config = PrototypeConfig.objects.filter(prototype=action.prototype, action=action).order_by("id") return allowed -def get_upgradable_func(obj: [Cluster, HostProvider]): - return bool(get_upgrade(obj)) - - def get_api_url_kwargs(obj, request, no_obj_type=False): obj_type = obj.prototype.type - kwargs = { - f"{obj_type}_id": obj.id, - } + + if obj_type == "adcm": # TODO: this is a temporary patch for `config` endpoint + kwargs = {"adcm_pk": obj.pk} + else: + kwargs = {f"{obj_type}_id": obj.id} # Do not include object_type in kwargs if no_obj_type == True if not no_obj_type: @@ -173,30 +174,6 @@ def get_api_url_kwargs(obj, request, no_obj_type=False): return kwargs -class CommonAPIURL(HyperlinkedIdentityField): - def get_url(self, obj, view_name, request, _format): - kwargs = get_api_url_kwargs(obj, request) - - return reverse(view_name, kwargs=kwargs, request=request, format=_format) - - -class ObjectURL(HyperlinkedIdentityField): - def get_url(self, obj, view_name, request, _format): - kwargs = get_api_url_kwargs(obj, request, True) - - return reverse(view_name, kwargs=kwargs, request=request, format=_format) - - -class UrlField(HyperlinkedIdentityField): - def get_kwargs(self, obj): - return {} - - def get_url(self, obj, view_name, request, _format): - kwargs = self.get_kwargs(obj) - - return reverse(self.view_name, kwargs=kwargs, request=request, format=_format) - - def getlist_from_querydict(query_params, field_name): params = query_params.get(field_name) if params is None: @@ -226,12 +203,118 @@ def fix_ordering(field, view): return fix -class ActionFilter(drf_filters.FilterSet): - button_is_null = drf_filters.BooleanFilter(field_name="button", lookup_expr="isnull") +def get_maintenance_mode_response(obj: Host | ClusterObject | ServiceComponent, serializer: Serializer) -> Response: + # pylint: disable=too-many-branches + + turn_on_action_name = settings.ADCM_TURN_ON_MM_ACTION_NAME + turn_off_action_name = settings.ADCM_TURN_OFF_MM_ACTION_NAME + prototype = obj.prototype + if isinstance(obj, Host): + obj_name = "host" + turn_on_action_name = settings.ADCM_HOST_TURN_ON_MM_ACTION_NAME + turn_off_action_name = settings.ADCM_HOST_TURN_OFF_MM_ACTION_NAME + prototype = obj.cluster.prototype + elif isinstance(obj, ClusterObject): + obj_name = "service" + elif isinstance(obj, ServiceComponent): + obj_name = "component" + else: + obj_name = "obj" + + service_has_hc = None + if obj_name == "service": + service_has_hc = HostComponent.objects.filter(service=obj).exists() + + component_has_hc = None + if obj_name == "component": + component_has_hc = HostComponent.objects.filter(component=obj).exists() + + if obj.maintenance_mode_attr == MaintenanceMode.CHANGING: + return Response( + data={ + "code": "MAINTENANCE_MODE", + "level": "error", + "desc": "Maintenance mode is changing now", + }, + status=HTTP_409_CONFLICT, + ) + + if obj.maintenance_mode_attr == MaintenanceMode.OFF: + if serializer.validated_data["maintenance_mode"] == MaintenanceMode.OFF: + return Response( + data={ + "code": "MAINTENANCE_MODE", + "level": "error", + "desc": "Maintenance mode already off", + }, + status=HTTP_409_CONFLICT, + ) + + if obj_name == "host" or service_has_hc or component_has_hc: + serializer = _change_mm_via_action( + prototype=prototype, action_name=turn_on_action_name, obj=obj, serializer=serializer + ) + else: + obj.maintenance_mode = MaintenanceMode.ON + serializer.validated_data["maintenance_mode"] = MaintenanceMode.ON + + serializer.save() + _update_mm_hierarchy_issues(obj=obj) + + return Response() + + if obj.maintenance_mode_attr == MaintenanceMode.ON: + if serializer.validated_data["maintenance_mode"] == MaintenanceMode.ON: + return Response( + data={ + "code": "MAINTENANCE_MODE", + "level": "error", + "desc": "Maintenance mode already on", + }, + status=HTTP_409_CONFLICT, + ) + + if obj_name == "host" or service_has_hc or component_has_hc: + serializer = _change_mm_via_action( + prototype=prototype, action_name=turn_off_action_name, obj=obj, serializer=serializer + ) + else: + obj.maintenance_mode = MaintenanceMode.OFF + serializer.validated_data["maintenance_mode"] = MaintenanceMode.OFF + + serializer.save() + _update_mm_hierarchy_issues(obj=obj) + + return Response() + + return Response( + data={"error": f'Unknown {obj_name} maintenance mode "{obj.maintenance_mode}"'}, + status=HTTP_400_BAD_REQUEST, + ) + + +class CommonAPIURL(HyperlinkedIdentityField): + def get_url(self, obj, view_name, request, _format): + kwargs = get_api_url_kwargs(obj, request) + + return reverse(view_name, kwargs=kwargs, request=request, format=_format) + + +class ObjectURL(HyperlinkedIdentityField): + def get_url(self, obj, view_name, request, _format): + kwargs = get_api_url_kwargs(obj, request, True) + + return reverse(view_name, kwargs=kwargs, request=request, format=_format) + - class Meta: - model = Action - fields = ("name", "button") +class UrlField(HyperlinkedIdentityField): + def get_kwargs(self, obj): + return {} + + def get_url(self, obj, view_name, request, _format): + kwargs = self.get_kwargs(obj) + + return reverse(self.view_name, kwargs=kwargs, request=request, format=_format) class AdcmOrderingFilter(OrderingFilter): diff --git a/python/api/validators.py b/python/api/validators.py index e759ff3869..c83accdb31 100644 --- a/python/api/validators.py +++ b/python/api/validators.py @@ -1,3 +1,15 @@ +# 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 re from rest_framework.exceptions import ValidationError diff --git a/python/api/views.py b/python/api/views.py index 59aebda2bd..cbdc5c05ab 100644 --- a/python/api/views.py +++ b/python/api/views.py @@ -10,11 +10,11 @@ # See the License for the specific language governing permissions and # limitations under the License. +from django.conf import settings from rest_framework import permissions, routers from rest_framework.response import Response from rest_framework.views import APIView -from adcm.settings import ADCM_VERSION from adcm.utils import has_google_oauth, has_yandex_oauth from cm.stack import NAME_REGEX @@ -22,7 +22,7 @@ class APIRoot(routers.APIRootView): permission_classes = (permissions.AllowAny,) api_root_dict = { - "adcm": "adcm", + "adcm": "adcm-list", "audit": "audit:root", "cluster": "cluster", "provider": "provider", @@ -32,10 +32,10 @@ class APIRoot(routers.APIRootView): "group-config": "group-config-list", "config": "config-list", "config-log": "config-log-list", - "job": "job", + "job": "joblog-list", "stack": "stack", "stats": "stats", - "task": "task", + "task": "tasklog-list", "info": "adcm-info", "concern": "concern", "rbac": "rbac:root", @@ -62,7 +62,7 @@ class ADCMInfo(APIView): def get(request): return Response( { - "adcm_version": ADCM_VERSION, + "adcm_version": settings.ADCM_VERSION, "google_oauth": has_google_oauth(), "yandex_oauth": has_yandex_oauth(), } diff --git a/python/api_ui/views.py b/python/api_ui/views.py index ec92c150a6..616d23b3c9 100644 --- a/python/api_ui/views.py +++ b/python/api_ui/views.py @@ -10,14 +10,12 @@ # See the License for the specific language governing permissions and # limitations under the License. -import rest_framework -from rest_framework import routers +from rest_framework.permissions import AllowAny +from rest_framework.routers import APIRootView -class APIRoot(routers.APIRootView): - """ADCM API UI root""" - - permission_classes = (rest_framework.permissions.AllowAny,) +class APIRoot(APIRootView): + permission_classes = (AllowAny,) api_root_dict = { - 'rbac': 'rbac-ui:root', + "rbac": "rbac-ui:root", } diff --git a/python/audit/cases/__init__.py b/python/audit/cases/__init__.py index e69de29bb2..824dd6c8fe 100644 --- a/python/audit/cases/__init__.py +++ b/python/audit/cases/__init__.py @@ -0,0 +1,11 @@ +# 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. diff --git a/python/audit/cases/adcm.py b/python/audit/cases/adcm.py index 434efc50ef..0aecbc1e65 100644 --- a/python/audit/cases/adcm.py +++ b/python/audit/cases/adcm.py @@ -1,3 +1,15 @@ +# 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 audit.cases.common import obj_pk_case from audit.models import ( AuditLogOperationType, @@ -12,10 +24,7 @@ def adcm_case(path: list[str, ...]) -> tuple[AuditOperation, AuditObject | None] audit_object = None match path: - case ( - ["adcm", adcm_pk, "config", "history"] - | ["adcm", adcm_pk, "config", "history", _, "restore"] - ): + case (["adcm", adcm_pk, "config", "history"] | ["adcm", adcm_pk, "config", "history", _, "restore"]): audit_operation, audit_object = obj_pk_case( obj_type=AuditObjectType.ADCM, operation_type=AuditLogOperationType.Update, diff --git a/python/audit/cases/cluster.py b/python/audit/cases/cluster.py index 45df726d36..42ed1fa889 100644 --- a/python/audit/cases/cluster.py +++ b/python/audit/cases/cluster.py @@ -1,5 +1,17 @@ +# 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 django.db.models import Model -from django.views import View +from rest_framework.generics import GenericAPIView from rest_framework.response import Response from audit.cases.common import ( @@ -20,7 +32,7 @@ CONFIGURATION_STR = "configuration " -def get_export_cluster_and_service_names(response: Response, view: View) -> tuple[str, str]: +def get_export_cluster_and_service_names(response: Response, view: GenericAPIView) -> tuple[str, str]: cluster, service = None, None cluster_name, service_name = "", "" if response and response.data and isinstance(response.data.get("export_cluster_id"), int): @@ -61,7 +73,7 @@ def make_export_name(cluster_name: str, service_name: str) -> str: # pylint: disable-next=too-many-locals,too-many-branches,too-many-statements def cluster_case( path: list[str, ...], - view: View, + view: GenericAPIView, response: Response, deleted_obj: Model, ) -> tuple[AuditOperation, AuditObject | None]: @@ -119,7 +131,13 @@ def cluster_case( object_type=AuditObjectType.Cluster, ) - case ["cluster", cluster_pk, "host", host_pk]: + case ["cluster", cluster_pk, "host", host_pk] | [ + "cluster", + cluster_pk, + "host", + host_pk, + "maintenance-mode", + ]: if view.request.method == "DELETE": name = "host removed" if not isinstance(deleted_obj, Host): @@ -223,7 +241,7 @@ def cluster_case( object_type=AuditObjectType.Cluster, ) - case ["cluster", cluster_pk, "service", service_pk, "bind"]: + case ["cluster", _, "service", service_pk, "bind"]: service = ClusterObject.objects.get(pk=service_pk) cluster_name, service_name = get_export_cluster_and_service_names(response, view) audit_operation = AuditOperation( @@ -277,6 +295,13 @@ def cluster_case( operation_aux_str="import ", ) + case ["cluster", _, "service", service_pk, "maintenance-mode"]: + audit_operation, audit_object = obj_pk_case( + obj_type=AuditObjectType.Service, + operation_type=AuditLogOperationType.Update, + obj_pk=service_pk, + ) + case ( ["cluster", _, "service", _, "component", component_pk, "config", "history"] | [ @@ -340,8 +365,7 @@ def cluster_case( ) case ( - ["cluster", cluster_pk, "config", "history"] - | ["cluster", cluster_pk, "config", "history", _, "restore"] + ["cluster", cluster_pk, "config", "history"] | ["cluster", cluster_pk, "config", "history", _, "restore"] ): audit_operation, audit_object = obj_pk_case( obj_type=AuditObjectType.Cluster, @@ -365,4 +389,16 @@ def cluster_case( operation_aux_str=CONFIGURATION_STR, ) + case ( + ["component", component_pk] + | ["component", component_pk, _] + | ["cluster", _, "service", _, "component", component_pk, "maintenance-mode"] + | ["service", _, "component", component_pk, "maintenance-mode"] + ): + audit_operation, audit_object = obj_pk_case( + obj_type=AuditObjectType.Component, + operation_type=AuditLogOperationType.Update, + obj_pk=component_pk, + ) + return audit_operation, audit_object diff --git a/python/audit/cases/common.py b/python/audit/cases/common.py index 1efee83a71..2ef7670b64 100644 --- a/python/audit/cases/common.py +++ b/python/audit/cases/common.py @@ -1,4 +1,14 @@ -from django.db.models import Model +# 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 rest_framework.response import Response from audit.models import ( @@ -10,7 +20,14 @@ AuditObjectType, AuditOperation, ) -from cm.models import Action, ClusterObject, TaskLog, Upgrade +from cm.models import ( + Action, + ADCMEntity, + ClusterObject, + ServiceComponent, + TaskLog, + Upgrade, +) def _get_audit_operation( @@ -49,7 +66,7 @@ def _task_case(task_pk: str, action: str) -> tuple[AuditOperation, AuditObject | operation_type=AuditLogOperationType.Update, ) - if task: + if task and task.task_object: audit_object = get_or_create_audit_obj( object_id=task.task_object.pk, object_name=task.task_object.name, @@ -61,7 +78,7 @@ def _task_case(task_pk: str, action: str) -> tuple[AuditOperation, AuditObject | return audit_operation, audit_object -def get_obj_name(obj: Model, obj_type: str) -> str: +def get_obj_name(obj: ClusterObject | ServiceComponent | ADCMEntity, obj_type: str) -> str: if obj_type == "service": obj_name = obj.display_name cluster = getattr(obj, "cluster") @@ -82,9 +99,9 @@ def get_obj_name(obj: Model, obj_type: str) -> str: def get_or_create_audit_obj( - object_id: str, - object_name: str, - object_type: str, + object_id: str, + object_name: str, + object_type: str, ) -> AuditObject: audit_object = AuditObject.objects.filter( object_id=object_id, @@ -184,9 +201,7 @@ def action_case(path: list[str, ...]) -> tuple[AuditOperation, AuditObject | Non action = Action.objects.filter(pk=action_pk).first() if action: - audit_operation.name = audit_operation.name.format( - action_display_name=action.display_name - ) + audit_operation.name = audit_operation.name.format(action_display_name=action.display_name) obj = PATH_STR_TO_OBJ_CLASS_MAP[obj_type].objects.filter(pk=obj_pk).first() if obj: diff --git a/python/audit/cases/config.py b/python/audit/cases/config.py index a7149b1d43..56647f15f5 100644 --- a/python/audit/cases/config.py +++ b/python/audit/cases/config.py @@ -1,3 +1,14 @@ +# 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 django.contrib.contenttypes.models import ContentType from django.db.models import Model from django.views import View diff --git a/python/audit/cases/host_and_provider.py b/python/audit/cases/host_and_provider.py index d23a9c236a..84c42badb7 100644 --- a/python/audit/cases/host_and_provider.py +++ b/python/audit/cases/host_and_provider.py @@ -1,3 +1,14 @@ +# 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 django.db.models import Model from django.views import View from rest_framework.response import Response @@ -22,11 +33,17 @@ def host_and_provider_case( audit_object = None match path: - case ["host", host_pk] | ["provider", _, "host", host_pk]: + case ( + ["host", host_pk] + | ["host", host_pk, _] + | ["provider", _, "host", host_pk] + | ["provider", _, "host", host_pk, "maintenance-mode"] + ): if view.request.method == "DELETE": operation_type = AuditLogOperationType.Delete else: operation_type = AuditLogOperationType.Update + object_name = None audit_operation = AuditOperation( name=f"{AuditObjectType.Host.capitalize()} {operation_type}d", diff --git a/python/audit/cases/rbac.py b/python/audit/cases/rbac.py index 458e980a66..1996af3546 100644 --- a/python/audit/cases/rbac.py +++ b/python/audit/cases/rbac.py @@ -1,3 +1,14 @@ +# 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 dataclasses import dataclass from django.db.models import Model @@ -22,9 +33,9 @@ class RbacCaseData: def _rbac_case( - obj_type: AuditObjectType, - response: Response | None, - data: RbacCaseData | None = None, + obj_type: AuditObjectType, + response: Response | None, + data: RbacCaseData | None = None, ) -> tuple[AuditOperation, AuditObject | None]: if data: if data.view.action == "destroy": @@ -55,65 +66,69 @@ def _rbac_case( def rbac_case( - path: list[str, ...], - view: View, - response: Response, - deleted_obj: Model, + path: list[str, ...], + view: View, + response: Response, + deleted_obj: Model, ) -> tuple[AuditOperation, AuditObject | None]: audit_operation = None audit_object = None match path: - case["rbac", "group"]: + case ["rbac", "group"]: audit_operation, audit_object = _rbac_case( obj_type=AuditObjectType.Group, response=response, ) - case["rbac", "group", group_pk]: + case ["rbac", "group", group_pk]: data = RbacCaseData(view=view, deleted_obj=deleted_obj, obj_pk=group_pk) audit_operation, audit_object = _rbac_case( obj_type=AuditObjectType.Group, - response=response, data=data, + response=response, + data=data, ) - case["rbac", "policy"]: + case ["rbac", "policy"]: audit_operation, audit_object = _rbac_case( obj_type=AuditObjectType.Policy, response=response, ) - case["rbac", "policy", policy_pk]: + case ["rbac", "policy", policy_pk]: data = RbacCaseData(view=view, deleted_obj=deleted_obj, obj_pk=policy_pk) audit_operation, audit_object = _rbac_case( obj_type=AuditObjectType.Policy, - response=response, data=data, + response=response, + data=data, ) - case["rbac", "role"]: + case ["rbac", "role"]: audit_operation, audit_object = _rbac_case( obj_type=AuditObjectType.Role, response=response, ) - case["rbac", "role", role_pk]: + case ["rbac", "role", role_pk]: data = RbacCaseData(view=view, deleted_obj=deleted_obj, obj_pk=role_pk) audit_operation, audit_object = _rbac_case( obj_type=AuditObjectType.Role, - response=response, data=data, + response=response, + data=data, ) - case["rbac", "user"]: + case ["rbac", "user"]: audit_operation, audit_object = _rbac_case( obj_type=AuditObjectType.User, response=response, ) - case["rbac", "user", user_pk]: + case ["rbac", "user", user_pk]: data = RbacCaseData(view=view, deleted_obj=deleted_obj, obj_pk=user_pk) audit_operation, audit_object = _rbac_case( obj_type=AuditObjectType.User, - response=response, data=data, + response=response, + data=data, ) return audit_operation, audit_object diff --git a/python/audit/cases/service.py b/python/audit/cases/service.py index 824838dd19..61c6a160da 100644 --- a/python/audit/cases/service.py +++ b/python/audit/cases/service.py @@ -1,3 +1,14 @@ +# 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 django.db.models import Model from django.views import View from rest_framework.response import Response @@ -13,7 +24,7 @@ from cm.models import Cluster, ClusterBind, ClusterObject -def service_case( +def service_case( # pylint: disable=too-many-branches path: list[str, ...], view: View, response: Response, @@ -24,7 +35,6 @@ def service_case( match path: case ["service"]: - cluster = None audit_operation = AuditOperation( name="service added", operation_type=AuditLogOperationType.Update, @@ -50,13 +60,20 @@ def service_case( else: audit_object = None - case ["service", _]: + case ["service", service_pk] | ["service", service_pk, "maintenance-mode"]: deleted_obj: ClusterObject - audit_operation = AuditOperation( - name="service removed", - operation_type=AuditLogOperationType.Update, - ) - if deleted_obj: + if view.request.method == "DELETE": + audit_operation = AuditOperation( + name="service removed", + operation_type=AuditLogOperationType.Update, + ) + else: + audit_operation = AuditOperation( + name=f"{AuditObjectType.Service.capitalize()} {AuditLogOperationType.Update}d", + operation_type=AuditLogOperationType.Update, + ) + + if deleted_obj and "maintenance-mode" not in path: audit_operation.name = f"{deleted_obj.display_name} {audit_operation.name}" audit_object = get_or_create_audit_obj( object_id=deleted_obj.cluster.pk, @@ -64,7 +81,15 @@ def service_case( object_type=AuditObjectType.Cluster, ) else: - audit_object = None + obj = ClusterObject.objects.filter(pk=service_pk).first() + if obj: + audit_object = get_or_create_audit_obj( + object_id=service_pk, + object_name=get_obj_name(obj=obj, obj_type=AuditObjectType.Service), + object_type=AuditObjectType.Service, + ) + else: + audit_object = None case ["service", service_pk, "bind"]: obj = ClusterObject.objects.get(pk=service_pk) diff --git a/python/audit/cases/stack.py b/python/audit/cases/stack.py index 65c5a5336c..233366ce06 100644 --- a/python/audit/cases/stack.py +++ b/python/audit/cases/stack.py @@ -1,20 +1,31 @@ +# 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 django.db.models import Model from rest_framework.response import Response -from audit.cases.common import obj_pk_case, response_case +from audit.cases.common import get_or_create_audit_obj, obj_pk_case, response_case from audit.models import ( AuditLogOperationType, AuditObject, AuditObjectType, AuditOperation, ) -from cm.models import Bundle +from cm.models import Bundle, Prototype def stack_case( - path: list[str, ...], - response: Response, - deleted_obj: Model, + path: list[str, ...], + response: Response, + deleted_obj: Model, ) -> tuple[AuditOperation | None, AuditObject | None]: audit_operation = None audit_object = None @@ -50,7 +61,6 @@ def stack_case( operation_type=AuditLogOperationType.Update, obj_pk=bundle_pk, ) - case ["stack", "bundle", bundle_pk, "license", "accept"]: audit_operation, audit_object = obj_pk_case( obj_type=AuditObjectType.Bundle, @@ -58,5 +68,13 @@ def stack_case( obj_pk=bundle_pk, operation_aux_str="license accepted", ) + case ["stack", "prototype", prototype_pk, "license", "accept"]: + prototype = Prototype.objects.get(pk=prototype_pk) + audit_object = get_or_create_audit_obj( + object_id=prototype_pk, object_name=prototype.name, object_type=AuditObjectType.Prototype + ) + audit_operation = AuditOperation( + name=f"{prototype.type.capitalize()} license accepted", operation_type=AuditLogOperationType.Update + ) return audit_operation, audit_object diff --git a/python/audit/filters.py b/python/audit/filters.py index 26b564316d..1c804ba5fb 100644 --- a/python/audit/filters.py +++ b/python/audit/filters.py @@ -10,40 +10,42 @@ # See the License for the specific language governing permissions and # limitations under the License. -from django_filters import rest_framework as drf_filters +from django_filters.rest_framework import ( + CharFilter, + ChoiceFilter, + DateFilter, + FilterSet, + IsoDateTimeFromToRangeFilter, +) from audit.models import AuditLog, AuditObjectType, AuditSession -class AuditLogListFilter(drf_filters.FilterSet): - object_type = drf_filters.ChoiceFilter( - field_name='audit_object__object_type', choices=AuditObjectType.choices, label='Object type' +class AuditLogListFilter(FilterSet): + object_type = ChoiceFilter( + field_name="audit_object__object_type", choices=AuditObjectType.choices, label="Object type" ) - object_name = drf_filters.CharFilter( - field_name='audit_object__object_name', label='Object name' - ) - operation_date = drf_filters.DateFilter( - field_name='operation_time', lookup_expr='date', label='Operation date' - ) - username = drf_filters.CharFilter(field_name='user__username', label='Username') + object_name = CharFilter(field_name="audit_object__object_name", label="Object name") + operation_date = DateFilter(field_name="operation_time", lookup_expr="date", label="Operation date") + username = CharFilter(field_name="user__username", label="Username") + operation_time = IsoDateTimeFromToRangeFilter() class Meta: model = AuditLog fields = [ - 'operation_type', - 'operation_name', - 'operation_result', + "operation_type", + "operation_name", + "operation_result", ] -class AuditSessionListFilter(drf_filters.FilterSet): - username = drf_filters.CharFilter(field_name='user__username', label='Username') - login_date = drf_filters.DateFilter( - field_name='login_time', lookup_expr='date', label='Login date' - ) +class AuditSessionListFilter(FilterSet): + username = CharFilter(field_name="user__username", label="Username") + login_date = DateFilter(field_name="login_time", lookup_expr="date", label="Login date") + login_time = IsoDateTimeFromToRangeFilter() class Meta: model = AuditSession fields = [ - 'login_result', + "login_result", ] diff --git a/python/audit/management/commands/clearaudit.py b/python/audit/management/commands/clearaudit.py index 0ba3aab948..45f086b0da 100644 --- a/python/audit/management/commands/clearaudit.py +++ b/python/audit/management/commands/clearaudit.py @@ -18,6 +18,7 @@ from shutil import rmtree from tarfile import TarFile +from django.conf import settings from django.core.management.base import BaseCommand from django.db.models import Count, Q from django.utils import timezone @@ -31,7 +32,6 @@ # pylint: disable=protected-access class Command(BaseCommand): - encoding = "utf-8" config_key = "audit_data_retention" archive_base_dir = "/adcm/data/audit/" archive_tmp_dir = "/adcm/data/audit/tmp" @@ -40,12 +40,12 @@ class Command(BaseCommand): read=dict( name=os.path.join(archive_base_dir, archive_name), mode="r:gz", - encoding="utf-8", + encoding=settings.ENCODING_UTF_8, ), write=dict( name=os.path.join(archive_base_dir, archive_name), mode="w:gz", - encoding="utf-8", + encoding=settings.ENCODING_UTF_8, compresslevel=9, ), ) @@ -77,11 +77,7 @@ def __handle(self): target_logins = AuditSession.objects.filter(login_time__lt=threshold_date) target_objects = ( AuditObject.objects.filter(is_deleted=True) - .annotate( - not_deleted_auditlogs_count=Count( - "auditlog", filter=~Q(auditlog__in=target_operations) - ) - ) + .annotate(not_deleted_auditlogs_count=Count("auditlog", filter=~Q(auditlog__in=target_operations))) .filter(not_deleted_auditlogs_count__lte=0) ) @@ -165,7 +161,7 @@ def __prepare_csvs(self, *querysets, base_dir): qs_fields = header mode = "at" if header else "wt" - with open(tmp_cvf_name, mode, newline="", encoding=self.encoding) as csv_file: + with open(tmp_cvf_name, mode, newline="", encoding=settings.ENCODING_UTF_8) as csv_file: writer = csv.writer(csv_file) if header is None: @@ -182,7 +178,7 @@ def __prepare_csvs(self, *querysets, base_dir): def __get_csv_header(self, path): header = None if Path(path).is_file(): - with open(path, "rt", encoding=self.encoding) as csv_file: + with open(path, "rt", encoding=settings.ENCODING_UTF_8) as csv_file: header = csv_file.readline().strip().split(",") return header diff --git a/python/audit/middleware.py b/python/audit/middleware.py index 93ebb7dd44..8b13cced74 100644 --- a/python/audit/middleware.py +++ b/python/audit/middleware.py @@ -12,6 +12,7 @@ import json from json.decoder import JSONDecodeError +from django.conf import settings from django.contrib.auth.models import AnonymousUser, User from django.urls import resolve @@ -41,9 +42,7 @@ def _audit(request_path: str, user: User | AnonymousUser | None = None, username result = AuditSessionLoginResult.UserNotFound user = None - auditsession = AuditSession.objects.create( - user=user, login_result=result, login_details=details - ) + auditsession = AuditSession.objects.create(user=user, login_result=result, login_details=details) cef_logger(audit_instance=auditsession, signature_id=resolve(request_path).route) def __call__(self, request): @@ -55,7 +54,7 @@ def __call__(self, request): }: try: - username = json.loads(request.body.decode("utf-8")).get("username") + username = json.loads(request.body.decode(settings.ENCODING_UTF_8)).get("username") except JSONDecodeError: username = "" diff --git a/python/audit/migrations/0001_initial.py b/python/audit/migrations/0001_initial.py index fb03066322..71c9acc4a0 100644 --- a/python/audit/migrations/0001_initial.py +++ b/python/audit/migrations/0001_initial.py @@ -1,3 +1,15 @@ +# 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. + # Generated by Django 3.2.15 on 2022-09-28 05:03 import django.db.models.deletion @@ -19,9 +31,7 @@ class Migration(migrations.Migration): fields=[ ( 'id', - models.BigAutoField( - auto_created=True, primary_key=True, serialize=False, verbose_name='ID' - ), + models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), ), ('object_id', models.PositiveIntegerField()), ('object_name', models.CharField(max_length=253)), @@ -52,9 +62,7 @@ class Migration(migrations.Migration): fields=[ ( 'id', - models.BigAutoField( - auto_created=True, primary_key=True, serialize=False, verbose_name='ID' - ), + models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), ), ( 'login_result', @@ -85,9 +93,7 @@ class Migration(migrations.Migration): fields=[ ( 'id', - models.BigAutoField( - auto_created=True, primary_key=True, serialize=False, verbose_name='ID' - ), + models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), ), ('operation_name', models.CharField(max_length=160)), ( diff --git a/python/audit/migrations/0002_alter_auditobject_object_type.py b/python/audit/migrations/0002_alter_auditobject_object_type.py new file mode 100644 index 0000000000..a9ee50cc18 --- /dev/null +++ b/python/audit/migrations/0002_alter_auditobject_object_type.py @@ -0,0 +1,46 @@ +# 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. + +# Generated by Django 3.2.15 on 2022-11-21 17:30 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('audit', '0001_initial'), + ] + + operations = [ + migrations.AlterField( + model_name='auditobject', + name='object_type', + field=models.CharField( + choices=[ + ('prototype', 'prototype'), + ('cluster', 'cluster'), + ('service', 'service'), + ('component', 'component'), + ('host', 'host'), + ('provider', 'provider'), + ('bundle', 'bundle'), + ('adcm', 'adcm'), + ('user', 'user'), + ('group', 'group'), + ('role', 'role'), + ('policy', 'policy'), + ], + max_length=16, + ), + ), + ] diff --git a/python/audit/models.py b/python/audit/models.py index a03237e6a6..6a04012ee0 100644 --- a/python/audit/models.py +++ b/python/audit/models.py @@ -22,12 +22,14 @@ ClusterObject, Host, HostProvider, + Prototype, ServiceComponent, ) from rbac.models import Group, Policy, Role, User class AuditObjectType(models.TextChoices): + Prototype = "prototype", "prototype" Cluster = "cluster", "cluster" Service = "service", "service" Component = "component", "component" @@ -102,6 +104,7 @@ class AuditOperation: Group: AuditObjectType.Group, Role: AuditObjectType.Role, Policy: AuditObjectType.Policy, + Prototype: AuditObjectType.Prototype, } AUDIT_OBJECT_TYPE_TO_MODEL_MAP = {v: k for k, v in MODEL_TO_AUDIT_OBJECT_TYPE_MAP.items()} diff --git a/python/audit/serializers.py b/python/audit/serializers.py index 53db46df59..01c0df9b42 100644 --- a/python/audit/serializers.py +++ b/python/audit/serializers.py @@ -11,51 +11,51 @@ # limitations under the License. -from rest_framework import serializers +from rest_framework.serializers import ( + CharField, + HyperlinkedModelSerializer, + IntegerField, +) from audit.models import AuditLog, AuditSession -class AuditLogSerializer(serializers.HyperlinkedModelSerializer): - object_id = serializers.IntegerField( - read_only=True, source='audit_object.object_id', allow_null=True - ) - object_type = serializers.CharField( - read_only=True, source='audit_object.object_type', allow_null=True - ) - object_name = serializers.CharField( - read_only=True, source='audit_object.object_name', allow_null=True - ) +class AuditLogSerializer(HyperlinkedModelSerializer): + object_id = IntegerField(read_only=True, source="audit_object.object_id", allow_null=True) + object_type = CharField(read_only=True, source="audit_object.object_type", allow_null=True) + object_name = CharField(read_only=True, source="audit_object.object_name", allow_null=True) + username = CharField(read_only=True, source="user.username", allow_null=True) class Meta: model = AuditLog fields = [ - 'id', - 'object_id', - 'object_type', - 'object_name', - 'operation_type', - 'operation_name', - 'operation_result', - 'operation_time', - 'user_id', - 'object_changes', - 'url', + "id", + "object_id", + "object_type", + "object_name", + "operation_type", + "operation_name", + "operation_result", + "operation_time", + "user_id", + "username", + "object_changes", + "url", ] - extra_kwargs = {'url': {'view_name': 'audit:audit-operations-detail'}} + extra_kwargs = {"url": {"view_name": "audit:auditlog-detail"}} -class AuditSessionSerializer(serializers.HyperlinkedModelSerializer): +class AuditSessionSerializer(HyperlinkedModelSerializer): class Meta: model = AuditSession fields = [ - 'id', - 'user_id', - 'login_result', - 'login_time', - 'login_details', - 'url', + "id", + "user_id", + "login_result", + "login_time", + "login_details", + "url", ] extra_kwargs = { - 'url': {'view_name': 'audit:audit-logins-detail'}, + "url": {"view_name": "audit:auditsession-detail"}, } diff --git a/python/audit/tests/test_action.py b/python/audit/tests/test_action.py index fb5aed0bed..b056067a7d 100644 --- a/python/audit/tests/test_action.py +++ b/python/audit/tests/test_action.py @@ -58,9 +58,7 @@ def setUp(self) -> None: self.config.save(update_fields=["current"]) self.adcm_name = "ADCM" - self.adcm = ADCM.objects.create( - prototype=adcm_prototype, name=self.adcm_name, config=self.config - ) + self.adcm = ADCM.objects.create(prototype=adcm_prototype, name=self.adcm_name, config=self.config) self.action = Action.objects.create( display_name="test_adcm_action", prototype=adcm_prototype, @@ -102,7 +100,7 @@ def get_cluster_service_component(self) -> tuple[Cluster, ClusterObject, Service return cluster, service, component - def check_obj_updated( # pylint: disable=too-many-arguments + def check_obj_updated( self, log: AuditLog, obj_pk: int, @@ -132,11 +130,7 @@ def check_obj_updated( # pylint: disable=too-many-arguments def test_adcm_launch(self): with patch(self.action_create_view, return_value=Response(status=HTTP_201_CREATED)): - self.client.post( - path=reverse( - "run-task", kwargs={"adcm_id": self.adcm.pk, "action_id": self.action.pk} - ) - ) + self.client.post(path=reverse("run-task", kwargs={"adcm_pk": self.adcm.pk, "action_id": self.action.pk})) log: AuditLog = AuditLog.objects.order_by("operation_time").last() @@ -151,9 +145,7 @@ def test_adcm_launch(self): ) with patch(self.action_create_view, return_value=Response(status=HTTP_201_CREATED)): - self.client.post( - path=reverse("run-task", kwargs={"adcm_id": 999, "action_id": self.action.pk}) - ) + self.client.post(path=reverse("run-task", kwargs={"adcm_pk": 999, "action_id": self.action.pk})) log: AuditLog = AuditLog.objects.order_by("operation_time").last() @@ -266,7 +258,7 @@ def test_host_denied(self): component_policy.apply() paths = [ - reverse("run-task", kwargs={"adcm_id": self.adcm.pk, "action_id": self.action.pk}), + reverse("run-task", kwargs={"adcm_pk": self.adcm.pk, "action_id": self.action.pk}), reverse("run-task", kwargs={"cluster_id": cluster.pk, "action_id": self.action.pk}), reverse("run-task", kwargs={"host_id": host.pk, "action_id": self.action.pk}), reverse("run-task", kwargs={"component_id": component.pk, "action_id": self.action.pk}), diff --git a/python/audit/tests/test_adcm.py b/python/audit/tests/test_adcm.py index 4ec9337ce4..58b979db52 100644 --- a/python/audit/tests/test_adcm.py +++ b/python/audit/tests/test_adcm.py @@ -43,9 +43,7 @@ def setUp(self) -> None: config.save(update_fields=["current"]) self.adcm_name = "ADCM" - self.adcm = ADCM.objects.create( - prototype=self.prototype, name=self.adcm_name, config=config - ) + self.adcm = ADCM.objects.create(prototype=self.prototype, name=self.adcm_name, config=config) self.action = Action.objects.create( display_name="test_adcm_action", prototype=self.prototype, @@ -61,9 +59,7 @@ def setUp(self) -> None: ) self.adcm_conf_updated_str = "ADCM configuration updated" - def check_adcm_updated( - self, log: AuditLog, operation_name: str, operation_result: str, user: User | None = None - ): + def check_adcm_updated(self, log: AuditLog, operation_name: str, operation_result: str, user: User | None = None): if log.audit_object: self.assertEqual(log.audit_object.object_id, self.adcm.pk) self.assertEqual(log.audit_object.object_name, self.adcm.name) @@ -84,7 +80,7 @@ def check_adcm_updated( def test_update_and_restore(self): self.client.post( - path=reverse("config-history", kwargs={"adcm_id": self.adcm.pk}), + path=reverse("config-history", kwargs={"adcm_pk": self.adcm.pk}), data={"config": {}}, content_type=APPLICATION_JSON, ) @@ -101,7 +97,7 @@ def test_update_and_restore(self): response: Response = self.client.patch( path=reverse( "config-history-version-restore", - kwargs={"adcm_id": self.adcm.pk, "version": self.config_log.pk}, + kwargs={"adcm_pk": self.adcm.pk, "version": self.config_log.pk}, ), content_type=APPLICATION_JSON, ) @@ -120,7 +116,7 @@ def test_update_and_restore(self): def test_denied(self): with self.no_rights_user_logged_in: response: Response = self.client.post( - path=reverse("config-history", kwargs={"adcm_id": self.adcm.pk}), + path=reverse("config-history", kwargs={"adcm_pk": self.adcm.pk}), data={"config": {}}, content_type=APPLICATION_JSON, ) diff --git a/python/audit/tests/test_api.py b/python/audit/tests/test_api.py new file mode 100644 index 0000000000..ca4a28691e --- /dev/null +++ b/python/audit/tests/test_api.py @@ -0,0 +1,85 @@ +# 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 timedelta + +from django.urls import reverse +from rest_framework.response import Response + +from adcm.tests.base import APPLICATION_JSON, BaseTestCase +from audit.models import AuditLog, AuditSession +from cm.models import ADCM, Bundle, Prototype + + +class TestAuditAPI(BaseTestCase): + def test_filter_session_login_time(self): + login_time = AuditSession.objects.first().login_time + login_time_before = login_time - timedelta(minutes=1) + response: Response = self.client.get( + reverse("audit:auditsession-list"), + {"login_time_before": login_time_before.isoformat()}, + ) + + self.assertEqual(response.data["count"], 0) + + login_time_after = login_time + timedelta(minutes=1) + response: Response = self.client.get( + reverse("audit:auditsession-list"), + {"login_time_after": login_time_after.isoformat()}, + ) + + self.assertEqual(response.data["count"], 0) + + response: Response = self.client.get( + reverse("audit:auditsession-list"), + {"login_time_after": login_time_before.isoformat(), "login_time_before": login_time_after.isoformat()}, + ) + + self.assertEqual(response.data["count"], 1) + + def test_filter_operations_operation_time(self): + adcm = ADCM.objects.create( + prototype=Prototype.objects.create(bundle=Bundle.objects.create(), type="adcm"), name="ADCM" + ) + self.client.post( + path=reverse("config-history", kwargs={"adcm_pk": adcm.pk}), + data={"config": {}}, + content_type=APPLICATION_JSON, + ) + operation_time = AuditLog.objects.first().operation_time + operation_time_before = operation_time - timedelta(minutes=1) + + response: Response = self.client.get( + reverse("audit:auditlog-list"), + {"operation_time_before": operation_time_before.isoformat()}, + ) + + self.assertEqual(response.data["count"], 0) + + operation_time_after = operation_time + timedelta(minutes=1) + + response: Response = self.client.get( + reverse("audit:auditlog-list"), + {"operation_time_after": operation_time_after.isoformat()}, + ) + + self.assertEqual(response.data["count"], 0) + + response: Response = self.client.get( + reverse("audit:auditlog-list"), + { + "operation_time_after": operation_time_before.isoformat(), + "operation_time_before": operation_time_after.isoformat(), + }, + ) + + self.assertEqual(response.data["count"], 1) diff --git a/python/audit/tests/test_audit_object_rename.py b/python/audit/tests/test_audit_object_rename.py index fbf4d47187..267de11a04 100644 --- a/python/audit/tests/test_audit_object_rename.py +++ b/python/audit/tests/test_audit_object_rename.py @@ -1,3 +1,14 @@ +# 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 django.urls import reverse from adcm.tests.base import APPLICATION_JSON, BaseTestCase @@ -8,7 +19,7 @@ ConfigLog, Host, HostProvider, - MaintenanceModeType, + MaintenanceMode, ObjectConfig, Prototype, ) @@ -38,7 +49,7 @@ def setUp(self) -> None: fqdn="test_fqdn", prototype=host_prototype, provider=provider, - maintenance_mode=MaintenanceModeType.On, + maintenance_mode=MaintenanceMode.ON, config=config, ) @@ -115,7 +126,7 @@ def test_host_rename(self): self.client.patch( path=reverse("host-details", kwargs={"host_id": self.host.pk}), - data={"fqdn": new_test_host_fqdn, "maintenance_mode": MaintenanceModeType.On}, + data={"fqdn": new_test_host_fqdn, "maintenance_mode": MaintenanceMode.ON}, content_type=APPLICATION_JSON, ) diff --git a/python/audit/tests/test_audit_objects.py b/python/audit/tests/test_audit_objects.py index 119a8615a1..ae05ca204f 100644 --- a/python/audit/tests/test_audit_objects.py +++ b/python/audit/tests/test_audit_objects.py @@ -53,9 +53,7 @@ def _prepare_prototypes(self): # UTILITIES def create_provider_via_api(self, name: str = "Provider") -> Response: - return self.client.post( - path=reverse("provider"), data={"prototype_id": self.provider_proto.id, "name": name} - ) + return self.client.post(path=reverse("provider"), data={"prototype_id": self.provider_proto.id, "name": name}) def create_host_via_api(self, fqdn: str, provider_id: int) -> Response: return self.client.post( @@ -64,9 +62,7 @@ def create_host_via_api(self, fqdn: str, provider_id: int) -> Response: ) def create_cluster_via_api(self, name: str = "Cluster") -> Response: - return self.client.post( - path=reverse("cluster"), data={"prototype_id": self.cluster_proto.id, "name": name} - ) + return self.client.post(path=reverse("cluster"), data={"prototype_id": self.cluster_proto.id, "name": name}) def _get_id_from_create_response(self, resp: Response) -> int: self.assertEqual(resp.status_code, HTTP_201_CREATED) @@ -76,9 +72,7 @@ def _get_id_from_create_response(self, resp: Response) -> int: def test_cluster_flow(self): provider_id = self._get_id_from_create_response(self.create_provider_via_api()) - host_id = self._get_id_from_create_response( - self.create_host_via_api("test-fqdn", provider_id) - ) + host_id = self._get_id_from_create_response(self.create_host_via_api("test-fqdn", provider_id)) cluster_id = self._get_id_from_create_response(self.create_cluster_via_api()) filter_kwargs = dict(object_id=cluster_id, object_type=AuditObjectType.Cluster) @@ -92,23 +86,17 @@ def test_cluster_flow(self): data={"cluster_id": cluster_id, "prototype_id": self.service_proto.id}, ) ) - resp = self.client.post( - path=reverse("host", kwargs={"cluster_id": cluster_id}), data={"host_id": host_id} - ) + resp = self.client.post(path=reverse("host", kwargs={"cluster_id": cluster_id}), data={"host_id": host_id}) self.assertEqual(resp.status_code, HTTP_201_CREATED) self.assertEqual(AuditObject.objects.filter(**filter_kwargs).count(), 1) cluster_ao.refresh_from_db() self.assertFalse(cluster_ao.is_deleted) resp = self.client.delete( - path=reverse( - "service-details", kwargs={"cluster_id": cluster_id, "service_id": service_id} - ) + path=reverse("service-details", kwargs={"cluster_id": cluster_id, "service_id": service_id}) ) self.assertEqual(resp.status_code, HTTP_204_NO_CONTENT) - resp = self.client.delete( - path=reverse("host-details", kwargs={"cluster_id": cluster_id, "host_id": host_id}) - ) + resp = self.client.delete(path=reverse("host-details", kwargs={"cluster_id": cluster_id, "host_id": host_id})) self.assertEqual(resp.status_code, HTTP_204_NO_CONTENT) cluster_ao.refresh_from_db() self.assertFalse(cluster_ao.is_deleted) @@ -123,9 +111,7 @@ def test_cluster_flow(self): def test_provider_flow(self): provider_id = self._get_id_from_create_response(self.create_provider_via_api()) - host_id = self._get_id_from_create_response( - self.create_host_via_api("test-fqdn", provider_id) - ) + host_id = self._get_id_from_create_response(self.create_host_via_api("test-fqdn", provider_id)) self.assertEqual(AuditObject.objects.count(), 2) self.assertEqual(AuditObject.objects.filter(is_deleted=True).count(), 0) resp = self.client.post( diff --git a/python/audit/tests/test_authentication.py b/python/audit/tests/test_authentication.py index a6a440922b..0c7451edca 100644 --- a/python/audit/tests/test_authentication.py +++ b/python/audit/tests/test_authentication.py @@ -33,16 +33,12 @@ def setUp(self) -> None: object_config.save(update_fields=["current"]) ADCM.objects.create(prototype=prototype, config=object_config) - self.admin: User = User.objects.create_superuser( - username="admin", email="admin@arenadata.io", password="admin" - ) + self.admin: User = User.objects.create_superuser(username="admin", email="admin@arenadata.io", password="admin") self.disabled_user: User = User.objects.create_user( username="disabled_user", password="disabled_user", is_active=False ) - def check_audit_session( - self, user_id: int | None, login_result: AuditSessionLoginResult, username: str - ) -> None: + def check_audit_session(self, user_id: int | None, login_result: AuditSessionLoginResult, username: str) -> None: log: AuditSession = AuditSession.objects.order_by("login_time").last() self.assertEqual(log.user_id, user_id) @@ -54,23 +50,17 @@ def test_login_success(self): reverse("rest_framework:login"), data={"username": self.admin.username, "password": self.admin.username}, ) - self.check_audit_session( - self.admin.id, AuditSessionLoginResult.Success, self.admin.username - ) + self.check_audit_session(self.admin.id, AuditSessionLoginResult.Success, self.admin.username) def test_login_wrong_password(self): self.client.post( reverse("rest_framework:login"), data={"username": self.admin.username, "password": "qwerty"}, ) - self.check_audit_session( - self.admin.id, AuditSessionLoginResult.WrongPassword, self.admin.username - ) + self.check_audit_session(self.admin.id, AuditSessionLoginResult.WrongPassword, self.admin.username) self.client.post(reverse("rest_framework:login"), data={"username": self.admin.username}) - self.check_audit_session( - self.admin.id, AuditSessionLoginResult.WrongPassword, self.admin.username - ) + self.check_audit_session(self.admin.id, AuditSessionLoginResult.WrongPassword, self.admin.username) def test_login_account_disabled(self): self.client.post( @@ -104,22 +94,14 @@ def test_token_success(self): reverse("token"), data={"username": self.admin.username, "password": self.admin.username}, ) - self.check_audit_session( - self.admin.id, AuditSessionLoginResult.Success, self.admin.username - ) + self.check_audit_session(self.admin.id, AuditSessionLoginResult.Success, self.admin.username) def test_token_wrong_password(self): - self.client.post( - reverse("token"), data={"username": self.admin.username, "password": "qwerty"} - ) - self.check_audit_session( - self.admin.id, AuditSessionLoginResult.WrongPassword, self.admin.username - ) + self.client.post(reverse("token"), data={"username": self.admin.username, "password": "qwerty"}) + self.check_audit_session(self.admin.id, AuditSessionLoginResult.WrongPassword, self.admin.username) self.client.post(reverse("token"), data={"username": self.admin.username}) - self.check_audit_session( - self.admin.id, AuditSessionLoginResult.WrongPassword, self.admin.username - ) + self.check_audit_session(self.admin.id, AuditSessionLoginResult.WrongPassword, self.admin.username) def test_token_account_disabled(self): self.client.post( @@ -133,9 +115,7 @@ def test_token_account_disabled(self): ) def test_token_user_not_found(self): - self.client.post( - reverse("token"), data={"username": "unknown_user", "password": "unknown_user"} - ) + self.client.post(reverse("token"), data={"username": "unknown_user", "password": "unknown_user"}) self.check_audit_session(None, AuditSessionLoginResult.UserNotFound, "unknown_user") self.client.post(reverse("token"), data={}) @@ -152,22 +132,14 @@ def test_rbac_token_success(self): reverse("rbac:token"), data={"username": self.admin.username, "password": self.admin.username}, ) - self.check_audit_session( - self.admin.id, AuditSessionLoginResult.Success, self.admin.username - ) + self.check_audit_session(self.admin.id, AuditSessionLoginResult.Success, self.admin.username) def test_rbac_token_wrong_password(self): - self.client.post( - reverse("rbac:token"), data={"username": self.admin.username, "password": "qwerty"} - ) - self.check_audit_session( - self.admin.id, AuditSessionLoginResult.WrongPassword, self.admin.username - ) + self.client.post(reverse("rbac:token"), data={"username": self.admin.username, "password": "qwerty"}) + self.check_audit_session(self.admin.id, AuditSessionLoginResult.WrongPassword, self.admin.username) self.client.post(reverse("rbac:token"), data={"username": self.admin.username}) - self.check_audit_session( - self.admin.id, AuditSessionLoginResult.WrongPassword, self.admin.username - ) + self.check_audit_session(self.admin.id, AuditSessionLoginResult.WrongPassword, self.admin.username) def test_rbac_token_account_disabled(self): self.client.post( @@ -181,9 +153,7 @@ def test_rbac_token_account_disabled(self): ) def test_rbac_token_user_not_found(self): - self.client.post( - reverse("rbac:token"), data={"username": "unknown_user", "password": "unknown_user"} - ) + self.client.post(reverse("rbac:token"), data={"username": "unknown_user", "password": "unknown_user"}) self.check_audit_session(None, AuditSessionLoginResult.UserNotFound, "unknown_user") self.client.post(reverse("rbac:token"), data={}) diff --git a/python/audit/tests/test_bundle.py b/python/audit/tests/test_bundle.py index 75a3ec1447..f37d9b5ab9 100644 --- a/python/audit/tests/test_bundle.py +++ b/python/audit/tests/test_bundle.py @@ -11,8 +11,10 @@ # limitations under the License. from datetime import datetime +from pathlib import Path from unittest.mock import patch +from django.conf import settings from django.urls import reverse from rest_framework.response import Response from rest_framework.status import HTTP_400_BAD_REQUEST, HTTP_403_FORBIDDEN @@ -35,16 +37,16 @@ def setUp(self) -> None: super().setUp() bundle_name = "test_bundle" - self.bundle = Bundle.objects.create( + self.bundle = Bundle.objects.create(name=bundle_name) + self.prototype = Prototype.objects.create( + bundle=self.bundle, + type="cluster", name=bundle_name, license_path="test_bundle_license_path", license="unaccepted", ) - Prototype.objects.create(bundle=self.bundle, type="cluster", name=bundle_name) - def check_log_upload( - self, log: AuditLog, operation_result: AuditLogOperationResult, user: User - ) -> None: + def check_log_upload(self, log: AuditLog, operation_result: AuditLogOperationResult, user: User) -> None: self.assertFalse(log.audit_object) self.assertEqual(log.operation_name, "Bundle uploaded") self.assertEqual(log.operation_type, AuditLogOperationType.Create) @@ -53,9 +55,7 @@ def check_log_upload( self.assertEqual(log.user.pk, user.pk) self.assertEqual(log.object_changes, {}) - def check_log_load_no_obj( - self, log: AuditLog, operation_result: AuditLogOperationResult, user: User - ) -> None: + def check_log_load_no_obj(self, log: AuditLog, operation_result: AuditLogOperationResult, user: User) -> None: self.assertFalse(log.audit_object) self.assertEqual(log.operation_name, "Bundle loaded") self.assertEqual(log.operation_type, AuditLogOperationType.Create) @@ -64,9 +64,7 @@ def check_log_load_no_obj( self.assertEqual(log.user.pk, user.pk) self.assertEqual(log.object_changes, {}) - def check_log_denied( - self, log: AuditLog, operation_name: str, operation_type: AuditLogOperationType - ) -> None: + def check_log_denied(self, log: AuditLog, operation_name: str, operation_type: AuditLogOperationType) -> None: self.assertEqual(log.audit_object.object_id, self.bundle.pk) self.assertEqual(log.audit_object.object_name, self.bundle.name) self.assertEqual(log.audit_object.object_type, AuditObjectType.Bundle) @@ -78,6 +76,18 @@ def check_log_denied( self.assertEqual(log.user.pk, self.no_rights_user.pk) self.assertEqual(log.object_changes, {}) + def check_prototype_licence(self, log: AuditLog, operation_result: AuditLogOperationResult, user: User): + self.assertEqual(log.audit_object.object_id, self.prototype.pk) + self.assertEqual(log.audit_object.object_name, self.prototype.name) + self.assertEqual(log.audit_object.object_type, AuditObjectType.Prototype) + self.assertFalse(log.audit_object.is_deleted) + self.assertEqual(log.operation_name, "Cluster license accepted") + self.assertEqual(log.operation_type, AuditLogOperationType.Update) + self.assertEqual(log.operation_result, operation_result) + self.assertIsInstance(log.operation_time, datetime) + self.assertEqual(log.user.pk, user.pk) + self.assertEqual(log.object_changes, {}) + def check_log_deleted(self, log: AuditLog, operation_result: AuditLogOperationResult): self.assertEqual(log.audit_object.object_id, self.bundle.pk) self.assertEqual(log.audit_object.object_name, self.bundle.name) @@ -97,7 +107,7 @@ def load_bundle(self) -> Response: ) def upload_bundle(self) -> None: - with open(self.test_bundle_path, encoding="utf-8") as f: + with open(self.test_bundle_path, encoding=settings.ENCODING_UTF_8) as f: self.client.post( path=reverse("upload-bundle"), data={"file": f}, @@ -127,12 +137,12 @@ def test_upload_success(self): log: AuditLog = AuditLog.objects.first() - self.check_log_upload( - log=log, operation_result=AuditLogOperationResult.Success, user=self.test_user - ) + self.check_log_upload(log=log, operation_result=AuditLogOperationResult.Success, user=self.test_user) + + Path(settings.DOWNLOAD_DIR, self.test_bundle_filename).unlink() def test_upload_fail(self): - with open(self.test_bundle_path, encoding="utf-8") as f: + with open(self.test_bundle_path, encoding=settings.ENCODING_UTF_8) as f: self.client.post( path=reverse("upload-bundle"), data={"no_file": f}, @@ -140,24 +150,20 @@ def test_upload_fail(self): log: AuditLog = AuditLog.objects.first() - self.check_log_upload( - log=log, operation_result=AuditLogOperationResult.Fail, user=self.test_user - ) + self.check_log_upload(log=log, operation_result=AuditLogOperationResult.Fail, user=self.test_user) def test_upload_denied(self): - with open(self.test_bundle_path, encoding="utf-8") as f: + with open(self.test_bundle_path, encoding=settings.ENCODING_UTF_8) as f: with self.no_rights_user_logged_in: response: Response = self.client.post( path=reverse("upload-bundle"), - data={"no_file": f}, + data={"file": f}, ) log: AuditLog = AuditLog.objects.first() self.assertEqual(response.status_code, HTTP_403_FORBIDDEN) - self.check_log_upload( - log=log, operation_result=AuditLogOperationResult.Denied, user=self.no_rights_user - ) + self.check_log_upload(log=log, operation_result=AuditLogOperationResult.Denied, user=self.no_rights_user) def test_load(self): self.upload_bundle_and_check() @@ -165,9 +171,7 @@ def test_load(self): log: AuditLog = AuditLog.objects.order_by("operation_time").last() - self.check_log_load_no_obj( - log=log, operation_result=AuditLogOperationResult.Fail, user=self.test_user - ) + self.check_log_load_no_obj(log=log, operation_result=AuditLogOperationResult.Fail, user=self.test_user) def test_load_failed(self): self.client.post( @@ -177,9 +181,7 @@ def test_load_failed(self): log: AuditLog = AuditLog.objects.order_by("operation_time").last() - self.check_log_load_no_obj( - log=log, operation_result=AuditLogOperationResult.Fail, user=self.test_user - ) + self.check_log_load_no_obj(log=log, operation_result=AuditLogOperationResult.Fail, user=self.test_user) response: Response = self.client.post( path=reverse("load-bundle"), @@ -189,12 +191,10 @@ def test_load_failed(self): log: AuditLog = AuditLog.objects.order_by("operation_time").last() self.assertEqual(response.status_code, HTTP_400_BAD_REQUEST) - self.check_log_load_no_obj( - log=log, operation_result=AuditLogOperationResult.Fail, user=self.test_user - ) + self.check_log_load_no_obj(log=log, operation_result=AuditLogOperationResult.Fail, user=self.test_user) def test_load_denied(self): - self.upload_bundle_and_check() + self.upload_bundle() with self.no_rights_user_logged_in: response: Response = self.load_bundle() @@ -202,9 +202,7 @@ def test_load_denied(self): log: AuditLog = AuditLog.objects.order_by("operation_time").last() self.assertEqual(response.status_code, HTTP_403_FORBIDDEN) - self.check_log_load_no_obj( - log=log, operation_result=AuditLogOperationResult.Denied, user=self.no_rights_user - ) + self.check_log_load_no_obj(log=log, operation_result=AuditLogOperationResult.Denied, user=self.no_rights_user) def test_load_and_delete(self): response: Response = self.upload_bundle_and_check() @@ -217,8 +215,7 @@ def test_load_and_delete(self): def test_update(self): with patch("api.stack.views.update_bundle"): self.client.put( - path=reverse("bundle-update", kwargs={"bundle_id": self.bundle.pk}), - data={"name": "new_bundle_name"}, + path=reverse("bundle-update", kwargs={"bundle_pk": self.bundle.pk}), ) log: AuditLog = AuditLog.objects.order_by("operation_time").last() @@ -237,19 +234,16 @@ def test_update(self): def test_update_denied(self): with self.no_rights_user_logged_in: response: Response = self.client.put( - path=reverse("bundle-update", kwargs={"bundle_id": self.bundle.pk}), - data={"name": "new_bundle_name"}, + path=reverse("bundle-update", kwargs={"bundle_pk": self.bundle.pk}), ) log: AuditLog = AuditLog.objects.order_by("operation_time").last() self.assertEqual(response.status_code, HTTP_403_FORBIDDEN) - self.check_log_denied( - log=log, operation_name="Bundle updated", operation_type=AuditLogOperationType.Update - ) + self.check_log_denied(log=log, operation_name="Bundle updated", operation_type=AuditLogOperationType.Update) def test_license_accepted(self): - self.client.put(path=reverse("accept-license", kwargs={"bundle_id": self.bundle.pk})) + self.client.put(path=reverse("accept-license", kwargs={"bundle_pk": self.bundle.pk})) log: AuditLog = AuditLog.objects.order_by("operation_time").last() @@ -266,9 +260,7 @@ def test_license_accepted(self): def test_license_accepted_denied(self): with self.no_rights_user_logged_in: - response: Response = self.client.put( - path=reverse("accept-license", kwargs={"bundle_id": self.bundle.pk}) - ) + response: Response = self.client.put(path=reverse("accept-license", kwargs={"bundle_pk": self.bundle.pk})) log: AuditLog = AuditLog.objects.order_by("operation_time").last() @@ -279,9 +271,26 @@ def test_license_accepted_denied(self): operation_type=AuditLogOperationType.Update, ) + def test_prototype_license_accepted(self): + self.client.put(path=reverse("accept-license", kwargs={"prototype_pk": self.prototype.pk})) + + log: AuditLog = AuditLog.objects.order_by("operation_time").last() + self.check_prototype_licence(log, AuditLogOperationResult.Success, self.test_user) + + def test_prototype_license_accepted_denied(self): + with self.no_rights_user_logged_in: + response: Response = self.client.put( + path=reverse("accept-license", kwargs={"prototype_pk": self.prototype.pk}) + ) + + log: AuditLog = AuditLog.objects.order_by("operation_time").last() + + self.assertEqual(response.status_code, HTTP_403_FORBIDDEN) + self.check_prototype_licence(log, AuditLogOperationResult.Denied, self.no_rights_user) + def test_delete(self): with patch("api.stack.views.delete_bundle"): - self.client.delete(path=reverse("bundle-details", kwargs={"bundle_id": self.bundle.pk})) + self.client.delete(path=reverse("bundle-detail", kwargs={"bundle_pk": self.bundle.pk})) log: AuditLog = AuditLog.objects.order_by("operation_time").last() @@ -289,23 +298,19 @@ def test_delete(self): def test_delete_denied(self): with self.no_rights_user_logged_in: - response: Response = self.client.delete( - path=reverse("bundle-details", kwargs={"bundle_id": self.bundle.pk}) - ) + response: Response = self.client.delete(path=reverse("bundle-detail", kwargs={"bundle_pk": self.bundle.pk})) log: AuditLog = AuditLog.objects.order_by("operation_time").last() self.assertEqual(response.status_code, HTTP_403_FORBIDDEN) - self.check_log_denied( - log=log, operation_name="Bundle deleted", operation_type=AuditLogOperationType.Delete - ) + self.check_log_denied(log=log, operation_name="Bundle deleted", operation_type=AuditLogOperationType.Delete) def test_delete_failed(self): Cluster.objects.create( prototype=Prototype.objects.create(bundle=self.bundle, type="cluster"), name="test_cluster", ) - self.client.delete(path=reverse("bundle-details", kwargs={"bundle_id": self.bundle.pk})) + self.client.delete(path=reverse("bundle-detail", kwargs={"bundle_pk": self.bundle.pk})) log: AuditLog = AuditLog.objects.order_by("operation_time").last() diff --git a/python/audit/tests/test_cluster.py b/python/audit/tests/test_cluster.py index a9b71e6cc1..44fa5dbb96 100644 --- a/python/audit/tests/test_cluster.py +++ b/python/audit/tests/test_cluster.py @@ -44,7 +44,6 @@ Host, HostComponent, HostProvider, - MaintenanceModeType, ObjectConfig, Prototype, PrototypeExport, @@ -72,9 +71,7 @@ def setUp(self) -> None: config.current = self.config_log.pk config.save(update_fields=["current"]) - self.cluster = Cluster.objects.create( - prototype=self.cluster_prototype, name="test_cluster_2", config=config - ) + self.cluster = Cluster.objects.create(prototype=self.cluster_prototype, name="test_cluster_2", config=config) self.service_prototype = Prototype.objects.create( bundle=self.bundle, type="service", @@ -105,9 +102,7 @@ def setUp(self) -> None: self.cluster_deleted_str = "Cluster deleted" self.action_display_name = "test_cluster_action" - def check_log_no_obj( - self, log: AuditLog, operation_result: AuditLogOperationResult, user: User - ) -> None: + def check_log_no_obj(self, log: AuditLog, operation_result: AuditLogOperationResult, user: User) -> None: self.assertFalse(log.audit_object) self.assertEqual(log.operation_name, "Cluster created") self.assertEqual(log.operation_type, AuditLogOperationType.Create) @@ -116,7 +111,7 @@ def check_log_no_obj( self.assertEqual(log.user.pk, user.pk) self.assertEqual(log.object_changes, {}) - def check_log( # pylint: disable=too-many-arguments + def check_log( self, log: AuditLog, obj: Cluster | Host | HostComponent | ClusterObject | ServiceComponent, @@ -144,9 +139,7 @@ def check_log( # pylint: disable=too-many-arguments self.assertEqual(log.user.pk, user.pk) self.assertDictEqual(log.object_changes, object_changes) - def check_log_denied( - self, log: AuditLog, operation_name: str, operation_type: AuditLogOperationType - ) -> None: + def check_log_denied(self, log: AuditLog, operation_name: str, operation_type: AuditLogOperationType) -> None: self.assertEqual(log.audit_object.object_id, self.cluster.pk) self.assertEqual(log.audit_object.object_name, self.cluster.name) self.assertEqual(log.audit_object.object_type, AuditObjectType.Cluster) @@ -214,9 +207,7 @@ def get_sc(self) -> HostComponent: def get_cluster_service_for_bind(self): bundle = Bundle.objects.create(name="test_bundle_2") - cluster_prototype = Prototype.objects.create( - bundle=bundle, type="cluster", name="Export_cluster" - ) + cluster_prototype = Prototype.objects.create(bundle=bundle, type="cluster", name="Export_cluster") service_prototype = Prototype.objects.create( bundle=bundle, type="service", @@ -317,7 +308,7 @@ def test_delete_two_clusters(self): with open( Path(settings.BASE_DIR, "python/audit/tests/files", cluster_bundle_filename), - encoding="utf-8", + encoding=settings.ENCODING_UTF_8, ) as f: self.client.post( path=reverse("upload-bundle"), @@ -331,7 +322,7 @@ def test_delete_two_clusters(self): with open( Path(settings.BASE_DIR, "python/audit/tests/files", provider_bundle_filename), - encoding="utf-8", + encoding=settings.ENCODING_UTF_8, ) as f: self.client.post( path=reverse("upload-bundle"), @@ -343,9 +334,7 @@ def test_delete_two_clusters(self): data={"bundle_file": provider_bundle_filename}, ) - cluster_prototype = Prototype.objects.create( - bundle_id=cluster_bundle_response.data["id"], type="cluster" - ) + cluster_prototype = Prototype.objects.create(bundle_id=cluster_bundle_response.data["id"], type="cluster") cluster_1_response: Response = self.create_cluster( bundle_id=cluster_bundle_response.data["id"], name="new-test-cluster-1", @@ -357,9 +346,7 @@ def test_delete_two_clusters(self): prototype_id=cluster_prototype.pk, ) - provider_prototype = Prototype.objects.create( - bundle_id=provider_bundle_response.data["id"], type="provider" - ) + provider_prototype = Prototype.objects.create(bundle_id=provider_bundle_response.data["id"], type="provider") provider_response: Response = self.client.post( path=reverse("provider"), data={ @@ -368,9 +355,7 @@ def test_delete_two_clusters(self): }, ) - host_prototype = Prototype.objects.create( - bundle_id=provider_bundle_response.data["id"], type="host" - ) + host_prototype = Prototype.objects.create(bundle_id=provider_bundle_response.data["id"], type="host") host_1_response: Response = self.client.post( path=reverse("host"), data={ @@ -414,9 +399,7 @@ def test_delete_two_clusters(self): self.assertFalse(AuditObject.objects.filter(is_deleted=True)) - self.client.delete( - path=reverse("cluster-details", kwargs={"cluster_id": cluster_1_response.data["id"]}) - ) + self.client.delete(path=reverse("cluster-details", kwargs={"cluster_id": cluster_1_response.data["id"]})) self.assertEqual(AuditObject.objects.filter(is_deleted=True).count(), 1) @@ -434,9 +417,7 @@ def test_delete(self): operation_type=AuditLogOperationType.Delete, ) - response: Response = self.client.delete( - path=reverse("cluster-details", kwargs={"cluster_id": self.cluster.pk}) - ) + response: Response = self.client.delete(path=reverse("cluster-details", kwargs={"cluster_id": self.cluster.pk})) log: AuditLog = AuditLog.objects.order_by("operation_time").last() @@ -445,9 +426,7 @@ def test_delete(self): def test_delete_failed(self): cluster_pks = ClusterObject.objects.all().values_list("pk", flat=True).order_by("-pk") - res = self.client.delete( - path=reverse("cluster-details", kwargs={"cluster_id": cluster_pks[0] + 1}) - ) + res = self.client.delete(path=reverse("cluster-details", kwargs={"cluster_id": cluster_pks[0] + 1})) log: AuditLog = AuditLog.objects.order_by("operation_time").last() @@ -555,9 +534,7 @@ def test_bind_unbind_empty_data(self): ) self.client.delete( - path=reverse( - "cluster-bind-details", kwargs={"cluster_id": self.cluster.pk, "bind_id": 411} - ), + path=reverse("cluster-bind-details", kwargs={"cluster_id": self.cluster.pk, "bind_id": 411}), content_type=APPLICATION_JSON, ) @@ -597,9 +574,7 @@ def test_bind_unbind_cluster_to_cluster(self): bind = ClusterBind.objects.first() self.client.delete( - path=reverse( - "cluster-bind-details", kwargs={"cluster_id": self.cluster.pk, "bind_id": bind.pk} - ), + path=reverse("cluster-bind-details", kwargs={"cluster_id": self.cluster.pk, "bind_id": bind.pk}), content_type=APPLICATION_JSON, ) @@ -638,9 +613,7 @@ def test_bind_unbind_service_to_cluster(self): bind = ClusterBind.objects.first() self.client.delete( - path=reverse( - "cluster-bind-details", kwargs={"cluster_id": self.cluster.pk, "bind_id": bind.pk} - ), + path=reverse("cluster-bind-details", kwargs={"cluster_id": self.cluster.pk, "bind_id": bind.pk}), content_type=APPLICATION_JSON, ) @@ -807,7 +780,9 @@ def test_update_host_config(self): ) def test_update_host(self): - data = {"description": self.description, "maintenance_mode": "on"} + data = {"description": self.description} + self.cluster.prototype.allow_maintenance_mode = True + self.cluster.prototype.save(update_fields=["allow_maintenance_mode"]) self.host.cluster = self.cluster self.host.save(update_fields=["cluster"]) self.client.patch( @@ -826,14 +801,17 @@ def test_update_host(self): obj_type=AuditObjectType.Host, operation_name=self.host_updated, operation_type=AuditLogOperationType.Update, - operation_result=AuditLogOperationResult.Fail, + object_changes={ + "current": data, + "previous": {"description": ""}, + }, ) self.client.patch( path=reverse( "host-details", kwargs={"cluster_id": self.cluster.pk, "host_id": self.host.pk}, ), - data={"fqdn": "new_test_fqdn"}, + data={"fqdn": "new-test-fqdn"}, content_type=APPLICATION_JSON, ) log: AuditLog = AuditLog.objects.order_by("operation_time").last() @@ -846,30 +824,6 @@ def test_update_host(self): operation_type=AuditLogOperationType.Update, operation_result=AuditLogOperationResult.Fail, ) - self.host.maintenance_mode = MaintenanceModeType.Off - self.host.save(update_fields=["maintenance_mode"]) - - self.client.patch( - path=reverse( - "host-details", - kwargs={"cluster_id": self.cluster.pk, "host_id": self.host.pk}, - ), - data=data, - content_type=APPLICATION_JSON, - ) - log: AuditLog = AuditLog.objects.order_by("operation_time").last() - self.check_log( - log=log, - obj=self.host, - obj_name=self.host.name, - obj_type=AuditObjectType.Host, - operation_name=self.host_updated, - operation_type=AuditLogOperationType.Update, - object_changes={ - "current": data, - "previous": {"description": "", "maintenance_mode": "off"}, - }, - ) def test_update_host_config_denied(self): with self.no_rights_user_logged_in: @@ -1724,11 +1678,7 @@ def test_action_launch(self): state_available="any", ) with patch("api.action.views.create", return_value=Response(status=HTTP_201_CREATED)): - self.client.post( - path=reverse( - "run-task", kwargs={"cluster_id": self.cluster.pk, "action_id": action.pk} - ) - ) + self.client.post(path=reverse("run-task", kwargs={"cluster_id": self.cluster.pk, "action_id": action.pk})) log: AuditLog = AuditLog.objects.order_by("operation_time").last() @@ -1753,7 +1703,10 @@ def test_do_upgrade(self): max_version="99", ) - with patch("api.cluster.views.create", return_value=Response(status=HTTP_201_CREATED)): + with ( + patch("api.cluster.views.do_upgrade", return_value={}), + patch("api.cluster.views.check_obj"), + ): self.client.post( path=reverse( "do-cluster-upgrade", @@ -1777,7 +1730,10 @@ def test_do_upgrade_no_action(self): max_version="99", ) - with patch("api.cluster.views.create", return_value=Response(status=HTTP_201_CREATED)): + with ( + patch("api.cluster.views.do_upgrade", return_value={}), + patch("api.cluster.views.check_obj"), + ): self.client.post( path=reverse( "do-cluster-upgrade", diff --git a/python/audit/tests/test_component.py b/python/audit/tests/test_component.py index a610af02ea..e7ebdea161 100644 --- a/python/audit/tests/test_component.py +++ b/python/audit/tests/test_component.py @@ -30,6 +30,7 @@ Cluster, ClusterObject, ConfigLog, + MaintenanceMode, ObjectConfig, Prototype, ServiceComponent, @@ -49,9 +50,7 @@ def setUp(self) -> None: type="service", display_name="test_service", ) - self.service = ClusterObject.objects.create( - prototype=service_prototype, cluster=self.cluster - ) + self.service = ClusterObject.objects.create(prototype=service_prototype, cluster=self.cluster) self.component_prototype = Prototype.objects.create( bundle=bundle, type="component", @@ -76,7 +75,12 @@ def check_log( log: AuditLog, operation_result: AuditLogOperationResult = AuditLogOperationResult.Success, user: User | None = None, + operation_name: str = "Component configuration updated", + object_changes: dict | None = None, ): + if object_changes is None: + object_changes = {} + if user is None: user = self.test_user @@ -87,12 +91,12 @@ def check_log( ) self.assertEqual(log.audit_object.object_type, AuditObjectType.Component) self.assertFalse(log.audit_object.is_deleted) - self.assertEqual(log.operation_name, "Component configuration updated") + self.assertEqual(log.operation_name, operation_name) self.assertEqual(log.operation_type, AuditLogOperationType.Update) self.assertEqual(log.operation_result, operation_result) self.assertEqual(log.user.pk, user.pk) self.assertIsInstance(log.operation_time, datetime) - self.assertEqual(log.object_changes, {}) + self.assertEqual(log.object_changes, object_changes) def check_action_log(self, log: AuditLog) -> None: self.assertEqual(log.audit_object.object_id, self.component.pk) @@ -108,7 +112,7 @@ def check_action_log(self, log: AuditLog) -> None: self.assertIsInstance(log.operation_time, datetime) self.assertEqual(log.object_changes, {}) - def test_update(self): + def test_update_config(self): self.client.post( path=reverse("config-history", kwargs={"component_id": self.component.pk}), data={"config": {}}, @@ -119,7 +123,7 @@ def test_update(self): self.check_log(log=log) - def test_restore(self): + def test_restore_config(self): self.client.patch( path=reverse( "config-history-version-restore", @@ -132,7 +136,7 @@ def test_restore(self): self.check_log(log) - def test_restore_denied(self): + def test_restore_config_denied(self): with self.no_rights_user_logged_in: response: Response = self.client.patch( path=reverse( @@ -151,7 +155,7 @@ def test_restore_denied(self): user=self.no_rights_user, ) - def test_update_via_service(self): + def test_update_config_via_service(self): self.client.post( path=reverse( "config-history", @@ -165,7 +169,7 @@ def test_update_via_service(self): self.check_log(log) - def test_update_via_service_denied(self): + def test_update_config_via_service_denied(self): with self.no_rights_user_logged_in: response: Response = self.client.post( path=reverse( @@ -185,7 +189,7 @@ def test_update_via_service_denied(self): user=self.no_rights_user, ) - def test_restore_via_service(self): + def test_restore_config_via_service(self): self.client.patch( path=reverse( "config-history-version-restore", @@ -202,7 +206,7 @@ def test_restore_via_service(self): self.check_log(log) - def test_restore_via_service_denied(self): + def test_restore_config_via_service_denied(self): with self.no_rights_user_logged_in: response: Response = self.client.patch( path=reverse( @@ -279,3 +283,85 @@ def test_action_launch(self): log: AuditLog = AuditLog.objects.order_by("operation_time").last() self.check_action_log(log=log) + + def test_change_maintenance_mode(self): + self.client.post( + path=reverse("component-maintenance-mode", kwargs={"component_id": self.component.pk}), + data={"maintenance_mode": MaintenanceMode.ON}, + ) + + log: AuditLog = AuditLog.objects.order_by("operation_time").last() + + self.check_log( + log=log, + operation_name="Component updated", + object_changes={"current": {"maintenance_mode": "ON"}, "previous": {"maintenance_mode": "OFF"}}, + ) + + def test_change_maintenance_mode_via_service(self): + self.client.post( + path=reverse( + "component-maintenance-mode", + kwargs={"service_id": self.service.pk, "component_id": self.component.pk}, + ), + data={"maintenance_mode": MaintenanceMode.ON}, + ) + + log: AuditLog = AuditLog.objects.order_by("operation_time").last() + + self.check_log( + log=log, + operation_name="Component updated", + object_changes={"current": {"maintenance_mode": "ON"}, "previous": {"maintenance_mode": "OFF"}}, + ) + + def test_change_maintenance_mode_via_cluster(self): + self.client.post( + path=reverse( + "component-maintenance-mode", + kwargs={ + "cluster_id": self.cluster.pk, + "service_id": self.service.pk, + "component_id": self.component.pk, + }, + ), + data={"maintenance_mode": MaintenanceMode.ON}, + ) + + log: AuditLog = AuditLog.objects.order_by("operation_time").last() + + self.check_log( + log=log, + operation_name="Component updated", + object_changes={"current": {"maintenance_mode": "ON"}, "previous": {"maintenance_mode": "OFF"}}, + ) + + def test_change_maintenance_mode_failed(self): + self.client.post( + path=reverse("component-maintenance-mode", kwargs={"component_id": self.component.pk}), + data={"maintenance_mode": MaintenanceMode.CHANGING}, + ) + + log: AuditLog = AuditLog.objects.order_by("operation_time").last() + + self.check_log( + log=log, + operation_name="Component updated", + operation_result=AuditLogOperationResult.Fail, + ) + + def test_change_maintenance_mode_denied(self): + with self.no_rights_user_logged_in: + self.client.post( + path=reverse("component-maintenance-mode", kwargs={"component_id": self.component.pk}), + data={"maintenance_mode": MaintenanceMode.ON}, + ) + + log: AuditLog = AuditLog.objects.order_by("operation_time").last() + + self.check_log( + log=log, + operation_name="Component updated", + operation_result=AuditLogOperationResult.Denied, + user=self.no_rights_user, + ) diff --git a/python/audit/tests/test_config_log.py b/python/audit/tests/test_config_log.py index 9831f67b7d..246735f70f 100644 --- a/python/audit/tests/test_config_log.py +++ b/python/audit/tests/test_config_log.py @@ -99,8 +99,7 @@ def test_create_denied(self): def test_create_via_group_config(self): self.client.post( - path=f"/api/v1/group-config/{self.group_config.pk}/" - f"config/{self.config.pk}/config-log/", + path=f"/api/v1/group-config/{self.group_config.pk}/" f"config/{self.config.pk}/config-log/", data={"obj_ref": self.config.pk, "config": "{}"}, ) @@ -116,8 +115,7 @@ def test_create_via_group_config(self): def test_create_via_group_config_denied(self): with self.no_rights_user_logged_in: response: Response = self.client.post( - path=f"/api/v1/group-config/{self.group_config.pk}/" - f"config/{self.config.pk}/config-log/", + path=f"/api/v1/group-config/{self.group_config.pk}/" f"config/{self.config.pk}/config-log/", data={"obj_ref": self.config.pk, "config": "{}"}, ) diff --git a/python/audit/tests/test_group_config.py b/python/audit/tests/test_group_config.py index 5054d9c121..819348f56c 100644 --- a/python/audit/tests/test_group_config.py +++ b/python/audit/tests/test_group_config.py @@ -70,9 +70,7 @@ def setUp(self) -> None: object_type=ContentType.objects.get(app_label="cm", model="cluster"), config_id=self.config.pk, ) - self.host = Host.objects.create( - fqdn="test_host_fqdn", prototype=prototype, cluster=self.cluster - ) + self.host = Host.objects.create(fqdn="test_host_fqdn", prototype=prototype, cluster=self.cluster) self.conf_group_created_str = "configuration group created" self.created_operation_name = f"{self.name} {self.conf_group_created_str}" @@ -120,7 +118,7 @@ def get_component(self): config=self.config, ) - def check_log( # pylint: disable=too-many-arguments + def check_log( self, log: AuditLog, obj, @@ -158,9 +156,7 @@ def check_log_no_obj( self.assertEqual(log.user.pk, user.pk) self.assertEqual(log.object_changes, {}) - def check_log_updated( - self, log: AuditLog, operation_result: AuditLogOperationResult, user: User - ) -> None: + def check_log_updated(self, log: AuditLog, operation_result: AuditLogOperationResult, user: User) -> None: self.check_log( log=log, obj=self.cluster, @@ -291,8 +287,7 @@ def test_create_for_component(self): self.check_log( log=log, obj=component, - obj_name=f"{self.cluster.name}/{component.service.display_name}" - f"/{component.display_name}", + obj_name=f"{self.cluster.name}/{component.service.display_name}" f"/{component.display_name}", obj_type=AuditObjectType.Component, operation_name=self.created_operation_name, operation_type=AuditLogOperationType.Create, @@ -454,8 +449,7 @@ def test_add_remove_host(self): obj=self.cluster, obj_name=self.cluster.name, obj_type=AuditObjectType.Cluster, - operation_name=f"{self.host.fqdn} host added to " - f"{self.group_config.name} configuration group", + operation_name=f"{self.host.fqdn} host added to " f"{self.group_config.name} configuration group", operation_type=AuditLogOperationType.Update, operation_result=AuditLogOperationResult.Success, user=self.test_user, @@ -472,8 +466,7 @@ def test_add_remove_host(self): obj=self.cluster, obj_name=self.cluster.name, obj_type=AuditObjectType.Cluster, - operation_name=f"{self.host.fqdn} host removed from " - f"{self.group_config.name} configuration group", + operation_name=f"{self.host.fqdn} host removed from " f"{self.group_config.name} configuration group", operation_type=AuditLogOperationType.Update, operation_result=AuditLogOperationResult.Success, user=self.test_user, @@ -515,8 +508,7 @@ def test_add_remove_host_denied(self): obj=self.cluster, obj_name=self.cluster.name, obj_type=AuditObjectType.Cluster, - operation_name=f"{self.host.fqdn} host added to " - f"{self.group_config.name} configuration group", + operation_name=f"{self.host.fqdn} host added to " f"{self.group_config.name} configuration group", operation_type=AuditLogOperationType.Update, operation_result=AuditLogOperationResult.Denied, user=self.no_rights_user, @@ -542,8 +534,7 @@ def test_add_remove_host_denied(self): obj=self.cluster, obj_name=self.cluster.name, obj_type=AuditObjectType.Cluster, - operation_name=f"{self.host.fqdn} host removed from " - f"{self.group_config.name} configuration group", + operation_name=f"{self.host.fqdn} host removed from " f"{self.group_config.name} configuration group", operation_type=AuditLogOperationType.Update, operation_result=AuditLogOperationResult.Denied, user=self.no_rights_user, @@ -581,7 +572,7 @@ def create_cluster_from_bundle(self): "python/audit/tests/files", test_bundle_filename, ) - with open(test_bundle_path, encoding="utf-8") as f: + with open(test_bundle_path, encoding=settings.ENCODING_UTF_8) as f: response: Response = self.client.post( path=reverse("upload-bundle"), data={"file": f}, @@ -639,16 +630,13 @@ def test_group_config_operation_name(self): log: AuditLog = AuditLog.objects.order_by("operation_time").last() self.assertEqual(response.status_code, HTTP_201_CREATED) - self.check_log( - log=log, operation_result=AuditLogOperationResult.Success, user=self.test_user - ) + self.check_log(log=log, operation_result=AuditLogOperationResult.Success, user=self.test_user) def test_group_config_operation_name_denied(self): self.create_cluster_from_bundle() with self.no_rights_user_logged_in: response: Response = self.client.post( - path=f"/api/v1/group-config/{self.group_config_id}" - f"/config/{self.config_id}/config-log/", + path=f"/api/v1/group-config/{self.group_config_id}" f"/config/{self.config_id}/config-log/", data={ "config": {"param_1": "aaa", "param_2": None, "param_3": None}, "attr": { @@ -662,9 +650,7 @@ def test_group_config_operation_name_denied(self): log: AuditLog = AuditLog.objects.order_by("operation_time").last() self.assertEqual(response.status_code, HTTP_403_FORBIDDEN) - self.check_log( - log=log, operation_result=AuditLogOperationResult.Denied, user=self.no_rights_user - ) + self.check_log(log=log, operation_result=AuditLogOperationResult.Denied, user=self.no_rights_user) def test_group_config_operation_name_failed(self): self.create_cluster_from_bundle() diff --git a/python/audit/tests/test_host.py b/python/audit/tests/test_host.py index 0969bac2a1..2251f56a77 100644 --- a/python/audit/tests/test_host.py +++ b/python/audit/tests/test_host.py @@ -38,6 +38,7 @@ ConfigLog, Host, HostProvider, + MaintenanceMode, ObjectConfig, Prototype, ) @@ -72,7 +73,7 @@ def setUp(self) -> None: self.host_created_str = "Host created" self.action_display_name = "test_host_action" self.cluster = Cluster.objects.create( - prototype=Prototype.objects.create(bundle=self.bundle, type="cluster"), + prototype=Prototype.objects.create(bundle=self.bundle, type="cluster", allow_maintenance_mode=True), name="test_cluster", ) @@ -99,6 +100,7 @@ def check_host_updated_log( ) -> None: if object_changes is None: object_changes = {} + if user is None: user = self.test_user @@ -318,16 +320,12 @@ def test_delete_via_cluster_failed(self): def test_delete_denied(self): with self.no_rights_user_logged_in: - response: Response = self.client.delete( - path=reverse("host-details", kwargs={"host_id": self.host.pk}) - ) + response: Response = self.client.delete(path=reverse("host-details", kwargs={"host_id": self.host.pk})) log: AuditLog = AuditLog.objects.order_by("operation_time").last() self.assertEqual(response.status_code, HTTP_404_NOT_FOUND) - self.check_host_deleted_log( - log=log, operation_result=AuditLogOperationResult.Denied, user=self.no_rights_user - ) + self.check_host_deleted_log(log=log, operation_result=AuditLogOperationResult.Denied, user=self.no_rights_user) def test_delete_failed(self): self.host.cluster = self.cluster @@ -341,9 +339,7 @@ def test_delete_failed(self): def test_delete_via_provider(self): self.client.delete( - path=reverse( - "host-details", kwargs={"host_id": self.host.pk, "provider_id": self.provider.pk} - ), + path=reverse("host-details", kwargs={"host_id": self.host.pk, "provider_id": self.provider.pk}), ) log: AuditLog = AuditLog.objects.order_by("operation_time").last() @@ -362,18 +358,14 @@ def test_delete_via_provider_denied(self): log: AuditLog = AuditLog.objects.order_by("operation_time").last() self.assertEqual(response.status_code, HTTP_404_NOT_FOUND) - self.check_host_deleted_log( - log=log, operation_result=AuditLogOperationResult.Denied, user=self.no_rights_user - ) + self.check_host_deleted_log(log=log, operation_result=AuditLogOperationResult.Denied, user=self.no_rights_user) def test_delete_via_provider_failed(self): self.host.cluster = self.cluster self.host.save(update_fields=["cluster"]) self.client.delete( - path=reverse( - "host-details", kwargs={"host_id": self.host.pk, "provider_id": self.provider.pk} - ), + path=reverse("host-details", kwargs={"host_id": self.host.pk, "provider_id": self.provider.pk}), ) log: AuditLog = AuditLog.objects.order_by("operation_time").last() @@ -449,7 +441,7 @@ def test_update_host(self): self.client.patch( path=reverse("host-details", kwargs={"host_id": self.host.pk}), - data={"maintenance_mode": "on"}, + data={"fqdn": "/*-/*-"}, content_type=APPLICATION_JSON, ) @@ -472,9 +464,7 @@ def test_update_and_restore_denied(self): log: AuditLog = AuditLog.objects.order_by("operation_time").last() self.assertEqual(response.status_code, HTTP_403_FORBIDDEN) - self.check_host_updated_log( - log=log, operation_result=AuditLogOperationResult.Denied, user=self.no_rights_user - ) + self.check_host_updated_log(log=log, operation_result=AuditLogOperationResult.Denied, user=self.no_rights_user) with self.no_rights_user_logged_in: response: Response = self.client.patch( @@ -488,9 +478,7 @@ def test_update_and_restore_denied(self): log: AuditLog = AuditLog.objects.order_by("operation_time").last() self.assertEqual(response.status_code, HTTP_403_FORBIDDEN) - self.check_host_updated_log( - log=log, operation_result=AuditLogOperationResult.Denied, user=self.no_rights_user - ) + self.check_host_updated_log(log=log, operation_result=AuditLogOperationResult.Denied, user=self.no_rights_user) def test_update_and_restore_via_provider(self): self.client.post( @@ -537,9 +525,7 @@ def test_update_and_restore_via_provider_denied(self): log: AuditLog = AuditLog.objects.order_by("operation_time").last() self.assertEqual(response.status_code, HTTP_403_FORBIDDEN) - self.check_host_updated_log( - log=log, operation_result=AuditLogOperationResult.Denied, user=self.no_rights_user - ) + self.check_host_updated_log(log=log, operation_result=AuditLogOperationResult.Denied, user=self.no_rights_user) with self.no_rights_user_logged_in: response: Response = self.client.patch( @@ -553,9 +539,7 @@ def test_update_and_restore_via_provider_denied(self): log: AuditLog = AuditLog.objects.order_by("operation_time").last() self.assertEqual(response.status_code, HTTP_403_FORBIDDEN) - self.check_host_updated_log( - log=log, operation_result=AuditLogOperationResult.Denied, user=self.no_rights_user - ) + self.check_host_updated_log(log=log, operation_result=AuditLogOperationResult.Denied, user=self.no_rights_user) def test_action_launch(self): action = Action.objects.create( @@ -565,9 +549,7 @@ def test_action_launch(self): state_available="any", ) with patch("api.action.views.create", return_value=Response(status=HTTP_201_CREATED)): - self.client.post( - path=reverse("run-task", kwargs={"host_id": self.host.pk, "action_id": action.pk}) - ) + self.client.post(path=reverse("run-task", kwargs={"host_id": self.host.pk, "action_id": action.pk})) log: AuditLog = AuditLog.objects.order_by("operation_time").last() @@ -605,3 +587,106 @@ def test_action_launch(self): log: AuditLog = AuditLog.objects.order_by("operation_time").last() self.check_action_log(log=log) + + def test_change_maintenance_mode(self): + self.host.cluster = self.cluster + self.host.save(update_fields=["cluster"]) + + self.client.post( + path=reverse("host-maintenance-mode", kwargs={"host_id": self.host.pk}), + data={"maintenance_mode": MaintenanceMode.ON}, + ) + + log: AuditLog = AuditLog.objects.order_by("operation_time").last() + + self.check_host_updated_log( + log=log, + operation_name="Host updated", + object_changes={"current": {"maintenance_mode": "ON"}, "previous": {"maintenance_mode": "OFF"}}, + ) + + def test_change_maintenance_mode_via_cluster(self): + self.host.cluster = self.cluster + self.host.save(update_fields=["cluster"]) + + self.client.post( + path=reverse( + "host-maintenance-mode", + kwargs={"cluster_id": self.cluster.pk, "host_id": self.host.pk}, + ), + data={"maintenance_mode": MaintenanceMode.ON}, + ) + + log: AuditLog = AuditLog.objects.order_by("operation_time").last() + + self.check_host_updated_log( + log=log, + operation_name="Host updated", + object_changes={"current": {"maintenance_mode": "ON"}, "previous": {"maintenance_mode": "OFF"}}, + ) + + def test_change_maintenance_mode_via_provider(self): + self.host.cluster = self.cluster + self.host.save(update_fields=["cluster"]) + + self.client.post( + path=reverse( + "host-maintenance-mode", + kwargs={"provider_id": self.provider.pk, "host_id": self.host.pk}, + ), + data={"maintenance_mode": MaintenanceMode.ON}, + ) + + log: AuditLog = AuditLog.objects.order_by("operation_time").last() + + self.check_host_updated_log( + log=log, + operation_name="Host updated", + object_changes={"current": {"maintenance_mode": "ON"}, "previous": {"maintenance_mode": "OFF"}}, + ) + + def test_change_maintenance_mode_failed(self): + self.client.post( + path=reverse("host-maintenance-mode", kwargs={"host_id": self.host.pk}), + data={"maintenance_mode": MaintenanceMode.CHANGING}, + ) + + log: AuditLog = AuditLog.objects.order_by("operation_time").last() + + self.check_host_updated_log( + log=log, operation_result=AuditLogOperationResult.Fail, operation_name="Host updated" + ) + + def test_change_maintenance_mode_denied(self): + self.host.cluster = self.cluster + self.host.save(update_fields=["cluster"]) + + with self.no_rights_user_logged_in: + self.client.post( + path=reverse("host-maintenance-mode", kwargs={"host_id": self.host.pk}), + data={"maintenance_mode": MaintenanceMode.ON}, + ) + + log: AuditLog = AuditLog.objects.order_by("operation_time").last() + + self.check_host_updated_log( + log=log, + operation_result=AuditLogOperationResult.Denied, + operation_name="Host updated", + user=self.no_rights_user, + ) + + with self.no_rights_user_logged_in: + self.client.post( + path=reverse("host-maintenance-mode", kwargs={"cluster_id": self.cluster.pk, "host_id": self.host.pk}), + data={"maintenance_mode": MaintenanceMode.ON}, + ) + + log: AuditLog = AuditLog.objects.order_by("operation_time").last() + + self.check_host_updated_log( + log=log, + operation_result=AuditLogOperationResult.Denied, + operation_name="Host updated", + user=self.no_rights_user, + ) diff --git a/python/audit/tests/test_policy.py b/python/audit/tests/test_policy.py index 57adb355b6..832aa6ff16 100644 --- a/python/audit/tests/test_policy.py +++ b/python/audit/tests/test_policy.py @@ -55,7 +55,7 @@ def setUp(self) -> None: prototype=Prototype.objects.create(bundle=bundle, type="provider"), ) - def check_log( # pylint: disable=too-many-arguments + def check_log( self, log: AuditLog, obj: Policy | None, @@ -134,9 +134,7 @@ def test_create_denied(self): path=reverse(self.list_name), data={ "name": self.name, - "object": [ - {"id": self.cluster.pk, "name": self.cluster_name, "type": "cluster"} - ], + "object": [{"id": self.cluster.pk, "name": self.cluster_name, "type": "cluster"}], "role": {"id": self.role.pk}, "user": [{"id": self.test_user.pk}], }, @@ -241,9 +239,7 @@ def test_update_put_denied(self): path=reverse(self.detail_name, kwargs={"pk": self.policy.pk}), data={ "name": self.policy.name, - "object": [ - {"id": self.cluster.pk, "name": self.cluster_name, "type": "cluster"} - ], + "object": [{"id": self.cluster.pk, "name": self.cluster_name, "type": "cluster"}], "role": {"id": self.role.pk}, "user": [{"id": self.test_user.pk}], "description": "new_test_description", @@ -317,9 +313,7 @@ def test_update_patch_denied(self): response: Response = self.client.patch( path=reverse(self.detail_name, kwargs={"pk": self.policy.pk}), data={ - "object": [ - {"id": self.cluster.pk, "name": self.cluster_name, "type": "cluster"} - ], + "object": [{"id": self.cluster.pk, "name": self.cluster_name, "type": "cluster"}], "role": {"id": self.role.pk}, "user": [{"id": self.test_user.pk}], "description": "new_test_description", @@ -342,9 +336,7 @@ def test_update_patch_failed(self): self.client.patch( path=reverse(self.detail_name, kwargs={"pk": self.policy.pk}), data={ - "object": [ - {"id": self.cluster.pk, "name": self.cluster_name, "type": "cluster"} - ], + "object": [{"id": self.cluster.pk, "name": self.cluster_name, "type": "cluster"}], "role": {}, "user": [{"id": self.test_user.pk}], "description": "new_test_description", diff --git a/python/audit/tests/test_provider.py b/python/audit/tests/test_provider.py index fee6fc7874..ab1454f4eb 100644 --- a/python/audit/tests/test_provider.py +++ b/python/audit/tests/test_provider.py @@ -250,9 +250,7 @@ def test_delete_failed(self): def test_update_and_restore(self): config = ObjectConfig.objects.create(current=0, previous=0) - provider = HostProvider.objects.create( - prototype=self.prototype, name="test_provider", config=config - ) + provider = HostProvider.objects.create(prototype=self.prototype, name="test_provider", config=config) config_log = ConfigLog.objects.create(obj_ref=config, config="{}") config.current = config_log.pk @@ -293,9 +291,7 @@ def test_update_and_restore(self): def test_update_and_restore_denied(self): config = ObjectConfig.objects.create(current=1, previous=1) - provider = HostProvider.objects.create( - prototype=self.prototype, name="test_provider", config=config - ) + provider = HostProvider.objects.create(prototype=self.prototype, name="test_provider", config=config) ConfigLog.objects.create(obj_ref=config, config="{}") with self.no_rights_user_logged_in: @@ -346,17 +342,11 @@ def test_action_launch(self): state_available="any", ) with patch("api.action.views.create", return_value=Response(status=HTTP_201_CREATED)): - self.client.post( - path=reverse( - "run-task", kwargs={"provider_id": provider.pk, "action_id": action.pk} - ) - ) + self.client.post(path=reverse("run-task", kwargs={"provider_id": provider.pk, "action_id": action.pk})) log: AuditLog = AuditLog.objects.order_by("operation_time").last() - self.check_action_log( - log=log, provider=provider, operation_name=f"{action.display_name} action launched" - ) + self.check_action_log(log=log, provider=provider, operation_name=f"{action.display_name} action launched") def test_do_upgrade(self): provider = HostProvider.objects.create( diff --git a/python/audit/tests/test_role.py b/python/audit/tests/test_role.py index 4c738affd8..dc49bb0932 100644 --- a/python/audit/tests/test_role.py +++ b/python/audit/tests/test_role.py @@ -42,7 +42,7 @@ def setUp(self) -> None: self.role_created_str = "Role created" self.role_updated_str = "Role updated" - def check_log( # pylint: disable=too-many-arguments + def check_log( self, log: AuditLog, obj: Role | None, diff --git a/python/audit/tests/test_service.py b/python/audit/tests/test_service.py index 449599f032..9a5a55ec33 100644 --- a/python/audit/tests/test_service.py +++ b/python/audit/tests/test_service.py @@ -37,6 +37,7 @@ ClusterBind, ClusterObject, ConfigLog, + MaintenanceMode, ObjectConfig, Prototype, PrototypeExport, @@ -47,7 +48,7 @@ class TestService(BaseTestCase): - # pylint: disable=too-many-instance-attributes + # pylint: disable=too-many-instance-attributes,too-many-public-methods def setUp(self) -> None: super().setUp() @@ -77,7 +78,7 @@ def setUp(self) -> None: state_available="any", ) - def check_log( # pylint: disable=too-many-arguments + def check_log( self, log: AuditLog, obj, @@ -87,7 +88,11 @@ def check_log( # pylint: disable=too-many-arguments operation_type: AuditLogOperationType, operation_result: AuditLogOperationResult, user: User, + object_changes: dict | None = None, ): + if object_changes is None: + object_changes = {} + self.assertEqual(log.audit_object.object_id, obj.pk) self.assertEqual(log.audit_object.object_name, obj_name) self.assertEqual(log.audit_object.object_type, object_type) @@ -97,7 +102,7 @@ def check_log( # pylint: disable=too-many-arguments self.assertEqual(log.operation_result, operation_result) self.assertIsInstance(log.operation_time, datetime) self.assertEqual(log.user.pk, user.pk) - self.assertEqual(log.object_changes, {}) + self.assertEqual(log.object_changes, object_changes) def check_action_log(self, log: AuditLog) -> None: self.check_log( @@ -179,7 +184,7 @@ def get_service_and_cluster(self) -> tuple[ClusterObject, Cluster]: return service, cluster - def test_update(self): + def test_update_config(self): self.client.post( path=reverse("config-history", kwargs={"service_id": self.service.pk}), data={"config": {}}, @@ -199,7 +204,7 @@ def test_update(self): user=self.test_user, ) - def test_update_denied(self): + def test_update_config_denied(self): with self.no_rights_user_logged_in: response: Response = self.client.post( path=reverse("config-history", kwargs={"service_id": self.service.pk}), @@ -221,7 +226,7 @@ def test_update_denied(self): user=self.no_rights_user, ) - def test_restore(self): + def test_restore_config(self): self.client.patch( path=reverse( "config-history-version-restore", @@ -243,7 +248,7 @@ def test_restore(self): user=self.test_user, ) - def test_restore_denied(self): + def test_restore_config_denied(self): with self.no_rights_user_logged_in: response: Response = self.client.patch( path=reverse( @@ -337,7 +342,7 @@ def test_delete_new(self): bundle_filename = "import.tar" with open( Path(settings.BASE_DIR, "python/audit/tests/files", bundle_filename), - encoding="utf-8", + encoding=settings.ENCODING_UTF_8, ) as f: self.client.post( path=reverse("upload-bundle"), @@ -493,9 +498,7 @@ def test_bind_unbind_cluster_to_service(self): bind = ClusterBind.objects.first() self.client.delete( - path=reverse( - "service-bind-details", kwargs={"service_id": self.service.pk, "bind_id": bind.pk} - ), + path=reverse("service-bind-details", kwargs={"service_id": self.service.pk, "bind_id": bind.pk}), content_type=APPLICATION_JSON, ) @@ -535,9 +538,7 @@ def test_bind_unbind_service_to_service(self): bind = ClusterBind.objects.first() self.client.delete( - path=reverse( - "service-bind-details", kwargs={"service_id": self.service.pk, "bind_id": bind.pk} - ), + path=reverse("service-bind-details", kwargs={"service_id": self.service.pk, "bind_id": bind.pk}), content_type=APPLICATION_JSON, ) @@ -609,9 +610,7 @@ def test_bind_unbind_denied(self): def test_action_launch(self): with patch("api.action.views.create", return_value=Response(status=HTTP_201_CREATED)): self.client.post( - path=reverse( - "run-task", kwargs={"service_id": self.service.pk, "action_id": self.action.pk} - ) + path=reverse("run-task", kwargs={"service_id": self.service.pk, "action_id": self.action.pk}) ) log: AuditLog = AuditLog.objects.order_by("operation_time").last() @@ -633,3 +632,85 @@ def test_action_launch(self): log: AuditLog = AuditLog.objects.order_by("operation_time").last() self.check_action_log(log=log) + + def test_change_maintenance_mode(self): + self.client.post( + path=reverse("service-maintenance-mode", kwargs={"service_id": self.service.pk}), + data={"maintenance_mode": MaintenanceMode.ON}, + ) + + log: AuditLog = AuditLog.objects.order_by("operation_time").last() + + self.check_log( + log=log, + obj=self.service, + obj_name=f"{self.cluster.name}/{self.service.display_name}", + operation_name="Service updated", + object_type=AuditObjectType.Service, + operation_type=AuditLogOperationType.Update, + operation_result=AuditLogOperationResult.Success, + user=self.test_user, + object_changes={"current": {"maintenance_mode": "ON"}, "previous": {"maintenance_mode": "OFF"}}, + ) + + def test_change_maintenance_mode_via_cluster(self): + self.client.post( + path=reverse( + "service-maintenance-mode", + kwargs={"cluster_id": self.cluster.pk, "service_id": self.service.pk}, + ), + data={"maintenance_mode": MaintenanceMode.ON}, + ) + + log: AuditLog = AuditLog.objects.order_by("operation_time").last() + + self.check_log( + log=log, + obj=self.service, + obj_name=f"{self.cluster.name}/{self.service.display_name}", + operation_name="Service updated", + object_type=AuditObjectType.Service, + operation_type=AuditLogOperationType.Update, + operation_result=AuditLogOperationResult.Success, + user=self.test_user, + object_changes={"current": {"maintenance_mode": "ON"}, "previous": {"maintenance_mode": "OFF"}}, + ) + + def test_change_maintenance_mode_failed(self): + self.client.post( + path=reverse("service-maintenance-mode", kwargs={"service_id": self.service.pk}), + data={"maintenance_mode": MaintenanceMode.CHANGING}, + ) + + log: AuditLog = AuditLog.objects.order_by("operation_time").last() + + self.check_log( + log=log, + obj=self.service, + obj_name=f"{self.cluster.name}/{self.service.display_name}", + operation_name="Service updated", + object_type=AuditObjectType.Service, + operation_type=AuditLogOperationType.Update, + operation_result=AuditLogOperationResult.Fail, + user=self.test_user, + ) + + def test_change_maintenance_mode_denied(self): + with self.no_rights_user_logged_in: + self.client.post( + path=reverse("service-maintenance-mode", kwargs={"service_id": self.service.pk}), + data={"maintenance_mode": MaintenanceMode.ON}, + ) + + log: AuditLog = AuditLog.objects.order_by("operation_time").last() + + self.check_log( + log=log, + obj=self.service, + obj_name=f"{self.cluster.name}/{self.service.display_name}", + operation_name="Service updated", + object_type=AuditObjectType.Service, + operation_type=AuditLogOperationType.Update, + operation_result=AuditLogOperationResult.Denied, + user=self.no_rights_user, + ) diff --git a/python/audit/tests/test_task.py b/python/audit/tests/test_task.py index 10d686f7e7..fd35d00686 100644 --- a/python/audit/tests/test_task.py +++ b/python/audit/tests/test_task.py @@ -70,7 +70,7 @@ def check_log( def test_cancel(self): with patch("api.job.views.cancel_task"): - self.client.put(path=reverse("task-cancel", kwargs={"task_id": self.task.pk})) + self.client.put(path=reverse("tasklog-cancel", kwargs={"task_pk": self.task.pk})) log: AuditLog = AuditLog.objects.order_by("operation_time").last() @@ -85,7 +85,7 @@ def test_cancel(self): def test_cancel_denied(self): with self.no_rights_user_logged_in: response: Response = self.client.put( - path=reverse("task-cancel", kwargs={"task_id": self.task.pk}), + path=reverse("tasklog-cancel", kwargs={"task_pk": self.task.pk}), ) log: AuditLog = AuditLog.objects.order_by("operation_time").last() @@ -101,7 +101,7 @@ def test_cancel_denied(self): def test_restart(self): with patch("api.job.views.restart_task"): - self.client.put(path=reverse("task-restart", kwargs={"task_id": self.task.pk})) + self.client.put(path=reverse("tasklog-restart", kwargs={"task_pk": self.task.pk})) log: AuditLog = AuditLog.objects.order_by("operation_time").last() @@ -116,7 +116,7 @@ def test_restart(self): def test_restart_denied(self): with self.no_rights_user_logged_in: response: Response = self.client.put( - path=reverse("task-restart", kwargs={"task_id": self.task.pk}), + path=reverse("tasklog-restart", kwargs={"task_pk": self.task.pk}), ) log: AuditLog = AuditLog.objects.order_by("operation_time").last() @@ -134,7 +134,7 @@ def test_restart_failed(self): task_pks = TaskLog.objects.all().values_list("pk", flat=True).order_by("-pk") with patch("api.job.views.restart_task"): response: Response = self.client.put( - path=reverse("task-restart", kwargs={"task_id": task_pks[0] + 1}), + path=reverse("tasklog-restart", kwargs={"task_pk": task_pks[0] + 1}), ) log: AuditLog = AuditLog.objects.order_by("operation_time").last() diff --git a/python/audit/tests/test_user.py b/python/audit/tests/test_user.py index 031d074c2f..78685e7369 100644 --- a/python/audit/tests/test_user.py +++ b/python/audit/tests/test_user.py @@ -207,9 +207,7 @@ def test_update_put_denied(self): log: AuditLog = AuditLog.objects.order_by("operation_time").last() self.assertEqual(response.status_code, HTTP_403_FORBIDDEN) - self.check_log( - log=log, operation_result=AuditLogOperationResult.Denied, user=self.no_rights_user - ) + self.check_log(log=log, operation_result=AuditLogOperationResult.Denied, user=self.no_rights_user) def test_update_patch(self): prev_first_name = self.test_user.first_name @@ -243,6 +241,4 @@ def test_update_patch_denied(self): log: AuditLog = AuditLog.objects.order_by("operation_time").last() self.assertEqual(response.status_code, HTTP_403_FORBIDDEN) - self.check_log( - log=log, operation_result=AuditLogOperationResult.Denied, user=self.no_rights_user - ) + self.check_log(log=log, operation_result=AuditLogOperationResult.Denied, user=self.no_rights_user) diff --git a/python/audit/tests/test_views.py b/python/audit/tests/test_views.py index 5d06282fcb..f68854b815 100644 --- a/python/audit/tests/test_views.py +++ b/python/audit/tests/test_views.py @@ -179,11 +179,7 @@ def _get_date_func(datetime_str): if field in _template_field_mutation: template_mutations['delete'].append(field) template_mutations['merge'].update( - { - _template_field_mutation[field][0]: _template_field_mutation[field][1]( - template[field] - ) - } + {_template_field_mutation[field][0]: _template_field_mutation[field][1](template[field])} ) for field in template_mutations['delete']: del template[field] @@ -192,9 +188,7 @@ def _get_date_func(datetime_str): class TestViews(TestBase): - def _run_single_filter_test( - self, url_path, filter_kwargs, default_template, kwargs_name, create_kwargs=None - ): + def _run_single_filter_test(self, url_path, filter_kwargs, default_template, kwargs_name, create_kwargs=None): num_filter_target = randbelow(11) + 5 num_others = num_filter_target - randbelow(3) + 1 @@ -216,26 +210,20 @@ def _run_single_filter_test( def test_audit_visibility_regular_user(self): self._login_as(self.user_username, self.user_password) audit_entities = self._populate_audit_tables(num=3) - response = self.client.get( - path=reverse('audit:audit-operations-list'), content_type="application/json" - ) + response = self.client.get(path=reverse('audit:auditlog-list'), content_type="application/json") self._check_response(response, expected_status_code=403, should_fail=True) response = self.client.get( - path=reverse( - 'audit:audit-operations-detail', args=(audit_entities['audit_operations'][0].pk,) - ), + path=reverse('audit:auditlog-detail', args=(audit_entities['audit_operations'][0].pk,)), content_type="application/json", ) self._check_response(response, expected_status_code=403, should_fail=True) - response = self.client.get( - path=reverse('audit:audit-logins-list'), content_type="application/json" - ) + response = self.client.get(path=reverse('audit:auditsession-list'), content_type="application/json") self._check_response(response, expected_status_code=403, should_fail=True) response = self.client.get( - path=reverse('audit:audit-logins-detail', args=(audit_entities['audit_logins'][0].pk,)), + path=reverse('audit:auditsession-detail', args=(audit_entities['audit_logins'][0].pk,)), content_type="application/json", ) self._check_response(response, expected_status_code=403, should_fail=True) @@ -244,30 +232,20 @@ def test_audit_visibility_superuser(self): self._login_as(self.superuser_username, self.superuser_password) num_entities = 5 audit_entities = self._populate_audit_tables(num=num_entities) - response = self.client.get( - path=reverse('audit:audit-operations-list'), content_type="application/json" - ) + response = self.client.get(path=reverse('audit:auditlog-list'), content_type="application/json") self._check_response(response, template=self.default_auditlog, expected_count=num_entities) response = self.client.get( - path=reverse( - 'audit:audit-operations-detail', args=(audit_entities['audit_operations'][0].pk,) - ), + path=reverse('audit:auditlog-detail', args=(audit_entities['audit_operations'][0].pk,)), content_type="application/json", ) - self._check_response( - response, template=self.default_auditlog, expected_count=num_entities, list_view=False - ) + self._check_response(response, template=self.default_auditlog, expected_count=num_entities, list_view=False) - response = self.client.get( - path=reverse('audit:audit-logins-list'), content_type="application/json" - ) - self._check_response( - response, template=self.default_auditsession, expected_count=num_entities - ) + response = self.client.get(path=reverse('audit:auditsession-list'), content_type="application/json") + self._check_response(response, template=self.default_auditsession, expected_count=num_entities) response = self.client.get( - path=reverse('audit:audit-logins-detail', args=(audit_entities['audit_logins'][0].pk,)), + path=reverse('audit:auditsession-detail', args=(audit_entities['audit_logins'][0].pk,)), content_type="application/json", ) self._check_response( @@ -279,7 +257,7 @@ def test_audit_visibility_superuser(self): def test_filters_operations(self): self._login_as(self.superuser_username, self.superuser_password) - url_operations = reverse('audit:audit-operations-list') + url_operations = reverse('audit:auditlog-list') date = '2000-01-05' self._run_single_filter_test( @@ -345,7 +323,7 @@ def test_filters_operations(self): def test_filters_logins(self): self._login_as(self.superuser_username, self.superuser_password) - url_logins = reverse('audit:audit-logins-list') + url_logins = reverse('audit:auditsession-list') date = '2000-01-05' self._run_single_filter_test( diff --git a/python/audit/tests/tests_commands.py b/python/audit/tests/tests_commands.py index 0996f42d93..1b87ff62f3 100644 --- a/python/audit/tests/tests_commands.py +++ b/python/audit/tests/tests_commands.py @@ -1,3 +1,14 @@ +# 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 django.core.management import call_command @@ -43,9 +54,7 @@ def setUp(self) -> None: prototype = Prototype.objects.create(bundle=bundle, type="cluster") config_2 = ObjectConfig.objects.create(current=4, previous=3) cluster = Cluster.objects.create(name="test_cluster", prototype=prototype, config=config_2) - TaskLog.objects.create( - object_id=cluster.id, start_date=date, finish_date=date, status="success" - ) + TaskLog.objects.create(object_id=cluster.id, start_date=date, finish_date=date, status="success") JobLog.objects.create(start_date=date, finish_date=date) ConfigLog.objects.create(obj_ref=config_2) ConfigLog.objects.all().update(date=date) diff --git a/python/audit/urls.py b/python/audit/urls.py index 009c0fcdac..a9cf4b07c9 100644 --- a/python/audit/urls.py +++ b/python/audit/urls.py @@ -17,9 +17,9 @@ from audit.views import AuditLogViewSet, AuditRoot, AuditSessionViewSet router = SimpleRouter() -router.register("operation", AuditLogViewSet, basename="audit-operations") -router.register("login", AuditSessionViewSet, basename="audit-logins") +router.register("operation", AuditLogViewSet) +router.register("login", AuditSessionViewSet) urlpatterns = [ - path(r'', AuditRoot.as_view(), name='root'), + path("", AuditRoot.as_view(), name="root"), *router.urls, ] diff --git a/python/audit/utils.py b/python/audit/utils.py index 4d2247934c..a43770c9ef 100644 --- a/python/audit/utils.py +++ b/python/audit/utils.py @@ -13,13 +13,12 @@ from functools import wraps -from adwp_base.errors import AdwpEx from django.contrib.auth.models import User as DjangoUser from django.db.models import Model from django.http.response import Http404 from django.urls import resolve -from django.views.generic.base import View from rest_framework.exceptions import PermissionDenied, ValidationError +from rest_framework.generics import GenericAPIView from rest_framework.request import Request from rest_framework.status import ( HTTP_400_BAD_REQUEST, @@ -29,12 +28,13 @@ ) from rest_framework.viewsets import ModelViewSet -from api import cluster, host +from api.cluster.serializers import ClusterAuditSerializer +from api.component.serializers import ComponentAuditSerializer +from api.host.serializers import HostAuditSerializer +from api.service.serializers import ServiceAuditSerializer from audit.cases.cases import get_audit_operation_and_object -from audit.cases.common import get_or_create_audit_obj from audit.cef_logger import cef_logger from audit.models import ( - MODEL_TO_AUDIT_OBJECT_TYPE_MAP, AuditLog, AuditLogOperationResult, AuditLogOperationType, @@ -49,6 +49,7 @@ ClusterObject, Host, HostProvider, + ServiceComponent, TaskLog, ) from rbac.endpoints.group.serializers import GroupAuditSerializer @@ -58,18 +59,18 @@ from rbac.models import Group, Policy, Role, User -def _get_view_and_request(args) -> tuple[View, Request]: +def _get_view_and_request(args) -> tuple[GenericAPIView, Request]: if len(args) == 2: # for audit view methods - view: View = args[0] + view: GenericAPIView = args[0] request: Request = args[1] else: # for audit has_permissions method - view: View = args[2] + view: GenericAPIView = args[2] request: Request = args[1] return view, request -def _get_deleted_obj(view: View, request: Request, kwargs) -> Model | None: +def _get_deleted_obj(view: GenericAPIView, request: Request, kwargs) -> Model | None: # pylint: disable=too-many-branches try: @@ -120,9 +121,13 @@ def _get_object_changes(prev_data: dict, current_obj: Model) -> dict: elif isinstance(current_obj, Policy): serializer_class = PolicyAuditSerializer elif isinstance(current_obj, Cluster): - serializer_class = cluster.serializers.ClusterAuditSerializer + serializer_class = ClusterAuditSerializer elif isinstance(current_obj, Host): - serializer_class = host.serializers.HostAuditSerializer + serializer_class = HostAuditSerializer + elif isinstance(current_obj, ClusterObject): + serializer_class = ServiceAuditSerializer + elif isinstance(current_obj, ServiceComponent): + serializer_class = ComponentAuditSerializer if not serializer_class: return {} @@ -145,41 +150,47 @@ def _get_object_changes(prev_data: dict, current_obj: Model) -> dict: return object_changes -def _get_obj_changes_data(view: View | ModelViewSet) -> tuple[dict | None, Model | None]: +def _get_obj_changes_data(view: GenericAPIView | ModelViewSet) -> tuple[dict | None, Model | None]: + # pylint: disable=too-many-branches + prev_data = None current_obj = None serializer_class = None - model = None - if ( - isinstance(view, ModelViewSet) - and view.action in {"update", "partial_update"} - and view.kwargs.get("pk") - ): + pk = None + + if isinstance(view, ModelViewSet) and view.action in {"update", "partial_update"} and view.kwargs.get("pk"): pk = view.kwargs["pk"] if view.__class__.__name__ == "GroupViewSet": serializer_class = GroupAuditSerializer - model = Group elif view.__class__.__name__ == "RoleViewSet": serializer_class = RoleAuditSerializer - model = Role elif view.__class__.__name__ == "UserViewSet": serializer_class = UserAuditSerializer - model = User elif view.__class__.__name__ == "PolicyViewSet": serializer_class = PolicyAuditSerializer - model = Policy elif view.request.method in {"PATCH", "PUT"}: if view.__class__.__name__ == "ClusterDetail": - serializer_class = cluster.serializers.ClusterAuditSerializer + serializer_class = ClusterAuditSerializer pk = view.kwargs["cluster_id"] - model = Cluster elif view.__class__.__name__ == "HostDetail": - serializer_class = host.serializers.HostAuditSerializer + serializer_class = HostAuditSerializer pk = view.kwargs["host_id"] - model = Host + elif view.request.method == "POST": + if view.__class__.__name__ == "ServiceMaintenanceModeView": + serializer_class = ServiceAuditSerializer + pk = view.kwargs["service_id"] + elif view.__class__.__name__ == "HostMaintenanceModeView": + serializer_class = HostAuditSerializer + pk = view.kwargs["host_id"] + elif view.__class__.__name__ == "ComponentMaintenanceModeView": + serializer_class = ComponentAuditSerializer + pk = view.kwargs["component_id"] + if serializer_class: + model = view.get_queryset().model current_obj = model.objects.filter(pk=pk).first() prev_data = serializer_class(model.objects.filter(pk=pk).first()).data + if current_obj: prev_data = serializer_class(current_obj).data @@ -195,7 +206,7 @@ def wrapped(*args, **kwargs): audit_operation: AuditOperation audit_object: AuditObject operation_name: str - view: View | ModelViewSet + view: GenericAPIView | ModelViewSet request: Request object_changes: dict @@ -207,7 +218,14 @@ def wrapped(*args, **kwargs): if "bind_id" in kwargs: deleted_obj = ClusterBind.objects.filter(pk=kwargs["bind_id"]).first() else: - deleted_obj = None + if "host_id" in kwargs and "maintenance-mode" in request.path: + deleted_obj = Host.objects.filter(pk=kwargs["host_id"]).first() + elif "service_id" in kwargs and "maintenance-mode" in request.path: + deleted_obj = ClusterObject.objects.filter(pk=kwargs["service_id"]).first() + elif "component_id" in kwargs and "maintenance-mode" in request.path: + deleted_obj = ServiceComponent.objects.filter(pk=kwargs["component_id"]).first() + else: + deleted_obj = None prev_data, current_obj = _get_obj_changes_data(view=view) @@ -220,13 +238,12 @@ def wrapped(*args, **kwargs): status_code = res.status_code else: status_code = HTTP_403_FORBIDDEN - except (AdcmEx, AdwpEx, ValidationError) as exc: + except (AdcmEx, ValidationError) as exc: error = exc res = None if getattr(exc, "msg", None) and ( - "doesn't exist" in exc.msg - or "service is not installed in specified cluster" in exc.msg + "doesn't exist" in exc.msg or "service is not installed in specified cluster" in exc.msg ): _kwargs = None if "cluster_id" in kwargs: @@ -234,7 +251,7 @@ def wrapped(*args, **kwargs): elif "cluster_id" in view.kwargs: _kwargs = view.kwargs - if _kwargs: + if _kwargs and "maintenance-mode" not in request.path: deleted_obj = Cluster.objects.filter(pk=_kwargs["cluster_id"]).first() if "provider_id" in kwargs and "host_id" in kwargs: @@ -257,13 +274,13 @@ def wrapped(*args, **kwargs): if not deleted_obj: status_code = exc.status_code - if ( - status_code == HTTP_404_NOT_FOUND - and kwargs.get("action_id") - and Action.objects.filter(pk=kwargs["action_id"]).exists() - ): - status_code = HTTP_403_FORBIDDEN - + if status_code == HTTP_404_NOT_FOUND: + action_perm_denied = ( + kwargs.get("action_id") and Action.objects.filter(pk=kwargs["action_id"]).exists() + ) + task_perm_denied = kwargs.get("task_pk") and TaskLog.objects.filter(pk=kwargs["task_pk"]).exists() + if action_perm_denied or task_perm_denied: + status_code = HTTP_403_FORBIDDEN else: # when denied returns 404 from PermissionListMixin if getattr(exc, "msg", None) and ( # pylint: disable=too-many-boolean-expressions "There is host" in exc.msg @@ -272,6 +289,8 @@ def wrapped(*args, **kwargs): or ("host doesn't exist" in exc.msg and not isinstance(deleted_obj, Host)) ): status_code = error.status_code + elif isinstance(exc, ValidationError): + status_code = error.status_code else: status_code = HTTP_403_FORBIDDEN except PermissionDenied as exc: @@ -360,29 +379,3 @@ def make_audit_log(operation_type, result, operation_status): user=system_user, ) cef_logger(audit_instance=audit_log, signature_id="Background operation", empty_resource=True) - - -def audit_finish_task(obj, operation_name: str, status: str) -> None: - obj_type = MODEL_TO_AUDIT_OBJECT_TYPE_MAP.get(obj.__class__) - if not obj_type: - return - - audit_object = get_or_create_audit_obj( - object_id=obj.pk, - object_name=obj.name, - object_type=obj_type, - ) - if status == "success": - operation_result = AuditLogOperationResult.Success - else: - operation_result = AuditLogOperationResult.Fail - - audit_log = AuditLog.objects.create( - audit_object=audit_object, - operation_name=operation_name, - operation_type=AuditLogOperationType.Update, - operation_result=operation_result, - object_changes={}, - ) - - cef_logger(audit_instance=audit_log, signature_id="Action completion") diff --git a/python/audit/views.py b/python/audit/views.py index b59347420f..89c438518f 100644 --- a/python/audit/views.py +++ b/python/audit/views.py @@ -23,24 +23,22 @@ class AuditRoot(APIRootView): permission_classes = (AllowAny,) api_root_dict = { - 'operations': 'audit-operations-list', - 'logins': 'audit-logins-list', + "operations": "auditlog-list", + "logins": "auditsession-list", } # pylint: disable=too-many-ancestors class AuditLogViewSet(SuperuserOnlyMixin, ReadOnlyModelViewSet): - not_superuser_error_code = 'AUDIT_OPERATIONS_FORBIDDEN' - queryset = AuditLog.objects.select_related('audit_object', 'user').order_by( - '-operation_time', '-pk' - ) + not_superuser_error_code = "AUDIT_OPERATIONS_FORBIDDEN" + queryset = AuditLog.objects.select_related("audit_object", "user").order_by("-operation_time", "-pk") serializer_class = AuditLogSerializer filterset_class = AuditLogListFilter # pylint: disable=too-many-ancestors class AuditSessionViewSet(SuperuserOnlyMixin, ReadOnlyModelViewSet): - not_superuser_error_code = 'AUDIT_LOGINS_FORBIDDEN' - queryset = AuditSession.objects.select_related('user').order_by('-login_time', '-pk') + not_superuser_error_code = "AUDIT_LOGINS_FORBIDDEN" + queryset = AuditSession.objects.select_related("user").order_by("-login_time", "-pk") serializer_class = AuditSessionSerializer filterset_class = AuditSessionListFilter diff --git a/python/backupdb.py b/python/backupdb.py index ddaa079357..5ed437edd0 100755 --- a/python/backupdb.py +++ b/python/backupdb.py @@ -15,13 +15,12 @@ import os import sqlite3 +from django.conf import settings from django.core.exceptions import ImproperlyConfigured from django.db import DEFAULT_DB_ALIAS, connections from django.db.migrations.executor import MigrationExecutor import adcm.init_django # pylint: disable=unused-import -from adcm.settings import DATABASES -from cm import config from cm.logger import logger @@ -38,25 +37,25 @@ def check_migrations(): def backup_sqlite(dbfile): dt = datetime.datetime.now().strftime("%Y%m%d_%H%M%S") - backupfile = os.path.join(config.BASE_DIR, 'data', 'var', f'{dt}.db') + backupfile = os.path.join(settings.BASE_DIR, "data", "var", f"{dt}.db") old = sqlite3.connect(dbfile) new = sqlite3.connect(backupfile) with new: old.backup(new) new.close() old.close() - logger.info('Backup sqlite db to %s', backupfile) + logger.info("Backup sqlite db to %s", backupfile) def backup_db(): if not check_migrations(): return - db = DATABASES['default'] - if db['ENGINE'] != 'django.db.backends.sqlite3': - logger.error('Backup for %s not implemented yet', db['ENGINE']) + db = settings.DATABASES["default"] + if db["ENGINE"] != "django.db.backends.sqlite3": + logger.error("Backup for %s not implemented yet", db["ENGINE"]) return - backup_sqlite(db['NAME']) + backup_sqlite(db["NAME"]) -if __name__ == '__main__': +if __name__ == "__main__": backup_db() diff --git a/python/check_adcm_bundle.py b/python/check_adcm_bundle.py index ed775248c5..e6e6f6bb05 100755 --- a/python/check_adcm_bundle.py +++ b/python/check_adcm_bundle.py @@ -17,10 +17,11 @@ import sys import tarfile -import cm.config +from django.conf import settings + from check_adcm_config import check_config -TMP_DIR = '/tmp/adcm_bundle_tmp' +TMP_DIR = "/tmp/adcm_bundle_tmp" def untar(bundle_file): @@ -33,7 +34,7 @@ def untar(bundle_file): def get_config_files(path): conf_list = [] - conf_files = ('config.yaml', 'config.yml') + conf_files = ("config.yaml", "config.yml") for root, _, files in os.walk(path): for conf_file in conf_files: if conf_file in files: @@ -51,15 +52,13 @@ def check_bundle(bundle_file, use_directory=False, verbose=False): if verbose: print(f'Bundle "{bundle_file}"') for conf_file in get_config_files(TMP_DIR): - check_config(conf_file, os.path.join(cm.config.CODE_DIR, 'cm', 'adcm_schema.yaml'), verbose) + check_config(conf_file, str(settings.CODE_DIR / "cm" / "adcm_schema.yaml"), verbose) -if __name__ == '__main__': - parser = argparse.ArgumentParser(description='Check ADCM bundle file') +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="Check ADCM bundle file") parser.add_argument("bundle_file", type=str, help="ADCM bundle file name (bundle.tgz)") - parser.add_argument( - "-d", "--dir", action="store_true", help="use bundle_file as bundle directory name" - ) + parser.add_argument("-d", "--dir", action="store_true", help="use bundle_file as bundle directory name") parser.add_argument("-v", "--verbose", action="store_true", help="print OK result") args = parser.parse_args() check_bundle(args.bundle_file, args.dir, args.verbose) diff --git a/python/check_adcm_config.py b/python/check_adcm_config.py index e6a30eabb7..353b59febe 100755 --- a/python/check_adcm_config.py +++ b/python/check_adcm_config.py @@ -12,30 +12,31 @@ # limitations under the License. import argparse -import os import sys +from pathlib import Path import ruyaml +from django.conf import settings +import adcm.init_django # pylint: disable=unused-import import cm.checker -import cm.config -def check_config(data_file, schema_file, print_ok=True): - rules = ruyaml.round_trip_load(open(schema_file, encoding='utf_8')) +def check_config(data_file, schema_file, print_ok=True): # pylint: disable=too-many-return-statements + rules = ruyaml.round_trip_load(open(schema_file, encoding=settings.ENCODING_UTF_8)) try: # ruyaml.version_info=(0, 15, 0) # switch off duplicate key error - data = ruyaml.round_trip_load(open(data_file, encoding='utf_8'), version="1.1") + data = ruyaml.round_trip_load(open(data_file, encoding=settings.ENCODING_UTF_8), version="1.1") except FileNotFoundError as e: print(e) return 1 except ruyaml.constructor.DuplicateKeyError as e: print(f'Config file "{data_file}" Duplicate Keys Error:') - print(f'{e.context}\n{e.context_mark}\n{e.problem}\n{e.problem_mark}') + print(f"{e.context}\n{e.context_mark}\n{e.problem}\n{e.problem_mark}") return 1 except (ruyaml.parser.ParserError, ruyaml.scanner.ScannerError, NotImplementedError) as e: print(f'Config file "{data_file}" YAML Parser Error:') - print(f'{e}') + print(f"{e}") return 1 try: @@ -51,19 +52,19 @@ def check_config(data_file, schema_file, print_ok=True): return 1 except cm.checker.FormatError as e: print(f'Data File "{data_file}" Errors:') - print(f'\tline {e.line}: {e.message}') + print(f"\tline {e.line}: {e.message}") if e.errors: for ee in e.errors: - if 'Input data for' in ee.message: + if "Input data for" in ee.message: continue - print(f'\tline {ee.line}: {ee.message}') + print(f"\tline {ee.line}: {ee.message}") print(f'Schema File "{schema_file}" line {rules[e.rule].lc.line}, Rule: "{e.rule}"') return 1 -if __name__ == '__main__': - parser = argparse.ArgumentParser(description='Check ADCM config file') +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="Check ADCM config file") parser.add_argument("config_file", type=str, help="ADCM config file name (config.yaml)") args = parser.parse_args() - r = check_config(args.config_file, os.path.join(cm.config.CODE_DIR, 'cm', 'adcm_schema.yaml')) + r = check_config(args.config_file, Path(settings.CODE_DIR, "cm", "adcm_schema.yaml")) sys.exit(r) diff --git a/python/cm/adcm_config.py b/python/cm/adcm_config.py index 4469510466..45a5e57836 100644 --- a/python/cm/adcm_config.py +++ b/python/cm/adcm_config.py @@ -21,14 +21,8 @@ import yspec.checker from ansible.parsing.vault import VaultAES256, VaultSecret +from django.conf import settings -from cm.config import ( - ANSIBLE_SECRET, - ANSIBLE_VAULT_HEADER, - BUNDLE_DIR, - ENCODING, - FILE_DIR, -) from cm.errors import raise_adcm_ex from cm.logger import logger from cm.models import ( @@ -146,12 +140,25 @@ def get_default(c, proto=None): # pylint: disable=too-many-branches if proto: if c.default: value = read_file_type(proto, c.default, proto.bundle.hash, c.name, c.subname) + elif c.type == "secretfile": + if proto: + if c.default: + value = ansible_encrypt_and_format( + read_file_type(proto, c.default, proto.bundle.hash, c.name, c.subname) + ) + + if c.type == "secretmap": + new_value = {} + for k, v in value.items(): + new_value[k] = ansible_encrypt_and_format(v) + + value = new_value return value def type_is_complex(conf_type): - if conf_type in ("json", "structure", "list", "map"): + if conf_type in ("json", "structure", "list", "map", "secretmap"): return True return False @@ -163,18 +170,18 @@ def read_file_type(proto, default, bundle_hash, name, subname): return read_bundle_file(proto, default, bundle_hash, msg) -def read_bundle_file(proto, fname, bundle_hash, pattern, ref=None): +def read_bundle_file(proto, fname, bundle_hash, pattern, ref=None) -> str | None: if not ref: ref = proto_ref(proto) if fname[0:2] == "./": - path = Path(BUNDLE_DIR, bundle_hash, proto.path, fname) + path = Path(settings.BUNDLE_DIR, bundle_hash, proto.path, fname) else: - path = Path(BUNDLE_DIR, bundle_hash, fname) + path = Path(settings.BUNDLE_DIR, bundle_hash, fname) fd = None try: - fd = open(path, "r", encoding="utf_8") + fd = open(path, "r", encoding=settings.ENCODING_UTF_8) except FileNotFoundError: msg = '{} "{}" is not found ({})' raise_adcm_ex("CONFIG_TYPE_ERROR", msg.format(pattern, path, ref)) @@ -210,9 +217,7 @@ def get_prototype_config(proto: Prototype, action: Action = None) -> Tuple[dict, conf = {} attr = {} flist = ("default", "required", "type", "limits") - for c in PrototypeConfig.objects.filter(prototype=proto, action=action, type="group").order_by( - "id" - ): + for c in PrototypeConfig.objects.filter(prototype=proto, action=action, type="group").order_by("id"): spec[c.name] = {} conf[c.name] = {} if "activatable" in c.limits: @@ -362,7 +367,7 @@ def cook_file_type_name(obj, key, sub_key): else: filename = ["task", str(obj.id), key, sub_key] - return str(Path(FILE_DIR, ".".join(filename))) + return str(Path(settings.FILE_DIR, ".".join(filename))) def save_file_type(obj, key, subkey, value): @@ -385,7 +390,7 @@ def save_file_type(obj, key, subkey, value): if value[-1] == "-": value += "\n" - fd = open(filename, "w", encoding="utf_8") + fd = open(filename, "w", encoding=settings.ENCODING_UTF_8) fd.write(value) fd.close() Path(filename).chmod(0o0600) @@ -393,45 +398,53 @@ def save_file_type(obj, key, subkey, value): return filename +def ansible_encrypt(msg): + vault = VaultAES256() + secret = VaultSecret(bytes(settings.ANSIBLE_SECRET, settings.ENCODING_UTF_8)) + + return vault.encrypt(bytes(msg, settings.ENCODING_UTF_8), secret) + + +def ansible_encrypt_and_format(msg): + ciphertext = ansible_encrypt(msg) + + return f"{settings.ANSIBLE_VAULT_HEADER}\n{str(ciphertext, settings.ENCODING_UTF_8)}" + + def process_file_type(obj: Any, spec: dict, conf: dict): for key in conf: if "type" in spec[key]: if spec[key]["type"] == "file": save_file_type(obj, key, "", conf[key]) + elif spec[key]["type"] == "secretfile": + value = ansible_encrypt_and_format(conf[key]) + save_file_type(obj, key, "", value) + conf[key] = value elif conf[key]: for subkey in conf[key]: if spec[key][subkey]["type"] == "file": save_file_type(obj, key, subkey, conf[key][subkey]) - - -def ansible_encrypt(msg): - vault = VaultAES256() - secret = VaultSecret(bytes(ANSIBLE_SECRET, ENCODING)) - - return vault.encrypt(bytes(msg, ENCODING), secret) - - -def ansible_encrypt_and_format(msg): - ciphertext = ansible_encrypt(msg) - - return f"{ANSIBLE_VAULT_HEADER}\n{str(ciphertext, ENCODING)}" + elif spec[key][subkey]["type"] == "secretfile": + value = ansible_encrypt_and_format(conf[key][subkey]) + save_file_type(obj, key, subkey, value) + conf[key][subkey] = value def ansible_decrypt(msg): - if ANSIBLE_VAULT_HEADER not in msg: + if settings.ANSIBLE_VAULT_HEADER not in msg: return msg _, ciphertext = msg.split("\n") vault = VaultAES256() - secret = VaultSecret(bytes(ANSIBLE_SECRET, ENCODING)) + secret = VaultSecret(bytes(settings.ANSIBLE_SECRET, settings.ENCODING_UTF_8)) - return str(vault.decrypt(ciphertext, secret), ENCODING) + return str(vault.decrypt(ciphertext, secret), settings.ENCODING_UTF_8) def is_ansible_encrypted(msg): if not isinstance(msg, str): - msg = str(msg, ENCODING) - if ANSIBLE_VAULT_HEADER in msg: + msg = str(msg, settings.ENCODING_UTF_8) + if settings.ANSIBLE_VAULT_HEADER in msg: return True return False @@ -456,6 +469,17 @@ def update_password(passwd): return conf +def process_secretmap(spec: dict, conf: dict) -> dict: + for key in conf: + if spec[key].get("type") != "secretmap" or settings.ANSIBLE_VAULT_HEADER in conf[key]: + continue + + for k, v in conf[key].items(): + conf[key][k] = ansible_encrypt_and_format(v) + + return conf + + def process_config(obj, spec, old_conf): # pylint: disable=too-many-branches if not old_conf: return old_conf @@ -464,18 +488,18 @@ def process_config(obj, spec, old_conf): # pylint: disable=too-many-branches for key in conf: # pylint: disable=too-many-nested-blocks if "type" in spec[key]: if conf[key] is not None: - if spec[key]["type"] == "file": + if spec[key]["type"] in {"file", "secretfile"}: conf[key] = cook_file_type_name(obj, key, "") elif spec[key]["type"] in SECURE_PARAM_TYPES: - if ANSIBLE_VAULT_HEADER in conf[key]: + if settings.ANSIBLE_VAULT_HEADER in conf[key]: conf[key] = {"__ansible_vault": conf[key]} elif conf[key]: for subkey in conf[key]: if conf[key][subkey] is not None: - if spec[key][subkey]["type"] == "file": + if spec[key][subkey]["type"] in {"file", "secretfile"}: conf[key][subkey] = cook_file_type_name(obj, key, subkey) elif spec[key][subkey]["type"] in SECURE_PARAM_TYPES: - if ANSIBLE_VAULT_HEADER in conf[key][subkey]: + if settings.ANSIBLE_VAULT_HEADER in conf[key][subkey]: conf[key][subkey] = {"__ansible_vault": conf[key][subkey]} return conf @@ -551,7 +575,7 @@ def get_action_variant(obj, conf): c.limits["source"]["value"] = get_variant(obj, cl.config, c.limits) -def config_is_ro(obj, key, limits): +def config_is_ro(obj, key, limits): # pylint: disable=too-many-return-statements if not limits: return False @@ -594,7 +618,7 @@ def check_read_only(obj, spec, conf, old_conf): if isinstance(flat_conf[s], list) and not flat_conf[s]: continue - if spec[s].type == "map": + if spec[s].type in {"map", "secretmap"}: if isinstance(flat_conf[s], dict) and not flat_conf[s]: continue @@ -636,9 +660,7 @@ def restore_read_only(obj, spec, conf, old_conf): # # pylint: disable=too-many- return conf -def check_json_config( - proto, obj, new_config, current_config=None, new_attr=None, current_attr=None -): +def check_json_config(proto, obj, new_config, current_config=None, new_attr=None, current_attr=None): spec, flat_spec, _, _ = get_prototype_config(proto) check_attr(proto, obj, new_attr, flat_spec, current_attr) @@ -690,9 +712,7 @@ def check_agreement_group_attr(group_keys, custom_group_keys, spec): raise_adcm_ex("ATTRIBUTE_ERROR", f"the `{key}` field cannot be included in the group") -def check_value_unselected_field( - current_config, new_config, current_attr, new_attr, group_keys, spec, obj -): +def check_value_unselected_field(current_config, new_config, current_attr, new_attr, group_keys, spec, obj): """ Check value unselected field :param current_config: Current config @@ -708,8 +728,7 @@ def check_value_unselected_field( def check_empty_values(key, current, new): key_in_config = key in current and key in new if key_in_config and ( - (bool(current[key]) is False and new[key] is None) - or (current[key] is None and bool(new[key]) is False) + (bool(current[key]) is False and new[key] is None) or (current[key] is None and bool(new[key]) is False) ): return True @@ -739,18 +758,11 @@ def check_empty_values(key, current, new): obj, ) else: - if spec[k]["type"] in ["list", "map", "string", "structure"]: - if config_is_ro(obj, k, spec[k]["limits"]) or check_empty_values( - k, current_config, new_config - ): + if spec[k]["type"] in {"list", "map", "secretmap", "string", "structure"}: + if config_is_ro(obj, k, spec[k]["limits"]) or check_empty_values(k, current_config, new_config): continue - if ( - not v - and k in current_config - and k in new_config - and current_config[k] != new_config[k] - ): + if not v and k in current_config and k in new_config and current_config[k] != new_config[k]: msg = ( f"Value of `{k}` field is different in current and new config." f" Current: ({current_config[k]}), New: ({new_config[k]})" @@ -933,6 +945,7 @@ def sub_key_is_required(_key): # for process_file_type() function not need `if old_conf:` process_file_type(group or obj, spec, conf) process_password(spec, conf) + conf = process_secretmap(spec=spec, conf=conf) return conf @@ -952,15 +965,13 @@ def check_config_type( def check_str(_idx, _v): if not isinstance(_v, str): - _msg = ( - f'{label} ("{_v}") of element "{_idx}" of config key "{key}/{subkey}"' - f" should be string ({ref})" - ) + _msg = f'{label} ("{_v}") of element "{_idx}" of config key "{key}/{subkey}"' f" should be string ({ref})" raise_adcm_ex("CONFIG_VALUE_ERROR", _msg) if ( - value is None + value is None # pylint: disable=too-many-boolean-expressions or (spec["type"] == "map" and value == {}) + or (spec["type"] == "secretmap" and value == {}) or (spec["type"] == "list" and value == []) ): if inactive: @@ -985,7 +996,7 @@ def check_str(_idx, _v): for idx, v in enumerate(value): check_str(idx, v) - if spec["type"] == "map": + if spec["type"] in {"map", "secretmap"}: if not isinstance(value, dict): raise_adcm_ex("CONFIG_VALUE_ERROR", tmpl1.format("should be a map")) @@ -1002,7 +1013,7 @@ def check_str(_idx, _v): if "required" in spec and spec["required"] and value == "": raise_adcm_ex("CONFIG_VALUE_ERROR", tmpl1.format(should_not_be_empty)) - if spec["type"] == "file": + if spec["type"] in {"file", "secretfile"}: if not isinstance(value, str): raise_adcm_ex("CONFIG_VALUE_ERROR", tmpl2.format("should be string")) @@ -1100,7 +1111,7 @@ def set_object_config(obj, keys, value): check_config_type(proto, key, subkey, obj_to_dict(pconf, ("type", "limits", "option")), value) replace_object_config(obj, key, subkey, value, pconf) - if pconf.type == "file": + if pconf.type in {"file", "secretfile"}: save_file_type(obj, key, subkey, value) log_value = value @@ -1131,9 +1142,7 @@ def get_main_info(obj: Optional[ADCMEntity]) -> Optional[str]: def get_adcm_config(section=None): adcm_object = ADCM.objects.last() - current_configlog = ConfigLog.objects.get( - obj_ref=adcm_object.config, id=adcm_object.config.current - ) + current_configlog = ConfigLog.objects.get(obj_ref=adcm_object.config, id=adcm_object.config.current) if not section: return current_configlog.attr, current_configlog.config diff --git a/python/cm/adcm_schema.yaml b/python/cm/adcm_schema.yaml index eed22c2e77..ff9387e5cd 100644 --- a/python/cm/adcm_schema.yaml +++ b/python/cm/adcm_schema.yaml @@ -274,8 +274,10 @@ config_dict_sub: secrettext: config_dict_sub_string text: config_dict_sub_string file: config_dict_sub_string + secretfile: config_dict_sub_string list: config_dict_sub_list map: config_dict_sub_map + secretmap: config_dict_sub_map structure: config_dict_sub_structure json: config_dict_sub_json option: config_dict_sub_option @@ -385,8 +387,10 @@ config_list_object: secrettext: config_list_string text: config_list_string file: config_list_string + secretfile: config_list_string list: config_list_list map: config_list_map + secretmap: config_list_map structure: config_list_structure variant: config_list_variant json: config_list_json @@ -430,8 +434,10 @@ config_sub_list_object: secrettext: config_list_string text: config_list_string file: config_list_string + secretfile: config_list_string list: config_list_list map: config_list_map + secretmap: config_list_map structure: config_list_structure variant: config_list_variant json: config_list_json @@ -705,7 +711,6 @@ common_action: &common_action description: string params: json ui_options: json - button: string allow_to_terminate: boolean partial_execution: boolean host_action: boolean diff --git a/python/cm/ansible_plugin.py b/python/cm/ansible_plugin.py index 4df5283238..71adbf935c 100644 --- a/python/cm/ansible_plugin.py +++ b/python/cm/ansible_plugin.py @@ -14,14 +14,16 @@ import fcntl import json -import os from collections import defaultdict +# isort: off from ansible.errors import AnsibleError from ansible.utils.vars import merge_hash - from ansible.plugins.action import ActionBase -from cm import config + +# isort: on +from django.conf import settings + from cm.adcm_config import set_object_config from cm.api import add_hc, get_hc from cm.api_context import ctx @@ -37,6 +39,7 @@ Host, HostProvider, JobLog, + JobStatus, LogStorage, Prototype, ServiceComponent, @@ -72,19 +75,17 @@ MSG_NO_ROUTE = "Incorrect combination of args. Bad Dobby!" MSG_NO_SERVICE_NAME = "You must specify service name in arguments." MSG_NO_MULTI_STATE_TO_DELETE = ( - "You try to delete absent multi_state. You should define missing_ok as True " - "or choose an existing multi_state" + "You try to delete absent multi_state. You should define missing_ok as True or choose an existing multi_state" ) def job_lock(job_id): - fname = os.path.join(config.RUN_DIR, f'{job_id}/config.json') - fd = open(fname, 'r', encoding='utf_8') + fd = open(settings.RUN_DIR / f"{job_id}/config.json", "r", encoding=settings.ENCODING_UTF_8) try: fcntl.flock(fd.fileno(), fcntl.LOCK_EX) # pylint: disable=I1101 return fd except IOError as e: - return err('LOCK_ERROR', e) + return err("LOCK_ERROR", e) def job_unlock(fd): @@ -98,14 +99,14 @@ def check_context_type(task_vars, *context_type, err_msg=None): """ if not task_vars: raise AnsibleError(MSG_NO_CONFIG) - if 'context' not in task_vars: + if "context" not in task_vars: raise AnsibleError(MSG_NO_CONTEXT) - if not isinstance(task_vars['context'], dict): + if not isinstance(task_vars["context"], dict): raise AnsibleError(MSG_NO_CONTEXT) - context = task_vars['context'] - if context['type'] not in context_type: + context = task_vars["context"] + if context["type"] not in context_type: if err_msg is None: - err_msg = MSG_WRONG_CONTEXT.format(', '.join(context_type), context['type']) + err_msg = MSG_WRONG_CONTEXT.format(", ".join(context_type), context["type"]) raise AnsibleError(err_msg) @@ -114,7 +115,7 @@ def get_object_id_from_context(task_vars, id_type, *context_type, err_msg=None): Get object id from context. """ check_context_type(task_vars, *context_type, err_msg=err_msg) - context = task_vars['context'] + context = task_vars["context"] if id_type not in context: raise AnsibleError(MSG_WRONG_CONTEXT_ID.format(id_type)) return context[id_type] @@ -130,8 +131,8 @@ def _wrap_call(self, func, *args): try: func(*args) except AdcmEx as e: - return {'failed': True, 'msg': e.msg} - return {'changed': True} + return {"failed": True, "msg": e.msg} + return {"changed": True} def _check_mandatory(self): for arg in self._MANDATORY_ARGS: @@ -171,65 +172,57 @@ def _do_host_from_provider(self, task_vars, context): def run(self, tmp=None, task_vars=None): # pylint: disable=too-many-branches self._check_mandatory() obj_type = self._task.args["type"] - job_id = task_vars['job']['id'] + job_id = task_vars["job"]["id"] lock = job_lock(job_id) - if obj_type == 'cluster': - check_context_type(task_vars, 'cluster', 'service', 'component') - res = self._do_cluster( - task_vars, {'cluster_id': self._get_job_var(task_vars, 'cluster_id')} - ) + if obj_type == "cluster": + check_context_type(task_vars, "cluster", "service", "component") + res = self._do_cluster(task_vars, {"cluster_id": self._get_job_var(task_vars, "cluster_id")}) elif obj_type == "service" and "service_name" in self._task.args: - check_context_type(task_vars, 'cluster', 'service', 'component') - res = self._do_service_by_name( - task_vars, {'cluster_id': self._get_job_var(task_vars, 'cluster_id')} - ) + check_context_type(task_vars, "cluster", "service", "component") + res = self._do_service_by_name(task_vars, {"cluster_id": self._get_job_var(task_vars, "cluster_id")}) elif obj_type == "service": - check_context_type(task_vars, 'service', 'component') + check_context_type(task_vars, "service", "component") res = self._do_service( task_vars, { - 'cluster_id': self._get_job_var(task_vars, 'cluster_id'), - 'service_id': self._get_job_var(task_vars, 'service_id'), + "cluster_id": self._get_job_var(task_vars, "cluster_id"), + "service_id": self._get_job_var(task_vars, "service_id"), }, ) elif obj_type == "host" and "host_id" in self._task.args: - check_context_type(task_vars, 'provider') + check_context_type(task_vars, "provider") res = self._do_host_from_provider(task_vars, {}) elif obj_type == "host": - check_context_type(task_vars, 'host') - res = self._do_host(task_vars, {'host_id': self._get_job_var(task_vars, 'host_id')}) + check_context_type(task_vars, "host") + res = self._do_host(task_vars, {"host_id": self._get_job_var(task_vars, "host_id")}) elif obj_type == "provider": - check_context_type(task_vars, 'provider', 'host') - res = self._do_provider( - task_vars, {'provider_id': self._get_job_var(task_vars, 'provider_id')} - ) + check_context_type(task_vars, "provider", "host") + res = self._do_provider(task_vars, {"provider_id": self._get_job_var(task_vars, "provider_id")}) elif obj_type == "component" and "component_name" in self._task.args: if "service_name" in self._task.args: - check_context_type(task_vars, 'cluster', 'service', 'component') + check_context_type(task_vars, "cluster", "service", "component") res = self._do_component_by_name( task_vars, { - 'cluster_id': self._get_job_var(task_vars, 'cluster_id'), - 'service_id': None, + "cluster_id": self._get_job_var(task_vars, "cluster_id"), + "service_id": None, }, ) else: - check_context_type(task_vars, 'cluster', 'service', 'component') - if task_vars['job'].get('service_id', None) is None: + check_context_type(task_vars, "cluster", "service", "component") + if task_vars["job"].get("service_id", None) is None: raise AnsibleError(MSG_NO_SERVICE_NAME) res = self._do_component_by_name( task_vars, { - 'cluster_id': self._get_job_var(task_vars, 'cluster_id'), - 'service_id': self._get_job_var(task_vars, 'service_id'), + "cluster_id": self._get_job_var(task_vars, "cluster_id"), + "service_id": self._get_job_var(task_vars, "service_id"), }, ) elif obj_type == "component": - check_context_type(task_vars, 'component') - res = self._do_component( - task_vars, {'component_id': self._get_job_var(task_vars, 'component_id')} - ) + check_context_type(task_vars, "component") + res = self._do_component(task_vars, {"component_id": self._get_job_var(task_vars, "component_id")}) else: raise AnsibleError(MSG_NO_ROUTE) @@ -243,9 +236,7 @@ def run(self, tmp=None, task_vars=None): # pylint: disable=too-many-branches def get_component_by_name(cluster_id, service_id, component_name, service_name): if service_id is not None: - comp = ServiceComponent.obj.get( - cluster_id=cluster_id, service_id=service_id, prototype__name=component_name - ) + comp = ServiceComponent.obj.get(cluster_id=cluster_id, service_id=service_id, prototype__name=component_name) else: comp = ServiceComponent.obj.get( cluster_id=cluster_id, @@ -257,7 +248,7 @@ def get_component_by_name(cluster_id, service_id, component_name, service_name): def get_service_by_name(cluster_id, service_name): cluster = Cluster.obj.get(id=cluster_id) - proto = Prototype.obj.get(type='service', name=service_name, bundle=cluster.prototype.bundle) + proto = Prototype.obj.get(type="service", name=service_name, bundle=cluster.prototype.bundle) return ClusterObject.obj.get(cluster=cluster, prototype=proto) @@ -298,7 +289,7 @@ def set_service_state_by_name(cluster_id, service_name, state): def set_service_state(cluster_id, service_id, state): - obj = ClusterObject.obj.get(id=service_id, cluster__id=cluster_id, prototype__type='service') + obj = ClusterObject.obj.get(id=service_id, cluster__id=cluster_id, prototype__type="service") return _set_object_state(obj, state) @@ -319,13 +310,11 @@ def set_service_multi_state_by_name(cluster_id, service_name, multi_state): def set_service_multi_state(cluster_id, service_id, multi_state): - obj = ClusterObject.obj.get(id=service_id, cluster__id=cluster_id, prototype__type='service') + obj = ClusterObject.obj.get(id=service_id, cluster__id=cluster_id, prototype__type="service") return _set_object_multi_state(obj, multi_state) -def set_component_multi_state_by_name( - cluster_id, service_id, component_name, service_name, multi_state -): +def set_component_multi_state_by_name(cluster_id, service_id, component_name, service_name, multi_state): obj = get_component_by_name(cluster_id, service_id, component_name, service_name) return _set_object_multi_state(obj, multi_state) @@ -346,42 +335,40 @@ def set_host_multi_state(host_id, multi_state): def change_hc(job_id, cluster_id, operations): # pylint: disable=too-many-branches - ''' + """ For use in ansible plugin adcm_hc - ''' + """ lock = job_lock(job_id) job = JobLog.objects.get(id=job_id) action = Action.objects.get(id=job.action_id) if action.hostcomponentmap: - err('ACTION_ERROR', 'You can not change hc in plugin for action with hc_acl') + err("ACTION_ERROR", "You can not change hc in plugin for action with hc_acl") cluster = Cluster.obj.get(id=cluster_id) hc = get_hc(cluster) for op in operations: - service = ClusterObject.obj.get(cluster=cluster, prototype__name=op['service']) - component = ServiceComponent.obj.get( - cluster=cluster, service=service, prototype__name=op['component'] - ) - host = Host.obj.get(cluster=cluster, fqdn=op['host']) + service = ClusterObject.obj.get(cluster=cluster, prototype__name=op["service"]) + component = ServiceComponent.obj.get(cluster=cluster, service=service, prototype__name=op["component"]) + host = Host.obj.get(cluster=cluster, fqdn=op["host"]) item = { - 'host_id': host.id, - 'service_id': service.id, - 'component_id': component.id, + "host_id": host.id, + "service_id": service.id, + "component_id": component.id, } - if op['action'] == 'add': + if op["action"] == "add": if item not in hc: hc.append(item) else: msg = 'There is already component "{}" on host "{}"' - err('COMPONENT_CONFLICT', msg.format(component.prototype.name, host.fqdn)) - elif op['action'] == 'remove': + err("COMPONENT_CONFLICT", msg.format(component.prototype.name, host.fqdn)) + elif op["action"] == "remove": if item in hc: hc.remove(item) else: msg = 'There is no component "{}" on host "{}"' - err('COMPONENT_CONFLICT', msg.format(component.prototype.name, host.fqdn)) + err("COMPONENT_CONFLICT", msg.format(component.prototype.name, host.fqdn)) else: - err('INVALID_INPUT', f'unknown hc action "{op["action"]}"') + err("INVALID_INPUT", f'unknown hc action "{op["action"]}"') add_hc(cluster, hc) job_unlock(lock) @@ -408,7 +395,7 @@ def set_service_config_by_name(cluster_id, service_name, keys, value): def set_service_config(cluster_id, service_id, keys, value): - obj = ClusterObject.obj.get(id=service_id, cluster__id=cluster_id, prototype__type='service') + obj = ClusterObject.obj.get(id=service_id, cluster__id=cluster_id, prototype__type="service") return set_object_config(obj, keys, value) @@ -446,13 +433,11 @@ def unset_service_multi_state_by_name(cluster_id, service_name, multi_state, mis def unset_service_multi_state(cluster_id, service_id, multi_state, missing_ok): - obj = ClusterObject.obj.get(id=service_id, cluster__id=cluster_id, prototype__type='service') + obj = ClusterObject.obj.get(id=service_id, cluster__id=cluster_id, prototype__type="service") return _unset_object_multi_state(obj, multi_state, missing_ok) -def unset_component_multi_state_by_name( - cluster_id, service_id, component_name, service_name, multi_state, missing_ok -): +def unset_component_multi_state_by_name(cluster_id, service_id, component_name, service_name, multi_state, missing_ok): obj = get_component_by_name(cluster_id, service_id, component_name, service_name) return _unset_object_multi_state(obj, multi_state, missing_ok) @@ -473,8 +458,8 @@ def unset_host_multi_state(host_id, multi_state, missing_ok): def log_group_check(group: GroupCheckLog, fail_msg: str, success_msg: str): - logs = CheckLog.objects.filter(group=group).values('result') - result = all(log['result'] for log in logs) + logs = CheckLog.objects.filter(group=group).values("result") + result = all(log["result"] for log in logs) if result: msg = success_msg @@ -489,34 +474,34 @@ def log_group_check(group: GroupCheckLog, fail_msg: str, success_msg: str): def log_check(job_id: int, group_data: dict, check_data: dict) -> CheckLog: lock = job_lock(job_id) job = JobLog.obj.get(id=job_id) - if job.status != config.Job.RUNNING: - err('JOB_NOT_FOUND', f'job #{job.pk} has status "{job.status}", not "running"') + if job.status != JobStatus.RUNNING: + err("JOB_NOT_FOUND", f'job #{job.pk} has status "{job.status}", not "running"') - group_title = group_data.pop('title') + group_title = group_data.pop("title") if group_title: group, _ = GroupCheckLog.objects.get_or_create(job=job, title=group_title) else: group = None - check_data.update({'job': job, 'group': group}) + check_data.update({"job": job, "group": group}) cl = CheckLog.objects.create(**check_data) if group is not None: - group_data.update({'group': group}) + group_data.update({"group": group}) log_group_check(**group_data) - ls, _ = LogStorage.objects.get_or_create(job=job, name='ansible', type='check', format='json') + ls, _ = LogStorage.objects.get_or_create(job=job, name="ansible", type="check", format="json") post_event( - 'add_job_log', - 'job', + "add_job_log", + "job", job_id, { - 'id': ls.pk, - 'type': ls.type, - 'name': ls.name, - 'format': ls.format, + "id": ls.pk, + "type": ls.type, + "name": ls.name, + "format": ls.format, }, ) job_unlock(lock) @@ -530,23 +515,19 @@ def get_check_log(job_id: int): for cl in CheckLog.objects.filter(job_id=job_id): group = cl.group if group is None: - data.append( - {'title': cl.title, 'type': 'check', 'message': cl.message, 'result': cl.result} - ) + data.append({"title": cl.title, "type": "check", "message": cl.message, "result": cl.result}) else: if group not in group_subs: data.append( { - 'title': group.title, - 'type': 'group', - 'message': group.message, - 'result': group.result, - 'content': group_subs[group], + "title": group.title, + "type": "group", + "message": group.message, + "result": group.result, + "content": group_subs[group], } ) - group_subs[group].append( - {'title': cl.title, 'type': 'check', 'message': cl.message, 'result': cl.result} - ) + group_subs[group].append({"title": cl.title, "type": "check", "message": cl.message, "result": cl.result}) return data @@ -556,9 +537,7 @@ def finish_check(job_id: int): return job = JobLog.objects.get(id=job_id) - LogStorage.objects.filter(job=job, name='ansible', type='check', format='json').update( - body=json.dumps(data) - ) + LogStorage.objects.filter(job=job, name="ansible", type="check", format="json").update(body=json.dumps(data)) GroupCheckLog.objects.filter(job=job).delete() CheckLog.objects.filter(job=job).delete() diff --git a/python/cm/api.py b/python/cm/api.py index 0d6ce762b5..fd119de1eb 100644 --- a/python/cm/api.py +++ b/python/cm/api.py @@ -1,6 +1,6 @@ -# 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 +# 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 # @@ -9,17 +9,17 @@ # 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. -# pylint:disable=logging-fstring-interpolation,too-many-lines + +# pylint: disable=too-many-lines import json +from functools import wraps from django.core.exceptions import MultipleObjectsReturned from django.db import transaction from django.utils import timezone from version_utils import rpm -import cm.issue -import cm.status_api from cm.adcm_config import ( check_json_config, init_object_config, @@ -29,12 +29,17 @@ save_obj_config, ) from cm.api_context import ctx -from cm.errors import AdcmEx -from cm.errors import raise_adcm_ex as err +from cm.errors import raise_adcm_ex +from cm.issue import ( + check_bound_components, + check_component_constraint, + check_component_requires, + update_hierarchy_issues, + update_issue_after_deleting, +) from cm.logger import logger from cm.models import ( ADCMEntity, - Bundle, Cluster, ClusterBind, ClusterObject, @@ -45,20 +50,23 @@ Host, HostComponent, HostProvider, - MaintenanceModeType, + MaintenanceMode, + ObjectType, Prototype, PrototypeExport, PrototypeImport, ServiceComponent, TaskLog, ) +from cm.status_api import api_request, post_event from rbac.models import re_apply_object_policy -def check_license(bundle: Bundle) -> None: - if bundle.license == "unaccepted": - msg = 'License for bundle "{}" {} {} is not accepted' - err("LICENSE_ERROR", msg.format(bundle.name, bundle.version, bundle.edition)) +def check_license(proto: Prototype) -> None: + if proto.license == "unaccepted": + raise_adcm_ex( + "LICENSE_ERROR", f'License for prototype "{proto.name}" {proto.type} {proto.version} is not accepted' + ) def version_in(version: str, ver: PrototypeImport) -> bool: @@ -68,6 +76,7 @@ def version_in(version: str, ver: PrototypeImport) -> bool: elif ver.min_version: if rpm.compare_versions(version, ver.min_version) < 0: return False + if ver.max_strict: if rpm.compare_versions(version, ver.max_version) >= 0: return False @@ -78,23 +87,6 @@ def version_in(version: str, ver: PrototypeImport) -> bool: return True -def check_proto_type(proto, check_type): - if proto.type != check_type: - msg = "Prototype type should be {}, not {}" - err("OBJ_TYPE_ERROR", msg.format(check_type, proto.type)) - - -def load_host_map(): - hosts = list(Host.objects.values("id", "maintenance_mode")) - for host in hosts: - if host["maintenance_mode"] == MaintenanceModeType.On: - host["maintenance_mode"] = True - else: - host["maintenance_mode"] = False - - return cm.status_api.api_request("post", "/object/host/", hosts) - - def load_service_map(): comps = {} hosts = {} @@ -102,38 +94,38 @@ def load_service_map(): services = {} passive = {} for c in ServiceComponent.objects.filter(prototype__monitoring="passive"): - passive[c.id] = True + passive[c.pk] = True for hc in HostComponent.objects.all(): - if hc.component.id in passive: + if hc.component.pk in passive: continue - key = f"{hc.host.id}.{hc.component.id}" - hc_map[key] = {"cluster": hc.cluster.id, "service": hc.service.id} - if str(hc.cluster.id) not in comps: - comps[str(hc.cluster.id)] = {} + key = f"{hc.host.pk}.{hc.component.pk}" + hc_map[key] = {"cluster": hc.cluster.pk, "service": hc.service.pk} + if str(hc.cluster.pk) not in comps: + comps[str(hc.cluster.pk)] = {} - if str(hc.service.id) not in comps[str(hc.cluster.id)]: - comps[str(hc.cluster.id)][str(hc.service.id)] = [] + if str(hc.service.pk) not in comps[str(hc.cluster.pk)]: + comps[str(hc.cluster.pk)][str(hc.service.pk)] = [] - comps[str(hc.cluster.id)][str(hc.service.id)].append(key) + comps[str(hc.cluster.pk)][str(hc.service.pk)].append(key) for host in Host.objects.filter(prototype__monitoring="active"): if host.cluster: - cluster_id = host.cluster.id + cluster_pk = host.cluster.pk else: - cluster_id = 0 + cluster_pk = 0 - if cluster_id not in hosts: - hosts[cluster_id] = [] + if cluster_pk not in hosts: + hosts[cluster_pk] = [] - hosts[cluster_id].append(host.id) + hosts[cluster_pk].append(host.pk) for co in ClusterObject.objects.filter(prototype__monitoring="active"): - if co.cluster.id not in services: - services[co.cluster.id] = [] + if co.cluster.pk not in services: + services[co.cluster.pk] = [] - services[co.cluster.id].append(co.id) + services[co.cluster.pk].append(co.pk) m = { "hostservice": hc_map, @@ -141,32 +133,77 @@ def load_service_map(): "service": services, "host": hosts, } - cm.status_api.api_request("post", "/servicemap/", m) - load_host_map() + api_request("post", "/servicemap/", m) + load_mm_objects() + + +def load_mm_objects(): + """send ids of all objects in mm to status server""" + clusters = Cluster.objects.filter(prototype__type=ObjectType.Cluster, prototype__allow_maintenance_mode=True) + + service_ids = set() + component_ids = set() + host_ids = [] + + for service in ClusterObject.objects.filter(cluster__in=clusters).prefetch_related("servicecomponent_set"): + if service.maintenance_mode == MaintenanceMode.ON: + service_ids.add(service.pk) + for component in service.servicecomponent_set.all(): + if component.maintenance_mode == MaintenanceMode.ON: + component_ids.add(component.pk) + + for host in Host.objects.filter(cluster__in=clusters): + if host.maintenance_mode == MaintenanceMode.ON: + host_ids.append(host.pk) + + data = { + "services": list(service_ids), + "components": list(component_ids), + "hosts": host_ids, + } + return api_request("post", "/object/mm/", data) + + +def update_mm_objects(func): + @wraps(func) + def wrapper(*args, **kwargs): + res = func(*args, **kwargs) + load_mm_objects() + return res + + return wrapper def add_cluster(proto, name, desc=""): - check_proto_type(proto, "cluster") - check_license(proto.bundle) + if proto.type != "cluster": + raise_adcm_ex("OBJ_TYPE_ERROR", f"Prototype type should be cluster, not {proto.type}") + + check_license(proto) with transaction.atomic(): cluster = Cluster.objects.create(prototype=proto, name=name, description=desc) obj_conf = init_object_config(proto, cluster) cluster.config = obj_conf cluster.save() - cm.issue.update_hierarchy_issues(cluster) + update_hierarchy_issues(cluster) - cm.status_api.post_event("create", "cluster", cluster.id) + post_event("create", "cluster", cluster.pk) load_service_map() - logger.info(f"cluster #{cluster.id} {cluster.name} is added") + logger.info("cluster #%s %s is added", cluster.pk, cluster.name) + return cluster def add_host(proto, provider, fqdn, desc=""): - check_proto_type(proto, "host") - check_license(proto.bundle) + if proto.type != "host": + raise_adcm_ex("OBJ_TYPE_ERROR", f"Prototype type should be host, not {proto.type}") + + check_license(proto) if proto.bundle != provider.prototype.bundle: - msg = "Host prototype bundle #{} does not match with host provider bundle #{}" - err("FOREIGN_HOST", msg.format(proto.bundle.id, provider.prototype.bundle.id)) + raise_adcm_ex( + "FOREIGN_HOST", + f"Host prototype bundle #{proto.bundle.pk} does not match with " + f"host provider bundle #{provider.prototype.bundle.pk}", + ) with transaction.atomic(): host = Host.objects.create(prototype=proto, provider=provider, fqdn=fqdn, description=desc) @@ -174,47 +211,38 @@ def add_host(proto, provider, fqdn, desc=""): host.config = obj_conf host.save() host.add_to_concerns(ctx.lock) - cm.issue.update_hierarchy_issues(host.provider) + update_hierarchy_issues(host.provider) re_apply_object_policy(provider) ctx.event.send_state() - cm.status_api.post_event("create", "host", host.id, "provider", str(provider.id)) + post_event("create", "host", host.pk, "provider", str(provider.pk)) load_service_map() - logger.info(f"host #{host.id} {host.fqdn} is added") - return host - - -def add_provider_host(provider_id, fqdn, desc=""): - """ - add provider host - - This is intended for use in adcm_add_host ansible plugin only - """ - provider = HostProvider.obj.get(id=provider_id) - proto = Prototype.objects.get(bundle=provider.prototype.bundle, type="host") + logger.info("host #%s %s is added", host.pk, host.fqdn) - return add_host(proto, provider, fqdn, desc) + return host def add_host_provider(proto, name, desc=""): - check_proto_type(proto, "provider") - check_license(proto.bundle) + if proto.type != "provider": + raise_adcm_ex("OBJ_TYPE_ERROR", f"Prototype type should be provider, not {proto.type}") + + check_license(proto) with transaction.atomic(): provider = HostProvider.objects.create(prototype=proto, name=name, description=desc) obj_conf = init_object_config(proto, provider) provider.config = obj_conf provider.save() provider.add_to_concerns(ctx.lock) - cm.issue.update_hierarchy_issues(provider) + update_hierarchy_issues(provider) ctx.event.send_state() - cm.status_api.post_event("create", "provider", provider.id) - logger.info(f"host provider #{provider.id} {provider.name} is added") + post_event("create", "provider", provider.pk) + logger.info("host provider #%s %s is added", provider.pk, provider.name) + return provider -def _cancel_locking_tasks(obj: ADCMEntity, obj_deletion=False): - """Cancel all tasks that have locks on object""" +def cancel_locking_tasks(obj: ADCMEntity, obj_deletion=False): for lock in obj.concerns.filter(type=ConcernType.Lock): for task in TaskLog.objects.filter(lock=lock): task.cancel(obj_deletion=obj_deletion) @@ -223,79 +251,78 @@ def _cancel_locking_tasks(obj: ADCMEntity, obj_deletion=False): def delete_host_provider(provider, cancel_tasks=True): hosts = Host.objects.filter(provider=provider) if hosts: - msg = 'There is host #{} "{}" of host {}' - err("PROVIDER_CONFLICT", msg.format(hosts[0].id, hosts[0].fqdn, obj_ref(provider))) + raise_adcm_ex( + "PROVIDER_CONFLICT", f'There is host #{hosts[0].pk} "{hosts[0].fqdn}" of host {obj_ref(provider)}' + ) + if cancel_tasks: - _cancel_locking_tasks(provider, obj_deletion=True) + cancel_locking_tasks(provider, obj_deletion=True) - provider_id = provider.id + provider_pk = provider.pk provider.delete() - cm.status_api.post_event("delete", "provider", provider_id) - logger.info(f"host provider #{provider_id} is deleted") + post_event("delete", "provider", provider_pk) + logger.info("host provider #%s is deleted", provider_pk) def add_host_to_cluster(cluster, host): if host.cluster: - if host.cluster.id != cluster.id: - msg = f"Host #{host.id} belong to cluster #{host.cluster.id}" - err("FOREIGN_HOST", msg) + if host.cluster.pk != cluster.pk: + raise_adcm_ex("FOREIGN_HOST", f"Host #{host.pk} belong to cluster #{host.cluster.pk}") else: - err("HOST_CONFLICT") + raise_adcm_ex("HOST_CONFLICT") with transaction.atomic(): - DummyData.objects.filter(id=1).update(date=timezone.now()) - if cluster.prototype.allow_maintenance_mode: - host.maintenance_mode = MaintenanceModeType.Off.value + DummyData.objects.filter(pk=1).update(date=timezone.now()) host.cluster = cluster host.save() host.add_to_concerns(ctx.lock) - cm.issue.update_hierarchy_issues(host) + update_hierarchy_issues(host) re_apply_object_policy(cluster) - cm.status_api.post_event("add", "host", host.id, "cluster", str(cluster.id)) + post_event("add", "host", host.pk, "cluster", str(cluster.pk)) load_service_map() - logger.info( - "host #%s %s is added to cluster #%s %s", host.id, host.fqdn, cluster.id, cluster.name - ) + logger.info("host #%s %s is added to cluster #%s %s", host.pk, host.fqdn, cluster.pk, cluster.name) + return host -def get_cluster_and_host(cluster_id, fqdn, host_id): - cluster = Cluster.obj.get(id=cluster_id) +def get_cluster_and_host(cluster_pk, fqdn, host_pk): + cluster = Cluster.obj.get(pk=cluster_pk) host = None if fqdn: host = Host.obj.get(fqdn=fqdn) - elif host_id: - if not isinstance(host_id, int): - err("HOST_NOT_FOUND", f'host_id must be integer (got "{host_id}")') - host = Host.obj.get(id=host_id) + elif host_pk: + if not isinstance(host_pk, int): + raise_adcm_ex("HOST_NOT_FOUND", f'host_id must be integer (got "{host_pk}")') + + host = Host.obj.get(pk=host_pk) else: - err("HOST_NOT_FOUND", "fqdn or host_id is mandatory args") + raise_adcm_ex("HOST_NOT_FOUND", "fqdn or host_id is mandatory args") return cluster, host -def add_host_to_cluster_by_id(cluster_id, fqdn, host_id): +def add_host_to_cluster_by_pk(cluster_pk, fqdn, host_pk): """ add host to cluster This is intended for use in adcm_add_host_to_cluster ansible plugin only """ - cluster, host = get_cluster_and_host(cluster_id, fqdn, host_id) - return add_host_to_cluster(cluster, host) + return add_host_to_cluster(*get_cluster_and_host(cluster_pk=cluster_pk, fqdn=fqdn, host_pk=host_pk)) -def remove_host_from_cluster_by_id(cluster_id, fqdn, host_id): +def remove_host_from_cluster_by_pk(cluster_pk, fqdn, host_pk): """ remove host from cluster This is intended for use in adcm_remove_host_from_cluster ansible plugin only """ - cluster, host = get_cluster_and_host(cluster_id, fqdn, host_id) + + cluster, host = get_cluster_and_host(cluster_pk, fqdn, host_pk) if host.cluster != cluster: - err("HOST_CONFLICT", "you can remove host only from you own cluster") + raise_adcm_ex("HOST_CONFLICT", "you can remove host only from you own cluster") remove_host_from_cluster(host) @@ -303,32 +330,33 @@ def remove_host_from_cluster_by_id(cluster_id, fqdn, host_id): def delete_host(host, cancel_tasks=True): cluster = host.cluster if cluster: - msg = 'Host #{} "{}" belong to {}' - err("HOST_CONFLICT", msg.format(host.id, host.fqdn, obj_ref(cluster))) + raise_adcm_ex("HOST_CONFLICT", f'Host #{host.pk} "{host.fqdn}" belong to {obj_ref(cluster)}') if cancel_tasks: - _cancel_locking_tasks(host, obj_deletion=True) + cancel_locking_tasks(host, obj_deletion=True) - host_id = host.id + host_pk = host.pk host.delete() - cm.status_api.post_event("delete", "host", host_id) + post_event("delete", "host", host_pk) load_service_map() - cm.issue.update_issue_after_deleting() - logger.info(f"host #{host_id} is deleted") + update_issue_after_deleting() + logger.info("host #%s is deleted", host_pk) -def delete_host_by_id(host_id): +def delete_host_by_pk(host_pk): """ Host deleting This is intended for use in adcm_delete_host ansible plugin only """ - host = Host.obj.get(id=host_id) + + host = Host.obj.get(pk=host_pk) delete_host(host, cancel_tasks=False) def _clean_up_related_hc(service: ClusterObject) -> None: """Unconditional removal of HostComponents related to removing ClusterObject""" + qs = ( HostComponent.objects.filter(cluster=service.cluster) .exclude(service=service) @@ -341,77 +369,64 @@ def _clean_up_related_hc(service: ClusterObject) -> None: save_hc(service.cluster, new_hc_list) -def _clean_up_related_bind(service: ClusterObject) -> None: - """Unconditional removal of ClusterBind related to removing ClusterObject""" - ClusterBind.objects.filter(source_service=service).delete() - - -def delete_service_by_id(service_id): +def delete_service_by_pk(service_pk): """ Unconditional removal of service from cluster This is intended for use in adcm_delete_service ansible plugin only """ + with transaction.atomic(): - DummyData.objects.filter(id=1).update(date=timezone.now()) - service = ClusterObject.obj.get(id=service_id) + DummyData.objects.filter(pk=1).update(date=timezone.now()) + service = ClusterObject.obj.get(pk=service_pk) _clean_up_related_hc(service) - _clean_up_related_bind(service) - delete_service(service, cancel_tasks=False) + ClusterBind.objects.filter(source_service=service).delete() + delete_service(service=service) -def delete_service_by_name(service_name, cluster_id): +def delete_service_by_name(service_name, cluster_pk): """ Unconditional removal of service from cluster This is intended for use in adcm_delete_service ansible plugin only """ + with transaction.atomic(): - DummyData.objects.filter(id=1).update(date=timezone.now()) - service = ClusterObject.obj.get(cluster__id=cluster_id, prototype__name=service_name) + DummyData.objects.filter(pk=1).update(date=timezone.now()) + service = ClusterObject.obj.get(cluster__pk=cluster_pk, prototype__name=service_name) _clean_up_related_hc(service) - _clean_up_related_bind(service) - delete_service(service, cancel_tasks=False) - - -def delete_service(service: ClusterObject, cancel_tasks=True) -> None: - if HostComponent.objects.filter(cluster=service.cluster, service=service).exists(): - err("SERVICE_CONFLICT", f"Service #{service.id} has component(s) on host(s)") + ClusterBind.objects.filter(source_service=service).delete() + delete_service(service=service) - if ClusterBind.objects.filter(source_service=service).exists(): - err("SERVICE_CONFLICT", f"Service #{service.id} has exports(s)") - if cancel_tasks: - _cancel_locking_tasks(service, obj_deletion=True) - - service_id = service.id - cluster = service.cluster +def delete_service(service: ClusterObject) -> None: + service_pk = service.pk service.delete() - cm.issue.update_issue_after_deleting() - cm.issue.update_hierarchy_issues(cluster) - re_apply_object_policy(cluster) - cm.status_api.post_event("delete", "service", service_id) + update_issue_after_deleting() + update_hierarchy_issues(service.cluster) + re_apply_object_policy(service.cluster) + post_event("delete", "service", service_pk) load_service_map() - logger.info(f"service #{service_id} is deleted") + logger.info("service #%s is deleted", service_pk) def delete_cluster(cluster, cancel_tasks=True): if cancel_tasks: - _cancel_locking_tasks(cluster, obj_deletion=True) + cancel_locking_tasks(cluster, obj_deletion=True) - cluster_id = cluster.id + cluster_pk = cluster.pk hosts = cluster.host_set.all() - host_ids = [str(host.id) for host in hosts] - hosts.update(maintenance_mode=MaintenanceModeType.Disabled) + host_pks = [str(host.pk) for host in hosts] + hosts.update(maintenance_mode=MaintenanceMode.OFF) logger.debug( "Deleting cluster #%s. Set `%s` maintenance mode value for `%s` hosts.", - cluster_id, - MaintenanceModeType.Disabled, - ", ".join(host_ids), + cluster_pk, + MaintenanceMode.OFF, + ", ".join(host_pks), ) cluster.delete() - cm.issue.update_issue_after_deleting() - cm.status_api.post_event("delete", "cluster", cluster_id) + update_issue_after_deleting() + post_event("delete", "cluster", cluster_pk) load_service_map() @@ -419,22 +434,22 @@ def remove_host_from_cluster(host): cluster = host.cluster hc = HostComponent.objects.filter(cluster=cluster, host=host) if hc: - return err("HOST_CONFLICT", f"Host #{host.id} has component(s)") + return raise_adcm_ex("HOST_CONFLICT", f"Host #{host.pk} has component(s)") with transaction.atomic(): - host.maintenance_mode = MaintenanceModeType.Disabled.value + host.maintenance_mode = MaintenanceMode.OFF host.cluster = None host.save() for group in cluster.group_config.all(): group.hosts.remove(host) - cm.issue.update_hierarchy_issues(host) + update_hierarchy_issues(host) host.remove_from_concerns(ctx.lock) - cm.issue.update_hierarchy_issues(cluster) + update_hierarchy_issues(cluster) re_apply_object_policy(cluster) ctx.event.send_state() - cm.status_api.post_event("remove", "host", host.id, "cluster", str(cluster.id)) + post_event("remove", "host", host.pk, "cluster", str(cluster.pk)) load_service_map() return host @@ -444,27 +459,27 @@ def unbind(cbind): import_obj = get_bind_obj(cbind.cluster, cbind.service) export_obj = get_bind_obj(cbind.source_cluster, cbind.source_service) check_import_default(import_obj, export_obj) - cbind_id = cbind.id - cbind_cluster_id = cbind.cluster.id + cbind_pk = cbind.pk + cbind_cluster_pk = cbind.cluster.pk with transaction.atomic(): DummyData.objects.filter(id=1).update(date=timezone.now()) cbind.delete() - cm.issue.update_hierarchy_issues(cbind.cluster) + update_hierarchy_issues(cbind.cluster) - cm.status_api.post_event("delete", "bind", cbind_id, "cluster", str(cbind_cluster_id)) + post_event("delete", "bind", cbind_pk, "cluster", str(cbind_cluster_pk)) def add_service_to_cluster(cluster, proto): - check_proto_type(proto, "service") - check_license(proto.bundle) + if proto.type != "service": + raise_adcm_ex("OBJ_TYPE_ERROR", f"Prototype type should be service, not {proto.type}") + + check_license(proto) if not proto.shared: if cluster.prototype.bundle != proto.bundle: - msg = '{} does not belong to bundle "{}" {}' - err( + raise_adcm_ex( "SERVICE_CONFLICT", - msg.format( - proto_ref(proto), cluster.prototype.bundle.name, cluster.prototype.version - ), + f'{proto_ref(proto)} does not belong to bundle ' + f'"{cluster.prototype.bundle.name}" {cluster.prototype.version}', ) with transaction.atomic(): @@ -473,14 +488,12 @@ def add_service_to_cluster(cluster, proto): cs.config = obj_conf cs.save() add_components_to_service(cluster, cs) - cm.issue.update_hierarchy_issues(cs) + update_hierarchy_issues(cs) re_apply_object_policy(cluster) - cm.status_api.post_event("add", "service", cs.id, "cluster", str(cluster.id)) + post_event("add", "service", cs.pk, "cluster", str(cluster.pk)) load_service_map() - logger.info( - f"service #{cs.id} {cs.prototype.name} is added to cluster #{cluster.id} {cluster.name}" - ) + logger.info("service #%s %s is added to cluster #%s %s", cs.pk, cs.prototype.name, cluster.pk, cluster.name) return cs @@ -491,43 +504,35 @@ def add_components_to_service(cluster, service): obj_conf = init_object_config(comp, sc) sc.config = obj_conf sc.save() - cm.issue.update_hierarchy_issues(sc) + update_hierarchy_issues(sc) -def get_bundle_proto(bundle): - proto = Prototype.objects.filter(bundle=bundle, name=bundle.name) - - return proto[0] - - -def get_license(bundle): - if not bundle.license_path: +def get_license(proto: Prototype) -> str | None: + if not proto.license_path: return None - - ref = f'bundle "{bundle.name}" {bundle.version}' - proto = get_bundle_proto(bundle) - - return read_bundle_file(proto, bundle.license_path, bundle.hash, "license file", ref) + if not isinstance(proto, Prototype): + raise_adcm_ex("LICENSE_ERROR") + return read_bundle_file(proto, proto.license_path, proto.bundle.hash, "license file") -def accept_license(bundle): - if not bundle.license_path: - err("LICENSE_ERROR", "This bundle has no license") +def accept_license(proto: Prototype) -> None: + if not proto.license_path: + raise_adcm_ex("LICENSE_ERROR", "This bundle has no license") - if bundle.license == "absent": - err("LICENSE_ERROR", "This bundle has no license") + if proto.license == "absent": + raise_adcm_ex("LICENSE_ERROR", "This bundle has no license") - bundle.license = "accepted" - bundle.save() + proto.license = "accepted" + proto.save() -def update_obj_config(obj_conf, conf, attr, desc=""): +def update_obj_config(obj_conf, conf, attr, desc="") -> ConfigLog: if not isinstance(attr, dict): - err("INVALID_CONFIG_UPDATE", "attr should be a map") + raise_adcm_ex("INVALID_CONFIG_UPDATE", "attr should be a map") obj = obj_conf.object if obj is None: - err("INVALID_CONFIG_UPDATE", f'unknown object type "{obj_conf}"') + raise_adcm_ex("INVALID_CONFIG_UPDATE", f'unknown object type "{obj_conf}"') group = None if isinstance(obj, GroupConfig): @@ -541,13 +546,13 @@ def update_obj_config(obj_conf, conf, attr, desc=""): new_conf = check_json_config(proto, group or obj, conf, old_conf.config, attr, old_conf.attr) with transaction.atomic(): cl = save_obj_config(obj_conf, new_conf, attr, desc) - cm.issue.update_hierarchy_issues(obj) + update_hierarchy_issues(obj) re_apply_object_policy(obj) if group is not None: - cm.status_api.post_event("change_config", "group-config", group.id, "version", str(cl.id)) + post_event("change_config", "group-config", group.pk, "version", str(cl.pk)) else: - cm.status_api.post_event("change_config", proto.type, obj.id, "version", str(cl.id)) + post_event("change_config", proto.type, obj.pk, "version", str(cl.pk)) return cl @@ -560,9 +565,9 @@ def get_hc(cluster): for hc in HostComponent.objects.filter(cluster=cluster): hc_map.append( { - "host_id": hc.host.id, - "service_id": hc.service.id, - "component_id": hc.component.id, + "host_id": hc.host.pk, + "service_id": hc.service.pk, + "component_id": hc.component.pk, } ) @@ -572,15 +577,14 @@ def get_hc(cluster): def check_sub_key(hc_in): def check_sub(_sub_key, _sub_type, _item): if _sub_key not in _item: - _msg = '"{}" sub-field of hostcomponent is required' - raise AdcmEx("INVALID_INPUT", _msg.format(_sub_key)) + raise_adcm_ex("INVALID_INPUT", f'"{_sub_key}" sub-field of hostcomponent is required') + if not isinstance(_item[_sub_key], _sub_type): - _msg = '"{}" sub-field of hostcomponent should be "{}"' - raise AdcmEx("INVALID_INPUT", _msg.format(_sub_key, _sub_type)) + raise_adcm_ex("INVALID_INPUT", f'"{_sub_key}" sub-field of hostcomponent should be "{_sub_type}"') seen = {} if not isinstance(hc_in, list): - raise AdcmEx("INVALID_INPUT", "hostcomponent should be array") + raise_adcm_ex("INVALID_INPUT", "hostcomponent should be array") for item in hc_in: for sub_key, sub_type in (("service_id", int), ("host_id", int), ("component_id", int)): @@ -590,26 +594,23 @@ def check_sub(_sub_key, _sub_type, _item): if key not in seen: seen[key] = 1 else: - msg = "duplicate ({}) in host service list" - - raise AdcmEx("INVALID_INPUT", msg.format(item)) + raise_adcm_ex("INVALID_INPUT", f"duplicate ({item}) in host service list") def make_host_comp_list(cluster, hc_in): host_comp_list = [] for item in hc_in: - host = Host.obj.get(id=item["host_id"]) - service = ClusterObject.obj.get(id=item["service_id"], cluster=cluster) - comp = ServiceComponent.obj.get(id=item["component_id"], cluster=cluster, service=service) + host = Host.obj.get(pk=item["host_id"]) + service = ClusterObject.obj.get(pk=item["service_id"], cluster=cluster) + comp = ServiceComponent.obj.get(pk=item["component_id"], cluster=cluster, service=service) if not host.cluster: - msg = f"host #{host.id} {host.fqdn} does not belong to any cluster" - - raise AdcmEx("FOREIGN_HOST", msg) - - if host.cluster.id != cluster.id: - msg = "host {} (cluster #{}) does not belong to cluster #{}" + raise_adcm_ex("FOREIGN_HOST", f"host #{host.pk} {host.fqdn} does not belong to any cluster") - raise AdcmEx("FOREIGN_HOST", msg.format(host.fqdn, host.cluster.id, cluster.id)) + if host.cluster.pk != cluster.pk: + raise_adcm_ex( + "FOREIGN_HOST", + f"host {host.fqdn} (cluster #{host.cluster.pk}) does not belong to cluster #{cluster.pk}", + ) host_comp_list.append((service, host, comp)) @@ -620,12 +621,10 @@ def check_hc(cluster, hc_in): check_sub_key(hc_in) host_comp_list = make_host_comp_list(cluster, hc_in) for service in ClusterObject.objects.filter(cluster=cluster): - cm.issue.check_component_constraint( - cluster, service.prototype, [i for i in host_comp_list if i[0] == service] - ) + check_component_constraint(cluster, service.prototype, [i for i in host_comp_list if i[0] == service]) - cm.issue.check_component_requires(host_comp_list) - cm.issue.check_bound_components(host_comp_list) + check_component_requires(host_comp_list) + check_bound_components(host_comp_list) check_maintenance_mode(cluster, host_comp_list) return host_comp_list @@ -636,17 +635,15 @@ def check_maintenance_mode(cluster, host_comp_list): try: HostComponent.objects.get(cluster=cluster, service=service, host=host, component=comp) except HostComponent.DoesNotExist: - if host.maintenance_mode == MaintenanceModeType.On.value: - raise AdcmEx("INVALID_HC_HOST_IN_MM") # pylint: disable=raise-missing-from + if host.maintenance_mode == MaintenanceMode.ON: + raise_adcm_ex("INVALID_HC_HOST_IN_MM") def still_existed_hc(cluster, host_comp_list): result = [] for (service, host, comp) in host_comp_list: try: - existed_hc = HostComponent.objects.get( - cluster=cluster, service=service, host=host, component=comp - ) + existed_hc = HostComponent.objects.get(cluster=cluster, service=service, host=host, component=comp) result.append(existed_hc) except HostComponent.DoesNotExist: continue @@ -654,7 +651,9 @@ def still_existed_hc(cluster, host_comp_list): return result -def save_hc(cluster, host_comp_list): # pylint: disable=too-many-locals +def save_hc(cluster, host_comp_list): + # pylint: disable=too-many-locals + hc_queryset = HostComponent.objects.filter(cluster=cluster) service_map = {hc.service for hc in hc_queryset} old_hosts = {i.host for i in hc_queryset.select_related("host").all()} @@ -681,7 +680,7 @@ def save_hc(cluster, host_comp_list): # pylint: disable=too-many-locals hc_queryset.delete() result = [] - for (proto, host, comp) in host_comp_list: + for proto, host, comp in host_comp_list: hc = HostComponent( cluster=cluster, service=proto, @@ -692,12 +691,12 @@ def save_hc(cluster, host_comp_list): # pylint: disable=too-many-locals result.append(hc) ctx.event.send_state() - cm.status_api.post_event("change_hostcomponentmap", "cluster", cluster.id) - cm.issue.update_hierarchy_issues(cluster) + post_event("change_hostcomponentmap", "cluster", cluster.pk) + update_hierarchy_issues(cluster) for provider in [host.provider for host in Host.objects.filter(cluster=cluster)]: - cm.issue.update_hierarchy_issues(provider) + update_hierarchy_issues(provider) - cm.issue.update_issue_after_deleting() + update_issue_after_deleting() load_service_map() for service in service_map: re_apply_object_policy(service) @@ -711,7 +710,7 @@ def save_hc(cluster, host_comp_list): # pylint: disable=too-many-locals def add_hc(cluster, hc_in): host_comp_list = check_hc(cluster, hc_in) with transaction.atomic(): - DummyData.objects.filter(id=1).update(date=timezone.now()) + DummyData.objects.filter(pk=1).update(date=timezone.now()) new_hc = save_hc(cluster, host_comp_list) return new_hc @@ -735,10 +734,10 @@ def get_export(_cluster, _service, _pi): export_proto = {} for pe in PrototypeExport.objects.filter(prototype__name=_pi.name): # Merge all export groups of prototype to one export - if pe.prototype.id in export_proto: + if pe.prototype.pk in export_proto: continue - export_proto[pe.prototype.id] = True + export_proto[pe.prototype.pk] = True if not version_in(pe.prototype.version, _pi): continue @@ -750,7 +749,7 @@ def get_export(_cluster, _service, _pi): "obj_name": cls.name, "bundle_name": cls.prototype.display_name, "bundle_version": cls.prototype.version, - "id": {"cluster_id": cls.id}, + "id": {"cluster_id": cls.pk}, "binded": bool(binded), "bind_id": getattr(binded, "id", None), } @@ -763,13 +762,13 @@ def get_export(_cluster, _service, _pi): "obj_name": co.cluster.name + "/" + co.prototype.display_name, "bundle_name": co.prototype.display_name, "bundle_version": co.prototype.version, - "id": {"cluster_id": co.cluster.id, "service_id": co.id}, + "id": {"cluster_id": co.cluster.pk, "service_id": co.pk}, "binded": bool(binded), "bind_id": getattr(binded, "id", None), } ) else: - err("BIND_ERROR", f"unexpected export type: {pe.prototype.type}") + raise_adcm_ex("BIND_ERROR", f"unexpected export type: {pe.prototype.type}") return exports @@ -781,7 +780,7 @@ def get_export(_cluster, _service, _pi): for pi in PrototypeImport.objects.filter(prototype=proto): imports.append( { - "id": pi.id, + "id": pi.pk, "name": pi.name, "required": pi.required, "multibind": pi.multibind, @@ -794,29 +793,29 @@ def get_export(_cluster, _service, _pi): def check_bind_post(bind_list): if not isinstance(bind_list, list): - err("BIND_ERROR", "bind should be an array") + raise_adcm_ex("BIND_ERROR", "bind should be an array") for b in bind_list: if not isinstance(b, dict): - err("BIND_ERROR", "bind item should be a map") + raise_adcm_ex("BIND_ERROR", "bind item should be a map") if "import_id" not in b: - err("BIND_ERROR", 'bind item does not have required "import_id" key') + raise_adcm_ex("BIND_ERROR", 'bind item does not have required "import_id" key') if not isinstance(b["import_id"], int): - err("BIND_ERROR", 'bind item "import_id" value should be integer') + raise_adcm_ex("BIND_ERROR", 'bind item "import_id" value should be integer') if "export_id" not in b: - err("BIND_ERROR", 'bind item does not have required "export_id" key') + raise_adcm_ex("BIND_ERROR", 'bind item does not have required "export_id" key') if not isinstance(b["export_id"], dict): - err("BIND_ERROR", 'bind item "export_id" value should be a map') + raise_adcm_ex("BIND_ERROR", 'bind item "export_id" value should be a map') if "cluster_id" not in b["export_id"]: - err("BIND_ERROR", 'bind item export_id does not have required "cluster_id" key') + raise_adcm_ex("BIND_ERROR", 'bind item export_id does not have required "cluster_id" key') if not isinstance(b["export_id"]["cluster_id"], int): - err("BIND_ERROR", 'bind item export_id "cluster_id" value should be integer') + raise_adcm_ex("BIND_ERROR", 'bind item export_id "cluster_id" value should be integer') def check_import_default(import_obj, export_obj): @@ -824,15 +823,14 @@ def check_import_default(import_obj, export_obj): if not pi.default: return - cl = ConfigLog.objects.get(obj_ref=import_obj.config, id=import_obj.config.current) + cl = ConfigLog.objects.get(obj_ref=import_obj.config, pk=import_obj.config.current) if not cl.attr: return for name in json.loads(pi.default): if name in cl.attr: if "active" in cl.attr[name] and not cl.attr[name]["active"]: - msg = 'Default import "{}" for {} is inactive' - err("BIND_ERROR", msg.format(name, obj_ref(import_obj))) + raise_adcm_ex("BIND_ERROR", f'Default import "{name}" for {obj_ref(import_obj)} is inactive') def get_bind_obj(cluster, service): @@ -843,12 +841,13 @@ def get_bind_obj(cluster, service): return obj -def multi_bind(cluster, service, bind_list): # pylint: disable=too-many-locals,too-many-statements - def get_pi(import_id, _import_obj): - _pi = PrototypeImport.obj.get(id=import_id) +def multi_bind(cluster, service, bind_list): + # pylint: disable=too-many-locals,too-many-statements + + def get_pi(import_pk, _import_obj): + _pi = PrototypeImport.obj.get(id=import_pk) if _pi.prototype != _import_obj.prototype: - _msg = "Import #{} does not belong to {}" - err("BIND_ERROR", _msg.format(import_id, obj_ref(_import_obj))) + raise_adcm_ex("BIND_ERROR", f"Import #{import_pk} does not belong to {obj_ref(_import_obj)}") return _pi @@ -857,16 +856,15 @@ def get_export_service(_b, _export_cluster): if "service_id" in _b["export_id"]: _export_co = ClusterObject.obj.get(id=_b["export_id"]["service_id"]) if _export_co.cluster != _export_cluster: - _msg = "export {} is not belong to {}" - err("BIND_ERROR", _msg.format(obj_ref(_export_co), obj_ref(_export_cluster))) + raise_adcm_ex("BIND_ERROR", f"export {obj_ref(_export_co)} is not belong to {obj_ref(_export_cluster)}") return _export_co def cook_key(_cluster, _service): if _service: - return f"{_cluster.id}.{_service.id}" + return f"{_cluster.pk}.{_service.pk}" - return str(_cluster.id) + return str(_cluster.pk) check_bind_post(bind_list) import_obj = get_bind_obj(cluster, service) @@ -885,24 +883,17 @@ def cook_key(_cluster, _service): export_obj = export_co if cook_key(export_cluster, export_co) in new_bind: - err("BIND_ERROR", "Bind list has duplicates") + raise_adcm_ex("BIND_ERROR", "Bind list has duplicates") if pi.name != export_obj.prototype.name: - msg = 'Export {} does not match import name "{}"' - err("BIND_ERROR", msg.format(obj_ref(export_obj), pi.name)) + raise_adcm_ex("BIND_ERROR", f'Export {obj_ref(export_obj)} does not match import name "{pi.name}"') if not version_in(export_obj.prototype.version, pi): - msg = 'Import "{}" of {} versions ({}, {}) does not match export version: {} ({})' - err( + raise_adcm_ex( "BIND_ERROR", - msg.format( - export_obj.prototype.name, - proto_ref(pi.prototype), - pi.min_version, - pi.max_version, - export_obj.prototype.version, - obj_ref(export_obj), - ), + f'Import "{export_obj.prototype.name}" of { proto_ref(pi.prototype)} ' + f'versions ({pi.min_version}, {pi.max_version}) does not match export ' + f'version: {export_obj.prototype.version} ({obj_ref(export_obj)})', ) cbind = ClusterBind( @@ -911,7 +902,7 @@ def cook_key(_cluster, _service): source_cluster=export_cluster, source_service=export_co, ) - new_bind[cook_key(export_cluster, export_co)] = (pi, cbind, export_obj) + new_bind[cook_key(export_cluster, export_co)] = pi, cbind, export_obj with transaction.atomic(): for key, value in old_bind.items(): @@ -927,32 +918,35 @@ def cook_key(_cluster, _service): if key in old_bind: continue - (pi, cb, export_obj) = value + pi, cb, export_obj = value check_multi_bind(pi, cluster, service, cb.source_cluster, cb.source_service) cb.save() logger.info("bind %s to %s", obj_ref(export_obj), obj_ref(import_obj)) - cm.issue.update_hierarchy_issues(cluster) + update_hierarchy_issues(cluster) return get_import(cluster, service) -def bind(cluster, service, export_cluster, export_service_id): # pylint: disable=too-many-branches +def bind(cluster, service, export_cluster, export_service_pk): + # pylint: disable=too-many-branches + """ Adapter between old and new bind interface /api/.../bind/ -> /api/.../import/ bind() -> multi_bind() """ + export_service = None - if export_service_id: - export_service = ClusterObject.obj.get(cluster=export_cluster, id=export_service_id) + if export_service_pk: + export_service = ClusterObject.obj.get(cluster=export_cluster, id=export_service_pk) if not PrototypeExport.objects.filter(prototype=export_service.prototype): - err("BIND_ERROR", f"{obj_ref(export_service)} do not have exports") + raise_adcm_ex("BIND_ERROR", f"{obj_ref(export_service)} do not have exports") name = export_service.prototype.name else: if not PrototypeExport.objects.filter(prototype=export_cluster.prototype): - err("BIND_ERROR", f"{obj_ref(export_cluster)} does not have exports") + raise_adcm_ex("BIND_ERROR", f"{obj_ref(export_cluster)} does not have exports") name = export_cluster.prototype.name @@ -964,7 +958,7 @@ def bind(cluster, service, export_cluster, export_service_id): # pylint: disabl try: pi = PrototypeImport.obj.get(prototype=import_obj.prototype, name=name) except MultipleObjectsReturned: - err("BIND_ERROR", "Old api does not support multi bind. Go to /api/v1/.../import/") + raise_adcm_ex("BIND_ERROR", "Old api does not support multi bind. Go to /api/v1/.../import/") bind_list = [] for imp in get_import(cluster, service): @@ -972,20 +966,20 @@ def bind(cluster, service, export_cluster, export_service_id): # pylint: disabl if exp["binded"]: bind_list.append({"import_id": imp["id"], "export_id": exp["id"]}) - item = {"import_id": pi.id, "export_id": {"cluster_id": export_cluster.id}} + item = {"import_id": pi.id, "export_id": {"cluster_id": export_cluster.pk}} if export_service: - item["export_id"]["service_id"] = export_service.id + item["export_id"]["service_id"] = export_service.pk bind_list.append(item) multi_bind(cluster, service, bind_list) res = { - "export_cluster_id": export_cluster.id, + "export_cluster_id": export_cluster.pk, "export_cluster_name": export_cluster.name, "export_cluster_prototype_name": export_cluster.prototype.name, } if export_service: - res["export_service_id"] = export_service.id + res["export_service_id"] = export_service.pk return res @@ -1005,9 +999,7 @@ def check_multi_bind(actual_import, cluster, service, export_cluster, export_ser if export_service: if source_proto == export_service.prototype: - msg = "can not multi bind {} to {}" - err("BIND_ERROR", msg.format(proto_ref(source_proto), obj_ref(cluster))) + raise_adcm_ex("BIND_ERROR", f"can not multi bind {proto_ref(source_proto)} to {obj_ref(cluster)}") else: if source_proto == export_cluster.prototype: - msg = "can not multi bind {} to {}" - err("BIND_ERROR", msg.format(proto_ref(source_proto), obj_ref(cluster))) + raise_adcm_ex("BIND_ERROR", f"can not multi bind {proto_ref(source_proto)} to {obj_ref(cluster)}") diff --git a/python/cm/api_context.py b/python/cm/api_context.py index 811802209f..422d5ad26a 100644 --- a/python/cm/api_context.py +++ b/python/cm/api_context.py @@ -46,9 +46,7 @@ def get_job_data(self): job_id = Path(ansible_config).parent.name try: - self.job = models.JobLog.objects.select_related('task', 'task__lock').get( - id=int(job_id) - ) + self.job = models.JobLog.objects.select_related('task', 'task__lock').get(id=int(job_id)) except (ValueError, models.ObjectDoesNotExist): return diff --git a/python/cm/apps.py b/python/cm/apps.py index 642d664866..c526c88624 100644 --- a/python/cm/apps.py +++ b/python/cm/apps.py @@ -11,11 +11,7 @@ # limitations under the License. # -*- coding: utf-8 -*- -from functools import partial - -from adwp_events.signals import m2m_change, model_change, model_delete from django.apps import AppConfig -from django.db.models.signals import m2m_changed, post_delete, post_save WATCHED_CM_MODELS = ( 'group-config', @@ -35,32 +31,16 @@ ) -def filter_out_event(module, name, obj): - # We filter the sending of events only for cm and rbac applications - # 'AnonymousUser', 'admin', 'status' are filtered out due to adwp_event design issue - if module in ['rbac.models'] and name in WATCHED_RBAC_MODELS: - if name == 'user' and obj.username in ['AnonymousUser', 'admin', 'status', 'system']: - return True - return False - if module in ['cm.models'] and name in WATCHED_CM_MODELS: - return False - return True - - class CmConfig(AppConfig): name = 'cm' - model_change = partial(model_change, filter_out=filter_out_event) - model_delete = partial(model_delete, filter_out=filter_out_event) - m2m_change = partial(m2m_change, filter_out=filter_out_event) def ready(self): # pylint: disable-next=import-outside-toplevel,unused-import from cm.signals import ( + m2m_change, mark_deleted_audit_object_handler, + model_change, + model_delete, rename_audit_object, rename_audit_object_host, ) - - post_save.connect(self.model_change, dispatch_uid='model_change') - post_delete.connect(self.model_delete, dispatch_uid='model_delete') - m2m_changed.connect(self.m2m_change, dispatch_uid='m2m_change') diff --git a/python/cm/bundle.py b/python/cm/bundle.py index d40b3d5074..ab688ea410 100644 --- a/python/cm/bundle.py +++ b/python/cm/bundle.py @@ -12,18 +12,16 @@ import functools import hashlib -import os -import os.path import shutil import tarfile +from pathlib import Path +from django.conf import settings from django.db import IntegrityError, transaction from version_utils import rpm import cm.stack import cm.status_api -from adcm.settings import ADCM_VERSION -from cm import config from cm.adcm_config import init_object_config, proto_ref, switch_config from cm.errors import raise_adcm_ex as err from cm.logger import logger @@ -82,7 +80,7 @@ def load_bundle(bundle_file): ProductCategory.re_collect() bundle.refresh_from_db() prepare_action_roles(bundle) - cm.status_api.post_event('create', 'bundle', bundle.id) + cm.status_api.post_event("create", "bundle", bundle.id) return bundle except: clear_stage() @@ -92,7 +90,7 @@ def load_bundle(bundle_file): def update_bundle(bundle): try: check_stage() - process_bundle(os.path.join(config.BUNDLE_DIR, bundle.hash), bundle.hash) + process_bundle(settings.BUNDLE_DIR / bundle.hash, bundle.hash) get_stage_bundle(bundle.name) second_pass() update_bundle_from_stage(bundle) @@ -111,17 +109,14 @@ def order_model_versions(model): count = 0 for obj in sorted( items, - key=functools.cmp_to_key( - lambda obj1, obj2: rpm.compare_versions(obj1.version, obj2.version) - ), + key=functools.cmp_to_key(lambda obj1, obj2: rpm.compare_versions(obj1.version, obj2.version)), ): if ver != obj.version: count += 1 - # log.debug("MODEL %s count: %s, %s %s", model, count, obj.name, obj.version) obj.version_order = count ver = obj.version # Update all table in one time. That is much faster than one by one method - model.objects.bulk_update(items, ['version_order']) + model.objects.bulk_update(items, ["version_order"]) def order_versions(): @@ -130,7 +125,7 @@ def order_versions(): def process_file(bundle_file): - path = os.path.join(config.DOWNLOAD_DIR, bundle_file) + path = str(settings.DOWNLOAD_DIR / bundle_file) bundle_hash = get_hash_safe(path) dir_path = untar_safe(bundle_hash, path) return (bundle_hash, dir_path) @@ -140,17 +135,17 @@ def untar_safe(bundle_hash, path): try: dir_path = untar(bundle_hash, path) except tarfile.ReadError: - err('BUNDLE_ERROR', f"Can\'t open bundle tar file: {path}") + err("BUNDLE_ERROR", f"Can't open bundle tar file: {path}") return dir_path def untar(bundle_hash, bundle): - path = os.path.join(config.BUNDLE_DIR, bundle_hash) - if os.path.isdir(path): + path = settings.BUNDLE_DIR / bundle_hash + if path.is_dir(): try: existed = Bundle.objects.get(hash=bundle_hash) - msg = 'Bundle already exists. Name: {}, version: {}, edition: {}' - err('BUNDLE_ERROR', msg.format(existed.name, existed.version, existed.edition)) + msg = "Bundle already exists. Name: {}, version: {}, edition: {}" + err("BUNDLE_ERROR", msg.format(existed.name, existed.version, existed.edition)) except Bundle.DoesNotExist: logger.warning( ( @@ -168,29 +163,29 @@ def get_hash_safe(path): try: bundle_hash = get_hash(path) except FileNotFoundError: - err('BUNDLE_ERROR', f"Can\'t find bundle file: {path}") + err("BUNDLE_ERROR", f"Can't find bundle file: {path}") except PermissionError: - err('BUNDLE_ERROR', f"Can\'t open bundle file: {path}") + err("BUNDLE_ERROR", f"Can't open bundle file: {path}") return bundle_hash def get_hash(bundle_file): sha1 = hashlib.sha1() - with open(bundle_file, 'rb') as fp: - for data in iter(lambda: fp.read(16384), b''): + with open(bundle_file, "rb") as fp: + for data in iter(lambda: fp.read(16384), b""): sha1.update(data) return sha1.hexdigest() def load_adcm(): check_stage() - adcm_file = os.path.join(config.BASE_DIR, 'conf', 'adcm', 'config.yaml') - conf = cm.stack.read_definition(adcm_file, 'yaml') + adcm_file = settings.BASE_DIR / "conf" / "adcm" / "config.yaml" + conf = cm.stack.read_definition(adcm_file, "yaml") if not conf: - logger.warning('Empty adcm config (%s)', adcm_file) + logger.warning("Empty adcm config (%s)", adcm_file) return try: - cm.stack.save_definition('', adcm_file, conf, {}, 'adcm', True) + cm.stack.save_definition("", adcm_file, conf, {}, "adcm", True) process_adcm() except: clear_stage() @@ -199,46 +194,46 @@ def load_adcm(): def process_adcm(): - sp = StagePrototype.objects.get(type='adcm') + adcm_stage_proto = StagePrototype.objects.get(type="adcm") adcm = ADCM.objects.filter() if adcm: old_proto = adcm[0].prototype - new_proto = sp + new_proto = adcm_stage_proto if old_proto.version == new_proto.version: - logger.debug('adcm vesrion %s, skip upgrade', old_proto.version) + logger.debug("adcm vesrion %s, skip upgrade", old_proto.version) elif rpm.compare_versions(old_proto.version, new_proto.version) < 0: - bundle = copy_stage('adcm', sp) + bundle = copy_stage("adcm", adcm_stage_proto) upgrade_adcm(adcm[0], bundle) else: - msg = 'Current adcm version {} is more than or equal to upgrade version {}' - err('UPGRADE_ERROR', msg.format(old_proto.version, new_proto.version)) + msg = "Current adcm version {} is more than or equal to upgrade version {}" + err("UPGRADE_ERROR", msg.format(old_proto.version, new_proto.version)) else: - bundle = copy_stage('adcm', sp) + bundle = copy_stage("adcm", adcm_stage_proto) init_adcm(bundle) def init_adcm(bundle): - proto = Prototype.objects.get(type='adcm', bundle=bundle) + proto = Prototype.objects.get(type="adcm", bundle=bundle) with transaction.atomic(): - adcm = ADCM.objects.create(prototype=proto, name='ADCM') + adcm = ADCM.objects.create(prototype=proto, name="ADCM") obj_conf = init_object_config(proto, adcm) adcm.config = obj_conf adcm.save() - logger.info('init adcm object version %s OK', proto.version) + logger.info("init adcm object version %s OK", proto.version) return adcm def upgrade_adcm(adcm, bundle): old_proto = adcm.prototype - new_proto = Prototype.objects.get(type='adcm', bundle=bundle) + new_proto = Prototype.objects.get(type="adcm", bundle=bundle) if rpm.compare_versions(old_proto.version, new_proto.version) >= 0: - msg = 'Current adcm version {} is more than or equal to upgrade version {}' - err('UPGRADE_ERROR', msg.format(old_proto.version, new_proto.version)) + msg = "Current adcm version {} is more than or equal to upgrade version {}" + err("UPGRADE_ERROR", msg.format(old_proto.version, new_proto.version)) with transaction.atomic(): adcm.prototype = new_proto adcm.save() switch_config(adcm, new_proto, old_proto) - logger.info('upgrade adcm OK from version %s to %s', old_proto.version, adcm.prototype.version) + logger.info("upgrade adcm OK from version %s to %s", old_proto.version, adcm.prototype.version) return adcm @@ -253,7 +248,7 @@ def process_bundle(path, bundle_hash): def check_stage(): def count(model): if model.objects.all().count(): - err('BUNDLE_ERROR', f'Stage is not empty {model}') + err("BUNDLE_ERROR", f"Stage is not empty {model}") for model in STAGE: count(model) @@ -278,15 +273,16 @@ def re_check_actions(): hc = act.hostcomponentmap ref = f'in hc_acl of action "{act.name}" of {proto_ref(act.prototype)}' for item in hc: - sp = StagePrototype.objects.filter(type='service', name=item['service']).first() - if not sp: + stage_proto = StagePrototype.objects.filter(type="service", name=item["service"]).first() + if not stage_proto: msg = 'Unknown service "{}" {}' - err('INVALID_ACTION_DEFINITION', msg.format(item['service'], ref)) - if not StagePrototype.objects.filter( - parent=sp, type='component', name=item['component'] - ): + err("INVALID_ACTION_DEFINITION", msg.format(item["service"], ref)) + if not StagePrototype.objects.filter(parent=stage_proto, type="component", name=item["component"]): msg = 'Unknown component "{}" of service "{}" {}' - err('INVALID_ACTION_DEFINITION', msg.format(item['component'], sp.name, ref)) + err( + "INVALID_ACTION_DEFINITION", + msg.format(item["component"], stage_proto.name, ref), + ) def check_component_requires(comp): @@ -295,15 +291,15 @@ def check_component_requires(comp): ref = f'in requires of component "{comp.name}" of {proto_ref(comp.parent)}' req_list = comp.requires for i, item in enumerate(req_list): - if 'service' in item: - service = StagePrototype.obj.get(name=item['service'], type='service') + if "service" in item: + service = StagePrototype.obj.get(name=item["service"], type="service") else: service = comp.parent - req_list[i]['service'] = comp.parent.name - req_comp = StagePrototype.obj.get(name=item['component'], type='component', parent=service) + req_list[i]["service"] = comp.parent.name + req_comp = StagePrototype.obj.get(name=item["component"], type="component", parent=service) if comp == req_comp: - msg = 'Component can not require themself {}' - err('COMPONENT_CONSTRAINT_ERROR', msg.format(ref)) + msg = "Component can not require themself {}" + err("COMPONENT_CONSTRAINT_ERROR", msg.format(ref)) comp.requires = req_list comp.save() @@ -313,83 +309,78 @@ def check_bound_component(comp): return ref = f'in "bound_to" of component "{comp.name}" of {proto_ref(comp.parent)}' bind = comp.bound_to - service = StagePrototype.obj.get(name=bind['service'], type='service') - bind_comp = StagePrototype.obj.get(name=bind['component'], type='component', parent=service) + service = StagePrototype.obj.get(name=bind["service"], type="service") + bind_comp = StagePrototype.obj.get(name=bind["component"], type="component", parent=service) if comp == bind_comp: - msg = 'Component can not require themself {}' - err('COMPONENT_CONSTRAINT_ERROR', msg.format(ref)) + msg = "Component can not require themself {}" + err("COMPONENT_CONSTRAINT_ERROR", msg.format(ref)) def re_check_components(): - for comp in StagePrototype.objects.filter(type='component'): + for comp in StagePrototype.objects.filter(type="component"): check_component_requires(comp) check_bound_component(comp) def check_variant_host(args, ref): - # log.debug('check_variant_host args: %s', args) def check_predicate(predicate, args): - if predicate == 'in_service': - # log.debug('check in_service %s', args) - StagePrototype.obj.get(type='service', name=args['service']) - elif predicate == 'in_component': - # log.debug('check in_component %s', args) - service = StagePrototype.obj.get(type='service', name=args['service']) - StagePrototype.obj.get(type='component', name=args['component'], parent=service) + if predicate == "in_service": + StagePrototype.obj.get(type="service", name=args["service"]) + elif predicate == "in_component": + service = StagePrototype.obj.get(type="service", name=args["service"]) + StagePrototype.obj.get(type="component", name=args["component"], parent=service) if args is None: return if isinstance(args, dict): - if 'predicate' not in args: + if "predicate" not in args: return - check_predicate(args['predicate'], args['args']) - check_variant_host(args['args'], ref) + check_predicate(args["predicate"], args["args"]) + check_variant_host(args["args"], ref) if isinstance(args, list): for i in args: - check_predicate(i['predicate'], i['args']) - check_variant_host(i['args'], ref) + check_predicate(i["predicate"], i["args"]) + check_variant_host(i["args"], ref) def re_check_config(): - for c in StagePrototypeConfig.objects.filter(type='variant'): + for c in StagePrototypeConfig.objects.filter(type="variant"): ref = proto_ref(c.prototype) lim = c.limits - if lim['source']['type'] == 'list': - keys = lim['source']['name'].split('/') + if lim["source"]["type"] == "list": + keys = lim["source"]["name"].split("/") name = keys[0] - subname = '' + subname = "" if len(keys) > 1: subname = keys[1] try: - s = StagePrototypeConfig.objects.get( - prototype=c.prototype, name=name, subname=subname - ) + s = StagePrototypeConfig.objects.get(prototype=c.prototype, name=name, subname=subname) except StagePrototypeConfig.DoesNotExist: msg = f'Unknown config source name "{{}}" for {ref} config "{c.name}/{c.subname}"' - err('INVALID_CONFIG_DEFINITION', msg.format(lim['source']['name'])) + err("INVALID_CONFIG_DEFINITION", msg.format(lim["source"]["name"])) if s == c: msg = f'Config parameter "{c.name}/{c.subname}" can not refer to itself ({ref})' - err('INVALID_CONFIG_DEFINITION', msg) - elif lim['source']['type'] == 'builtin': - if not lim['source']['args']: + err("INVALID_CONFIG_DEFINITION", msg) + elif lim["source"]["type"] == "builtin": + if not lim["source"]["args"]: continue - if lim['source']['name'] == 'host': + if lim["source"]["name"] == "host": msg = f'in source:args of {ref} config "{c.name}/{c.subname}"' - check_variant_host(lim['source']['args'], msg) - if 'service' in lim['source']['args']: - service = lim['source']['args']['service'] + check_variant_host(lim["source"]["args"], msg) + if "service" in lim["source"]["args"]: + service = lim["source"]["args"]["service"] try: - sp_service = StagePrototype.objects.get(type='service', name=service) + sp_service = StagePrototype.objects.get(type="service", name=service) except StagePrototype.DoesNotExist: msg = 'Service "{}" in source:args of {} config "{}/{}" does not exists' - err('INVALID_CONFIG_DEFINITION', msg.format(service, ref, c.name, c.subname)) - if 'component' in lim['source']['args']: - comp = lim['source']['args']['component'] + err("INVALID_CONFIG_DEFINITION", msg.format(service, ref, c.name, c.subname)) + if "component" in lim["source"]["args"]: + comp = lim["source"]["args"]["component"] try: - StagePrototype.objects.get(type='component', name=comp, parent=sp_service) + StagePrototype.objects.get(type="component", name=comp, parent=sp_service) except StagePrototype.DoesNotExist: msg = 'Component "{}" in source:args of {} config "{}/{}" does not exists' - err('INVALID_CONFIG_DEFINITION', msg.format(comp, ref, c.name, c.subname)) + err("INVALID_CONFIG_DEFINITION", msg.format(comp, ref, c.name, c.subname)) def second_pass(): @@ -401,27 +392,33 @@ def second_pass(): def copy_stage_prototype(stage_prototypes, bundle): prototypes = [] # Map for stage prototype id: new prototype for sp in stage_prototypes: - p = copy_obj( + proto = copy_obj( sp, Prototype, ( - 'type', - 'path', - 'name', - 'version', - 'required', - 'shared', - 'monitoring', - 'display_name', - 'description', - 'adcm_min_version', - 'venv', - 'config_group_customization', - 'allow_maintenance_mode', + "type", + "path", + "name", + "version", + "required", + "shared", + "license_path", + "license_hash", + "monitoring", + "display_name", + "description", + "adcm_min_version", + "venv", + "config_group_customization", + "allow_maintenance_mode", ), ) - p.bundle = bundle - prototypes.append(p) + if proto.license_path: + proto.license = "unaccepted" + if check_license(proto): + proto.license = "accepted" + proto.bundle = bundle + prototypes.append(proto) Prototype.objects.bulk_create(prototypes) @@ -432,15 +429,15 @@ def copy_stage_upgrade(stage_upgrades, bundle): su, Upgrade, ( - 'name', - 'description', - 'min_version', - 'max_version', - 'min_strict', - 'max_strict', - 'state_available', - 'state_on_success', - 'from_edition', + "name", + "description", + "min_version", + "max_version", + "min_strict", + "max_strict", + "state_available", + "state_on_success", + "from_edition", ), ) upg.bundle = bundle @@ -466,32 +463,31 @@ def copy_stage_actions(stage_actions, prototype): Action, prototype, ( - 'name', - 'type', - 'script', - 'script_type', - 'state_available', - 'state_unavailable', - 'state_on_success', - 'state_on_fail', - 'multi_state_available', - 'multi_state_unavailable', - 'multi_state_on_success_set', - 'multi_state_on_success_unset', - 'multi_state_on_fail_set', - 'multi_state_on_fail_unset', - 'params', - 'log_files', - 'hostcomponentmap', - 'button', - 'display_name', - 'description', - 'ui_options', - 'allow_to_terminate', - 'partial_execution', - 'host_action', - 'venv', - 'allow_in_maintenance_mode', + "name", + "type", + "script", + "script_type", + "state_available", + "state_unavailable", + "state_on_success", + "state_on_fail", + "multi_state_available", + "multi_state_unavailable", + "multi_state_on_success_set", + "multi_state_on_success_unset", + "multi_state_on_fail_set", + "multi_state_on_fail_unset", + "params", + "log_files", + "hostcomponentmap", + "display_name", + "description", + "ui_options", + "allow_to_terminate", + "partial_execution", + "host_action", + "venv", + "allow_in_maintenance_mode", ), ) Action.objects.bulk_create(actions) @@ -500,10 +496,10 @@ def copy_stage_actions(stage_actions, prototype): def copy_stage_sub_actons(bundle): sub_actions = [] for ssubaction in StageSubAction.objects.all(): - if ssubaction.action.prototype.type == 'component': + if ssubaction.action.prototype.type == "component": parent = Prototype.objects.get( bundle=bundle, - type='service', + type="service", name=ssubaction.action.prototype.parent.name, ) else: @@ -520,14 +516,14 @@ def copy_stage_sub_actons(bundle): ssubaction, SubAction, ( - 'name', - 'display_name', - 'script', - 'script_type', - 'state_on_fail', - 'multi_state_on_fail_set', - 'multi_state_on_fail_unset', - 'params', + "name", + "display_name", + "script", + "script_type", + "state_on_fail", + "multi_state_on_fail_set", + "multi_state_on_fail_unset", + "params", ), ) sub.action = action @@ -542,30 +538,30 @@ def copy_stage_component(stage_components, stage_proto, prototype, bundle): c, Prototype, ( - 'type', - 'path', - 'name', - 'version', - 'required', - 'monitoring', - 'bound_to', - 'constraint', - 'requires', - 'display_name', - 'description', - 'adcm_min_version', - 'config_group_customization', - 'venv', + "type", + "path", + "name", + "version", + "required", + "monitoring", + "bound_to", + "constraint", + "requires", + "display_name", + "description", + "adcm_min_version", + "config_group_customization", + "venv", ), ) comp.bundle = bundle comp.parent = prototype componets.append(comp) Prototype.objects.bulk_create(componets) - for sp in StagePrototype.objects.filter(type='component', parent=stage_proto): - p = Prototype.objects.get(name=sp.name, type='component', parent=prototype, bundle=bundle) - copy_stage_actions(StageAction.objects.filter(prototype=sp), p) - copy_stage_config(StagePrototypeConfig.objects.filter(prototype=sp), p) + for sp in StagePrototype.objects.filter(type="component", parent=stage_proto): + proto = Prototype.objects.get(name=sp.name, type="component", parent=prototype, bundle=bundle) + copy_stage_actions(StageAction.objects.filter(prototype=sp), proto) + copy_stage_config(StagePrototypeConfig.objects.filter(prototype=sp), proto) def copy_stage_import(stage_imports, prototype): @@ -574,14 +570,14 @@ def copy_stage_import(stage_imports, prototype): PrototypeImport, prototype, ( - 'name', - 'min_version', - 'max_version', - 'min_strict', - 'max_strict', - 'default', - 'required', - 'multibind', + "name", + "min_version", + "max_version", + "min_strict", + "max_strict", + "default", + "required", + "multibind", ), ) PrototypeImport.objects.bulk_create(imports) @@ -594,16 +590,16 @@ def copy_stage_config(stage_config, prototype): sc, PrototypeConfig, ( - 'name', - 'subname', - 'default', - 'type', - 'description', - 'display_name', - 'limits', - 'required', - 'ui_options', - 'group_customization', + "name", + "subname", + "default", + "type", + "description", + "display_name", + "limits", + "required", + "ui_options", + "group_customization", ), ) if sc.action: @@ -613,46 +609,36 @@ def copy_stage_config(stage_config, prototype): PrototypeConfig.objects.bulk_create(target_config) -def check_license(bundle): - b = Bundle.objects.filter(license_hash=bundle.license_hash, license='accepted') - if not b: - return False - return True +def check_license(proto): + return Prototype.objects.filter(license_hash=proto.license_hash, license="accepted").exists() def copy_stage(bundle_hash, bundle_proto): bundle = copy_obj( bundle_proto, Bundle, - ('name', 'version', 'edition', 'license_path', 'license_hash', 'description'), + ("name", "version", "edition", "description"), ) bundle.hash = bundle_hash - check_license(bundle) - if bundle.license_path: - bundle.license = 'unaccepted' - if check_license(bundle): - bundle.license = 'accepted' try: bundle.save() except IntegrityError: - shutil.rmtree(os.path.join(config.BUNDLE_DIR, bundle.hash)) + shutil.rmtree(settings.BUNDLE_DIR / bundle.hash) msg = 'Bundle "{}" {} already installed' - err('BUNDLE_ERROR', msg.format(bundle_proto.name, bundle_proto.version)) + err("BUNDLE_ERROR", msg.format(bundle_proto.name, bundle_proto.version)) - stage_prototypes = StagePrototype.objects.exclude(type='component') + stage_prototypes = StagePrototype.objects.exclude(type="component") copy_stage_prototype(stage_prototypes, bundle) for sp in stage_prototypes: - p = Prototype.objects.get(name=sp.name, type=sp.type, bundle=bundle) - copy_stage_actions(StageAction.objects.filter(prototype=sp), p) - copy_stage_config(StagePrototypeConfig.objects.filter(prototype=sp), p) - copy_stage_component( - StagePrototype.objects.filter(parent=sp, type='component'), sp, p, bundle - ) + proto = Prototype.objects.get(name=sp.name, type=sp.type, bundle=bundle) + copy_stage_actions(StageAction.objects.filter(prototype=sp), proto) + copy_stage_config(StagePrototypeConfig.objects.filter(prototype=sp), proto) + copy_stage_component(StagePrototype.objects.filter(parent=sp, type="component"), sp, proto, bundle) for se in StagePrototypeExport.objects.filter(prototype=sp): - pe = PrototypeExport(prototype=p, name=se.name) + pe = PrototypeExport(prototype=proto, name=se.name) pe.save() - copy_stage_import(StagePrototypeImport.objects.filter(prototype=sp), p) + copy_stage_import(StagePrototypeImport.objects.filter(prototype=sp), proto) copy_stage_sub_actons(bundle) copy_stage_upgrade(StageUpgrade.objects.all(), bundle) @@ -681,22 +667,22 @@ def update_bundle_from_stage( sp, Prototype, ( - 'type', - 'path', - 'name', - 'version', - 'required', - 'shared', - 'monitoring', - 'bound_to', - 'constraint', - 'requires', - 'display_name', - 'description', - 'adcm_min_version', - 'venv', - 'config_group_customization', - 'allow_maintenance_mode', + "type", + "path", + "name", + "version", + "required", + "shared", + "monitoring", + "bound_to", + "constraint", + "requires", + "display_name", + "description", + "adcm_min_version", + "venv", + "config_group_customization", + "allow_maintenance_mode", ), ) p.bundle = bundle @@ -708,29 +694,28 @@ def update_bundle_from_stage( action, saction, ( - 'type', - 'script', - 'script_type', - 'state_available', - 'state_on_success', - 'state_on_fail', - 'multi_state_available', - 'multi_state_on_success_set', - 'multi_state_on_success_unset', - 'multi_state_on_fail_set', - 'multi_state_on_fail_unset', - 'params', - 'log_files', - 'hostcomponentmap', - 'button', - 'display_name', - 'description', - 'ui_options', - 'allow_to_terminate', - 'partial_execution', - 'host_action', - 'venv', - 'allow_in_maintenance_mode', + "type", + "script", + "script_type", + "state_available", + "state_on_success", + "state_on_fail", + "multi_state_available", + "multi_state_on_success_set", + "multi_state_on_success_unset", + "multi_state_on_fail_set", + "multi_state_on_fail_unset", + "params", + "log_files", + "hostcomponentmap", + "display_name", + "description", + "ui_options", + "allow_to_terminate", + "partial_execution", + "host_action", + "venv", + "allow_in_maintenance_mode", ), ) except Action.DoesNotExist: @@ -738,30 +723,29 @@ def update_bundle_from_stage( saction, Action, ( - 'name', - 'type', - 'script', - 'script_type', - 'state_available', - 'state_on_success', - 'state_on_fail', - 'multi_state_available', - 'multi_state_on_success_set', - 'multi_state_on_success_unset', - 'multi_state_on_fail_set', - 'multi_state_on_fail_unset', - 'params', - 'log_files', - 'hostcomponentmap', - 'button', - 'display_name', - 'description', - 'ui_options', - 'allow_to_terminate', - 'partial_execution', - 'host_action', - 'venv', - 'allow_in_maintenance_mode', + "name", + "type", + "script", + "script_type", + "state_available", + "state_on_success", + "state_on_fail", + "multi_state_available", + "multi_state_on_success_set", + "multi_state_on_success_unset", + "multi_state_on_fail_set", + "multi_state_on_fail_unset", + "params", + "log_files", + "hostcomponentmap", + "display_name", + "description", + "ui_options", + "allow_to_terminate", + "partial_execution", + "host_action", + "venv", + "allow_in_maintenance_mode", ), ) action.prototype = p @@ -772,37 +756,35 @@ def update_bundle_from_stage( ssubaction, SubAction, ( - 'script', - 'script_type', - 'state_on_fail', - 'multi_state_on_fail_set', - 'multi_state_on_fail_unset', - 'params', + "script", + "script_type", + "state_on_fail", + "multi_state_on_fail_set", + "multi_state_on_fail_unset", + "params", ), ) sub.action = action sub.save() for sc in StagePrototypeConfig.objects.filter(prototype=sp): flist = ( - 'default', - 'type', - 'description', - 'display_name', - 'limits', - 'required', - 'ui_options', - 'group_customization', + "default", + "type", + "description", + "display_name", + "limits", + "required", + "ui_options", + "group_customization", ) act = None if sc.action: act = Action.objects.get(prototype=p, name=sc.action.name) try: - pconfig = PrototypeConfig.objects.get( - prototype=p, action=act, name=sc.name, subname=sc.subname - ) + pconfig = PrototypeConfig.objects.get(prototype=p, action=act, name=sc.name, subname=sc.subname) update_obj(pconfig, sc, flist) except PrototypeConfig.DoesNotExist: - pconfig = copy_obj(sc, PrototypeConfig, ('name', 'subname') + flist) + pconfig = copy_obj(sc, PrototypeConfig, ("name", "subname") + flist) pconfig.action = act pconfig.prototype = p pconfig.save() @@ -817,14 +799,14 @@ def update_bundle_from_stage( si, PrototypeImport, ( - 'name', - 'min_version', - 'max_version', - 'min_strict', - 'max_strict', - 'default', - 'required', - 'multibind', + "name", + "min_version", + "max_version", + "min_strict", + "max_strict", + "default", + "required", + "multibind", ), ) pi.prototype = p @@ -836,15 +818,15 @@ def update_bundle_from_stage( su, Upgrade, ( - 'name', - 'description', - 'min_version', - 'max_version', - 'min_strict', - 'max_strict', - 'state_available', - 'state_on_success', - 'from_edition', + "name", + "description", + "min_version", + "max_version", + "min_strict", + "max_strict", + "state_available", + "state_on_success", + "from_edition", ), ) upg.bundle = bundle @@ -861,75 +843,82 @@ def delete_bundle(bundle): if providers: p = providers[0] msg = 'There is provider #{} "{}" of bundle #{} "{}" {}' - err('BUNDLE_CONFLICT', msg.format(p.id, p.name, bundle.id, bundle.name, bundle.version)) + err("BUNDLE_CONFLICT", msg.format(p.id, p.name, bundle.id, bundle.name, bundle.version)) clusters = Cluster.objects.filter(prototype__bundle=bundle) if clusters: cl = clusters[0] msg = 'There is cluster #{} "{}" of bundle #{} "{}" {}' - err('BUNDLE_CONFLICT', msg.format(cl.id, cl.name, bundle.id, bundle.name, bundle.version)) + err("BUNDLE_CONFLICT", msg.format(cl.id, cl.name, bundle.id, bundle.name, bundle.version)) adcm = ADCM.objects.filter(prototype__bundle=bundle) if adcm: msg = 'There is adcm object of bundle #{} "{}" {}' - err('BUNDLE_CONFLICT', msg.format(bundle.id, bundle.name, bundle.version)) - if bundle.hash != 'adcm': - shutil.rmtree(os.path.join(config.BUNDLE_DIR, bundle.hash)) + err("BUNDLE_CONFLICT", msg.format(bundle.id, bundle.name, bundle.version)) + if bundle.hash != "adcm": + try: + shutil.rmtree(Path(settings.BUNDLE_DIR, bundle.hash)) + except FileNotFoundError: + logger.info( + "Bundle %s %s was removed in file system. Delete bundle in database", + bundle.name, + bundle.version, + ) bundle_id = bundle.id bundle.delete() - for role in Role.objects.filter(class_name='ParentRole'): + for role in Role.objects.filter(class_name="ParentRole"): if not role.child.all(): role.delete() ProductCategory.re_collect() - cm.status_api.post_event('delete', 'bundle', bundle_id) + cm.status_api.post_event("delete", "bundle", bundle_id) def check_services(): s = {} - for p in StagePrototype.objects.filter(type='service'): + for p in StagePrototype.objects.filter(type="service"): if p.name in s: - msg = 'There are more than one service with name {}' - err('BUNDLE_ERROR', msg.format(p.name)) + msg = "There are more than one service with name {}" + err("BUNDLE_ERROR", msg.format(p.name)) s[p.name] = p.version def check_adcm_version(bundle): if not bundle.adcm_min_version: return - if rpm.compare_versions(bundle.adcm_min_version, ADCM_VERSION) > 0: - msg = 'This bundle required ADCM version equal to {} or newer.' - err('BUNDLE_VERSION_ERROR', msg.format(bundle.adcm_min_version)) + if rpm.compare_versions(bundle.adcm_min_version, settings.ADCM_VERSION) > 0: + msg = "This bundle required ADCM version equal to {} or newer." + err("BUNDLE_VERSION_ERROR", msg.format(bundle.adcm_min_version)) def get_stage_bundle(bundle_file): - clusters = StagePrototype.objects.filter(type='cluster') - providers = StagePrototype.objects.filter(type='provider') + clusters = StagePrototype.objects.filter(type="cluster") + providers = StagePrototype.objects.filter(type="provider") if clusters: if len(clusters) > 1: msg = 'There are more than one ({}) cluster definition in bundle "{}"' - err('BUNDLE_ERROR', msg.format(len(clusters), bundle_file)) + err("BUNDLE_ERROR", msg.format(len(clusters), bundle_file)) if providers: msg = 'There are {} host provider definition in cluster type bundle "{}"' - err('BUNDLE_ERROR', msg.format(len(providers), bundle_file)) - hosts = StagePrototype.objects.filter(type='host') + err("BUNDLE_ERROR", msg.format(len(providers), bundle_file)) + hosts = StagePrototype.objects.filter(type="host") if hosts: msg = 'There are {} host definition in cluster type bundle "{}"' - err('BUNDLE_ERROR', msg.format(len(hosts), bundle_file)) + err("BUNDLE_ERROR", msg.format(len(hosts), bundle_file)) check_services() bundle = clusters[0] elif providers: if len(providers) > 1: msg = 'There are more than one ({}) host provider definition in bundle "{}"' - err('BUNDLE_ERROR', msg.format(len(providers), bundle_file)) - services = StagePrototype.objects.filter(type='service') + err("BUNDLE_ERROR", msg.format(len(providers), bundle_file)) + services = StagePrototype.objects.filter(type="service") if services: msg = 'There are {} service definition in host provider type bundle "{}"' - err('BUNDLE_ERROR', msg.format(len(services), bundle_file)) - hosts = StagePrototype.objects.filter(type='host') + err("BUNDLE_ERROR", msg.format(len(services), bundle_file)) + hosts = StagePrototype.objects.filter(type="host") if not hosts: msg = 'There isn\'t any host definition in host provider type bundle "{}"' - err('BUNDLE_ERROR', msg.format(bundle_file)) + err("BUNDLE_ERROR", msg.format(bundle_file)) bundle = providers[0] else: msg = 'There isn\'t any cluster or host provider definition in bundle "{}"' - err('BUNDLE_ERROR', msg.format(bundle_file)) + err("BUNDLE_ERROR", msg.format(bundle_file)) check_adcm_version(bundle) return bundle diff --git a/python/cm/checker.py b/python/cm/checker.py index bad32fe909..ed8b08e181 100644 --- a/python/cm/checker.py +++ b/python/cm/checker.py @@ -1,3 +1,14 @@ +# 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. # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information @@ -99,9 +110,7 @@ def match_any(data, rules, rule, path, parent=None, is_service=False): def match_list(data, rules, rule, path, parent=None, is_service=False): check_match_type('match_list', data, list, path, rule, parent) for i, v in enumerate(data): - process_rule( - v, rules, rules[rule]['item'], path + [('Value of list index', i)], parent, is_service - ) + process_rule(v, rules, rules[rule]['item'], path + [('Value of list index', i)], parent, is_service) return True diff --git a/python/cm/config.py b/python/cm/config.py deleted file mode 100644 index 5e85a68630..0000000000 --- a/python/cm/config.py +++ /dev/null @@ -1,69 +0,0 @@ -# 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 json -import os -import sys -from os.path import dirname - -ENCODING = 'utf-8' - -PYTHON_DIR = sys.exec_prefix -PYTHON_EXECUTABLE = sys.executable -PYTHON_VERSION = f'{sys.version_info.major}.{sys.version_info.minor}' -PYTHON_SITE_PACKAGES = os.path.join(PYTHON_DIR, f'lib/python{PYTHON_VERSION}/site-packages') - -BASE_DIR = dirname(dirname(dirname(os.path.abspath(__file__)))) -BASE_DIR = os.environ.get('ADCM_BASE_DIR', BASE_DIR) - -STACK_DIR = BASE_DIR -STACK_DIR = os.environ.get('ADCM_STACK_DIR', STACK_DIR) - -CODE_DIR = os.path.join(BASE_DIR, 'python') - -LOG_DIR = os.path.join(BASE_DIR, 'data', 'log') -RUN_DIR = os.path.join(BASE_DIR, 'data', 'run') - -BUNDLE_DIR = os.path.join(STACK_DIR, 'data', 'bundle') -DOWNLOAD_DIR = os.path.join(STACK_DIR, 'data', 'download') - -FILE_DIR = os.path.join(STACK_DIR, 'data', 'file') - -LOG_FILE = os.path.join(LOG_DIR, 'adcm.log') - -SECRETS_FILE = os.path.join(BASE_DIR, 'data', 'var', 'secrets.json') - -ROLE_SPEC = os.path.join(CODE_DIR, 'cm', 'role_spec.yaml') -ROLE_SCHEMA = os.path.join(CODE_DIR, 'cm', 'role_schema.yaml') - -STATUS_SECRET_KEY = '' - -ANSIBLE_SECRET = '' - -ANSIBLE_VAULT_HEADER = '$ANSIBLE_VAULT;1.1;AES256' - -DEFAULT_SALT = b'"j\xebi\xc0\xea\x82\xe0\xa8\xba\x9e\x12E>\x11D' - -if os.path.exists(SECRETS_FILE): - with open(SECRETS_FILE, encoding='utf_8') as f: - data = json.load(f) - STATUS_SECRET_KEY = data['token'] - ANSIBLE_SECRET = data['adcmuser']['password'] - - -class Job: - CREATED = 'created' - SUCCESS = 'success' - FAILED = 'failed' - RUNNING = 'running' - LOCKED = 'locked' - ABORTED = 'aborted' diff --git a/python/cm/daemon.py b/python/cm/daemon.py index c3b0dc35b3..4730f96666 100644 --- a/python/cm/daemon.py +++ b/python/cm/daemon.py @@ -16,6 +16,8 @@ import time from signal import SIGTERM +from django.conf import settings + class Daemon: """ @@ -24,7 +26,7 @@ class Daemon: Usage: subclass the Daemon class and override the run() method """ - def __init__(self, pidfile, stdin='/dev/null', stdout='/dev/null', stderr='/dev/null'): + def __init__(self, pidfile, stdin="/dev/null", stdout="/dev/null", stderr="/dev/null"): self.stdin = stdin self.stdout = stdout self.stderr = stderr @@ -61,7 +63,7 @@ def daemonize(self): sys.exit(1) try: - pidfile = open(self.pidfile, 'w+', encoding='utf_8') + pidfile = open(self.pidfile, "w+", encoding=settings.ENCODING_UTF_8) except IOError as e: sys.stderr.write(f"Can't open pid file {self.pidfile}\n") sys.stderr.write(f"{e.strerror}\n") @@ -70,9 +72,9 @@ def daemonize(self): # redirect standard file descriptors sys.stdout.flush() sys.stderr.flush() - si = open(self.stdin, 'r', encoding='utf_8') - so = open(self.stdout, 'a+', encoding='utf_8') - se = open(self.stderr, 'w+', encoding='utf_8') + si = open(self.stdin, "r", encoding=settings.ENCODING_UTF_8) + so = open(self.stdout, "a+", encoding=settings.ENCODING_UTF_8) + se = open(self.stderr, "w+", encoding=settings.ENCODING_UTF_8) os.dup2(si.fileno(), sys.stdin.fileno()) os.dup2(so.fileno(), sys.stdout.fileno()) os.dup2(se.fileno(), sys.stderr.fileno()) @@ -86,9 +88,9 @@ def delpid(self): os.remove(self.pidfile) def getpid(self): - '''get pid from pidfile''' + """get pid from pidfile""" try: - pf = open(self.pidfile, 'r', encoding='utf_8') + pf = open(self.pidfile, "r", encoding=settings.ENCODING_UTF_8) try: pid = int(pf.read().strip()) except ValueError: diff --git a/python/cm/errors.py b/python/cm/errors.py index 2a1b281f63..297584b837 100644 --- a/python/cm/errors.py +++ b/python/cm/errors.py @@ -13,8 +13,10 @@ from rest_framework.exceptions import APIException from rest_framework.status import ( HTTP_400_BAD_REQUEST, + HTTP_401_UNAUTHORIZED, HTTP_403_FORBIDDEN, HTTP_404_NOT_FOUND, + HTTP_405_METHOD_NOT_ALLOWED, HTTP_409_CONFLICT, HTTP_501_NOT_IMPLEMENTED, ) @@ -27,7 +29,7 @@ CRIT = "critical" ERRORS = { - "AUTH_ERROR": ("authenticate error", HTTP_409_CONFLICT, ERR), + "AUTH_ERROR": ("Wrong user or password", HTTP_401_UNAUTHORIZED, ERR), "STACK_LOAD_ERROR": ("stack loading error", HTTP_409_CONFLICT, ERR), "NO_MODEL_ERROR_CODE": ( "django model doesn't has __error_code__ attribute", @@ -136,10 +138,7 @@ "MESSAGE_TEMPLATING_ERROR": ("Message templating error", HTTP_409_CONFLICT, ERR), "ISSUE_INTEGRITY_ERROR": ("Issue object integrity error", HTTP_409_CONFLICT, ERR), "GROUP_CONFIG_HOST_ERROR": ( - ( - "host is not available for this object," - " or host already is a member of another group of this object" - ), + "host is not available for this object, or host already is a member of another group of this object", HTTP_400_BAD_REQUEST, ), "GROUP_CONFIG_HOST_EXISTS": ( @@ -148,10 +147,7 @@ ), "NOT_CHANGEABLE_FIELDS": ("fields cannot be changed", HTTP_400_BAD_REQUEST, ERR), "GROUP_CONFIG_TYPE_ERROR": ( - ( - "invalid type object for group config," - " valid types: `cluster`, `service`, `component` and `provider`" - ), + "invalid type object for group config, valid types: `cluster`, `service`, `component` and `provider`", HTTP_400_BAD_REQUEST, ERR, ), @@ -201,11 +197,26 @@ HTTP_409_CONFLICT, ERR, ), - "SERVICE_DELETE_ERROR": ( - "Service can't be deleted if it has not CREATED state", + "SERVICE_DELETE_ERROR": ("Service can't be deleted if it has not CREATED state", HTTP_409_CONFLICT, ERR), + "ROLE_MODULE_ERROR": ("No role module with this name", HTTP_409_CONFLICT, ERR), + "ROLE_CLASS_ERROR": ("No matching class in this module", HTTP_409_CONFLICT, ERR), + "ROLE_FILTER_ERROR": ("Incorrect filter in role", HTTP_409_CONFLICT, ERR), + "ROLE_CREATE_ERROR": ("Error during process of role creating", HTTP_409_CONFLICT, ERR), + "ROLE_UPDATE_ERROR": ("Error during process of role updating", HTTP_409_CONFLICT, ERR), + "ROLE_CONFLICT": ( + "Combination of cluster/service/component and provider permissions is not allowed", HTTP_409_CONFLICT, ERR, ), + "GROUP_CREATE_ERROR": ("Error during process of group creating", HTTP_409_CONFLICT, ERR), + "GROUP_UPDATE_ERROR": ("Error during process of group updating", HTTP_400_BAD_REQUEST, ERR), + "GROUP_DELETE_ERROR": ("Built-in group could not be deleted", HTTP_405_METHOD_NOT_ALLOWED, ERR), + "POLICY_INTEGRITY_ERROR": ("Incorrect role or user list of policy", HTTP_400_BAD_REQUEST, ERR), + "POLICY_CREATE_ERROR": ("Error during process of policy creating", HTTP_409_CONFLICT, ERR), + "POLICY_UPDATE_ERROR": ("Error during process of policy updating", HTTP_409_CONFLICT, ERR), + "USER_CREATE_ERROR": ("Error during process of user creating", HTTP_409_CONFLICT, ERR), + "USER_UPDATE_ERROR": ("Error during process of user updating", HTTP_400_BAD_REQUEST, ERR), + "USER_DELETE_ERROR": ("Built-in user could not be deleted", HTTP_405_METHOD_NOT_ALLOWED, ERR), } diff --git a/python/cm/hierarchy.py b/python/cm/hierarchy.py index 7b67e49e4a..b6e944f69d 100644 --- a/python/cm/hierarchy.py +++ b/python/cm/hierarchy.py @@ -17,7 +17,7 @@ ClusterObject, Host, HostComponent, - MaintenanceModeType, + MaintenanceMode, ServiceComponent, ) @@ -117,6 +117,7 @@ def _make_node(self, obj: ADCMEntity) -> Node: return node def _build_tree_down(self, node: Node) -> None: + children_values = [] if node.type == 'root': children_values = [n.value for n in node.children] @@ -124,9 +125,7 @@ def _build_tree_down(self, node: Node) -> None: children_values = ClusterObject.objects.filter(cluster=node.value).all() elif node.type == 'service': - children_values = ServiceComponent.objects.filter( - cluster=node.value.cluster, service=node.value - ).all() + children_values = ServiceComponent.objects.filter(cluster=node.value.cluster, service=node.value).all() elif node.type == 'component': children_values = [ @@ -157,14 +156,20 @@ def _build_tree_up(self, node: Node) -> None: if node.type == 'cluster': parent_values = [None] elif node.type == 'service': - parent_values = [node.value.cluster] + if node.value.maintenance_mode == MaintenanceMode.OFF: + parent_values = [node.value.cluster] + else: + parent_values = [] elif node.type == 'component': - parent_values = [node.value.service] + if node.value.maintenance_mode == MaintenanceMode.OFF: + parent_values = [node.value.service] + else: + parent_values = [] elif node.type == 'host': parent_values = [ hc.component for hc in HostComponent.objects.filter(host=node.value) - .exclude(host__maintenance_mode=MaintenanceModeType.On) + .exclude(host__maintenance_mode=MaintenanceMode.ON) .select_related('component') .all() ] diff --git a/python/cm/inventory.py b/python/cm/inventory.py index 93649108e6..67f8f1f4c7 100644 --- a/python/cm/inventory.py +++ b/python/cm/inventory.py @@ -11,12 +11,11 @@ # limitations under the License. import json -import os from itertools import chain +from django.conf import settings from django.contrib.contenttypes.models import ContentType -from cm import config from cm.adcm_config import get_prototype_config, process_config from cm.logger import logger from cm.models import ( @@ -28,7 +27,7 @@ Host, HostComponent, HostProvider, - MaintenanceModeType, + MaintenanceMode, Prototype, PrototypeExport, PrototypeImport, @@ -132,6 +131,8 @@ def get_service_variables(service: ClusterObject, service_config: dict = None): "state": service.state, "multi_state": service.multi_state, "config": service_config or get_obj_config(service), + MAINTENANCE_MODE: service.maintenance_mode == MaintenanceMode.ON, + "display_name": service.display_name, } @@ -141,6 +142,8 @@ def get_component_variables(component: ServiceComponent, component_config: dict "config": component_config or get_obj_config(component), "state": component.state, "multi_state": component.multi_state, + MAINTENANCE_MODE: component.maintenance_mode == MaintenanceMode.ON, + "display_name": component.display_name, } @@ -158,9 +161,7 @@ def get_provider_variables(provider: HostProvider, provider_config: dict = None) def get_group_config(obj, host: Host) -> dict | None: - group = host.group_config.filter( - object_id=obj.id, object_type=ContentType.objects.get_for_model(obj) - ).last() + group = host.group_config.filter(object_id=obj.id, object_type=ContentType.objects.get_for_model(obj)).last() group_config = None if group: conf, attr = group.get_config_and_attr() @@ -169,48 +170,32 @@ def get_group_config(obj, host: Host) -> dict | None: def get_host_vars(host: Host, obj): - groups = host.group_config.filter( - object_id=obj.id, object_type=ContentType.objects.get_for_model(obj) - ) + groups = host.group_config.filter(object_id=obj.id, object_type=ContentType.objects.get_for_model(obj)) variables = {} for group in groups: # TODO: What to do with activatable group in attr ??? conf, attr = group.get_config_and_attr() group_config = process_config_and_attr(group, conf, attr) if isinstance(group.object, Cluster): - variables.update( - {"cluster": get_cluster_variables(group.object, cluster_config=group_config)} - ) + variables.update({"cluster": get_cluster_variables(group.object, cluster_config=group_config)}) elif isinstance(group.object, ClusterObject): variables.update( { "services": { - group.object.prototype.name: get_service_variables( - group.object, service_config=group_config - ) + group.object.prototype.name: get_service_variables(group.object, service_config=group_config) } } ) - for service in ClusterObject.objects.filter(cluster=group.object.cluster).exclude( - pk=group.object.id - ): + for service in ClusterObject.objects.filter(cluster=group.object.cluster).exclude(pk=group.object.id): variables["services"][service.prototype.name] = get_service_variables( service, service_config=get_group_config(service, host) ) - for component in ServiceComponent.objects.filter( - cluster=group.object.cluster, service=service - ): - variables["services"][service.prototype.name][ - component.prototype.name - ] = get_component_variables( + for component in ServiceComponent.objects.filter(cluster=group.object.cluster, service=service): + variables["services"][service.prototype.name][component.prototype.name] = get_component_variables( component, component_config=get_group_config(component, host) ) - for component in ServiceComponent.objects.filter( - cluster=group.object.cluster, service=group.object - ): - variables["services"][group.object.prototype.name][ - component.prototype.name - ] = get_component_variables( + for component in ServiceComponent.objects.filter(cluster=group.object.cluster, service=group.object): + variables["services"][group.object.prototype.name][component.prototype.name] = get_component_variables( component, component_config=get_group_config(component, host) ) elif isinstance(group.object, ServiceComponent): @@ -233,14 +218,10 @@ def get_host_vars(host: Host, obj): ).exclude(pk=group.object.id): variables["services"][component.service.prototype.name][ component.prototype.name - ] = get_component_variables( - component, component_config=get_group_config(component, host) - ) + ] = get_component_variables(component, component_config=get_group_config(component, host)) else: # HostProvider - variables.update( - {"provider": get_provider_variables(group.object, provider_config=group_config)} - ) + variables.update({"provider": get_provider_variables(group.object, provider_config=group_config)}) return variables @@ -255,9 +236,7 @@ def get_cluster_config(cluster): for service in ClusterObject.objects.filter(cluster=cluster): res["services"][service.prototype.name] = get_service_variables(service) for component in ServiceComponent.objects.filter(cluster=cluster, service=service): - res["services"][service.prototype.name][ - component.prototype.name - ] = get_component_variables(component) + res["services"][service.prototype.name][component.prototype.name] = get_component_variables(component) return res @@ -266,31 +245,27 @@ def get_provider_config(provider_id): return {"provider": get_provider_variables(provider)} -def get_host_groups(cluster, delta, action_host=None): - def in_mm(hc: HostComponent) -> bool: - return hc.host.maintenance_mode == MaintenanceModeType.On.value - +def get_host_groups(cluster: Cluster, delta: dict, action_host: Host | None = None): groups = {} all_hosts = HostComponent.objects.filter(cluster=cluster) for hc in all_hosts: if action_host and hc.host.id not in action_host: continue - key1 = f"{hc.service.prototype.name}.{hc.component.prototype.name}" - if in_mm(hc): - key1 = f"{key1}.{MAINTENANCE_MODE}" - if key1 not in groups: - groups[key1] = {"hosts": {}} - groups[key1]["hosts"][hc.host.fqdn] = get_obj_config(hc.host) - groups[key1]["hosts"][hc.host.fqdn].update(get_host_vars(hc.host, hc.component)) - - key2 = f"{hc.service.prototype.name}" - if in_mm(hc): - key2 = f"{key2}.{MAINTENANCE_MODE}" - if key2 not in groups: - groups[key2] = {"hosts": {}} - groups[key2]["hosts"][hc.host.fqdn] = get_obj_config(hc.host) - groups[key2]["hosts"][hc.host.fqdn].update(get_host_vars(hc.host, hc.service)) + key_object_pairs: tuple[tuple[str, ClusterObject | ServiceComponent]] = ( + (f"{hc.service.prototype.name}.{hc.component.prototype.name}", hc.component), + (f"{hc.service.prototype.name}", hc.service), + ) + + for key, adcm_object in key_object_pairs: + if hc.host.maintenance_mode == MaintenanceMode.ON: + key = f"{key}.{MAINTENANCE_MODE}" + + if key not in groups: + groups[key] = {"hosts": {}} + + groups[key]["hosts"][hc.host.fqdn] = get_obj_config(hc.host) + groups[key]["hosts"][hc.host.fqdn].update(get_host_vars(hc.host, adcm_object)) for htype in delta: for key in delta[htype]: @@ -300,7 +275,7 @@ def in_mm(hc: HostComponent) -> bool: for fqdn in delta[htype][key]: host = delta[htype][key][fqdn] # TODO: What is `delta`? Need calculate delta for group_config? - if not host.maintenance_mode == MaintenanceModeType.On.value: + if host.maintenance_mode != MaintenanceMode.ON: groups[lkey]["hosts"][host.fqdn] = get_obj_config(host) return groups @@ -309,14 +284,12 @@ def in_mm(hc: HostComponent) -> bool: def get_hosts(host_list, obj, action_host=None): group = {} for host in host_list: - if host.maintenance_mode == MaintenanceModeType.On.value or ( - action_host and host.id not in action_host - ): + if host.maintenance_mode == MaintenanceMode.ON or (action_host and host.id not in action_host): continue group[host.fqdn] = get_obj_config(host) - group[host.fqdn]['adcm_hostid'] = host.id - group[host.fqdn]['state'] = host.state - group[host.fqdn]['multi_state'] = host.multi_state + group[host.fqdn]["adcm_hostid"] = host.id + group[host.fqdn]["state"] = host.state + group[host.fqdn]["multi_state"] = host.multi_state if not isinstance(obj, Host): group[host.fqdn].update(get_host_vars(host, obj)) return group @@ -341,23 +314,19 @@ def get_provider_hosts(provider, action_host=None): def get_host(host_id): host = Host.objects.get(id=host_id) - groups = { - "HOST": {"hosts": get_hosts([host], host), "vars": get_provider_config(host.provider.id)} - } + groups = {"HOST": {"hosts": get_hosts([host], host), "vars": get_provider_config(host.provider.id)}} return groups def get_target_host(host_id): host = Host.objects.get(id=host_id) - groups = { - "target": {"hosts": get_hosts([host], host), "vars": get_cluster_config(host.cluster)} - } + groups = {"target": {"hosts": get_hosts([host], host), "vars": get_cluster_config(host.cluster)}} return groups def prepare_job_inventory(obj, job_id, action, delta, action_host=None): logger.info("prepare inventory for job #%s, object: %s", job_id, obj) - fd = open(os.path.join(config.RUN_DIR, f"{job_id}/inventory.json"), "w", encoding="utf_8") + fd = open(settings.RUN_DIR / f"{job_id}/inventory.json", "w", encoding=settings.ENCODING_UTF_8) inv = {"all": {"children": {}}} cluster = get_object_cluster(obj) if cluster: diff --git a/python/cm/issue.py b/python/cm/issue.py index 1c625f8710..00b44af4d6 100644 --- a/python/cm/issue.py +++ b/python/cm/issue.py @@ -63,14 +63,6 @@ def check_config(obj): # pylint: disable=too-many-branches return True -def check_object_concern(obj): - if obj.concerns.filter(type=ConcernType.Lock).exists(): - err("LOCK_ERROR", f"object {obj} is locked") - - if obj.concerns.filter(type=ConcernType.Issue).exists(): - err("ISSUE_INTEGRITY_ERROR", f"object {obj} has issues") - - def check_required_services(cluster): bundle = cluster.prototype.bundle for proto in Prototype.objects.filter(bundle=bundle, type="service", required=True): @@ -137,9 +129,7 @@ def check_hc(cluster): for service in ClusterObject.objects.filter(cluster=cluster): try: - check_component_constraint( - cluster, service.prototype, [i for i in shc_list if i[0] == service] - ) + check_component_constraint(cluster, service.prototype, [i for i in shc_list if i[0] == service]) except AdcmEx: return False try: @@ -176,9 +166,7 @@ def component_on_host(component, host): return [i for i in shc_list if i[1] == host and i[2].prototype == component] def bound_host_components(service, comp): - return [ - i for i in shc_list if i[0].prototype.name == service and i[2].prototype.name == comp - ] + return [i for i in shc_list if i[0].prototype.name == service and i[2].prototype.name == comp] def check_bound_component(component): service = component.bound_to["service"] @@ -257,9 +245,7 @@ def check(comp, const): old_service_proto = Prototype.objects.get( name=service_prototype.name, type="service", bundle=old_bundle ) - Prototype.objects.get( - parent=old_service_proto, bundle=old_bundle, type="component", name=c.name - ) + Prototype.objects.get(parent=old_service_proto, bundle=old_bundle, type="component", name=c.name) except Prototype.DoesNotExist: continue check(c, c.constraint) @@ -297,17 +283,24 @@ def _gen_issue_name(obj: ADCMEntity, cause: ConcernCause) -> str: return f"{obj} has issue with {cause.value}" +def _create_concern_item(obj: ADCMEntity, issue_cause: ConcernCause) -> ConcernItem: + msg_name = _issue_template_map[issue_cause] + reason = MessageTemplate.get_message_from_template(msg_name.value, source=obj) + issue_name = _gen_issue_name(obj, issue_cause) + issue = ConcernItem.objects.create( + type=ConcernType.Issue, name=issue_name, reason=reason, owner=obj, cause=issue_cause + ) + return issue + + def create_issue(obj: ADCMEntity, issue_cause: ConcernCause) -> None: """Create newly discovered issue and add it to linked objects concerns""" issue = obj.get_own_issue(issue_cause) if issue is None: - msg_name = _issue_template_map[issue_cause] - reason = MessageTemplate.get_message_from_template(msg_name.value, source=obj) - issue_name = _gen_issue_name(obj, issue_cause) - issue = ConcernItem.objects.create( - type=ConcernType.Issue, name=issue_name, reason=reason, owner=obj, cause=issue_cause - ) - + issue = _create_concern_item(obj, issue_cause) + if issue.name != _gen_issue_name(obj, issue_cause): + issue.delete() + issue = _create_concern_item(obj, issue_cause) tree = Tree(obj) affected_nodes = tree.get_directly_affected(tree.built_from) for node in affected_nodes: diff --git a/python/cm/job.py b/python/cm/job.py index 160afe04a4..e7dac6add2 100644 --- a/python/cm/job.py +++ b/python/cm/job.py @@ -17,17 +17,43 @@ from pathlib import Path from typing import Any, Hashable, List, Optional, Tuple, Union +from django.conf import settings from django.db import transaction from django.utils import timezone -from audit.utils import audit_finish_task -from cm import adcm_config, api, config, inventory, issue, variant -from cm.adcm_config import process_file_type +from audit.cases.common import get_or_create_audit_obj +from audit.cef_logger import cef_logger +from audit.models import ( + MODEL_TO_AUDIT_OBJECT_TYPE_MAP, + AuditLog, + AuditLogOperationResult, + AuditLogOperationType, +) +from cm.adcm_config import ( + check_attr, + check_config_spec, + get_prototype_config, + process_file_type, +) +from cm.api import ( + check_hc, + check_maintenance_mode, + check_sub_key, + get_hc, + load_mm_objects, + make_host_comp_list, + save_hc, +) from cm.api_context import ctx -from cm.errors import AdcmEx -from cm.errors import raise_adcm_ex as err +from cm.errors import AdcmEx, raise_adcm_ex from cm.hierarchy import Tree -from cm.inventory import get_obj_config, process_config_and_attr +from cm.inventory import get_obj_config, prepare_job_inventory, process_config_and_attr +from cm.issue import ( + check_bound_components, + check_component_constraint, + check_component_requires, + update_hierarchy_issues, +) from cm.logger import logger from cm.models import ( ADCM, @@ -36,13 +62,16 @@ ADCMEntity, Cluster, ClusterObject, + ConcernType, ConfigLog, DummyData, Host, HostComponent, HostProvider, JobLog, + JobStatus, LogStorage, + MaintenanceMode, ObjectType, Prototype, ServiceComponent, @@ -52,6 +81,7 @@ get_object_cluster, ) from cm.status_api import post_event +from cm.variant import process_variant from rbac.roles import re_apply_policy_for_jobs @@ -66,7 +96,7 @@ def start_task( ) -> TaskLog: if action.type not in ActionType.values: msg = f'unknown type "{action.type}" for action {action} on {obj}' - err("WRONG_ACTION_TYPE", msg) + raise_adcm_ex("WRONG_ACTION_TYPE", msg) task = prepare_task(action, obj, conf, attr, hc, hosts, verbose) ctx.event.send_state() @@ -80,20 +110,26 @@ def check_action_hosts(action: Action, obj: ADCMEntity, cluster: Cluster, hosts: provider = None if obj.prototype.type == "provider": provider = obj + if not hosts: return + if not action.partial_execution: - err("TASK_ERROR", "Only action with partial_execution permission can receive host list") + raise_adcm_ex("TASK_ERROR", "Only action with partial_execution permission can receive host list") + if not isinstance(hosts, list): - err("TASK_ERROR", "Hosts should be array") + raise_adcm_ex("TASK_ERROR", "Hosts should be array") + for host_id in hosts: if not isinstance(host_id, int): - err("TASK_ERROR", f"host id should be integer ({host_id})") + raise_adcm_ex("TASK_ERROR", f"host id should be integer ({host_id})") + host = Host.obj.get(id=host_id) if cluster and host.cluster != cluster: - err("TASK_ERROR", f"host #{host_id} does not belong to cluster #{cluster.pk}") + raise_adcm_ex("TASK_ERROR", f"host #{host_id} does not belong to cluster #{cluster.pk}") + if provider and host.provider != provider: - err("TASK_ERROR", f"host #{host_id} does not belong to host provider #{provider.pk}") + raise_adcm_ex("TASK_ERROR", f"host #{host_id} does not belong to host provider #{provider.pk}") def prepare_task( @@ -109,9 +145,10 @@ def prepare_task( check_action_state(action, obj, cluster) _, spec = check_action_config(action, obj, conf, attr) if conf and not spec: - err("CONFIG_VALUE_ERROR", "Absent config in action prototype") + raise_adcm_ex("CONFIG_VALUE_ERROR", "Absent config in action prototype") + check_action_hosts(action, obj, cluster, hosts) - old_hc = api.get_hc(cluster) + old_hc = get_hc(cluster) host_map, post_upgrade_hc = check_hostcomponentmap(cluster, action, hc) if hasattr(action, "upgrade") and not action.hostcomponentmap: @@ -125,28 +162,30 @@ def prepare_task( task = create_task(action, obj, conf, attr, old_hc, hosts, verbose, post_upgrade_hc) if host_map or (hasattr(action, "upgrade") and host_map is not None): - api.save_hc(cluster, host_map) + save_hc(cluster, host_map) if conf: new_conf = process_config_and_attr(task, conf, attr, spec) process_file_type(task, spec, conf) task.config = new_conf task.save() + re_apply_policy_for_jobs(obj, task) + return task def restart_task(task: TaskLog): - if task.status in (config.Job.CREATED, config.Job.RUNNING): - err("TASK_ERROR", f"task #{task.pk} is running") - elif task.status == config.Job.SUCCESS: + if task.status in (JobStatus.CREATED, JobStatus.RUNNING): + raise_adcm_ex("TASK_ERROR", f"task #{task.pk} is running") + elif task.status == JobStatus.SUCCESS: run_task(task, ctx.event) ctx.event.send_state() - elif task.status in (config.Job.FAILED, config.Job.ABORTED): + elif task.status in (JobStatus.FAILED, JobStatus.ABORTED): run_task(task, ctx.event, "restart") ctx.event.send_state() else: - err("TASK_ERROR", f"task #{task.pk} has unexpected status: {task.status}") + raise_adcm_ex("TASK_ERROR", f"task #{task.pk} has unexpected status: {task.status}") def cancel_task(task: TaskLog): @@ -161,6 +200,7 @@ def get_host_object(action: Action, cluster: Cluster) -> Optional[ADCMEntity]: obj = ServiceComponent.obj.get(cluster=cluster, prototype=action.prototype) elif action.prototype.type == "cluster": obj = cluster + return obj @@ -170,42 +210,50 @@ def check_action_state(action: Action, task_object: ADCMEntity, cluster: Cluster else: obj = task_object - issue.check_object_concern(obj) + if obj.concerns.filter(type=ConcernType.Lock).exists(): + raise_adcm_ex("LOCK_ERROR", f"object {obj} is locked") + + if ( + action.name not in settings.ADCM_SERVICE_ACTION_NAMES_SET + and obj.concerns.filter(type=ConcernType.Issue).exists() + ): + raise_adcm_ex("ISSUE_INTEGRITY_ERROR", f"object {obj} has issues") if action.allowed(obj): return - err("TASK_ERROR", "action is disabled") + raise_adcm_ex("TASK_ERROR", "action is disabled") -def check_action_config( - action: Action, obj: ADCMEntity, conf: dict, attr: dict -) -> Tuple[dict, dict]: +def check_action_config(action: Action, obj: ADCMEntity, conf: dict, attr: dict) -> Tuple[dict, dict]: proto = action.prototype - spec, flat_spec, _, _ = adcm_config.get_prototype_config(proto, action) + spec, flat_spec, _, _ = get_prototype_config(proto, action) if not spec: return {}, {} + if not conf: - err("TASK_ERROR", "action config is required") - adcm_config.check_attr(proto, action, attr, flat_spec) - variant.process_variant(obj, spec, conf) - new_conf = adcm_config.check_config_spec(proto, action, spec, flat_spec, conf, None, attr) + raise_adcm_ex("TASK_ERROR", "action config is required") + + check_attr(proto, action, attr, flat_spec) + process_variant(obj, spec, conf) + new_conf = check_config_spec(proto, action, spec, flat_spec, conf, None, attr) + return new_conf, spec def add_to_dict(my_dict: dict, key: Hashable, subkey: Hashable, value: Any): if key not in my_dict: my_dict[key] = {} + my_dict[key][subkey] = value -def check_action_hc( - action_hc: List[dict], service: ClusterObject, component: ServiceComponent, action: Action -) -> bool: +def check_action_hc(action_hc: List[dict], service: ClusterObject, component: ServiceComponent, action: Action) -> bool: for item in action_hc: if item["service"] == service and item["component"] == component: if item["action"] == action: return True + return False @@ -219,15 +267,15 @@ def cook_delta( # pylint: disable=too-many-branches action_hc: List[dict], old: dict = None, ) -> dict: - def add_delta(delta, action, key, fqdn, host): - service, comp = key.split(".") - if not check_action_hc(action_hc, service, comp, action): + def add_delta(_delta, action, _key, fqdn, _host): + _service, _comp = _key.split(".") + if not check_action_hc(action_hc, _service, _comp, action): msg = ( - f'no permission to "{action}" component "{comp}" of ' - f'service "{service}" to/from hostcomponentmap' + f'no permission to "{action}" component "{_comp}" of ' f'service "{_service}" to/from hostcomponentmap' ) - err("WRONG_ACTION_HC", msg) - add_to_dict(delta[action], key, fqdn, host) + raise_adcm_ex("WRONG_ACTION_HC", msg) + + add_to_dict(_delta[action], _key, fqdn, _host) new = {} for service, host, comp in new_hc: @@ -246,6 +294,7 @@ def add_delta(delta, action, key, fqdn, host): for host in value: if host not in old[key]: add_delta(delta, "add", key, host, value[host]) + for host in old[key]: if host not in value: add_delta(delta, "remove", key, host, old[key][host]) @@ -261,46 +310,50 @@ def add_delta(delta, action, key, fqdn, host): logger.debug("OLD: %s", old) logger.debug("NEW: %s", new) logger.debug("DELTA: %s", delta) + return delta def check_hostcomponentmap(cluster: Cluster, action: Action, new_hc: List[dict]): - if not action.hostcomponentmap: return None, [] if not new_hc: - err("TASK_ERROR", "hc is required") + raise_adcm_ex("TASK_ERROR", "hc is required") if not cluster: - err("TASK_ERROR", "Only cluster objects can have action with hostcomponentmap") + raise_adcm_ex("TASK_ERROR", "Only cluster objects can have action with hostcomponentmap") for host_comp in new_hc: if not hasattr(action, "upgrade"): host = Host.obj.get(id=host_comp.get("host_id", 0)) - issue.check_object_concern(host) + if host.concerns.filter(type=ConcernType.Lock).exists(): + raise_adcm_ex("LOCK_ERROR", f"object {host} is locked") + + if host.concerns.filter(type=ConcernType.Issue).exists(): + raise_adcm_ex("ISSUE_INTEGRITY_ERROR", f"object {host} has issues") + post_upgrade_hc, clear_hc = check_upgrade_hc(action, new_hc) - old_hc = get_old_hc(api.get_hc(cluster)) + old_hc = get_old_hc(get_hc(cluster)) if not hasattr(action, "upgrade"): - prepared_hc_list = api.check_hc(cluster, clear_hc) + prepared_hc_list = check_hc(cluster, clear_hc) else: - api.check_sub_key(clear_hc) - prepared_hc_list = api.make_host_comp_list(cluster, clear_hc) + check_sub_key(clear_hc) + prepared_hc_list = make_host_comp_list(cluster, clear_hc) check_constraints_for_upgrade(cluster, action.upgrade, prepared_hc_list) + cook_delta(cluster, prepared_hc_list, action.hostcomponentmap, old_hc) + return prepared_hc_list, post_upgrade_hc def check_constraints_for_upgrade(cluster, upgrade, host_comp_list): try: - for service in ClusterObject.objects.filter(cluster=cluster): try: - prototype = Prototype.objects.get( - name=service.name, type="service", bundle=upgrade.bundle - ) - issue.check_component_constraint( + prototype = Prototype.objects.get(name=service.name, type="service", bundle=upgrade.bundle) + check_component_constraint( cluster, prototype, [i for i in host_comp_list if i[0] == service], @@ -308,16 +361,18 @@ def check_constraints_for_upgrade(cluster, upgrade, host_comp_list): ) except Prototype.DoesNotExist: pass - issue.check_component_requires(host_comp_list) - issue.check_bound_components(host_comp_list) - api.check_maintenance_mode(cluster, host_comp_list) + + check_component_requires(host_comp_list) + check_bound_components(host_comp_list) + check_maintenance_mode(cluster, host_comp_list) except AdcmEx as e: if e.code == "COMPONENT_CONSTRAINT_ERROR": e.msg = ( f"Host-component map of upgraded cluster should satisfy " f"constraints of new bundle. Now error is: {e.msg}" ) - err(e.code, e.msg) + + raise_adcm_ex(e.code, e.msg) def check_upgrade_hc(action, new_hc): @@ -327,10 +382,11 @@ def check_upgrade_hc(action, new_hc): for host_comp in new_hc: if "component_prototype_id" in host_comp: if not hasattr(action, "upgrade"): - err( + raise_adcm_ex( "WRONG_ACTION_HC", "Hc map with components prototype available only in upgrade action", ) + proto = Prototype.obj.get( type="component", id=host_comp["component_prototype_id"], @@ -340,45 +396,45 @@ def check_upgrade_hc(action, new_hc): if proto.name == hc_acl["component"]: buff += 1 if hc_acl["action"] != "add": - err( + raise_adcm_ex( "WRONG_ACTION_HC", "New components from bundle with upgrade you can only add, not remove", ) + if buff == 0: - err("INVALID_INPUT", "hc_acl doesn't allow actions with this component") + raise_adcm_ex("INVALID_INPUT", "hc_acl doesn't allow actions with this component") + post_upgrade_hc.append(host_comp) clear_hc.remove(host_comp) + return post_upgrade_hc, clear_hc -def check_service_task( # pylint: disable=inconsistent-return-statements - cluster_id: int, action: Action -) -> ClusterObject: +def check_service_task(cluster_id: int, action: Action) -> ClusterObject | None: cluster = Cluster.obj.get(id=cluster_id) try: service = ClusterObject.objects.get(cluster=cluster, prototype=action.prototype) return service except ClusterObject.DoesNotExist: - msg = ( - f"service #{action.prototype.pk} for action " - f'"{action.name}" is not installed in cluster #{cluster.pk}' - ) - err("CLUSTER_SERVICE_NOT_FOUND", msg) + msg = f"service #{action.prototype.pk} for action " f'"{action.name}" is not installed in cluster #{cluster.pk}' + raise_adcm_ex("CLUSTER_SERVICE_NOT_FOUND", msg) + + return None -def check_component_task( # pylint: disable=inconsistent-return-statements - cluster_id: int, action: Action -) -> ServiceComponent: +def check_component_task(cluster_id: int, action: Action) -> ServiceComponent | None: cluster = Cluster.obj.get(id=cluster_id) try: component = ServiceComponent.objects.get(cluster=cluster, prototype=action.prototype) + return component except ServiceComponent.DoesNotExist: msg = ( - f"component #{action.prototype.pk} for action " - f'"{action.name}" is not installed in cluster #{cluster.pk}' + f"component #{action.prototype.pk} for action " f'"{action.name}" is not installed in cluster #{cluster.pk}' ) - err("COMPONENT_NOT_FOUND", msg) + raise_adcm_ex("COMPONENT_NOT_FOUND", msg) + + return None def check_cluster(cluster_id: int) -> Cluster: @@ -394,11 +450,10 @@ def check_adcm(adcm_id: int) -> ADCM: def get_bundle_root(action: Action) -> str: - if action.prototype.type == "adcm": - return str(Path(config.BASE_DIR, "conf")) + return str(Path(settings.BASE_DIR, "conf")) - return config.BUNDLE_DIR + return str(settings.BUNDLE_DIR) def cook_script(action: Action, sub_action: SubAction): @@ -415,8 +470,7 @@ def cook_script(action: Action, sub_action: SubAction): def get_adcm_config(): - adcm = ADCM.obj.get() - return get_obj_config(adcm) + return get_obj_config(ADCM.obj.get()) def get_actual_hc(cluster: Cluster): @@ -429,6 +483,7 @@ def get_actual_hc(cluster: Cluster): def get_old_hc(saved_hc: List[dict]): if not saved_hc: return {} + old_hc = {} for hc in saved_hc: service = ClusterObject.objects.get(id=hc["service_id"]) @@ -436,6 +491,7 @@ def get_old_hc(saved_hc: List[dict]): host = Host.objects.get(id=hc["host_id"]) key = cook_comp_key(service.prototype.name, comp.prototype.name) add_to_dict(old_hc, key, host.fqdn, host) + return old_hc @@ -445,22 +501,25 @@ def re_prepare_job(task: TaskLog, job: JobLog): delta = {} if task.config: conf = task.config + if task.hosts: hosts = task.hosts + action = task.action obj = task.task_object cluster = get_object_cluster(obj) sub_action = None if job.sub_action_id: sub_action = job.sub_action + if action.hostcomponentmap: new_hc = get_actual_hc(cluster) old_hc = get_old_hc(task.hostcomponentmap) delta = cook_delta(cluster, new_hc, action.hostcomponentmap, old_hc) + prepare_job(action, sub_action, job.pk, obj, conf, delta, hosts, task.verbose) -# pylint: disable=too-many-arguments def prepare_job( action: Action, sub_action: SubAction, @@ -472,14 +531,11 @@ def prepare_job( verbose: bool, ): prepare_job_config(action, sub_action, job_id, obj, conf, verbose) - inventory.prepare_job_inventory(obj, job_id, action, delta, hosts) + prepare_job_inventory(obj, job_id, action, delta, hosts) prepare_ansible_config(job_id, action, sub_action) -def get_selector( - obj: ADCM | Cluster | ClusterObject | ServiceComponent | HostProvider | Host, action: Action -) -> dict: - +def get_selector(obj: ADCM | Cluster | ClusterObject | ServiceComponent | HostProvider | Host, action: Action) -> dict: selector = {obj.prototype.type: {"id": obj.pk, "name": obj.display_name}} if obj.prototype.type == ObjectType.Service: @@ -495,13 +551,9 @@ def get_selector( service = ClusterObject.objects.get(prototype=action.prototype, cluster=cluster) selector[ObjectType.Service] = {"id": service.pk, "name": service.display_name} elif action.prototype.type == ObjectType.Component: - service = ClusterObject.objects.get( - prototype=action.prototype.parent, cluster=cluster - ) + service = ClusterObject.objects.get(prototype=action.prototype.parent, cluster=cluster) selector[ObjectType.Service] = {"id": service.pk, "name": service.display_name} - component = ServiceComponent.objects.get( - prototype=action.prototype, cluster=cluster, service=service - ) + component = ServiceComponent.objects.get(prototype=action.prototype, cluster=cluster, service=service) selector[ObjectType.Component] = { "id": component.pk, "name": component.display_name, @@ -511,6 +563,7 @@ def get_selector( "id": obj.provider.pk, "name": obj.provider.display_name, } + return selector @@ -535,16 +588,18 @@ def prepare_job_config( obj: ADCM | Cluster | ClusterObject | ServiceComponent | HostProvider | Host, conf: dict, verbose: bool, -): # pylint: disable=too-many-branches,too-many-statements +): + # pylint: disable=too-many-branches,too-many-statements + job_conf = { "adcm": {"config": get_adcm_config()}, "context": prepare_context(action, obj), "env": { - "run_dir": config.RUN_DIR, - "log_dir": config.LOG_DIR, - "tmp_dir": str(Path(config.RUN_DIR, f"{job_id}", "tmp")), + "run_dir": str(settings.RUN_DIR), + "log_dir": str(settings.LOG_DIR), + "tmp_dir": str(Path(settings.RUN_DIR, f"{job_id}", "tmp")), "stack_dir": str(Path(get_bundle_root(action), action.prototype.bundle.hash)), - "status_api_token": config.STATUS_SECRET_KEY, + "status_api_token": str(settings.STATUS_SECRET_KEY), }, "job": { "id": job_id, @@ -584,9 +639,7 @@ def prepare_job_config( elif action.prototype.type == "component": if action.host_action: service = ClusterObject.obj.get(prototype=action.prototype.parent, cluster=cluster) - comp = ServiceComponent.obj.get( - prototype=action.prototype, cluster=cluster, service=service - ) + comp = ServiceComponent.obj.get(prototype=action.prototype, cluster=cluster, service=service) job_conf["job"]["hostgroup"] = f"{service.name}.{comp.name}" job_conf["job"]["service_id"] = service.pk job_conf["job"]["component_id"] = comp.pk @@ -610,17 +663,16 @@ def prepare_job_config( elif action.prototype.type == "adcm": job_conf["job"]["hostgroup"] = "127.0.0.1" else: - err("NOT_IMPLEMENTED", f'unknown prototype type "{action.prototype.type}"') + raise_adcm_ex("NOT_IMPLEMENTED", f'unknown prototype type "{action.prototype.type}"') if conf: job_conf["job"]["config"] = conf - fd = open(Path(config.RUN_DIR, f"{job_id}", "config.json"), "w", encoding="utf_8") + fd = open(Path(settings.RUN_DIR, f"{job_id}", "config.json"), "w", encoding=settings.ENCODING_UTF_8) json.dump(job_conf, fd, indent=3, sort_keys=True) fd.close() -# pylint: disable=too-many-arguments def create_task( action: Action, obj: ADCM | Cluster | ClusterObject | ServiceComponent | HostProvider | Host, @@ -631,7 +683,6 @@ def create_task( verbose: bool, post_upgrade_hc: List[dict], ) -> TaskLog: - """Create task and jobs and lock objects for action""" task = TaskLog.objects.create( action=action, task_object=obj, @@ -643,10 +694,10 @@ def create_task( verbose=verbose, start_date=timezone.now(), finish_date=timezone.now(), - status=config.Job.CREATED, + status=JobStatus.CREATED, selector=get_selector(obj, action), ) - set_task_status(task, config.Job.CREATED, ctx.event) + set_task_status(task, JobStatus.CREATED, ctx.event) if action.type == ActionType.Job.value: sub_actions = [None] @@ -661,14 +712,14 @@ def create_task( log_files=action.log_files, start_date=timezone.now(), finish_date=timezone.now(), - status=config.Job.CREATED, + status=JobStatus.CREATED, selector=get_selector(obj, action), ) log_type = sub_action.script_type if sub_action else action.script_type LogStorage.objects.create(job=job, name=log_type, type="stdout", format="txt") LogStorage.objects.create(job=job, name=log_type, type="stderr", format="txt") - set_job_status(job.pk, config.Job.CREATED, ctx.event) - Path(config.RUN_DIR, f"{job.pk}", "tmp").mkdir(parents=True, exist_ok=True) + set_job_status(job.pk, JobStatus.CREATED, ctx.event) + Path(settings.RUN_DIR, f"{job.pk}", "tmp").mkdir(parents=True, exist_ok=True) tree = Tree(obj) affected = (node.value for node in tree.get_all_affected(tree.built_from)) @@ -677,20 +728,18 @@ def create_task( return task -def get_state( - action: Action, job: JobLog, status: str -) -> Tuple[Optional[str], List[str], List[str]]: +def get_state(action: Action, job: JobLog, status: str) -> Tuple[Optional[str], List[str], List[str]]: sub_action = None if job and job.sub_action: sub_action = job.sub_action - if status == config.Job.SUCCESS: + if status == JobStatus.SUCCESS: multi_state_set = action.multi_state_on_success_set multi_state_unset = action.multi_state_on_success_unset state = action.state_on_success if not state: logger.warning('action "%s" success state is not set', action.name) - elif status == config.Job.FAILED: + elif status == JobStatus.FAILED: state = getattr_first("state_on_fail", sub_action, action) multi_state_set = getattr_first("multi_state_on_fail_set", sub_action, action) multi_state_unset = getattr_first("multi_state_on_fail_unset", sub_action, action) @@ -701,6 +750,7 @@ def get_state( state = None multi_state_set = [] multi_state_unset = [] + return state, multi_state_set, multi_state_unset @@ -714,7 +764,9 @@ def set_action_state( ): if not obj: logger.warning("empty object for action %s of task #%s", action.name, task.pk) + return + logger.info( 'action "%s" of task #%s will set %s state to "%s" ' 'add to multi_states "%s" and remove from multi_states "%s"', @@ -737,14 +789,16 @@ def set_action_state( def restore_hc(task: TaskLog, action: Action, status: str): - if status not in [config.Job.FAILED, config.Job.ABORTED]: + if status not in {JobStatus.FAILED, JobStatus.ABORTED}: return + if not action.hostcomponentmap: return cluster = get_object_cluster(task.task_object) if cluster is None: logger.error("no cluster in task #%s", task.pk) + return host_comp_list = [] @@ -755,11 +809,10 @@ def restore_hc(task: TaskLog, action: Action, status: str): host_comp_list.append((service, host, comp)) logger.warning("task #%s is failed, restore old hc", task.pk) - api.save_hc(cluster, host_comp_list) + save_hc(cluster, host_comp_list) def set_before_upgrade_state(action: Action, obj: Union[Cluster, HostProvider]) -> None: - """Save before state after upgrade""" if action.upgrade is not None: obj.before_upgrade["state"] = obj.state obj.save() @@ -779,6 +832,7 @@ def finish_task(task: TaskLog, job: Optional[JobLog], status: str): restore_hc(task, action, status) task.unlock_affected() set_task_status(task, status, ctx.event) + update_hierarchy_issues(obj) upgrade = Upgrade.objects.filter(action=action).first() if upgrade: @@ -786,29 +840,55 @@ def finish_task(task: TaskLog, job: Optional[JobLog], status: str): else: operation_name = f"{action.display_name} action completed" - audit_finish_task( - obj=obj, + if ( + action.name in {settings.ADCM_TURN_ON_MM_ACTION_NAME, settings.ADCM_HOST_TURN_ON_MM_ACTION_NAME} + and obj.maintenance_mode == MaintenanceMode.CHANGING + ): + obj.maintenance_mode = MaintenanceMode.OFF + obj.save() + + if ( + action.name in {settings.ADCM_TURN_OFF_MM_ACTION_NAME, settings.ADCM_HOST_TURN_OFF_MM_ACTION_NAME} + and obj.maintenance_mode == MaintenanceMode.CHANGING + ): + obj.maintenance_mode = MaintenanceMode.ON + obj.save() + + obj_type = MODEL_TO_AUDIT_OBJECT_TYPE_MAP.get(obj.__class__) + if not obj_type: + return + + audit_object = get_or_create_audit_obj( + object_id=obj.pk, + object_name=obj.name, + object_type=obj_type, + ) + if status == "success": + operation_result = AuditLogOperationResult.Success + else: + operation_result = AuditLogOperationResult.Fail + + audit_log = AuditLog.objects.create( + audit_object=audit_object, operation_name=operation_name, - status=status, + operation_type=AuditLogOperationType.Update, + operation_result=operation_result, + object_changes={}, ) + cef_logger(audit_instance=audit_log, signature_id="Action completion") ctx.event.send_state() + try: + load_mm_objects() + except Exception as e: # pylint: disable=broad-except + logger.warning("Error loading mm objects on task finish") + logger.exception(e) def cook_log_name(tag, level, ext="txt"): return f"{tag}-{level}.{ext}" -def get_log(job: JobLog) -> List[dict]: - log_storage = LogStorage.objects.filter(job=job) - logs = [] - - for ls in log_storage: - logs.append({"name": ls.name, "type": ls.type, "format": ls.format, "id": ls.pk}) - - return logs - - def log_custom(job_id, name, log_format, body): job = JobLog.obj.get(id=job_id) l1 = LogStorage.objects.create(job=job, name=name, type="custom", format=log_format, body=body) @@ -825,16 +905,12 @@ def log_custom(job_id, name, log_format, body): ) -def check_all_status(): - err("NOT_IMPLEMENTED") - - def run_task(task: TaskLog, event, args: str = ""): - err_file = open(Path(config.LOG_DIR, "task_runner.err"), "a+", encoding="utf_8") + err_file = open(Path(settings.LOG_DIR, "task_runner.err"), "a+", encoding=settings.ENCODING_UTF_8) cmd = [ "/adcm/python/job_venv_wrapper.sh", task.action.venv, - str(Path(config.CODE_DIR, "task_runner.py")), + str(Path(settings.CODE_DIR, "task_runner.py")), str(task.pk), args, ] @@ -845,7 +921,7 @@ def run_task(task: TaskLog, event, args: str = ""): ) logger.info("task run #%s, python process %s", task.pk, proc.pid) - set_task_status(task, config.Job.RUNNING, event) + set_task_status(task, JobStatus.RUNNING, event) def prepare_ansible_config(job_id: int, action: Action, sub_action: SubAction): @@ -862,7 +938,7 @@ def prepare_ansible_config(job_id: int, action: Action, sub_action: SubAction): if mitogen: config_parser["defaults"]["strategy"] = "mitogen_linear" config_parser["defaults"]["strategy_plugins"] = str( - Path(config.PYTHON_SITE_PACKAGES, "ansible_mitogen", "plugins", "strategy") + Path(settings.PYTHON_SITE_PACKAGES, "ansible_mitogen", "plugins", "strategy") ) config_parser["defaults"]["host_key_checking"] = "False" @@ -876,9 +952,7 @@ def prepare_ansible_config(job_id: int, action: Action, sub_action: SubAction): if "jinja2_native" in params: config_parser["defaults"]["jinja2_native"] = str(params["jinja2_native"]) - with open( - Path(config.RUN_DIR, f"{job_id}", "ansible.cfg"), "w", encoding="utf_8" - ) as config_file: + with open(Path(settings.RUN_DIR, f"{job_id}", "ansible.cfg"), "w", encoding=settings.ENCODING_UTF_8) as config_file: config_parser.write(config_file) @@ -895,11 +969,11 @@ def set_job_status(job_id: int, status: str, event, pid: int = 0): def abort_all(event): - for task in TaskLog.objects.filter(status=config.Job.RUNNING): - set_task_status(task, config.Job.ABORTED, event) + for task in TaskLog.objects.filter(status=JobStatus.RUNNING): + set_task_status(task, JobStatus.ABORTED, event) task.unlock_affected() - for job in JobLog.objects.filter(status=config.Job.RUNNING): - set_job_status(job.pk, config.Job.ABORTED, event) + for job in JobLog.objects.filter(status=JobStatus.RUNNING): + set_job_status(job.pk, JobStatus.ABORTED, event) ctx.event.send_state() diff --git a/python/cm/logger.py b/python/cm/logger.py index bd3cc995ab..301a596d24 100644 --- a/python/cm/logger.py +++ b/python/cm/logger.py @@ -12,19 +12,17 @@ import logging -from cm import config +from django.conf import settings -logger = logging.getLogger('adcm') +logger = logging.getLogger("adcm") logger.setLevel(logging.DEBUG) def get_log_handler(fname): - handler = logging.FileHandler(fname, 'a', 'utf-8') - fmt = logging.Formatter( - "%(asctime)s.%(msecs)03d %(levelname)s %(module)s %(message)s", "%m-%d %H:%M:%S" - ) + handler = logging.FileHandler(fname, "a", settings.ENCODING_UTF_8) + fmt = logging.Formatter("%(asctime)s.%(msecs)03d %(levelname)s %(module)s %(message)s", "%m-%d %H:%M:%S") handler.setFormatter(fmt) return handler -logger.addHandler(get_log_handler(config.LOG_FILE)) +logger.addHandler(get_log_handler(settings.LOG_FILE)) diff --git a/python/cm/management/__init__.py b/python/cm/management/__init__.py index e69de29bb2..824dd6c8fe 100644 --- a/python/cm/management/__init__.py +++ b/python/cm/management/__init__.py @@ -0,0 +1,11 @@ +# 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. diff --git a/python/cm/management/commands/__init__.py b/python/cm/management/commands/__init__.py index e69de29bb2..824dd6c8fe 100644 --- a/python/cm/management/commands/__init__.py +++ b/python/cm/management/commands/__init__.py @@ -0,0 +1,11 @@ +# 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. diff --git a/python/cm/management/commands/dumpcluster.py b/python/cm/management/commands/dumpcluster.py index e16427e1a6..5d9734491f 100644 --- a/python/cm/management/commands/dumpcluster.py +++ b/python/cm/management/commands/dumpcluster.py @@ -24,7 +24,6 @@ from django.conf import settings from django.core.management.base import BaseCommand -from cm.config import ANSIBLE_SECRET, DEFAULT_SALT from cm.models import ( Bundle, Cluster, @@ -89,7 +88,7 @@ def get_bundle(prototype_id): :return: Bundle object :rtype: dict """ - fields = ('name', 'version', 'edition', 'hash', 'description') + fields = ("name", "version", "edition", "hash", "description") prototype = Prototype.objects.get(id=prototype_id) bundle = get_object(Bundle, prototype.bundle_id, fields) return bundle @@ -105,7 +104,7 @@ def get_bundle_hash(prototype_id): :rtype: str """ bundle = get_bundle(prototype_id) - return bundle['hash'] + return bundle["hash"] def get_config(object_config_id): @@ -117,16 +116,16 @@ def get_config(object_config_id): :return: Current and previous config in dictionary format :rtype: dict """ - fields = ('config', 'attr', 'date', 'description') + fields = ("config", "attr", "date", "description") try: object_config = ObjectConfig.objects.get(id=object_config_id) except ObjectConfig.DoesNotExist: return None config = {} - for name in ['current', 'previous']: + for name in ["current", "previous"]: _id = getattr(object_config, name) if _id: - config[name] = get_object(ConfigLog, _id, fields, ['date']) + config[name] = get_object(ConfigLog, _id, fields, ["date"]) else: config[name] = None return config @@ -142,13 +141,13 @@ def get_groups(object_id, model_name): :return: List with GroupConfig on that object in dict format :rtype: list """ - fields = ('object_id', 'name', 'description', 'config', 'object_type') + fields = ("object_id", "name", "description", "config", "object_type") groups = [] for gc in GroupConfig.objects.filter(object_id=object_id, object_type__model=model_name): group = get_object(GroupConfig, gc.id, fields) - group['config'] = get_config(group['config']) - group['model_name'] = model_name - group['hosts'] = [host.id for host in gc.hosts.all()] + group["config"] = get_config(group["config"]) + group["model_name"] = model_name + group["hosts"] = [host.id for host in gc.hosts.all()] groups.append(group) return groups @@ -163,18 +162,18 @@ def get_cluster(cluster_id): :rtype: dict """ fields = ( - 'id', - 'name', - 'description', - 'config', - 'state', - 'prototype', - '_multi_state', + "id", + "name", + "description", + "config", + "state", + "prototype", + "_multi_state", ) cluster = get_object(Cluster, cluster_id, fields) - cluster['config'] = get_config(cluster['config']) - bundle = get_bundle(cluster.pop('prototype')) - cluster['bundle_hash'] = bundle['hash'] + cluster["config"] = get_config(cluster["config"]) + bundle = get_bundle(cluster.pop("prototype")) + cluster["bundle_hash"] = bundle["hash"] return cluster, bundle @@ -188,18 +187,18 @@ def get_provider(provider_id): :rtype: dict """ fields = ( - 'id', - 'prototype', - 'name', - 'description', - 'config', - 'state', - '_multi_state', + "id", + "prototype", + "name", + "description", + "config", + "state", + "_multi_state", ) provider = get_object(HostProvider, provider_id, fields) - provider['config'] = get_config(provider['config']) - bundle = get_bundle(provider.pop('prototype')) - provider['bundle_hash'] = bundle['hash'] + provider["config"] = get_config(provider["config"]) + bundle = get_bundle(provider.pop("prototype")) + provider["bundle_hash"] = bundle["hash"] return provider, bundle @@ -213,19 +212,19 @@ def get_host(host_id): :rtype: dict """ fields = ( - 'id', - 'prototype', - 'fqdn', - 'description', - 'provider', - 'provider__name', - 'config', - 'state', - '_multi_state', + "id", + "prototype", + "fqdn", + "description", + "provider", + "provider__name", + "config", + "state", + "_multi_state", ) host = get_object(Host, host_id, fields) - host['config'] = get_config(host['config']) - host['bundle_hash'] = get_bundle_hash(host.pop('prototype')) + host["config"] = get_config(host["config"]) + host["bundle_hash"] = get_bundle_hash(host.pop("prototype")) return host @@ -239,17 +238,16 @@ def get_service(service_id): :rtype: dict """ fields = ( - 'id', - 'prototype', - 'prototype__name', - # 'service', # TODO: you need to remove the field from the ClusterObject model - 'config', - 'state', - '_multi_state', + "id", + "prototype", + "prototype__name", + "config", + "state", + "_multi_state", ) service = get_object(ClusterObject, service_id, fields) - service['config'] = get_config(service['config']) - service['bundle_hash'] = get_bundle_hash(service.pop('prototype')) + service["config"] = get_config(service["config"]) + service["bundle_hash"] = get_bundle_hash(service.pop("prototype")) return service @@ -263,17 +261,17 @@ def get_component(component_id): :rtype: dict """ fields = ( - 'id', - 'prototype', - 'prototype__name', - 'service', - 'config', - 'state', - '_multi_state', + "id", + "prototype", + "prototype__name", + "service", + "config", + "state", + "_multi_state", ) component = get_object(ServiceComponent, component_id, fields) - component['config'] = get_config(component['config']) - component['bundle_hash'] = get_bundle_hash(component.pop('prototype')) + component["config"] = get_config(component["config"]) + component["bundle_hash"] = get_bundle_hash(component.pop("prototype")) return component @@ -287,11 +285,11 @@ def get_host_component(host_component_id): :rtype: dict """ fields = ( - 'cluster', - 'host', - 'service', - 'component', - 'state', + "cluster", + "host", + "service", + "component", + "state", ) host_component = get_object(HostComponent, host_component_id, fields) return host_component @@ -302,7 +300,7 @@ def encrypt_data(pass_from_user, result): kdf = PBKDF2HMAC( algorithm=hashes.SHA256(), length=32, - salt=DEFAULT_SALT, + salt=settings.DEFAULT_SALT, iterations=390000, backend=default_backend(), ) @@ -324,70 +322,68 @@ def dump(cluster_id, output): cluster, bundle = get_cluster(cluster_id) data = { - 'ADCM_VERSION': settings.ADCM_VERSION, - 'bundles': { - bundle['hash']: bundle, + "ADCM_VERSION": settings.ADCM_VERSION, + "bundles": { + bundle["hash"]: bundle, }, - 'cluster': cluster, - 'hosts': [], - 'providers': [], - 'services': [], - 'components': [], - 'host_components': [], - 'groups': [], + "cluster": cluster, + "hosts": [], + "providers": [], + "services": [], + "components": [], + "host_components": [], + "groups": [], } provider_ids = set() - data['groups'].extend(get_groups(cluster_id, 'cluster')) + data["groups"].extend(get_groups(cluster_id, "cluster")) - for host_obj in Host.objects.filter(cluster_id=cluster['id']): + for host_obj in Host.objects.filter(cluster_id=cluster["id"]): host = get_host(host_obj.id) - provider_ids.add(host['provider']) - data['hosts'].append(host) + provider_ids.add(host["provider"]) + data["hosts"].append(host) - host_ids = [host['id'] for host in data['hosts']] + host_ids = [host["id"] for host in data["hosts"]] for provider_obj in HostProvider.objects.filter(id__in=provider_ids): provider, bundle = get_provider(provider_obj.id) - data['providers'].append(provider) - data['groups'].extend(get_groups(provider_obj.id, 'hostprovider')) - data['bundles'][bundle['hash']] = bundle + data["providers"].append(provider) + data["groups"].extend(get_groups(provider_obj.id, "hostprovider")) + data["bundles"][bundle["hash"]] = bundle - for service_obj in ClusterObject.objects.filter(cluster_id=cluster['id']): + for service_obj in ClusterObject.objects.filter(cluster_id=cluster["id"]): service = get_service(service_obj.id) - data['groups'].extend(get_groups(service_obj.id, 'clusterobject')) - data['services'].append(service) + data["groups"].extend(get_groups(service_obj.id, "clusterobject")) + data["services"].append(service) - service_ids = [service['id'] for service in data['services']] + service_ids = [service["id"] for service in data["services"]] - for component_obj in ServiceComponent.objects.filter( - cluster_id=cluster['id'], service_id__in=service_ids - ): + for component_obj in ServiceComponent.objects.filter(cluster_id=cluster["id"], service_id__in=service_ids): component = get_component(component_obj.id) - data['groups'].extend(get_groups(component_obj.id, 'servicecomponent')) - data['components'].append(component) + data["groups"].extend(get_groups(component_obj.id, "servicecomponent")) + data["components"].append(component) - component_ids = [component['id'] for component in data['components']] + component_ids = [component["id"] for component in data["components"]] for host_component_obj in HostComponent.objects.filter( - cluster_id=cluster['id'], + cluster_id=cluster["id"], host_id__in=host_ids, service_id__in=service_ids, component_id__in=component_ids, ): host_component = get_host_component(host_component_obj.id) - data['host_components'].append(host_component) - data['adcm_password'] = ANSIBLE_SECRET - result = json.dumps(data, indent=2).encode('utf-8') + data["host_components"].append(host_component) + data["adcm_password"] = settings.ANSIBLE_SECRET + result = json.dumps(data, indent=2).encode(settings.ENCODING_UTF_8) password = getpass.getpass() encrypted = encrypt_data(password, result) if output is not None: - with open(output, 'wb') as f: + with open(output, "wb") as f: f.write(encrypted) - sys.stdout.write(f'Dump successfully done to file {output}\n') + sys.stdout.write(f"Dump successfully done to file {output}\n") else: - sys.stdout.write(encrypted.decode('utf8')) + sys.stdout.write(encrypted.decode(settings.ENCODING_UTF_8)) class Command(BaseCommand): @@ -398,25 +394,25 @@ class Command(BaseCommand): manage.py dumpcluster --cluster_id 1 --output cluster.json """ - help = 'Dump cluster object to JSON format' + help = "Dump cluster object to JSON format" def add_arguments(self, parser): """ Parsing command line arguments """ parser.add_argument( - '-c', - '--cluster_id', - action='store', - dest='cluster_id', + "-c", + "--cluster_id", + action="store", + dest="cluster_id", required=True, type=int, - help='Cluster ID', + help="Cluster ID", ) - parser.add_argument('-o', '--output', help='Specifies file to which the output is written.') + parser.add_argument("-o", "--output", help="Specifies file to which the output is written.") def handle(self, *args, **options): """Handler method""" - cluster_id = options['cluster_id'] - output = options['output'] + cluster_id = options["cluster_id"] + output = options["output"] dump(cluster_id, output) diff --git a/python/cm/management/commands/loadcluster.py b/python/cm/management/commands/loadcluster.py index 65167cac92..ed9a3a2702 100644 --- a/python/cm/management/commands/loadcluster.py +++ b/python/cm/management/commands/loadcluster.py @@ -30,7 +30,6 @@ from django.db.utils import IntegrityError from cm.adcm_config import save_file_type -from cm.config import ANSIBLE_SECRET, ANSIBLE_VAULT_HEADER, DEFAULT_SALT from cm.errors import AdcmEx from cm.models import ( Bundle, @@ -72,7 +71,7 @@ def get_prototype(**kwargs): :return: Prototype object :rtype: models.Prototype """ - bundle = Bundle.objects.get(hash=kwargs.pop('bundle_hash')) + bundle = Bundle.objects.get(hash=kwargs.pop("bundle_hash")) prototype = Prototype.objects.get(bundle=bundle, **kwargs) return prototype @@ -87,10 +86,10 @@ def create_config(config, prototype=None): :rtype: models.ObjectConfig """ if config is not None: - current_config = process_config(prototype, config['current']) - deserializer_datetime_fields(current_config, ['date']) - previous_config = process_config(prototype, config['previous']) - deserializer_datetime_fields(previous_config, ['date']) + current_config = process_config(prototype, config["current"]) + deserializer_datetime_fields(current_config, ["date"]) + previous_config = process_config(prototype, config["previous"]) + deserializer_datetime_fields(previous_config, ["date"]) conf = ObjectConfig.objects.create(current=0, previous=0) @@ -121,12 +120,12 @@ def create_group(group, ex_hosts_list, obj): :return: GroupConfig object :rtype: models.GroupConfig """ - model_name = group.pop('model_name') - ex_object_id = group.pop('object_id') - group.pop('object_type') - config = create_config(group.pop('config')) + model_name = group.pop("model_name") + ex_object_id = group.pop("object_id") + group.pop("object_type") + config = create_config(group.pop("config")) hosts = [] - for host in group.pop('hosts'): + for host in group.pop("hosts"): hosts.append(ex_hosts_list[host]) gc = GroupConfig.objects.create( object_id=obj.id, @@ -140,27 +139,25 @@ def create_group(group, ex_hosts_list, obj): def switch_encoding(msg): ciphertext = msg - if ANSIBLE_VAULT_HEADER in msg: + if settings.ANSIBLE_VAULT_HEADER in msg: _, ciphertext = msg.split("\n") vault = VaultAES256() - secret_old = VaultSecret(bytes(OLD_ADCM_PASSWORD, 'utf-8')) - data = str(vault.decrypt(ciphertext, secret_old), 'utf-8') - secret_new = VaultSecret(bytes(ANSIBLE_SECRET, 'utf-8')) - ciphertext = vault.encrypt(bytes(data, 'utf-8'), secret_new) - return f'{ANSIBLE_VAULT_HEADER}\n{str(ciphertext, "utf-8")}' + secret_old = VaultSecret(bytes(OLD_ADCM_PASSWORD, settings.ENCODING_UTF_8)) + data = str(vault.decrypt(ciphertext, secret_old), settings.ENCODING_UTF_8) + secret_new = VaultSecret(bytes(settings.ANSIBLE_SECRET, settings.ENCODING_UTF_8)) + ciphertext = vault.encrypt(bytes(data, settings.ENCODING_UTF_8), secret_new) + return f"{settings.ANSIBLE_VAULT_HEADER}\n{str(ciphertext, settings.ENCODING_UTF_8)}" def process_config(proto, config): if config is not None and proto is not None: - conf = config['config'] - for pconf in PrototypeConfig.objects.filter( - prototype=proto, type__in=('secrettext', 'password') - ): + conf = config["config"] + for pconf in PrototypeConfig.objects.filter(prototype=proto, type__in=("secrettext", "password")): if pconf.subname and conf[pconf.name][pconf.subname]: conf[pconf.name][pconf.subname] = switch_encoding(conf[pconf.name][pconf.subname]) elif conf.get(pconf.name) and not pconf.subname: conf[pconf.name] = switch_encoding(conf[pconf.name]) - config['config'] = conf + config["config"] = conf return config @@ -168,11 +165,11 @@ def create_file_from_config(obj, config): if config is not None: conf = config["current"]["config"] proto = obj.prototype - for pconf in PrototypeConfig.objects.filter(prototype=proto, type='file'): + for pconf in PrototypeConfig.objects.filter(prototype=proto, type="file"): if pconf.subname and conf[pconf.name].get(pconf.subname): save_file_type(obj, pconf.name, pconf.subname, conf[pconf.name][pconf.subname]) elif conf.get(pconf.name): - save_file_type(obj, pconf.name, '', conf[pconf.name]) + save_file_type(obj, pconf.name, "", conf[pconf.name]) def create_cluster(cluster): @@ -185,15 +182,13 @@ def create_cluster(cluster): :rtype: models.Cluster """ try: - Cluster.objects.get(name=cluster['name']) - raise AdcmEx('CLUSTER_CONFLICT', 'Cluster with the same name already exist') + Cluster.objects.get(name=cluster["name"]) + raise AdcmEx("CLUSTER_CONFLICT", "Cluster with the same name already exist") except Cluster.DoesNotExist: - prototype = get_prototype(bundle_hash=cluster.pop('bundle_hash'), type='cluster') - ex_id = cluster.pop('id') - config = cluster.pop('config') - cluster = Cluster.objects.create( - prototype=prototype, config=create_config(config, prototype), **cluster - ) + prototype = get_prototype(bundle_hash=cluster.pop("bundle_hash"), type="cluster") + ex_id = cluster.pop("id") + config = cluster.pop("config") + cluster = Cluster.objects.create(prototype=prototype, config=create_config(config, prototype), **cluster) create_file_from_config(cluster, config) return ex_id, cluster @@ -207,20 +202,18 @@ def create_provider(provider): :return: HostProvider object :rtype: models.HostProvider """ - bundle_hash = provider.pop('bundle_hash') - ex_id = provider.pop('id') + bundle_hash = provider.pop("bundle_hash") + ex_id = provider.pop("id") try: - same_name_provider = HostProvider.objects.get(name=provider['name']) + same_name_provider = HostProvider.objects.get(name=provider["name"]) if same_name_provider.prototype.bundle.hash != bundle_hash: - raise IntegrityError('Name of provider already in use in another bundle') - create_file_from_config(same_name_provider, provider['config']) + raise IntegrityError("Name of provider already in use in another bundle") + create_file_from_config(same_name_provider, provider["config"]) return ex_id, same_name_provider except HostProvider.DoesNotExist: - prototype = get_prototype(bundle_hash=bundle_hash, type='provider') - config = provider.pop('config') - provider = HostProvider.objects.create( - prototype=prototype, config=create_config(config, prototype), **provider - ) + prototype = get_prototype(bundle_hash=bundle_hash, type="provider") + config = provider.pop("config") + provider = HostProvider.objects.create(prototype=prototype, config=create_config(config, prototype), **provider) create_file_from_config(provider, config) return ex_id, provider @@ -236,17 +229,17 @@ def create_host(host, cluster): :return: Host object :rtype: models.Host """ - host.pop('provider') - provider = HostProvider.objects.get(name=host.pop('provider__name')) + host.pop("provider") + provider = HostProvider.objects.get(name=host.pop("provider__name")) try: - Host.objects.get(fqdn=host['fqdn']) + Host.objects.get(fqdn=host["fqdn"]) provider.delete() cluster.delete() - raise AdcmEx('HOST_CONFLICT', 'Host fqdn already in use') + raise AdcmEx("HOST_CONFLICT", "Host fqdn already in use") except Host.DoesNotExist: - prototype = get_prototype(bundle_hash=host.pop('bundle_hash'), type='host') - ex_id = host.pop('id') - config = host.pop('config') + prototype = get_prototype(bundle_hash=host.pop("bundle_hash"), type="host") + ex_id = host.pop("id") + config = host.pop("config") new_host = Host.objects.create( prototype=prototype, provider=provider, @@ -270,10 +263,10 @@ def create_service(service, cluster): :rtype: models.ClusterObject """ prototype = get_prototype( - bundle_hash=service.pop('bundle_hash'), type='service', name=service.pop('prototype__name') + bundle_hash=service.pop("bundle_hash"), type="service", name=service.pop("prototype__name") ) - ex_id = service.pop('id') - config = service.pop('config') + ex_id = service.pop("id") + config = service.pop("config") service = ClusterObject.objects.create( prototype=prototype, cluster=cluster, config=create_config(config, prototype), **service ) @@ -295,13 +288,13 @@ def create_component(component, cluster, service): :rtype: models.ServiceComponent """ prototype = get_prototype( - bundle_hash=component.pop('bundle_hash'), - type='component', - name=component.pop('prototype__name'), + bundle_hash=component.pop("bundle_hash"), + type="component", + name=component.pop("prototype__name"), parent=service.prototype, ) - ex_id = component.pop('id') - config = component.pop('config') + ex_id = component.pop("id") + config = component.pop("config") component = ServiceComponent.objects.create( prototype=prototype, cluster=cluster, @@ -330,7 +323,7 @@ def create_host_component(host_component, cluster, host, service, component): :return: HostComponent object :rtype: models.HostComponent """ - host_component.pop('cluster') + host_component.pop("cluster") host_component = HostComponent.objects.create( cluster=cluster, host=host, service=service, component=component, **host_component ) @@ -344,22 +337,22 @@ def check(data): :param data: Data from file :type data: dict """ - if settings.ADCM_VERSION != data['ADCM_VERSION']: + if settings.ADCM_VERSION != data["ADCM_VERSION"]: raise AdcmEx( - 'DUMP_LOAD_ADCM_VERSION_ERROR', + "DUMP_LOAD_ADCM_VERSION_ERROR", msg=( - f'ADCM versions do not match, dump version: {data["ADCM_VERSION"]},' - f' load version: {settings.ADCM_VERSION}' + f"ADCM versions do not match, dump version: {data['ADCM_VERSION']}," + f" load version: {settings.ADCM_VERSION}" ), ) - for bundle_hash, bundle in data['bundles'].items(): + for bundle_hash, bundle in data["bundles"].items(): try: Bundle.objects.get(hash=bundle_hash) except Bundle.DoesNotExist as err: raise AdcmEx( - 'DUMP_LOAD_BUNDLE_ERROR', - msg=f'Bundle "{bundle["name"]} {bundle["version"]}" not found', + "DUMP_LOAD_BUNDLE_ERROR", + msg=f"Bundle '{bundle['name']} {bundle['version']}' not found", ) from err @@ -368,7 +361,7 @@ def decrypt_file(pass_from_user, file): kdf = PBKDF2HMAC( algorithm=hashes.SHA256(), length=32, - salt=DEFAULT_SALT, + salt=settings.DEFAULT_SALT, iterations=390000, backend=default_backend(), ) @@ -393,60 +386,60 @@ def load(file_path): """ try: password = getpass.getpass() - with open(file_path, 'r', encoding='utf_8') as f: + with open(file_path, "r", encoding=settings.ENCODING_UTF_8) as f: encrypted = f.read() decrypted = decrypt_file(password, encrypted) - data = json.loads(decrypted.decode('utf-8')) + data = json.loads(decrypted.decode(settings.ENCODING_UTF_8)) except FileNotFoundError as err: - raise AdcmEx('DUMP_LOAD_CLUSTER_ERROR', msg='Loaded file not found') from err + raise AdcmEx("DUMP_LOAD_CLUSTER_ERROR", msg="Loaded file not found") from err except InvalidToken as err: - raise AdcmEx('WRONG_PASSWORD') from err + raise AdcmEx("WRONG_PASSWORD") from err check(data) - set_old_password(data['adcm_password']) - _, cluster = create_cluster(data['cluster']) + set_old_password(data["adcm_password"]) + _, cluster = create_cluster(data["cluster"]) ex_provider_ids = {} - for provider_data in data['providers']: + for provider_data in data["providers"]: ex_provider_id, provider = create_provider(provider_data) ex_provider_ids[ex_provider_id] = provider ex_host_ids = {} - for host_data in data['hosts']: + for host_data in data["hosts"]: ex_host_id, host = create_host(host_data, cluster) ex_host_ids[ex_host_id] = host ex_service_ids = {} - for service_data in data['services']: + for service_data in data["services"]: ex_service_id, service = create_service(service_data, cluster) ex_service_ids[ex_service_id] = service ex_component_ids = {} - for component_data in data['components']: + for component_data in data["components"]: ex_component_id, component = create_component( - component_data, cluster, ex_service_ids[component_data.pop('service')] + component_data, cluster, ex_service_ids[component_data.pop("service")] ) ex_component_ids[ex_component_id] = component - for host_component_data in data['host_components']: + for host_component_data in data["host_components"]: create_host_component( host_component_data, cluster, - ex_host_ids[host_component_data.pop('host')], - ex_service_ids[host_component_data.pop('service')], - ex_component_ids[host_component_data.pop('component')], + ex_host_ids[host_component_data.pop("host")], + ex_service_ids[host_component_data.pop("service")], + ex_component_ids[host_component_data.pop("component")], ) - for group_data in data['groups']: - if group_data['model_name'] == 'cluster': + for group_data in data["groups"]: + if group_data["model_name"] == "cluster": obj = cluster - elif group_data['model_name'] == 'clusterobject': - obj = ex_service_ids[group_data['object_id']] - elif group_data['model_name'] == 'servicecomponent': - obj = ex_component_ids[group_data['object_id']] - elif group_data['model_name'] == 'hostprovider': - obj = ex_provider_ids[group_data['object_id']] + elif group_data["model_name"] == "clusterobject": + obj = ex_service_ids[group_data["object_id"]] + elif group_data["model_name"] == "servicecomponent": + obj = ex_component_ids[group_data["object_id"]] + elif group_data["model_name"] == "hostprovider": + obj = ex_provider_ids[group_data["object_id"]] create_group(group_data, ex_host_ids, obj) - sys.stdout.write(f'Load successfully ended, cluster {cluster.display_name} created\n') + sys.stdout.write(f"Load successfully ended, cluster {cluster.display_name} created\n") class Command(BaseCommand): @@ -457,13 +450,13 @@ class Command(BaseCommand): manage.py loadcluster cluster.json """ - help = 'Load cluster object from JSON format' + help = "Load cluster object from JSON format" def add_arguments(self, parser): """Parsing command line arguments""" - parser.add_argument('file_path', nargs='?') + parser.add_argument("file_path", nargs="?") def handle(self, *args, **options): """Handler method""" - file_path = options.get('file_path') + file_path = options.get("file_path") load(file_path) diff --git a/python/cm/management/commands/logrotate.py b/python/cm/management/commands/logrotate.py index aa38bf44e4..adbfbe0f6e 100644 --- a/python/cm/management/commands/logrotate.py +++ b/python/cm/management/commands/logrotate.py @@ -16,13 +16,13 @@ from enum import Enum from subprocess import STDOUT, CalledProcessError, check_output +from django.conf import settings from django.core.management.base import BaseCommand from django.db import transaction from django.utils import timezone from audit.models import AuditLogOperationResult from audit.utils import make_audit_log -from cm import config from cm.models import ( ADCM, Cluster, @@ -68,10 +68,9 @@ class TargetType(Enum): class Command(BaseCommand): help = "Delete / rotate log files, db records, `run` directories" - __encoding = "utf-8" __nginx_logrotate_conf = "/etc/logrotate.d/nginx" __logrotate_cmd = f"logrotate {__nginx_logrotate_conf}" - __logrotate_cmd_debug = f"{__logrotate_cmd} -d" + __logrotate_cmd_debug = f"{__logrotate_cmd} -v" def add_arguments(self, parser): parser.add_argument( @@ -106,19 +105,17 @@ def __execute_cmd(self, cmd): self.__log(f"executing cmd: `{cmd}`", "info") try: out = check_output(cmd, shell=True, stderr=STDOUT) - out = out.decode(self.__encoding).strip("\n") + out = out.decode(settings.ENCODING_UTF_8).strip("\n") if out: self.__log(out, "debug") except CalledProcessError as e: - err_msg = e.stdout.decode(self.__encoding).strip("\n") + err_msg = e.stdout.decode(settings.ENCODING_UTF_8).strip("\n") msg = f"Error! cmd: `{cmd}` return code: `{e.returncode}` msg: `{err_msg}`" self.__log(msg, "exception") def __get_logrotate_config(self): adcm_object = ADCM.objects.first() - current_configlog = ConfigLog.objects.get( - obj_ref=adcm_object.config, id=adcm_object.config.current - ) + current_configlog = ConfigLog.objects.get(obj_ref=adcm_object.config, id=adcm_object.config.current) adcm_conf = current_configlog.config logrotate_config = { "logrotate": { @@ -137,7 +134,7 @@ def __generate_logrotate_conf_file(self): "no_compress": "" if self.config["logrotate"]["nginx"]["compress"] else "#", "num_rotations": self.config["logrotate"]["nginx"]["max_history"], } - with open(self.__nginx_logrotate_conf, "wt", encoding=self.__encoding) as conf_file: + with open(self.__nginx_logrotate_conf, "wt", encoding=settings.ENCODING_UTF_8) as conf_file: conf_file.write(LOGROTATE_CONF_FILE_TEMPLATE.format(**conf_file_args)) self.__log(f"conf file `{self.__nginx_logrotate_conf}` generated", "debug") @@ -146,8 +143,7 @@ def __run_nginx_log_rotation(self): self.__log("Nginx log rotation started", "info") self.__generate_logrotate_conf_file() self.__log( - f"Using config file `{self.__nginx_logrotate_conf}`:\n" - f"{open(self.__nginx_logrotate_conf, 'rt', encoding=self.__encoding).read()}", + f"Using config file `{self.__nginx_logrotate_conf}`", "debug", ) if self.verbose: @@ -175,9 +171,7 @@ def __run_configlog_rotation(self): target_configlogs = target_configlogs.exclude(pk__in=exclude_pks) target_configlog_ids = set(i[0] for i in target_configlogs.values_list("id")) target_objectconfig_ids = set( - cl.obj_ref.id - for cl in target_configlogs - if not self.__has_related_records(cl.obj_ref) + cl.obj_ref.id for cl in target_configlogs if not self.__has_related_records(cl.obj_ref) ) if target_configlog_ids or target_objectconfig_ids: make_audit_log("config", AuditLogOperationResult.Success, "launched") @@ -190,8 +184,7 @@ def __run_configlog_rotation(self): make_audit_log("config", AuditLogOperationResult.Success, "completed") self.__log( - f"Deleted {len(target_configlog_ids)} ConfigLogs and " - f"{len(target_objectconfig_ids)} ObjectConfigs", + f"Deleted {len(target_configlog_ids)} ConfigLogs and " f"{len(target_objectconfig_ids)} ObjectConfigs", "info", ) @@ -229,8 +222,7 @@ def __run_joblog_rotation(self): threshold_date_db = timezone.now() - timedelta(days=days_delta_db) threshold_date_fs = timezone.now() - timedelta(days=days_delta_fs) self.__log( - f"JobLog rotation started. Threshold dates: " - f"db - {threshold_date_db}, fs - {threshold_date_fs}", + f"JobLog rotation started. Threshold dates: " f"db - {threshold_date_db}, fs - {threshold_date_fs}", "info", ) is_deleted = False @@ -248,9 +240,9 @@ def __run_joblog_rotation(self): self.__log("db JobLog rotated", "info") if days_delta_fs > 0: # pylint: disable=too-many-nested-blocks - for name in os.listdir(config.RUN_DIR): + for name in os.listdir(settings.RUN_DIR): if not name.startswith("."): # a line of code is used for development - path = os.path.join(config.RUN_DIR, name) + path = settings.RUN_DIR / name try: m_time = datetime.fromtimestamp(os.path.getmtime(path), tz=timezone.utc) if timezone.now() - m_time > timedelta(days=days_delta_fs): diff --git a/python/cm/management/commands/run_ldap_sync.py b/python/cm/management/commands/run_ldap_sync.py index f153180d1e..2550758b2a 100644 --- a/python/cm/management/commands/run_ldap_sync.py +++ b/python/cm/management/commands/run_ldap_sync.py @@ -9,6 +9,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 logging from datetime import timedelta @@ -17,17 +18,14 @@ from audit.models import AuditLogOperationResult from audit.utils import make_audit_log -from cm.config import Job from cm.job import start_task -from cm.models import ADCM, Action, ConfigLog, TaskLog +from cm.models import ADCM, Action, ConfigLog, JobStatus, TaskLog logger = logging.getLogger("background_tasks") def get_settings(adcm_object): - current_configlog = ConfigLog.objects.get( - obj_ref=adcm_object.config, id=adcm_object.config.current - ) + current_configlog = ConfigLog.objects.get(obj_ref=adcm_object.config, id=adcm_object.config.current) if current_configlog.attr["ldap_integration"]["active"]: ldap_config = current_configlog.config["ldap_integration"] return ldap_config["sync_interval"] @@ -43,11 +41,11 @@ def handle(self, *args, **options): period = get_settings(adcm_object) if period <= 0: return - if TaskLog.objects.filter(action__name="run_ldap_sync", status=Job.RUNNING).exists(): + if TaskLog.objects.filter(action__name="run_ldap_sync", status=JobStatus.RUNNING).exists(): logger.debug("Sync has already launched, we need to wait for the task end") return last_sync = TaskLog.objects.filter( - action__name="run_ldap_sync", status__in=[Job.SUCCESS, Job.FAILED] + action__name="run_ldap_sync", status__in=[JobStatus.SUCCESS, JobStatus.FAILED] ).last() if last_sync is None: logger.debug("First ldap sync launched in %s", timezone.now()) diff --git a/python/cm/migrations/0001_initial.py b/python/cm/migrations/0001_initial.py index d292876baf..eb9cee09ca 100644 --- a/python/cm/migrations/0001_initial.py +++ b/python/cm/migrations/0001_initial.py @@ -9,8 +9,8 @@ # 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. + # Generated by Django 2.0.5 on 2018-06-08 14:24 -# pylint: disable=line-too-long import django.db.models.deletion from django.db import migrations, models @@ -28,9 +28,7 @@ class Migration(migrations.Migration): fields=[ ( 'id', - models.AutoField( - auto_created=True, primary_key=True, serialize=False, verbose_name='ID' - ), + models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), ), ('name', models.CharField(max_length=160)), ( @@ -57,9 +55,7 @@ class Migration(migrations.Migration): fields=[ ( 'id', - models.AutoField( - auto_created=True, primary_key=True, serialize=False, verbose_name='ID' - ), + models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), ), ('name', models.CharField(max_length=160)), ('version', models.CharField(max_length=80)), @@ -73,9 +69,7 @@ class Migration(migrations.Migration): fields=[ ( 'id', - models.AutoField( - auto_created=True, primary_key=True, serialize=False, verbose_name='ID' - ), + models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), ), ('name', models.CharField(max_length=80, unique=True)), ('description', models.CharField(blank=True, max_length=160)), @@ -87,9 +81,7 @@ class Migration(migrations.Migration): fields=[ ( 'id', - models.AutoField( - auto_created=True, primary_key=True, serialize=False, verbose_name='ID' - ), + models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), ), ('state', models.CharField(default='created', max_length=64)), ( @@ -103,9 +95,7 @@ class Migration(migrations.Migration): fields=[ ( 'id', - models.AutoField( - auto_created=True, primary_key=True, serialize=False, verbose_name='ID' - ), + models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), ), ('name', models.CharField(max_length=160)), ('description', models.CharField(blank=True, max_length=160)), @@ -118,9 +108,7 @@ class Migration(migrations.Migration): fields=[ ( 'id', - models.AutoField( - auto_created=True, primary_key=True, serialize=False, verbose_name='ID' - ), + models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), ), ('config', models.TextField()), ('date', models.DateTimeField(auto_now=True)), @@ -132,9 +120,7 @@ class Migration(migrations.Migration): fields=[ ( 'id', - models.AutoField( - auto_created=True, primary_key=True, serialize=False, verbose_name='ID' - ), + models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), ), ('fqdn', models.CharField(max_length=160, unique=True)), ('state', models.CharField(default='created', max_length=64)), @@ -154,9 +140,7 @@ class Migration(migrations.Migration): fields=[ ( 'id', - models.AutoField( - auto_created=True, primary_key=True, serialize=False, verbose_name='ID' - ), + models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), ), ('state', models.CharField(default='created', max_length=64)), ( @@ -165,9 +149,7 @@ class Migration(migrations.Migration): ), ( 'component', - models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, to='cm.Component' - ), + models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='cm.Component'), ), ( 'host', @@ -180,9 +162,7 @@ class Migration(migrations.Migration): fields=[ ( 'id', - models.AutoField( - auto_created=True, primary_key=True, serialize=False, verbose_name='ID' - ), + models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), ), ('task_id', models.PositiveIntegerField(default=0)), ('action_id', models.PositiveIntegerField()), @@ -208,9 +188,7 @@ class Migration(migrations.Migration): fields=[ ( 'id', - models.AutoField( - auto_created=True, primary_key=True, serialize=False, verbose_name='ID' - ), + models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), ), ('current', models.PositiveIntegerField()), ('previous', models.PositiveIntegerField()), @@ -221,9 +199,7 @@ class Migration(migrations.Migration): fields=[ ( 'id', - models.AutoField( - auto_created=True, primary_key=True, serialize=False, verbose_name='ID' - ), + models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), ), ( 'type', @@ -246,9 +222,7 @@ class Migration(migrations.Migration): fields=[ ( 'id', - models.AutoField( - auto_created=True, primary_key=True, serialize=False, verbose_name='ID' - ), + models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), ), ('name', models.CharField(max_length=160)), ('subname', models.CharField(blank=True, max_length=160)), @@ -271,9 +245,7 @@ class Migration(migrations.Migration): ('required', models.BooleanField(default=True)), ( 'prototype', - models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, to='cm.Prototype' - ), + models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='cm.Prototype'), ), ], ), @@ -282,9 +254,7 @@ class Migration(migrations.Migration): fields=[ ( 'id', - models.AutoField( - auto_created=True, primary_key=True, serialize=False, verbose_name='ID' - ), + models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), ), ('name', models.CharField(max_length=160)), ( @@ -311,9 +281,7 @@ class Migration(migrations.Migration): fields=[ ( 'id', - models.AutoField( - auto_created=True, primary_key=True, serialize=False, verbose_name='ID' - ), + models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), ), ('name', models.CharField(max_length=160)), ('description', models.CharField(blank=True, max_length=160)), @@ -326,9 +294,7 @@ class Migration(migrations.Migration): fields=[ ( 'id', - models.AutoField( - auto_created=True, primary_key=True, serialize=False, verbose_name='ID' - ), + models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), ), ( 'type', @@ -347,9 +313,7 @@ class Migration(migrations.Migration): fields=[ ( 'id', - models.AutoField( - auto_created=True, primary_key=True, serialize=False, verbose_name='ID' - ), + models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), ), ('name', models.CharField(max_length=160)), ('subname', models.CharField(blank=True, max_length=160)), @@ -372,9 +336,7 @@ class Migration(migrations.Migration): ('required', models.BooleanField(default=True)), ( 'prototype', - models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, to='cm.StagePrototype' - ), + models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='cm.StagePrototype'), ), ], ), @@ -383,9 +345,7 @@ class Migration(migrations.Migration): fields=[ ( 'id', - models.AutoField( - auto_created=True, primary_key=True, serialize=False, verbose_name='ID' - ), + models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), ), ('action_id', models.PositiveIntegerField()), ('object_id', models.PositiveIntegerField()), @@ -410,9 +370,7 @@ class Migration(migrations.Migration): fields=[ ( 'id', - models.AutoField( - auto_created=True, primary_key=True, serialize=False, verbose_name='ID' - ), + models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), ), ('login', models.CharField(max_length=32, unique=True)), ('profile', models.TextField()), @@ -425,16 +383,12 @@ class Migration(migrations.Migration): migrations.AddField( model_name='stagecomponent', name='prototype', - field=models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, to='cm.StagePrototype' - ), + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='cm.StagePrototype'), ), migrations.AddField( model_name='stageaction', name='prototype', - field=models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, to='cm.StagePrototype' - ), + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='cm.StagePrototype'), ), migrations.AddField( model_name='hostcomponent', @@ -444,9 +398,7 @@ class Migration(migrations.Migration): migrations.AddField( model_name='host', name='config', - field=models.OneToOneField( - null=True, on_delete=django.db.models.deletion.CASCADE, to='cm.ObjectConfig' - ), + field=models.OneToOneField(null=True, on_delete=django.db.models.deletion.CASCADE, to='cm.ObjectConfig'), ), migrations.AddField( model_name='host', @@ -456,9 +408,7 @@ class Migration(migrations.Migration): migrations.AddField( model_name='configlog', name='obj_ref', - field=models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, to='cm.ObjectConfig' - ), + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='cm.ObjectConfig'), ), migrations.AddField( model_name='component', @@ -468,9 +418,7 @@ class Migration(migrations.Migration): migrations.AddField( model_name='clusterobject', name='config', - field=models.OneToOneField( - null=True, on_delete=django.db.models.deletion.CASCADE, to='cm.ObjectConfig' - ), + field=models.OneToOneField(null=True, on_delete=django.db.models.deletion.CASCADE, to='cm.ObjectConfig'), ), migrations.AddField( model_name='clusterobject', @@ -480,9 +428,7 @@ class Migration(migrations.Migration): migrations.AddField( model_name='cluster', name='config', - field=models.OneToOneField( - null=True, on_delete=django.db.models.deletion.CASCADE, to='cm.ObjectConfig' - ), + field=models.OneToOneField(null=True, on_delete=django.db.models.deletion.CASCADE, to='cm.ObjectConfig'), ), migrations.AddField( model_name='cluster', diff --git a/python/cm/migrations/0002_auto_20180801_1117.py b/python/cm/migrations/0002_auto_20180801_1117.py index 48edc337f7..9611d2063f 100644 --- a/python/cm/migrations/0002_auto_20180801_1117.py +++ b/python/cm/migrations/0002_auto_20180801_1117.py @@ -9,8 +9,8 @@ # 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. + # Generated by Django 2.0.5 on 2018-08-01 11:17 -# pylint: disable=line-too-long import django.db.models.deletion from django.db import migrations, models @@ -28,9 +28,7 @@ class Migration(migrations.Migration): fields=[ ( 'id', - models.AutoField( - auto_created=True, primary_key=True, serialize=False, verbose_name='ID' - ), + models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), ), ('name', models.CharField(blank=True, max_length=160)), ('description', models.CharField(blank=True, max_length=160)), @@ -45,9 +43,7 @@ class Migration(migrations.Migration): fields=[ ( 'id', - models.AutoField( - auto_created=True, primary_key=True, serialize=False, verbose_name='ID' - ), + models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), ), ('name', models.CharField(blank=True, max_length=160)), ('description', models.CharField(blank=True, max_length=160)), diff --git a/python/cm/migrations/0003_auto_20180829_1020.py b/python/cm/migrations/0003_auto_20180829_1020.py index f03ecf43cc..45e104487d 100644 --- a/python/cm/migrations/0003_auto_20180829_1020.py +++ b/python/cm/migrations/0003_auto_20180829_1020.py @@ -9,8 +9,8 @@ # 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. + # Generated by Django 2.0.5 on 2018-08-29 10:20 -# pylint: disable=line-too-long from django.db import migrations, models diff --git a/python/cm/migrations/0004_auto_20180914_1042.py b/python/cm/migrations/0004_auto_20180914_1042.py index 7070aedd09..324ddfaef4 100644 --- a/python/cm/migrations/0004_auto_20180914_1042.py +++ b/python/cm/migrations/0004_auto_20180914_1042.py @@ -9,8 +9,8 @@ # 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. + # Generated by Django 2.0.5 on 2018-09-14 10:42 -# pylint: disable=line-too-long import django.db.models.deletion from django.db import migrations, models @@ -28,9 +28,7 @@ class Migration(migrations.Migration): fields=[ ( 'id', - models.AutoField( - auto_created=True, primary_key=True, serialize=False, verbose_name='ID' - ), + models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), ), ( 'cluster', @@ -60,16 +58,12 @@ class Migration(migrations.Migration): fields=[ ( 'id', - models.AutoField( - auto_created=True, primary_key=True, serialize=False, verbose_name='ID' - ), + models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), ), ('name', models.CharField(max_length=160)), ( 'prototype', - models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, to='cm.Prototype' - ), + models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='cm.Prototype'), ), ], ), @@ -78,18 +72,14 @@ class Migration(migrations.Migration): fields=[ ( 'id', - models.AutoField( - auto_created=True, primary_key=True, serialize=False, verbose_name='ID' - ), + models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), ), ('name', models.CharField(max_length=160)), ('min_version', models.CharField(max_length=80)), ('max_version', models.CharField(max_length=80)), ( 'prototype', - models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, to='cm.Prototype' - ), + models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='cm.Prototype'), ), ], ), @@ -98,16 +88,12 @@ class Migration(migrations.Migration): fields=[ ( 'id', - models.AutoField( - auto_created=True, primary_key=True, serialize=False, verbose_name='ID' - ), + models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), ), ('name', models.CharField(max_length=160)), ( 'prototype', - models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, to='cm.StagePrototype' - ), + models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='cm.StagePrototype'), ), ], ), @@ -116,18 +102,14 @@ class Migration(migrations.Migration): fields=[ ( 'id', - models.AutoField( - auto_created=True, primary_key=True, serialize=False, verbose_name='ID' - ), + models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), ), ('name', models.CharField(max_length=160)), ('min_version', models.CharField(max_length=80)), ('max_version', models.CharField(max_length=80)), ( 'prototype', - models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, to='cm.StagePrototype' - ), + models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='cm.StagePrototype'), ), ], ), diff --git a/python/cm/migrations/0005_auto_20180928_0945.py b/python/cm/migrations/0005_auto_20180928_0945.py index 3997a8229d..d2fec1fb4d 100644 --- a/python/cm/migrations/0005_auto_20180928_0945.py +++ b/python/cm/migrations/0005_auto_20180928_0945.py @@ -9,8 +9,8 @@ # 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. + # Generated by Django 2.0.5 on 2018-09-28 09:45 -# pylint: disable=line-too-long import django.db.models.deletion from django.db import migrations, models diff --git a/python/cm/migrations/0006_auto_20181009_1135.py b/python/cm/migrations/0006_auto_20181009_1135.py index f97e563b6f..f0fe186033 100644 --- a/python/cm/migrations/0006_auto_20181009_1135.py +++ b/python/cm/migrations/0006_auto_20181009_1135.py @@ -9,6 +9,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. + # Generated by Django 2.0.5 on 2018-10-09 11:35 from django.db import migrations, models diff --git a/python/cm/migrations/0007_auto_20181023_1048.py b/python/cm/migrations/0007_auto_20181023_1048.py index 5bd8444c4c..6af88c3447 100644 --- a/python/cm/migrations/0007_auto_20181023_1048.py +++ b/python/cm/migrations/0007_auto_20181023_1048.py @@ -9,6 +9,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. + # Generated by Django 2.0.5 on 2018-10-23 10:48 from django.db import migrations, models diff --git a/python/cm/migrations/0008_auto_20181107_1216.py b/python/cm/migrations/0008_auto_20181107_1216.py index d9d021b1b4..6251273084 100644 --- a/python/cm/migrations/0008_auto_20181107_1216.py +++ b/python/cm/migrations/0008_auto_20181107_1216.py @@ -9,6 +9,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. + # Generated by Django 2.0.5 on 2018-11-07 12:16 from django.db import migrations, models diff --git a/python/cm/migrations/0009_auto_20181113_1112.py b/python/cm/migrations/0009_auto_20181113_1112.py index 6ae87bccb7..e1f3e28180 100644 --- a/python/cm/migrations/0009_auto_20181113_1112.py +++ b/python/cm/migrations/0009_auto_20181113_1112.py @@ -9,8 +9,8 @@ # 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. + # Generated by Django 2.0.5 on 2018-11-13 11:12 -# pylint: disable=line-too-long import django.db.models.deletion from django.db import migrations, models @@ -28,9 +28,7 @@ class Migration(migrations.Migration): fields=[ ( 'id', - models.AutoField( - auto_created=True, primary_key=True, serialize=False, verbose_name='ID' - ), + models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), ), ('name', models.CharField(max_length=80, unique=True)), ('description', models.TextField(blank=True)), @@ -38,9 +36,7 @@ class Migration(migrations.Migration): ('stack', models.TextField(blank=True)), ( 'config', - models.OneToOneField( - null=True, on_delete=django.db.models.deletion.CASCADE, to='cm.ObjectConfig' - ), + models.OneToOneField(null=True, on_delete=django.db.models.deletion.CASCADE, to='cm.ObjectConfig'), ), ], ), diff --git a/python/cm/migrations/0010_auto_20181212_1213.py b/python/cm/migrations/0010_auto_20181212_1213.py index c2d4f1b4c3..6900fc80d2 100644 --- a/python/cm/migrations/0010_auto_20181212_1213.py +++ b/python/cm/migrations/0010_auto_20181212_1213.py @@ -9,6 +9,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. + # Generated by Django 2.0.5 on 2018-12-12 12:13 from django.db import migrations, models diff --git a/python/cm/migrations/0011_auto_20181220_1327.py b/python/cm/migrations/0011_auto_20181220_1327.py index d8b0d1c336..8791fe33dc 100644 --- a/python/cm/migrations/0011_auto_20181220_1327.py +++ b/python/cm/migrations/0011_auto_20181220_1327.py @@ -9,6 +9,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. + # Generated by Django 2.0.5 on 2018-12-20 13:27 from django.db import migrations, models diff --git a/python/cm/migrations/0012_auto_20181226_0926.py b/python/cm/migrations/0012_auto_20181226_0926.py index 60e03fd15c..31d7902b7c 100644 --- a/python/cm/migrations/0012_auto_20181226_0926.py +++ b/python/cm/migrations/0012_auto_20181226_0926.py @@ -9,8 +9,8 @@ # 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. + # Generated by Django 2.0.5 on 2018-12-26 09:26 -# pylint: disable=line-too-long import django.db.models.deletion from django.db import migrations, models @@ -28,9 +28,7 @@ class Migration(migrations.Migration): fields=[ ( 'id', - models.AutoField( - auto_created=True, primary_key=True, serialize=False, verbose_name='ID' - ), + models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), ), ( 'cluster', @@ -38,31 +36,23 @@ class Migration(migrations.Migration): ), ( 'component', - models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, to='cm.Component' - ), + models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='cm.Component'), ), ( 'service', - models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, to='cm.ClusterObject' - ), + models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='cm.ClusterObject'), ), ], ), migrations.AlterField( model_name='hostcomponent', name='component', - field=models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, to='cm.ServiceComponent' - ), + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='cm.ServiceComponent'), ), migrations.AlterField( model_name='hostcomponent', name='service', - field=models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, to='cm.ClusterObject' - ), + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='cm.ClusterObject'), ), migrations.AlterUniqueTogether( name='servicecomponent', diff --git a/python/cm/migrations/0013_auto_20190116_1143.py b/python/cm/migrations/0013_auto_20190116_1143.py index 5e46423e1c..c684568606 100644 --- a/python/cm/migrations/0013_auto_20190116_1143.py +++ b/python/cm/migrations/0013_auto_20190116_1143.py @@ -9,8 +9,8 @@ # 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. + # Generated by Django 2.0.5 on 2019-01-16 11:43 -# pylint: disable=line-too-long import django.db.models.deletion from django.db import migrations, models diff --git a/python/cm/migrations/0014_auto_20190124_1344.py b/python/cm/migrations/0014_auto_20190124_1344.py index fd68e3eb96..11dc0ab5fe 100644 --- a/python/cm/migrations/0014_auto_20190124_1344.py +++ b/python/cm/migrations/0014_auto_20190124_1344.py @@ -9,8 +9,8 @@ # 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. + # Generated by Django 2.0.5 on 2019-01-24 13:44 -# pylint: disable=line-too-long import datetime @@ -36,33 +36,25 @@ class Migration(migrations.Migration): migrations.AddField( model_name='joblog', name='finish_date', - field=models.DateTimeField( - default=datetime.datetime(2018, 1, 1, 12, 0, 0, 100500, tzinfo=utc) - ), + field=models.DateTimeField(default=datetime.datetime(2018, 1, 1, 12, 0, 0, 100500, tzinfo=utc)), preserve_default=False, ), migrations.AddField( model_name='joblog', name='start_date', - field=models.DateTimeField( - default=datetime.datetime(2018, 1, 1, 12, 0, 0, 100500, tzinfo=utc) - ), + field=models.DateTimeField(default=datetime.datetime(2018, 1, 1, 12, 0, 0, 100500, tzinfo=utc)), preserve_default=False, ), migrations.AddField( model_name='tasklog', name='finish_date', - field=models.DateTimeField( - default=datetime.datetime(2018, 1, 1, 12, 0, 0, 100500, tzinfo=utc) - ), + field=models.DateTimeField(default=datetime.datetime(2018, 1, 1, 12, 0, 0, 100500, tzinfo=utc)), preserve_default=False, ), migrations.AddField( model_name='tasklog', name='start_date', - field=models.DateTimeField( - default=datetime.datetime(2018, 1, 1, 12, 0, 0, 100500, tzinfo=utc) - ), + field=models.DateTimeField(default=datetime.datetime(2018, 1, 1, 12, 0, 0, 100500, tzinfo=utc)), preserve_default=False, ), ] diff --git a/python/cm/migrations/0015_auto_20190128_1142.py b/python/cm/migrations/0015_auto_20190128_1142.py index 98304ea4d5..cbf185757b 100644 --- a/python/cm/migrations/0015_auto_20190128_1142.py +++ b/python/cm/migrations/0015_auto_20190128_1142.py @@ -9,6 +9,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. + # Generated by Django 2.0.5 on 2019-01-28 11:42 from django.db import migrations, models diff --git a/python/cm/migrations/0016_auto_20190218_1214.py b/python/cm/migrations/0016_auto_20190218_1214.py index 7460fb8308..a7d5168992 100644 --- a/python/cm/migrations/0016_auto_20190218_1214.py +++ b/python/cm/migrations/0016_auto_20190218_1214.py @@ -9,6 +9,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. + # Generated by Django 2.0.5 on 2019-02-18 12:14 from django.db import migrations diff --git a/python/cm/migrations/0017_auto_20190220_1137.py b/python/cm/migrations/0017_auto_20190220_1137.py index 0207f399d9..aafc9633e5 100644 --- a/python/cm/migrations/0017_auto_20190220_1137.py +++ b/python/cm/migrations/0017_auto_20190220_1137.py @@ -9,6 +9,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. + # Generated by Django 2.0.5 on 2019-02-20 11:37 from django.db import migrations diff --git a/python/cm/migrations/0018_auto_20190220_1101.py b/python/cm/migrations/0018_auto_20190220_1101.py index 5872215dd6..f00ff247da 100644 --- a/python/cm/migrations/0018_auto_20190220_1101.py +++ b/python/cm/migrations/0018_auto_20190220_1101.py @@ -9,8 +9,8 @@ # 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. + # Generated by Django 2.0.5 on 2019-02-19 10:51 -# pylint: disable=line-too-long import django.db.models.deletion from django.db import migrations, models @@ -28,16 +28,12 @@ class Migration(migrations.Migration): fields=[ ( 'id', - models.AutoField( - auto_created=True, primary_key=True, serialize=False, verbose_name='ID' - ), + models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), ), ('name', models.CharField(choices=[('ADCM', 'ADCM')], max_length=16, unique=True)), ( 'config', - models.OneToOneField( - null=True, on_delete=django.db.models.deletion.CASCADE, to='cm.ObjectConfig' - ), + models.OneToOneField(null=True, on_delete=django.db.models.deletion.CASCADE, to='cm.ObjectConfig'), ), ], ), diff --git a/python/cm/migrations/0019_auto_20190314_1321.py b/python/cm/migrations/0019_auto_20190314_1321.py index be456a0ceb..2427a0a299 100644 --- a/python/cm/migrations/0019_auto_20190314_1321.py +++ b/python/cm/migrations/0019_auto_20190314_1321.py @@ -9,8 +9,8 @@ # 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. + # Generated by Django 2.1.7 on 2019-03-14 13:21 -# pylint: disable=line-too-long from django.db import migrations, models diff --git a/python/cm/migrations/0020_auto_20190321_1126.py b/python/cm/migrations/0020_auto_20190321_1126.py index 2f0d0654f9..b74ed96441 100644 --- a/python/cm/migrations/0020_auto_20190321_1126.py +++ b/python/cm/migrations/0020_auto_20190321_1126.py @@ -9,6 +9,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. + # Generated by Django 2.1.7 on 2019-03-21 11:26 from django.db import migrations, models diff --git a/python/cm/migrations/0021_auto_20190607_1027.py b/python/cm/migrations/0021_auto_20190607_1027.py index 2f83117d66..ec4b4d40f7 100644 --- a/python/cm/migrations/0021_auto_20190607_1027.py +++ b/python/cm/migrations/0021_auto_20190607_1027.py @@ -9,8 +9,8 @@ # 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. + # Generated by Django 2.1.7 on 2019-06-07 10:27 -# pylint: disable=line-too-long import django.db.models.deletion from django.db import migrations, models @@ -23,12 +23,8 @@ class Migration(migrations.Migration): ] operations = [ - migrations.RenameField( - model_name='clusterbind', old_name='source', new_name='source_cluster' - ), - migrations.RenameField( - model_name='clusterbind', old_name='service', new_name='source_service' - ), + migrations.RenameField(model_name='clusterbind', old_name='source', new_name='source_cluster'), + migrations.RenameField(model_name='clusterbind', old_name='service', new_name='source_service'), migrations.RunSQL("DROP INDEX IF EXISTS cm_clusterbind_service_id_2f524997;"), migrations.AddField( model_name='clusterbind', diff --git a/python/cm/migrations/0022_auto_20190620_1251.py b/python/cm/migrations/0022_auto_20190620_1251.py index fd14853a42..2ef8ba3097 100644 --- a/python/cm/migrations/0022_auto_20190620_1251.py +++ b/python/cm/migrations/0022_auto_20190620_1251.py @@ -9,9 +9,8 @@ # 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. -# Generated by Django 2.2.1 on 2019-06-20 12:51 -# pylint: disable=line-too-long +# Generated by Django 2.2.1 on 2019-06-20 12:51 import django.db.models.deletion from django.db import migrations, models diff --git a/python/cm/migrations/0023_auto_20190624_1441.py b/python/cm/migrations/0023_auto_20190624_1441.py index e898f8721e..cdf7966a74 100644 --- a/python/cm/migrations/0023_auto_20190624_1441.py +++ b/python/cm/migrations/0023_auto_20190624_1441.py @@ -9,6 +9,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. + # Generated by Django 2.2.1 on 2019-06-24 14:41 from django.db import migrations, models diff --git a/python/cm/migrations/0024_auto_20190715_1548.py b/python/cm/migrations/0024_auto_20190715_1548.py index 16fe9888b0..d0ad700830 100644 --- a/python/cm/migrations/0024_auto_20190715_1548.py +++ b/python/cm/migrations/0024_auto_20190715_1548.py @@ -9,6 +9,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. + # Generated by Django 2.2.1 on 2019-07-15 15:48 from django.db import migrations diff --git a/python/cm/migrations/0025_auto_20190719_1036.py b/python/cm/migrations/0025_auto_20190719_1036.py index e3081f844e..9311af6669 100644 --- a/python/cm/migrations/0025_auto_20190719_1036.py +++ b/python/cm/migrations/0025_auto_20190719_1036.py @@ -9,6 +9,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. + # Generated by Django 2.1.7 on 2019-07-19 10:36 from django.db import migrations, models diff --git a/python/cm/migrations/0026_auto_20190805_1442.py b/python/cm/migrations/0026_auto_20190805_1442.py index bd925a5b3b..7452f3a214 100644 --- a/python/cm/migrations/0026_auto_20190805_1442.py +++ b/python/cm/migrations/0026_auto_20190805_1442.py @@ -9,8 +9,8 @@ # 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. + # Generated by Django 2.2.4 on 2019-08-05 14:42 -# pylint: disable=line-too-long from django.db import migrations, models diff --git a/python/cm/migrations/0027_auto_20190809_1134.py b/python/cm/migrations/0027_auto_20190809_1134.py index a59b01ce07..790237ad7b 100644 --- a/python/cm/migrations/0027_auto_20190809_1134.py +++ b/python/cm/migrations/0027_auto_20190809_1134.py @@ -10,6 +10,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. + # Generated by Django 2.2.4 on 2019-08-05 14:42 from django.db import migrations, models diff --git a/python/cm/migrations/0028_auto_20190813_1005.py b/python/cm/migrations/0028_auto_20190813_1005.py index 5a1d020e92..cab69b8154 100644 --- a/python/cm/migrations/0028_auto_20190813_1005.py +++ b/python/cm/migrations/0028_auto_20190813_1005.py @@ -9,6 +9,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. + # Generated by Django 2.1.7 on 2019-08-13 10:05 from django.db import migrations, models diff --git a/python/cm/migrations/0029_auto_20190814_1306.py b/python/cm/migrations/0029_auto_20190814_1306.py index 15faa10f73..0a745a14bb 100644 --- a/python/cm/migrations/0029_auto_20190814_1306.py +++ b/python/cm/migrations/0029_auto_20190814_1306.py @@ -9,6 +9,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. + # Generated by Django 2.2.4 on 2019-08-14 13:06 from django.db import migrations, models diff --git a/python/cm/migrations/0030_auto_20190820_1600.py b/python/cm/migrations/0030_auto_20190820_1600.py index 67df5883b3..fd409f4a7b 100644 --- a/python/cm/migrations/0030_auto_20190820_1600.py +++ b/python/cm/migrations/0030_auto_20190820_1600.py @@ -9,6 +9,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. + # Generated by Django 2.2.1 on 2019-07-15 15:48 from django.db import migrations diff --git a/python/cm/migrations/0031_auto_20190926_1600.py b/python/cm/migrations/0031_auto_20190926_1600.py index bd726e5274..a9fd6ea1f8 100644 --- a/python/cm/migrations/0031_auto_20190926_1600.py +++ b/python/cm/migrations/0031_auto_20190926_1600.py @@ -9,6 +9,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. + # Generated by Django 2.2.1 on 2019-09-26 16:00 from django.db import migrations @@ -23,9 +24,7 @@ def add_group(apps, proto): group[pc.name] = True for name in group: - pc = PrototypeConfig( - prototype=proto, name=name, display_name=name, type='group', limits='{}' - ) + pc = PrototypeConfig(prototype=proto, name=name, display_name=name, type='group', limits='{}') pc.save() diff --git a/python/cm/migrations/0032_auto_20191015_1600.py b/python/cm/migrations/0032_auto_20191015_1600.py index 4580b2f0d9..301e701d7d 100644 --- a/python/cm/migrations/0032_auto_20191015_1600.py +++ b/python/cm/migrations/0032_auto_20191015_1600.py @@ -9,9 +9,8 @@ # 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. -# Generated by Django 2.2.5 on 2019-09-04 12:59 -# pylint: disable=line-too-long +# Generated by Django 2.2.5 on 2019-09-04 12:59 import django.db.models.deletion from django.db import migrations, models @@ -29,9 +28,7 @@ class Migration(migrations.Migration): fields=[ ( 'id', - models.AutoField( - auto_created=True, primary_key=True, serialize=False, verbose_name='ID' - ), + models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), ), ('name', models.CharField(max_length=160)), ('display_name', models.CharField(blank=True, max_length=160)), @@ -56,9 +53,7 @@ class Migration(migrations.Migration): fields=[ ( 'id', - models.AutoField( - auto_created=True, primary_key=True, serialize=False, verbose_name='ID' - ), + models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), ), ('name', models.CharField(max_length=160)), ('display_name', models.CharField(blank=True, max_length=160)), @@ -74,9 +69,7 @@ class Migration(migrations.Migration): ('params', models.TextField(blank=True)), ( 'action', - models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, to='cm.StageAction' - ), + models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='cm.StageAction'), ), ], ), diff --git a/python/cm/migrations/0033_auto_20191023_1459.py b/python/cm/migrations/0033_auto_20191023_1459.py index f32feb754c..39c44db085 100644 --- a/python/cm/migrations/0033_auto_20191023_1459.py +++ b/python/cm/migrations/0033_auto_20191023_1459.py @@ -9,6 +9,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. + # Generated by Django 2.2.4 on 2019-10-23 14:59 from django.db import migrations, models diff --git a/python/cm/migrations/0034_auto_20191029_1041.py b/python/cm/migrations/0034_auto_20191029_1041.py index e70b10f6e7..856af2d429 100644 --- a/python/cm/migrations/0034_auto_20191029_1041.py +++ b/python/cm/migrations/0034_auto_20191029_1041.py @@ -9,6 +9,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. + # Generated by Django 2.2.4 on 2019-10-29 10:41 from django.db import migrations, models diff --git a/python/cm/migrations/0035_auto_20191031_1600.py b/python/cm/migrations/0035_auto_20191031_1600.py index d04b63e9d9..fb89352bd4 100644 --- a/python/cm/migrations/0035_auto_20191031_1600.py +++ b/python/cm/migrations/0035_auto_20191031_1600.py @@ -9,6 +9,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. + # Generated by Django 2.2.1 on 2019-10-31 16:00 from django.db import migrations diff --git a/python/cm/migrations/0036_auto_20191111_1109.py b/python/cm/migrations/0036_auto_20191111_1109.py index 00315bf4ef..ec7f013f37 100644 --- a/python/cm/migrations/0036_auto_20191111_1109.py +++ b/python/cm/migrations/0036_auto_20191111_1109.py @@ -9,8 +9,8 @@ # 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. + # Generated by Django 2.2.4 on 2019-11-11 11:09 -# pylint: disable=line-too-long from django.db import migrations, models diff --git a/python/cm/migrations/0037_auto_20191120_1600.py b/python/cm/migrations/0037_auto_20191120_1600.py index 48031d9b39..d7fe818dc0 100644 --- a/python/cm/migrations/0037_auto_20191120_1600.py +++ b/python/cm/migrations/0037_auto_20191120_1600.py @@ -9,6 +9,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. + # Generated by Django 2.2.1 on 2019-11-20 16:00 from django.db import migrations diff --git a/python/cm/migrations/0038_auto_20191119_1200.py b/python/cm/migrations/0038_auto_20191119_1200.py index 3937c75144..476f58f7f1 100644 --- a/python/cm/migrations/0038_auto_20191119_1200.py +++ b/python/cm/migrations/0038_auto_20191119_1200.py @@ -9,6 +9,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. + # Generated by Django 2.2.4 on 2019-11-19 12:02 from django.db import migrations, models diff --git a/python/cm/migrations/0039_auto_20191203_0909.py b/python/cm/migrations/0039_auto_20191203_0909.py index e0c4262b2c..0eb18d86bd 100644 --- a/python/cm/migrations/0039_auto_20191203_0909.py +++ b/python/cm/migrations/0039_auto_20191203_0909.py @@ -1,3 +1,15 @@ +# 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. + # Generated by Django 2.2.6 on 2019-12-03 09:09 from django.db import migrations, models diff --git a/python/cm/migrations/0040_auto_20191218_1358.py b/python/cm/migrations/0040_auto_20191218_1358.py index 7c239381bc..242bf2f716 100644 --- a/python/cm/migrations/0040_auto_20191218_1358.py +++ b/python/cm/migrations/0040_auto_20191218_1358.py @@ -9,8 +9,8 @@ # 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. + # Generated by Django 2.2.6 on 2019-12-18 13:58 -# pylint: disable=line-too-long from django.db import migrations, models @@ -27,9 +27,7 @@ class Migration(migrations.Migration): fields=[ ( 'id', - models.AutoField( - auto_created=True, primary_key=True, serialize=False, verbose_name='ID' - ), + models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), ), ('job_id', models.PositiveIntegerField(default=0)), ('title', models.TextField()), diff --git a/python/cm/migrations/0041_auto_20191220_1338.py b/python/cm/migrations/0041_auto_20191220_1338.py index 8f53deb7da..b32afc9239 100644 --- a/python/cm/migrations/0041_auto_20191220_1338.py +++ b/python/cm/migrations/0041_auto_20191220_1338.py @@ -9,6 +9,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. + # Generated by Django 3.0 on 2019-12-20 13:38 from django.db import migrations, models diff --git a/python/cm/migrations/0043_auto_20200109_1600.py b/python/cm/migrations/0043_auto_20200109_1600.py index 009afaaf17..c98d16cb88 100644 --- a/python/cm/migrations/0043_auto_20200109_1600.py +++ b/python/cm/migrations/0043_auto_20200109_1600.py @@ -9,6 +9,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. + # Generated by Django 2.2.1 on 2020-01-09 16:00 from django.db import migrations diff --git a/python/cm/migrations/0044_auto_20200115_1058.py b/python/cm/migrations/0044_auto_20200115_1058.py index 4a2252b18c..0823a8b0c1 100644 --- a/python/cm/migrations/0044_auto_20200115_1058.py +++ b/python/cm/migrations/0044_auto_20200115_1058.py @@ -9,6 +9,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. + # Generated by Django 2.2.9 on 2020-01-15 10:58 from django.db import migrations, models diff --git a/python/cm/migrations/0045_auto_20200117_1253.py b/python/cm/migrations/0045_auto_20200117_1253.py index 882255e812..813a9568b7 100644 --- a/python/cm/migrations/0045_auto_20200117_1253.py +++ b/python/cm/migrations/0045_auto_20200117_1253.py @@ -9,6 +9,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. + # Generated by Django 2.2.9 on 2020-01-17 12:53 from django.db import migrations, models @@ -26,9 +27,7 @@ class Migration(migrations.Migration): fields=[ ( 'id', - models.AutoField( - auto_created=True, primary_key=True, serialize=False, verbose_name='ID' - ), + models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), ), ('date', models.DateTimeField(auto_now=True)), ], diff --git a/python/cm/migrations/0046_auto_20200120_1417.py b/python/cm/migrations/0046_auto_20200120_1417.py index dd399b9b18..0c2ad3d57c 100644 --- a/python/cm/migrations/0046_auto_20200120_1417.py +++ b/python/cm/migrations/0046_auto_20200120_1417.py @@ -9,6 +9,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. + # Generated by Django 3.0 on 2020-01-20 14:17 from django.db import migrations, models diff --git a/python/cm/migrations/0047_auto_20200210_1152.py b/python/cm/migrations/0047_auto_20200210_1152.py index b09ec32ad8..20d31fbf0b 100644 --- a/python/cm/migrations/0047_auto_20200210_1152.py +++ b/python/cm/migrations/0047_auto_20200210_1152.py @@ -9,6 +9,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. + # Generated by Django 3.0.3 on 2020-02-10 11:52 from django.db import migrations, models diff --git a/python/cm/migrations/0048_auto_20200228_1032.py b/python/cm/migrations/0048_auto_20200228_1032.py index 211b8ae815..f5747a8c47 100644 --- a/python/cm/migrations/0048_auto_20200228_1032.py +++ b/python/cm/migrations/0048_auto_20200228_1032.py @@ -9,22 +9,21 @@ # 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. + # Generated by Django 3.0.3 on 2020-02-28 10:32 -# pylint: disable=line-too-long import json import os import django.db.models.deletion +from django.conf import settings from django.db import migrations, models -from cm import config - def get_body(job, name, type_log, format_log): - file_path = os.path.join(config.LOG_DIR, f'{job.id}-{name}-{type_log}.{format_log}') + file_path = settings.LOG_DIR / f"{job.id}-{name}-{type_log}.{format_log}" if os.path.exists(file_path): - with open(file_path, 'r', encoding='utf_8') as f: + with open(file_path, "r", encoding=settings.ENCODING_UTF_8) as f: body = f.read() return body else: @@ -32,42 +31,42 @@ def get_body(job, name, type_log, format_log): def add_logs(apps, schema_editor): - LogStorage = apps.get_model('cm', 'LogStorage') - JobLog = apps.get_model('cm', 'JobLog') + LogStorage = apps.get_model("cm", "LogStorage") + JobLog = apps.get_model("cm", "JobLog") jobs = JobLog.objects.all() for job in jobs: LogStorage.objects.create( job=job, - name='ansible', - type='stdout', - format='txt', - body=get_body(job, 'ansible', 'out', 'txt'), + name="ansible", + type="stdout", + format="txt", + body=get_body(job, "ansible", "out", "txt"), ) LogStorage.objects.create( job=job, - name='ansible', - type='stderr', - format='txt', - body=get_body(job, 'ansible', 'err', 'txt'), + name="ansible", + type="stderr", + format="txt", + body=get_body(job, "ansible", "err", "txt"), ) try: log_files = job.log_files - if 'check' in log_files: + if "check" in log_files: LogStorage.objects.create( job=job, - name='ansible', - type='check', - format='json', - body=get_body(job, 'check', 'out', 'json'), + name="ansible", + type="check", + format="json", + body=get_body(job, "check", "out", "json"), ) except json.JSONDecodeError: pass def remove_logs(apps, schema_editor): - LogStorage = apps.get_model('cm', 'LogStorage') - JobLog = apps.get_model('cm', 'JobLog') + LogStorage = apps.get_model("cm", "LogStorage") + JobLog = apps.get_model("cm", "JobLog") jobs = JobLog.objects.all() for job in jobs: @@ -77,50 +76,48 @@ def remove_logs(apps, schema_editor): class Migration(migrations.Migration): dependencies = [ - ('cm', '0047_auto_20200210_1152'), + ("cm", "0047_auto_20200210_1152"), ] operations = [ migrations.AlterField( - model_name='action', - name='allow_to_terminate', + model_name="action", + name="allow_to_terminate", field=models.BooleanField(default=False), ), migrations.AlterField( - model_name='stageaction', - name='allow_to_terminate', + model_name="stageaction", + name="allow_to_terminate", field=models.BooleanField(default=False), ), migrations.CreateModel( - name='LogStorage', + name="LogStorage", fields=[ ( - 'id', - models.AutoField( - auto_created=True, primary_key=True, serialize=False, verbose_name='ID' - ), + "id", + models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID"), ), - ('name', models.TextField(default='')), - ('body', models.TextField(blank=True, null=True)), + ("name", models.TextField(default="")), + ("body", models.TextField(blank=True, null=True)), ( - 'type', + "type", models.CharField( choices=[ - ('stdout', 'stdout'), - ('stderr', 'stderr'), - ('check', 'check'), - ('custom', 'custom'), + ("stdout", "stdout"), + ("stderr", "stderr"), + ("check", "check"), + ("custom", "custom"), ], max_length=16, ), ), ( - 'format', - models.CharField(choices=[('txt', 'txt'), ('json', 'json')], max_length=16), + "format", + models.CharField(choices=[("txt", "txt"), ("json", "json")], max_length=16), ), ( - 'job', - models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='cm.JobLog'), + "job", + models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="cm.JobLog"), ), ], ), diff --git a/python/cm/migrations/0050_role.py b/python/cm/migrations/0050_role.py index e58ccbb6d8..fc58365cfd 100644 --- a/python/cm/migrations/0050_role.py +++ b/python/cm/migrations/0050_role.py @@ -9,9 +9,8 @@ # 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. -# Generated by Django 2.2.9 on 2020-03-31 08:35 -# pylint: disable=line-too-long +# Generated by Django 2.2.9 on 2020-03-31 08:35 from django.conf import settings from django.db import migrations, models @@ -31,9 +30,7 @@ class Migration(migrations.Migration): fields=[ ( 'id', - models.AutoField( - auto_created=True, primary_key=True, serialize=False, verbose_name='ID' - ), + models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), ), ('name', models.CharField(max_length=32, unique=True)), ('description', models.TextField(blank=True)), diff --git a/python/cm/migrations/0051_auto_20200403_0825.py b/python/cm/migrations/0051_auto_20200403_0825.py index 7d6bc0331e..02230353d0 100644 --- a/python/cm/migrations/0051_auto_20200403_0825.py +++ b/python/cm/migrations/0051_auto_20200403_0825.py @@ -12,8 +12,6 @@ # Generated by Django 3.0.5 on 2020-04-03 08:25 -# pylint: disable=line-too-long - import django.db.models.deletion from django.db import migrations, models @@ -30,9 +28,7 @@ class Migration(migrations.Migration): fields=[ ( 'id', - models.AutoField( - auto_created=True, primary_key=True, serialize=False, verbose_name='ID' - ), + models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), ), ('job_id', models.PositiveIntegerField(default=0)), ('title', models.TextField()), diff --git a/python/cm/migrations/0052_tasklog_attr.py b/python/cm/migrations/0052_tasklog_attr.py index f333933a6c..9dea192e26 100644 --- a/python/cm/migrations/0052_tasklog_attr.py +++ b/python/cm/migrations/0052_tasklog_attr.py @@ -9,7 +9,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. -# + # Generated by Django 3.0.5 on 2020-04-10 08:33 from django.db import migrations, models diff --git a/python/cm/migrations/0053_auto_20200415_1247.py b/python/cm/migrations/0053_auto_20200415_1247.py index c45968a464..54e1721292 100644 --- a/python/cm/migrations/0053_auto_20200415_1247.py +++ b/python/cm/migrations/0053_auto_20200415_1247.py @@ -9,8 +9,8 @@ # 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. + # Generated by Django 3.0.5 on 2020-04-15 12:47 -# pylint: disable=line-too-long from django.db import migrations, models diff --git a/python/cm/migrations/0055_auto_20200424_1241.py b/python/cm/migrations/0055_auto_20200424_1241.py index 5144a8dc53..4949837d18 100644 --- a/python/cm/migrations/0055_auto_20200424_1241.py +++ b/python/cm/migrations/0055_auto_20200424_1241.py @@ -9,7 +9,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. -# + # Generated by Django 3.0.5 on 2020-04-24 12:41 from django.db import migrations, models diff --git a/python/cm/migrations/0056_auto_20200714_0741.py b/python/cm/migrations/0056_auto_20200714_0741.py index 2c25c13227..00a83593be 100644 --- a/python/cm/migrations/0056_auto_20200714_0741.py +++ b/python/cm/migrations/0056_auto_20200714_0741.py @@ -9,7 +9,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. -# + # Generated by Django 3.0.7 on 2020-07-14 07:41 from django.db import migrations, models diff --git a/python/cm/migrations/0057_auto_20200831_1055.py b/python/cm/migrations/0057_auto_20200831_1055.py index 60ffbb9509..7dba0015f3 100644 --- a/python/cm/migrations/0057_auto_20200831_1055.py +++ b/python/cm/migrations/0057_auto_20200831_1055.py @@ -9,7 +9,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. -# + # Generated by Django 3.1 on 2020-08-31 10:55 from django.db import migrations, models diff --git a/python/cm/migrations/0058_encrypt_passwords.py b/python/cm/migrations/0058_encrypt_passwords.py index d24c9e7d70..c4b08f6bfc 100644 --- a/python/cm/migrations/0058_encrypt_passwords.py +++ b/python/cm/migrations/0058_encrypt_passwords.py @@ -21,9 +21,7 @@ def get_prototype_config(proto, PrototypeConfig): spec = {} flist = ('default', 'required', 'type', 'limits') - for c in PrototypeConfig.objects.filter(prototype=proto, action=None, type='group').order_by( - 'id' - ): + for c in PrototypeConfig.objects.filter(prototype=proto, action=None, type='group').order_by('id'): spec[c.name] = {} for c in PrototypeConfig.objects.filter(prototype=proto, action=None).order_by('id'): diff --git a/python/cm/migrations/0059_auto_20200904_0910.py b/python/cm/migrations/0059_auto_20200904_0910.py index a67867c2ea..2ca4f851ce 100644 --- a/python/cm/migrations/0059_auto_20200904_0910.py +++ b/python/cm/migrations/0059_auto_20200904_0910.py @@ -9,7 +9,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. -# + # Generated by Django 3.0.5 on 2020-09-04 09:10 from django.db import migrations, models diff --git a/python/cm/migrations/0060_auto_20201201_1122.py b/python/cm/migrations/0060_auto_20201201_1122.py index 38ffcce1d1..f5ca4a17fb 100644 --- a/python/cm/migrations/0060_auto_20201201_1122.py +++ b/python/cm/migrations/0060_auto_20201201_1122.py @@ -9,9 +9,8 @@ # 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. -# + # Generated by Django 3.1.2 on 2020-12-01 11:22 -# pylint: disable=line-too-long import django.db.models.deletion from django.db import migrations, models @@ -100,9 +99,7 @@ class Migration(migrations.Migration): migrations.AddField( model_name='servicecomponent', name='config', - field=models.OneToOneField( - null=True, on_delete=django.db.models.deletion.CASCADE, to='cm.objectconfig' - ), + field=models.OneToOneField(null=True, on_delete=django.db.models.deletion.CASCADE, to='cm.objectconfig'), ), migrations.AddField( model_name='servicecomponent', diff --git a/python/cm/migrations/0061_auto_20201208_1250.py b/python/cm/migrations/0061_auto_20201208_1250.py index f623ca56cc..2771420bb6 100644 --- a/python/cm/migrations/0061_auto_20201208_1250.py +++ b/python/cm/migrations/0061_auto_20201208_1250.py @@ -9,7 +9,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. -# + # Generated by Django 3.1.2 on 2020-12-08 12:50 from django.db import migrations diff --git a/python/cm/migrations/0062_auto_20201225_0949.py b/python/cm/migrations/0062_auto_20201225_0949.py index 2726369b69..54af0c02db 100644 --- a/python/cm/migrations/0062_auto_20201225_0949.py +++ b/python/cm/migrations/0062_auto_20201225_0949.py @@ -9,7 +9,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. -# + # Generated by Django 3.1.2 on 2020-12-25 09:49 from django.db import migrations, models diff --git a/python/cm/migrations/0063_tasklog_verbose.py b/python/cm/migrations/0063_tasklog_verbose.py index 2c1af41405..09fd588181 100644 --- a/python/cm/migrations/0063_tasklog_verbose.py +++ b/python/cm/migrations/0063_tasklog_verbose.py @@ -9,7 +9,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. -# + # Generated by Django 3.1.2 on 2021-01-12 15:28 from django.db import migrations, models diff --git a/python/cm/migrations/0064_auto_20210210_1532.py b/python/cm/migrations/0064_auto_20210210_1532.py index 912ef66cbc..dc84abe57d 100644 --- a/python/cm/migrations/0064_auto_20210210_1532.py +++ b/python/cm/migrations/0064_auto_20210210_1532.py @@ -9,8 +9,8 @@ # 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. + # Generated by Django 3.1.1 on 2021-02-10 15:32 -# pylint: disable=line-too-long from django.db import migrations, models diff --git a/python/cm/migrations/0065_auto_20210220_0902.py b/python/cm/migrations/0065_auto_20210220_0902.py index 0a1a7bb357..8c7c1767ba 100644 --- a/python/cm/migrations/0065_auto_20210220_0902.py +++ b/python/cm/migrations/0065_auto_20210220_0902.py @@ -9,6 +9,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. + # Generated by Django 3.1.1 on 2021-02-20 09:02 from django.db import migrations, models diff --git a/python/cm/migrations/0066_auto_20210427_0853.py b/python/cm/migrations/0066_auto_20210427_0853.py index b62660d195..43de456d41 100644 --- a/python/cm/migrations/0066_auto_20210427_0853.py +++ b/python/cm/migrations/0066_auto_20210427_0853.py @@ -9,8 +9,8 @@ # 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. + # Generated by Django 3.1.2 on 2021-04-27 08:53 -# pylint: disable=line-too-long import django.db.models.deletion from django.db import migrations, models diff --git a/python/cm/migrations/0067_tasklog_object_type.py b/python/cm/migrations/0067_tasklog_object_type.py index bae6efe6c6..771422c166 100644 --- a/python/cm/migrations/0067_tasklog_object_type.py +++ b/python/cm/migrations/0067_tasklog_object_type.py @@ -9,6 +9,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. + # Generated by Django 3.1.2 on 2021-06-04 15:37 import django.db.models.deletion diff --git a/python/cm/migrations/0068_alter_tasklog_action.py b/python/cm/migrations/0068_alter_tasklog_action.py index b24b065fdd..040393127a 100644 --- a/python/cm/migrations/0068_alter_tasklog_action.py +++ b/python/cm/migrations/0068_alter_tasklog_action.py @@ -9,8 +9,8 @@ # 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. + # Generated by Django 3.2.4 on 2021-08-05 14:31 -# pylint: disable=line-too-long import django.db.models.deletion from django.db import migrations, models diff --git a/python/cm/migrations/0069_auto_20210607_1125.py b/python/cm/migrations/0069_auto_20210607_1125.py index 8fb8c6a622..636fd20be0 100644 --- a/python/cm/migrations/0069_auto_20210607_1125.py +++ b/python/cm/migrations/0069_auto_20210607_1125.py @@ -9,6 +9,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. + # Generated by Django 3.2 on 2021-06-07 11:25 import django.db.models.deletion @@ -28,9 +29,7 @@ class Migration(migrations.Migration): fields=[ ( 'id', - models.AutoField( - auto_created=True, primary_key=True, serialize=False, verbose_name='ID' - ), + models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), ), ('object_id', models.PositiveIntegerField()), ('name', models.CharField(max_length=30)), @@ -57,9 +56,7 @@ class Migration(migrations.Migration): migrations.AddField( model_name='groupconfig', name='object_type', - field=models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, to='contenttypes.contenttype' - ), + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.contenttype'), ), migrations.AlterUniqueTogether( name='groupconfig', diff --git a/python/cm/migrations/0070_lock_refactoring.py b/python/cm/migrations/0070_lock_refactoring.py index ac6233e082..2f7a8c5a32 100644 --- a/python/cm/migrations/0070_lock_refactoring.py +++ b/python/cm/migrations/0070_lock_refactoring.py @@ -9,6 +9,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. + # Generated by Django 3.2 on 2021-06-22 09:44 import django.db.models.deletion @@ -27,9 +28,7 @@ class Migration(migrations.Migration): fields=[ ( 'id', - models.AutoField( - auto_created=True, primary_key=True, serialize=False, verbose_name='ID' - ), + models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), ), ('name', models.CharField(max_length=160, null=True)), ('reason', models.JSONField(default=dict)), @@ -65,44 +64,32 @@ class Migration(migrations.Migration): migrations.AddField( model_name='adcm', name='concerns', - field=models.ManyToManyField( - blank=True, related_name='adcm_entities', to='cm.ConcernItem' - ), + field=models.ManyToManyField(blank=True, related_name='adcm_entities', to='cm.ConcernItem'), ), migrations.AddField( model_name='cluster', name='concerns', - field=models.ManyToManyField( - blank=True, related_name='cluster_entities', to='cm.ConcernItem' - ), + field=models.ManyToManyField(blank=True, related_name='cluster_entities', to='cm.ConcernItem'), ), migrations.AddField( model_name='clusterobject', name='concerns', - field=models.ManyToManyField( - blank=True, related_name='clusterobject_entities', to='cm.ConcernItem' - ), + field=models.ManyToManyField(blank=True, related_name='clusterobject_entities', to='cm.ConcernItem'), ), migrations.AddField( model_name='host', name='concerns', - field=models.ManyToManyField( - blank=True, related_name='host_entities', to='cm.ConcernItem' - ), + field=models.ManyToManyField(blank=True, related_name='host_entities', to='cm.ConcernItem'), ), migrations.AddField( model_name='hostprovider', name='concerns', - field=models.ManyToManyField( - blank=True, related_name='hostprovider_entities', to='cm.ConcernItem' - ), + field=models.ManyToManyField(blank=True, related_name='hostprovider_entities', to='cm.ConcernItem'), ), migrations.AddField( model_name='servicecomponent', name='concerns', - field=models.ManyToManyField( - blank=True, related_name='servicecomponent_entities', to='cm.ConcernItem' - ), + field=models.ManyToManyField(blank=True, related_name='servicecomponent_entities', to='cm.ConcernItem'), ), migrations.AddField( model_name='tasklog', diff --git a/python/cm/migrations/0071_messagetemplate.py b/python/cm/migrations/0071_messagetemplate.py index 10684ec8fc..d973982ba9 100644 --- a/python/cm/migrations/0071_messagetemplate.py +++ b/python/cm/migrations/0071_messagetemplate.py @@ -1,3 +1,15 @@ +# 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. + # Generated by Django 3.2 on 2021-07-22 07:18 from django.db import migrations, models @@ -33,9 +45,7 @@ class Migration(migrations.Migration): fields=[ ( 'id', - models.AutoField( - auto_created=True, primary_key=True, serialize=False, verbose_name='ID' - ), + models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), ), ('name', models.CharField(max_length=160, unique=True)), ('template', models.JSONField()), diff --git a/python/cm/migrations/0072_multi_state.py b/python/cm/migrations/0072_multi_state.py index f3b717a905..17f35b878a 100644 --- a/python/cm/migrations/0072_multi_state.py +++ b/python/cm/migrations/0072_multi_state.py @@ -1,3 +1,15 @@ +# 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. + # Generated by Django 3.2 on 2021-08-12 07:46 from django.db import migrations, models diff --git a/python/cm/migrations/0073_issues_refactoring.py b/python/cm/migrations/0073_issues_refactoring.py index a36b77baeb..9e63a4ce96 100644 --- a/python/cm/migrations/0073_issues_refactoring.py +++ b/python/cm/migrations/0073_issues_refactoring.py @@ -9,6 +9,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. + # Generated by Django 3.2 on 2021-08-17 08:02 from django.db import migrations, models diff --git a/python/cm/migrations/0075_actions_multistate.py b/python/cm/migrations/0075_actions_multistate.py index e59d4a2579..8c9b4f0618 100644 --- a/python/cm/migrations/0075_actions_multistate.py +++ b/python/cm/migrations/0075_actions_multistate.py @@ -1,3 +1,15 @@ +# 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. + # Generated by Django 3.2 on 2021-09-14 09:27 from django.db import migrations, models diff --git a/python/cm/migrations/0076_auto_20211013_1250.py b/python/cm/migrations/0076_auto_20211013_1250.py index 66e88c2a3c..9057ea5eab 100644 --- a/python/cm/migrations/0076_auto_20211013_1250.py +++ b/python/cm/migrations/0076_auto_20211013_1250.py @@ -1,3 +1,15 @@ +# 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. + # Generated by Django 3.2 on 2021-10-13 12:50 from django.db import migrations, models diff --git a/python/cm/migrations/0078_alter_groupconfig_name.py b/python/cm/migrations/0078_alter_groupconfig_name.py index 939a37c18f..660ca44df3 100644 --- a/python/cm/migrations/0078_alter_groupconfig_name.py +++ b/python/cm/migrations/0078_alter_groupconfig_name.py @@ -34,9 +34,7 @@ class Migration(migrations.Migration): migrations.AlterField( model_name='groupconfig', name='name', - field=models.CharField( - max_length=30, validators=[cm.models.validate_line_break_character] - ), + field=models.CharField(max_length=30, validators=[cm.models.validate_line_break_character]), ), migrations.RunPython(remove_line_break_character), ] diff --git a/python/cm/migrations/0082_remove_role.py b/python/cm/migrations/0082_remove_role.py index eea5e39ffc..bf7d1852c0 100644 --- a/python/cm/migrations/0082_remove_role.py +++ b/python/cm/migrations/0082_remove_role.py @@ -9,6 +9,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. + # Generated by Django 3.2.7 on 2021-09-29 09:24 from django.db import migrations diff --git a/python/cm/migrations/0083_add_product_category.py b/python/cm/migrations/0083_add_product_category.py index 593d1f6e05..0580760007 100644 --- a/python/cm/migrations/0083_add_product_category.py +++ b/python/cm/migrations/0083_add_product_category.py @@ -22,9 +22,7 @@ def fill_category(apps, schema_editor): ProductCategory = apps.get_model('cm', 'ProductCategory') Prototype = apps.get_model('cm', 'Prototype') for bundle in Bundle.objects.all(): - prototype = Prototype.objects.filter( - bundle=bundle, name=bundle.name, type='cluster' - ).first() + prototype = Prototype.objects.filter(bundle=bundle, name=bundle.name, type='cluster').first() if prototype: # skip non-product bundles value = prototype.display_name or bundle.name category, _ = ProductCategory.objects.get_or_create(value=value) @@ -44,9 +42,7 @@ class Migration(migrations.Migration): fields=[ ( 'id', - models.AutoField( - auto_created=True, primary_key=True, serialize=False, verbose_name='ID' - ), + models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), ), ('value', models.CharField(max_length=160, unique=True)), ('visible', models.BooleanField(default=True)), @@ -58,9 +54,7 @@ class Migration(migrations.Migration): migrations.AddField( model_name='bundle', name='category', - field=models.ForeignKey( - null=True, on_delete=django.db.models.deletion.RESTRICT, to='cm.productcategory' - ), + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.RESTRICT, to='cm.productcategory'), ), migrations.RunPython(fill_category), ] diff --git a/python/cm/migrations/0084_ansible_venv.py b/python/cm/migrations/0084_ansible_venv.py index 0f9b7830bd..6f0e12bef9 100644 --- a/python/cm/migrations/0084_ansible_venv.py +++ b/python/cm/migrations/0084_ansible_venv.py @@ -1,3 +1,15 @@ +# 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. + # Generated by Django 3.2.11 on 2022-01-30 11:37 from django.db import migrations, models diff --git a/python/cm/migrations/0085_add_action_to_upgrade.py b/python/cm/migrations/0085_add_action_to_upgrade.py index ec580e06c1..939c7b4a23 100644 --- a/python/cm/migrations/0085_add_action_to_upgrade.py +++ b/python/cm/migrations/0085_add_action_to_upgrade.py @@ -1,3 +1,15 @@ +# 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. + # Generated by Django 3.2.4 on 2022-03-23 11:06 import django.db.models.deletion @@ -14,15 +26,11 @@ class Migration(migrations.Migration): migrations.AddField( model_name='stageupgrade', name='action', - field=models.OneToOneField( - null=True, on_delete=django.db.models.deletion.CASCADE, to='cm.stageaction' - ), + field=models.OneToOneField(null=True, on_delete=django.db.models.deletion.CASCADE, to='cm.stageaction'), ), migrations.AddField( model_name='upgrade', name='action', - field=models.OneToOneField( - null=True, on_delete=django.db.models.deletion.CASCADE, to='cm.action' - ), + field=models.OneToOneField(null=True, on_delete=django.db.models.deletion.CASCADE, to='cm.action'), ), ] diff --git a/python/cm/migrations/0086_set_mitogen_to_false.py b/python/cm/migrations/0086_set_mitogen_to_false.py index 5598e974a0..a212808763 100644 --- a/python/cm/migrations/0086_set_mitogen_to_false.py +++ b/python/cm/migrations/0086_set_mitogen_to_false.py @@ -24,9 +24,7 @@ def update_adcm_config_settings(apps, schema_editor): if 'ansible_settings' in config and 'mitogen' in config['ansible_settings']: config['ansible_settings']['mitogen'] = False obj_conf = adcm.config - config_log = ConfigLog( - obj_ref=obj_conf, config=config, attr=cl.attr, description=cl.description - ) + config_log = ConfigLog(obj_ref=obj_conf, config=config, attr=cl.attr, description=cl.description) config_log.save() obj_conf.previous = obj_conf.current obj_conf.current = config_log.id diff --git a/python/cm/migrations/0088_fix_group_keys_structure.py b/python/cm/migrations/0088_fix_group_keys_structure.py index 579023816f..810a177925 100644 --- a/python/cm/migrations/0088_fix_group_keys_structure.py +++ b/python/cm/migrations/0088_fix_group_keys_structure.py @@ -64,9 +64,7 @@ def get_config_spec(apps, group): obj = HostProvider.objects.get(id=group.object_id) else: raise models.ObjectDoesNotExist - for field in PrototypeConfig.objects.filter( - prototype=obj.prototype, action__isnull=True - ).order_by('id'): + for field in PrototypeConfig.objects.filter(prototype=obj.prototype, action__isnull=True).order_by('id'): group_customization = field.group_customization if group_customization is None: group_customization = obj.prototype.config_group_customization diff --git a/python/cm/migrations/0091_migrate_adcm_logrotate_config.py b/python/cm/migrations/0091_migrate_adcm_logrotate_config.py index f72b482580..673b30ccee 100644 --- a/python/cm/migrations/0091_migrate_adcm_logrotate_config.py +++ b/python/cm/migrations/0091_migrate_adcm_logrotate_config.py @@ -26,9 +26,7 @@ def migrate_logrotate_config(apps, schema_editor): # run on a clean database, no migration required return - adcm_configlog = ConfigLog.objects.get( - obj_ref=adcm_object.config, id=adcm_object.config.current - ) + adcm_configlog = ConfigLog.objects.get(obj_ref=adcm_object.config, id=adcm_object.config.current) # pylint: disable=simplifiable-if-statement if adcm_configlog.config.get('logrotate', {}).get('nginx_server', False): diff --git a/python/cm/migrations/0093_auto_20220928_0556.py b/python/cm/migrations/0093_auto_20220928_0556.py index 29028803e0..c73df53abc 100644 --- a/python/cm/migrations/0093_auto_20220928_0556.py +++ b/python/cm/migrations/0093_auto_20220928_0556.py @@ -1,3 +1,15 @@ +# 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. + # Generated by Django 3.2.15 on 2022-09-28 05:56 from django.db import migrations, models diff --git a/python/cm/migrations/0094_increase_max_length.py b/python/cm/migrations/0094_increase_max_length.py index 28f96b4e85..a6527d0701 100644 --- a/python/cm/migrations/0094_increase_max_length.py +++ b/python/cm/migrations/0094_increase_max_length.py @@ -9,6 +9,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. + # Generated by Django 3.2.13 on 2022-10-04 08:25 from django.db import migrations, models diff --git a/python/cm/migrations/0095_JobStatus.py b/python/cm/migrations/0095_JobStatus.py new file mode 100755 index 0000000000..e2b499d7b7 --- /dev/null +++ b/python/cm/migrations/0095_JobStatus.py @@ -0,0 +1,55 @@ +# 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. + +# Generated by Django 3.2.15 on 2022-10-31 09:13 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("cm", "0094_increase_max_length"), + ] + + operations = [ + migrations.AlterField( + model_name="joblog", + name="status", + field=models.CharField( + choices=[ + ("created", "created"), + ("success", "success"), + ("failed", "failed"), + ("running", "running"), + ("locked", "locked"), + ("aborted", "aborted"), + ], + max_length=16, + ), + ), + migrations.AlterField( + model_name="tasklog", + name="status", + field=models.CharField( + choices=[ + ("created", "created"), + ("success", "success"), + ("failed", "failed"), + ("running", "running"), + ("locked", "locked"), + ("aborted", "aborted"), + ], + max_length=16, + ), + ), + ] diff --git a/python/cm/migrations/0096_auto_20221101_1010.py b/python/cm/migrations/0096_auto_20221101_1010.py new file mode 100644 index 0000000000..38cc110cc2 --- /dev/null +++ b/python/cm/migrations/0096_auto_20221101_1010.py @@ -0,0 +1,81 @@ +# 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. + +# Generated by Django 3.2.15 on 2022-11-01 10:10 + +from django.db import migrations, models + + +def migrate_host_maintenance_mode(apps, schema_editor): + host_model = apps.get_model("cm", "Host") + + host_model.objects.filter(maintenance_mode="on").update(maintenance_mode="ON") + host_model.objects.filter(models.Q(maintenance_mode="off") | models.Q(maintenance_mode="disabled")).update( + maintenance_mode="OFF" + ) + + +def migrate_host_maintenance_mode_reverse(apps, schema_editor): + host_model = apps.get_model("cm", "Host") + + host_model.objects.filter( + models.Q(cluster__isnull=True) | models.Q(cluster__prototype__allow_maintenance_mode=False) + ).update(maintenance_mode="disabled") + + host_model.objects.filter(maintenance_mode="ON").update(maintenance_mode="on") + host_model.objects.filter(maintenance_mode="OFF").update(maintenance_mode="off") + + +class Migration(migrations.Migration): + + dependencies = [ + ('cm', '0095_JobStatus'), + ] + + operations = [ + migrations.RemoveField( + model_name='action', + name='button', + ), + migrations.RemoveField( + model_name='stageaction', + name='button', + ), + migrations.AddField( + model_name='clusterobject', + name='_maintenance_mode', + field=models.CharField( + choices=[('ON', 'ON'), ('OFF', 'OFF'), ('CHANGING', 'CHANGING')], + default='OFF', + max_length=64, + ), + ), + migrations.AddField( + model_name='servicecomponent', + name='_maintenance_mode', + field=models.CharField( + choices=[('ON', 'ON'), ('OFF', 'OFF'), ('CHANGING', 'CHANGING')], + default='OFF', + max_length=64, + ), + ), + migrations.AlterField( + model_name='host', + name='maintenance_mode', + field=models.CharField( + choices=[('ON', 'ON'), ('OFF', 'OFF'), ('CHANGING', 'CHANGING')], + default='OFF', + max_length=64, + ), + ), + migrations.RunPython(code=migrate_host_maintenance_mode, reverse_code=migrate_host_maintenance_mode_reverse), + ] diff --git a/python/cm/migrations/0097_auto_20221109_1238.py b/python/cm/migrations/0097_auto_20221109_1238.py new file mode 100644 index 0000000000..0dfad3d756 --- /dev/null +++ b/python/cm/migrations/0097_auto_20221109_1238.py @@ -0,0 +1,84 @@ +# 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. + +# Generated by Django 3.2.15 on 2022-11-09 12:38 + +from django.db import migrations, models + + +def migrate_bundle_license(apps, schema_editor): + bundle_model = apps.get_model("cm", "Bundle") + proto_model = apps.get_model("cm", "Prototype") + bundles = bundle_model.objects.filter(license__in=["accepted", "unaccepted"]) + for bundle in bundles: + proto_model.objects.filter(bundle=bundle, name=bundle.name).update( + license=bundle.license, license_hash=bundle.license_hash, license_path=bundle.license_path + ) + + +def migrate_bundle_license_reverse(apps, schema_editor): + proto_model = apps.get_model("cm", "Prototype") + + prototypes = proto_model.objects.filter(license__in=["accepted", "unaccepted"], type__in=["cluster", "provider"]) + for proto in prototypes: + proto.bundle.update(license=proto.license, license_hash=proto.license_hash, license_path=proto.license_path) + + +class Migration(migrations.Migration): + + dependencies = [ + ('cm', '0096_auto_20221101_1010'), + ] + + operations = [ + migrations.AddField( + model_name='prototype', + name='license', + field=models.CharField( + choices=[('absent', 'absent'), ('accepted', 'accepted'), ('unaccepted', 'unaccepted')], + default='absent', + max_length=16, + ), + ), + migrations.AddField( + model_name='prototype', + name='license_hash', + field=models.CharField(default=None, max_length=64, null=True), + ), + migrations.AddField( + model_name='prototype', + name='license_path', + field=models.CharField(default=None, max_length=160, null=True), + ), + migrations.AddField( + model_name='stageprototype', + name='license', + field=models.CharField( + choices=[('absent', 'absent'), ('accepted', 'accepted'), ('unaccepted', 'unaccepted')], + default='absent', + max_length=16, + ), + ), + migrations.RunPython(code=migrate_bundle_license, reverse_code=migrate_bundle_license_reverse), + migrations.RemoveField( + model_name='bundle', + name='license', + ), + migrations.RemoveField( + model_name='bundle', + name='license_hash', + ), + migrations.RemoveField( + model_name='bundle', + name='license_path', + ), + ] diff --git a/python/cm/migrations/0098_auto_20221115_1255.py b/python/cm/migrations/0098_auto_20221115_1255.py new file mode 100644 index 0000000000..77961b8f12 --- /dev/null +++ b/python/cm/migrations/0098_auto_20221115_1255.py @@ -0,0 +1,77 @@ +# 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. + +# Generated by Django 3.2.15 on 2022-11-15 12:55 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('cm', '0097_auto_20221109_1238'), + ] + + operations = [ + migrations.AlterField( + model_name='prototypeconfig', + name='type', + field=models.CharField( + choices=[ + ('string', 'string'), + ('text', 'text'), + ('password', 'password'), + ('secrettext', 'secrettext'), + ('json', 'json'), + ('integer', 'integer'), + ('float', 'float'), + ('option', 'option'), + ('variant', 'variant'), + ('boolean', 'boolean'), + ('file', 'file'), + ('secretfile', 'secretfile'), + ('list', 'list'), + ('map', 'map'), + ('secretmap', 'secretmap'), + ('structure', 'structure'), + ('group', 'group'), + ], + max_length=16, + ), + ), + migrations.AlterField( + model_name='stageprototypeconfig', + name='type', + field=models.CharField( + choices=[ + ('string', 'string'), + ('text', 'text'), + ('password', 'password'), + ('secrettext', 'secrettext'), + ('json', 'json'), + ('integer', 'integer'), + ('float', 'float'), + ('option', 'option'), + ('variant', 'variant'), + ('boolean', 'boolean'), + ('file', 'file'), + ('secretfile', 'secretfile'), + ('list', 'list'), + ('map', 'map'), + ('secretmap', 'secretmap'), + ('structure', 'structure'), + ('group', 'group'), + ], + max_length=16, + ), + ), + ] diff --git a/python/cm/models.py b/python/cm/models.py index 4da305f1d9..4ebaf43078 100644 --- a/python/cm/models.py +++ b/python/cm/models.py @@ -25,6 +25,7 @@ from itertools import chain from typing import Dict, Iterable, List, Optional +from django.conf import settings from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation from django.contrib.contenttypes.models import ContentType from django.core.exceptions import ObjectDoesNotExist, ValidationError @@ -33,7 +34,6 @@ from django.dispatch import receiver from django.utils import timezone -from cm.config import FILE_DIR, Job from cm.errors import AdcmEx from cm.logger import logger @@ -53,6 +53,12 @@ class ObjectType(models.TextChoices): Host = "host", "host" +class MaintenanceMode(models.TextChoices): + ON = "ON", "ON" + OFF = "OFF", "OFF" + CHANGING = "CHANGING", "CHANGING" + + LICENSE_STATE = ( ("absent", "absent"), ("accepted", "accepted"), @@ -60,7 +66,7 @@ class ObjectType(models.TextChoices): ) -def get_model_by_type(object_type): +def get_model_by_type(object_type): # pylint: disable=too-many-return-statements if object_type == "adcm": return ADCM if object_type == "cluster": @@ -97,14 +103,14 @@ class ADCMManager(models.Manager): """ Custom model manager catch ObjectDoesNotExist error and re-raise it as custom AdcmEx exception. AdcmEx is derived from DRF APIException, so it handled gracefully - by DRF and is reported out as nicely formated error instead of ugly exception. + by DRF and is reported out as nicely formatted error instead of ugly exception. - Using ADCMManager can shorten you code significaly. Insted of + Using ADCMManager can shorten you code significantly. Instead of try: cluster = Cluster.objects.get(id=id) except Cluster.DoesNotExist: - raise AdcmEx(f'Cluster {id} is not found') + raise AdcmEx(Cluster {id} is not found) You can just write @@ -112,8 +118,8 @@ class ADCMManager(models.Manager): and DRF magic do the rest. - Please pay attention, to use ADCMManager you need reffer to "obj" model attribute, - not "objects". "objects" attribute is reffered to standard Django model manager, + Please pay attention, to use ADCMManager you need refer to "obj" model attribute, + not "objects". "objects" attribute is referred to standard Django model manager, so if you need familiar behavior you can use it as usual. """ @@ -143,8 +149,7 @@ def from_db(cls, db, field_names, values): if len(values) != len(cls._meta.concrete_fields): values_iter = iter(values) values = [ - next(values_iter) if f.attname in field_names else models.DEFERRED - for f in cls._meta.concrete_fields + next(values_iter) if f.attname in field_names else models.DEFERRED for f in cls._meta.concrete_fields ] instance = cls(*values) instance._state.adding = False @@ -174,9 +179,6 @@ class Bundle(ADCMModel): version = models.CharField(max_length=80) version_order = models.PositiveIntegerField(default=0) edition = models.CharField(max_length=80, default="community") - license = models.CharField(max_length=16, choices=LICENSE_STATE, default="absent") - license_path = models.CharField(max_length=160, default=None, null=True) - license_hash = models.CharField(max_length=64, default=None, null=True) hash = models.CharField(max_length=64) description = models.TextField(blank=True) date = models.DateTimeField(auto_now=True) @@ -190,7 +192,7 @@ class Meta: class ProductCategory(ADCMModel): """ - Categories are used for some models categorization. + Categories are used for some model's categorization. It's same as Bundle.name but unlinked from it due to simplicity reasons. """ @@ -201,9 +203,7 @@ class ProductCategory(ADCMModel): def re_collect(cls) -> None: """Re-sync category list with installed bundles""" for bundle in Bundle.objects.filter(category=None).all(): - prototype = Prototype.objects.filter( - bundle=bundle, name=bundle.name, type=ObjectType.Cluster - ).first() + prototype = Prototype.objects.filter(bundle=bundle, name=bundle.name, type=ObjectType.Cluster).first() if prototype: value = prototype.display_name or bundle.name bundle.category, _ = cls.objects.get_or_create(value=value) @@ -222,6 +222,12 @@ def get_default_from_edition(): ("passive", "passive"), ) +NO_LDAP_SETTINGS = "The Action is not available. You need to fill in the LDAP integration settings." +SERVICE_IN_MM = "The Action is not available. Service in 'Maintenance mode'" +COMPONENT_IN_MM = "The Action is not available. Component in 'Maintenance mode'" +HOST_IN_MM = "The Action is not available. Host in 'Maintenance mode'" +MANY_HOSTS_IN_MM = "The Action is not available. One or more hosts in 'Maintenance mode'" + def get_default_constraint(): return [0, "+"] @@ -233,6 +239,9 @@ class Prototype(ADCMModel): parent = models.ForeignKey("self", on_delete=models.CASCADE, null=True, default=None) path = models.CharField(max_length=160, default="") name = models.CharField(max_length=256) + license = models.CharField(max_length=16, choices=LICENSE_STATE, default="absent") + license_path = models.CharField(max_length=160, default=None, null=True) + license_hash = models.CharField(max_length=64, default=None, null=True) display_name = models.CharField(max_length=256, blank=True) version = models.CharField(max_length=80) version_order = models.PositiveIntegerField(default=0) @@ -539,7 +548,9 @@ def allowed(self, obj: ADCMEntity) -> bool: else: return False else: - return self.action.allowed(obj) + if hasattr(self, "action"): + return self.action.allowed(obj) + return False class ADCM(ADCMEntity): @@ -586,7 +597,7 @@ def edition(self): @property def license(self): - return self.prototype.bundle.license + return self.prototype.license @property def display_name(self): @@ -625,7 +636,7 @@ def edition(self): @property def license(self): - return self.prototype.bundle.license + return self.prototype.license @property def display_name(self): @@ -641,21 +652,15 @@ def serialized_issue(self): return result if result["issue"] else {} -class MaintenanceModeType(models.TextChoices): - Disabled = "disabled", "disabled" - On = "on", "on" - Off = "off", "off" - - class Host(ADCMEntity): fqdn = models.CharField(max_length=253, unique=True) description = models.TextField(blank=True) provider = models.ForeignKey(HostProvider, on_delete=models.CASCADE, null=True, default=None) cluster = models.ForeignKey(Cluster, on_delete=models.SET_NULL, null=True, default=None) maintenance_mode = models.CharField( - max_length=16, - choices=MaintenanceModeType.choices, - default=MaintenanceModeType.Disabled.value, + max_length=64, + choices=MaintenanceMode.choices, + default=MaintenanceMode.OFF, ) __error_code__ = "HOST_NOT_FOUND" @@ -684,6 +689,18 @@ def serialized_issue(self): result["issue"]["provider"] = provider_issue return result if result["issue"] else {} + @property + def is_maintenance_mode_available(self) -> bool: + cluster: Cluster | None = self.cluster + if not cluster: + return False + + return cluster.prototype.allow_maintenance_mode + + @property + def maintenance_mode_attr(self) -> MaintenanceMode.choices: + return self.maintenance_mode + class ClusterObject(ADCMEntity): cluster = models.ForeignKey(Cluster, on_delete=models.CASCADE) @@ -694,6 +711,11 @@ class ClusterObject(ADCMEntity): content_type_field="object_type", on_delete=models.CASCADE, ) + _maintenance_mode = models.CharField( + max_length=64, + choices=MaintenanceMode.choices, + default=MaintenanceMode.OFF, + ) __error_code__ = "CLUSTER_SERVICE_NOT_FOUND" @@ -730,6 +752,49 @@ def serialized_issue(self): } return result if result["issue"] else {} + @property + def maintenance_mode_attr(self) -> MaintenanceMode.choices: + return self._maintenance_mode + + @property + def maintenance_mode(self) -> MaintenanceMode.choices: + if self._maintenance_mode != MaintenanceMode.OFF: + return self._maintenance_mode + + service_components = ServiceComponent.objects.filter(service=self) + if service_components: + if all( + service_component.maintenance_mode_attr == MaintenanceMode.ON + for service_component in service_components + ): + return MaintenanceMode.ON + + hosts_maintenance_modes = [] + for service_component in service_components: + host_ids = HostComponent.objects.filter( + component=service_component, + ).values_list("host_id", flat=True) + hosts_maintenance_modes.extend(Host.objects.get(pk=host_id).maintenance_mode for host_id in host_ids) + + if hosts_maintenance_modes: + return ( + MaintenanceMode.ON + if all( + host_maintenance_mode == MaintenanceMode.ON for host_maintenance_mode in hosts_maintenance_modes + ) + else MaintenanceMode.OFF + ) + + return self._maintenance_mode + + @maintenance_mode.setter + def maintenance_mode(self, value: MaintenanceMode.choices) -> None: + self._maintenance_mode = value + + @property + def is_maintenance_mode_available(self) -> bool: + return self.cluster.prototype.allow_maintenance_mode + class Meta: unique_together = (("cluster", "prototype"),) @@ -744,6 +809,11 @@ class ServiceComponent(ADCMEntity): content_type_field="object_type", on_delete=models.CASCADE, ) + _maintenance_mode = models.CharField( + max_length=64, + choices=MaintenanceMode.choices, + default=MaintenanceMode.OFF, + ) __error_code__ = "COMPONENT_NOT_FOUND" @@ -784,6 +854,43 @@ def serialized_issue(self): } return result if result["issue"] else {} + @property + def maintenance_mode_attr(self) -> MaintenanceMode.choices: + return self._maintenance_mode + + @property + def maintenance_mode(self) -> MaintenanceMode.choices: + if self._maintenance_mode != MaintenanceMode.OFF: + return self._maintenance_mode + + if self.service.maintenance_mode_attr == MaintenanceMode.ON: + return self.service.maintenance_mode_attr + + host_ids = HostComponent.objects.filter(component=self).values_list("host_id", flat=True) + if host_ids: + return ( + MaintenanceMode.ON + if all(Host.objects.get(pk=host_id).maintenance_mode == MaintenanceMode.ON for host_id in host_ids) + else MaintenanceMode.OFF + ) + + return self._maintenance_mode + + @maintenance_mode.setter + def maintenance_mode(self, value: MaintenanceMode.choices) -> None: + self._maintenance_mode = value + + @property + def is_maintenance_mode_available(self) -> bool: + return self.cluster.prototype.allow_maintenance_mode + + def requires_service_name(self, service_name: str) -> bool: + for item in self.requires: + if item.get("service") == service_name: + return True + + return False + class Meta: unique_together = (("cluster", "service", "prototype"),) @@ -801,9 +908,7 @@ class GroupConfig(ADCMModel): name = models.CharField(max_length=30, validators=[validate_line_break_character]) description = models.TextField(blank=True) hosts = models.ManyToManyField(Host, blank=True, related_name="group_config") - config = models.OneToOneField( - ObjectConfig, on_delete=models.CASCADE, null=True, related_name="group_config" - ) + config = models.OneToOneField(ObjectConfig, on_delete=models.CASCADE, null=True, related_name="group_config") __error_code__ = "GROUP_CONFIG_NOT_FOUND" @@ -815,9 +920,9 @@ class Meta: def get_config_spec(self): """Return spec for config""" spec = {} - for field in PrototypeConfig.objects.filter( - prototype=self.object.prototype, action__isnull=True - ).order_by("id"): + for field in PrototypeConfig.objects.filter(prototype=self.object.prototype, action__isnull=True).order_by( + "id" + ): group_customization = field.group_customization if group_customization is None: group_customization = self.object.prototype.config_group_customization @@ -855,9 +960,7 @@ def create_group_keys( value = False group_keys.setdefault(k, {"value": value, "fields": {}}) custom_group_keys.setdefault(k, {"value": v["group_customization"], "fields": {}}) - self.create_group_keys( - v["fields"], group_keys[k]["fields"], custom_group_keys[k]["fields"] - ) + self.create_group_keys(v["fields"], group_keys[k]["fields"], custom_group_keys[k]["fields"]) else: group_keys[k] = False custom_group_keys[k] = v["group_customization"] @@ -900,9 +1003,7 @@ def merge_config(self, object_config: dict, group_config: dict, group_keys: dict for k, v in group_keys.items(): if isinstance(v, Mapping): config.setdefault(k, {}) - self.merge_config( - object_config[k], group_config[k], group_keys[k]["fields"], config[k] - ) + self.merge_config(object_config[k], group_config[k], group_keys[k]["fields"], config[k]) else: if v and k in group_config: config[k] = group_config[k] @@ -952,13 +1053,9 @@ def host_candidate(self): if isinstance(self.object, (Cluster, HostProvider)): hosts = self.object.host_set.all() elif isinstance(self.object, ClusterObject): - hosts = Host.objects.filter( - cluster=self.object.cluster, hostcomponent__service=self.object - ).distinct() + hosts = Host.objects.filter(cluster=self.object.cluster, hostcomponent__service=self.object).distinct() elif isinstance(self.object, ServiceComponent): - hosts = Host.objects.filter( - cluster=self.object.cluster, hostcomponent__component=self.object - ).distinct() + hosts = Host.objects.filter(cluster=self.object.cluster, hostcomponent__component=self.object).distinct() else: raise AdcmEx("GROUP_CONFIG_TYPE_ERROR") return hosts.exclude(group_config__in=self.object.group_config.all()) @@ -989,7 +1086,7 @@ def preparing_file_type_field(self, config=None): field.subname, ] ) - filepath = os.path.join(FILE_DIR, filename) + filepath = str(settings.FILE_DIR / filename) if field.subname: value = config[field.name][field.subname] @@ -1001,7 +1098,7 @@ def preparing_file_type_field(self, config=None): if value != "": if value[-1] == "-": value += "\n" - with open(filepath, "w", encoding="utf-8") as f: + with open(filepath, "w", encoding=settings.ENCODING_UTF_8) as f: f.write(value) os.chmod(filepath, 0o0600) else: @@ -1070,8 +1167,6 @@ class AbstractAction(ADCMModel): ui_options = models.JSONField(default=dict) type = models.CharField(max_length=16, choices=ActionType.choices) - button = models.CharField(max_length=64, default=None, null=True) - script = models.CharField(max_length=160) script_type = models.CharField(max_length=16, choices=SCRIPT_TYPE) @@ -1178,6 +1273,58 @@ def allowed(self, obj: ADCMEntity) -> bool: return state_allowed and multi_state_allowed + def get_start_impossible_reason(self, obj: ADCMEntity) -> None: + # pylint: disable=too-many-branches + + start_impossible_reason = None + if obj.prototype.type == "adcm": + current_configlog = ConfigLog.objects.get(obj_ref=obj.config, id=obj.config.current) + if not current_configlog.attr["ldap_integration"]["active"]: + start_impossible_reason = NO_LDAP_SETTINGS + + if obj.prototype.type == "cluster": + if ( + not self.allow_in_maintenance_mode + and Host.objects.filter(cluster=obj, maintenance_mode=MaintenanceMode.ON).exists() + ): + start_impossible_reason = MANY_HOSTS_IN_MM + elif obj.prototype.type == "service": + if not self.allow_in_maintenance_mode: + if obj.maintenance_mode == MaintenanceMode.ON: + start_impossible_reason = SERVICE_IN_MM + + if HostComponent.objects.filter( + service=obj, cluster=obj.cluster, host__maintenance_mode=MaintenanceMode.ON + ).exists(): + start_impossible_reason = MANY_HOSTS_IN_MM + elif obj.prototype.type == "component": + if not self.allow_in_maintenance_mode: + if obj.maintenance_mode == MaintenanceMode.ON: + start_impossible_reason = COMPONENT_IN_MM + + if HostComponent.objects.filter( + component=obj, + cluster=obj.cluster, + service=obj.service, + host__maintenance_mode=MaintenanceMode.ON, + ).exists(): + start_impossible_reason = MANY_HOSTS_IN_MM + elif obj.prototype.type == "host": + if not self.allow_in_maintenance_mode: + if obj.maintenance_mode == MaintenanceMode.ON: + start_impossible_reason = HOST_IN_MM + + if ( + self.host_action + and HostComponent.objects.filter( + component_id__in=HostComponent.objects.filter(host=obj).values_list("component_id"), + host__maintenance_mode=MaintenanceMode.ON, + ).exists() + ): + start_impossible_reason = MANY_HOSTS_IN_MM + + return start_impossible_reason + class AbstractSubAction(ADCMModel): action = None @@ -1222,8 +1369,10 @@ class Meta: ("variant", "variant"), ("boolean", "boolean"), ("file", "file"), + ("secretfile", "secretfile"), ("list", "list"), ("map", "map"), + ("secretmap", "secretmap"), ("structure", "structure"), ("group", "group"), ) @@ -1273,9 +1422,7 @@ class Meta: class ClusterBind(ADCMModel): cluster = models.ForeignKey(Cluster, on_delete=models.CASCADE) service = models.ForeignKey(ClusterObject, on_delete=models.CASCADE, null=True, default=None) - source_cluster = models.ForeignKey( - Cluster, related_name="source_cluster", on_delete=models.CASCADE - ) + source_cluster = models.ForeignKey(Cluster, related_name="source_cluster", on_delete=models.CASCADE) source_service = models.ForeignKey( ClusterObject, related_name="source_service", @@ -1290,12 +1437,13 @@ class Meta: unique_together = (("cluster", "service", "source_cluster", "source_service"),) -JOB_STATUS = ( - ("created", "created"), - ("running", "running"), - ("success", "success"), - ("failed", "failed"), -) +class JobStatus(models.TextChoices): + CREATED = "created", "created" + SUCCESS = "success", "success" + FAILED = "failed", "failed" + RUNNING = "running", "running" + LOCKED = "locked", "locked" + ABORTED = "aborted", "aborted" class UserProfile(ADCMModel): @@ -1310,7 +1458,7 @@ class TaskLog(ADCMModel): action = models.ForeignKey(Action, on_delete=models.SET_NULL, null=True, default=None) pid = models.PositiveIntegerField(blank=True, default=0) selector = models.JSONField(default=dict) - status = models.CharField(max_length=16, choices=JOB_STATUS) + status = models.CharField(max_length=16, choices=JobStatus.choices) config = models.JSONField(null=True, default=None) attr = models.JSONField(default=dict) hostcomponentmap = models.JSONField(null=True, default=None) @@ -1321,6 +1469,8 @@ class TaskLog(ADCMModel): finish_date = models.DateTimeField() lock = models.ForeignKey("ConcernItem", null=True, on_delete=models.SET_NULL, default=None) + __error_code__ = "TASK_NOT_FOUND" + def lock_affected(self, objects: Iterable[ADCMEntity]) -> None: if self.lock: return @@ -1362,9 +1512,9 @@ def cancel(self, event_queue: "cm.status_api.Event" = None, obj_deletion=False): "Termination is too early, try to execute later", ) errors = { - Job.FAILED: ("TASK_IS_FAILED", f"task #{self.pk} is failed"), - Job.ABORTED: ("TASK_IS_ABORTED", f"task #{self.pk} is aborted"), - Job.SUCCESS: ("TASK_IS_SUCCESS", f"task #{self.pk} is success"), + JobStatus.FAILED: ("TASK_IS_FAILED", f"task #{self.pk} is failed"), + JobStatus.ABORTED: ("TASK_IS_ABORTED", f"task #{self.pk} is aborted"), + JobStatus.SUCCESS: ("TASK_IS_SUCCESS", f"task #{self.pk} is success"), } action = self.action if action and not action.allow_to_terminate and not obj_deletion: @@ -1372,10 +1522,10 @@ def cancel(self, event_queue: "cm.status_api.Event" = None, obj_deletion=False): "NOT_ALLOWED_TERMINATION", f"not allowed termination task #{self.pk} for action #{action.pk}", ) - if self.status in [Job.FAILED, Job.ABORTED, Job.SUCCESS]: + if self.status in [JobStatus.FAILED, JobStatus.ABORTED, JobStatus.SUCCESS]: raise AdcmEx(*errors.get(self.status)) i = 0 - while not JobLog.objects.filter(task=self, status=Job.RUNNING) and i < 10: + while not JobLog.objects.filter(task=self, status=JobStatus.RUNNING) and i < 10: time.sleep(0.5) i += 1 if i == 10: @@ -1385,12 +1535,6 @@ def cancel(self, event_queue: "cm.status_api.Event" = None, obj_deletion=False): event_queue.send_state() os.kill(self.pid, signal.SIGTERM) - @staticmethod - def get_adcm_tasks_qs(): - return TaskLog.objects.filter( - object_type=ContentType.objects.get(app_label="cm", model="adcm") - ) - class JobLog(ADCMModel): task = models.ForeignKey(TaskLog, on_delete=models.SET_NULL, null=True, default=None) @@ -1399,16 +1543,12 @@ class JobLog(ADCMModel): pid = models.PositiveIntegerField(blank=True, default=0) selector = models.JSONField(default=dict) log_files = models.JSONField(default=list) - status = models.CharField(max_length=16, choices=JOB_STATUS) + status = models.CharField(max_length=16, choices=JobStatus.choices) start_date = models.DateTimeField() finish_date = models.DateTimeField(db_index=True) __error_code__ = "JOB_NOT_FOUND" - @staticmethod - def get_adcm_jobs_qs(): - return JobLog.objects.filter(task__in=TaskLog.get_adcm_tasks_qs()) - class GroupCheckLog(ADCMModel): job = models.ForeignKey(JobLog, on_delete=models.SET_NULL, null=True, default=None) @@ -1450,9 +1590,7 @@ class LogStorage(ADCMModel): class Meta: constraints = [ - models.UniqueConstraint( - fields=["job"], condition=models.Q(type="check"), name="unique_check_job" - ) + models.UniqueConstraint(fields=["job"], condition=models.Q(type="check"), name="unique_check_job") ] @@ -1467,6 +1605,7 @@ class StagePrototype(ADCMModel): display_name = models.CharField(max_length=1000, blank=True) version = models.CharField(max_length=80) edition = models.CharField(max_length=80, default="community") + license = models.CharField(max_length=16, choices=LICENSE_STATE, default="absent") license_path = models.CharField(max_length=160, default=None, null=True) license_hash = models.CharField(max_length=64, default=None, null=True) required = models.BooleanField(default=False) @@ -1705,7 +1844,7 @@ class ConcernItem(ADCMModel): `blocking` blocks actions from running `owner` is object-origin of concern `cause` is owner's parameter causing concern - `related_objects` are back-refs from affected ADCMEntities.concerns + `related_objects` are back-refs from affected `ADCMEntities.concerns` """ type = models.CharField(max_length=8, choices=ConcernType.choices, default=ConcernType.Lock) diff --git a/python/cm/signals.py b/python/cm/signals.py index 3565bf9a77..24d19346c6 100644 --- a/python/cm/signals.py +++ b/python/cm/signals.py @@ -9,24 +9,30 @@ # 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 django.db.models.signals import post_delete, pre_save +import casestyle +from django.db import transaction +from django.db.models.signals import m2m_changed, post_delete, post_save, pre_save from django.dispatch import receiver from django.utils import timezone from audit.models import MODEL_TO_AUDIT_OBJECT_TYPE_MAP, AuditObject from audit.utils import mark_deleted_audit_object +from cm.logger import logger from cm.models import ( ADCM, + ADCMEntity, Bundle, Cluster, ClusterObject, DummyData, + GroupConfig, Host, HostProvider, + Prototype, ServiceComponent, ) -from rbac.models import Group, Policy +from cm.status_api import post_event +from rbac.models import Group, Policy, Role, User @receiver(post_delete, sender=Cluster) @@ -36,6 +42,7 @@ @receiver(post_delete, sender=HostProvider) @receiver(post_delete, sender=Bundle) @receiver(post_delete, sender=ADCM) +@receiver(post_delete, sender=Prototype) def mark_deleted_audit_object_handler(sender, instance, **kwargs) -> None: mark_deleted_audit_object(instance=instance, object_type=MODEL_TO_AUDIT_OBJECT_TYPE_MAP[sender]) @@ -74,3 +81,76 @@ def rename_audit_object_host(sender, instance, **kwargs) -> None: audit_obj.object_name = instance.fqdn audit_obj.save(update_fields=["object_name"]) + + +def get_names(sender, **kwargs): + """getting model name, module name and object""" + if hasattr(sender, 'get_endpoint'): + name = sender.get_endpoint() + else: + name = casestyle.kebabcase(sender.__name__) + return name, sender.__module__, kwargs['instance'] + + +def _post_event(action, module, name, obj_pk): + """Wrapper for post_event to run in on_commit hook""" + transaction.on_commit(lambda: post_event(action, name, obj_pk, {'module': module})) + + +@receiver(post_save, sender=User) +@receiver(post_save, sender=Group) +@receiver(post_save, sender=Policy) +@receiver(post_save, sender=Role) +@receiver(post_save, sender=GroupConfig) +def model_change(sender, **kwargs): + """post_save handler""" + name, module, obj = get_names(sender, **kwargs) + if 'filter_out' in kwargs: + if kwargs['filter_out'](module, name, obj): + return + action = 'update' + if 'created' in kwargs and kwargs['created']: + action = 'create' + args = (action, module, name, obj.pk) + logger.info('%s %s %s #%s', *args) + _post_event(*args) + + +@receiver(post_delete, sender=User) +@receiver(post_delete, sender=Group) +@receiver(post_delete, sender=Policy) +@receiver(post_delete, sender=Role) +@receiver(post_delete, sender=GroupConfig) +def model_delete(sender, **kwargs): + """post_delete handler""" + name, module, obj = get_names(sender, **kwargs) + if 'filter_out' in kwargs: + if kwargs['filter_out'](module, name, obj): + return + action = 'delete' + args = (action, module, name, obj.pk) + logger.info('%s %s %s #%s', *args) + _post_event(*args) + + +@receiver(m2m_changed, sender=GroupConfig) +@receiver(m2m_changed, sender=ADCMEntity.concerns.through) +@receiver(m2m_changed, sender=Policy) +@receiver(m2m_changed, sender=Role) +@receiver(m2m_changed, sender=User) +@receiver(m2m_changed, sender=Group) +def m2m_change(sender, **kwargs): + """m2m_changed handler""" + name, module, obj = get_names(sender, **kwargs) + if 'filter_out' in kwargs: + if kwargs['filter_out'](module, name, obj): + return + if kwargs['action'] == 'post_add': + action = 'add' + elif kwargs['action'] == 'post_remove': + action = 'delete' + else: + return + args = (action, module, name, obj.pk) + logger.info('%s %s %s #%s', *args) + _post_event(*args) diff --git a/python/cm/stack.py b/python/cm/stack.py index cbbe1d574e..02f4e9babf 100644 --- a/python/cm/stack.py +++ b/python/cm/stack.py @@ -22,12 +22,12 @@ import ruyaml import yaml import yspec.checker +from django.conf import settings from django.db import IntegrityError from rest_framework import status from version_utils import rpm import cm.checker -from cm import config from cm.adcm_config import ( check_config_type, proto_ref, @@ -49,27 +49,28 @@ NAME_REGEX = r"[0-9a-zA-Z_\.-]+" -def save_definition(path, fname, conf, obj_list, bundle_hash, adcm=False): +def save_definition(path, fname, conf, obj_list, bundle_hash, adcm_=False): if isinstance(conf, dict): - save_object_definition(path, fname, conf, obj_list, bundle_hash, adcm) + save_object_definition(path, fname, conf, obj_list, bundle_hash, adcm_) else: for obj_def in conf: - save_object_definition(path, fname, obj_def, obj_list, bundle_hash, adcm) + save_object_definition(path, fname, obj_def, obj_list, bundle_hash, adcm_) def cook_obj_id(conf): return f"{conf['type']}.{conf['name']}.{conf['version']}" -def save_object_definition(path, fname, conf, obj_list, bundle_hash, adcm=False): +def save_object_definition(path, fname, conf, obj_list, bundle_hash, adcm_=False): def_type = conf["type"] - if def_type == "adcm" and not adcm: - msg = "Invalid type \"{}\" in object definition: {}" - return err("INVALID_OBJECT_DEFINITION", msg.format(def_type, fname)) + if def_type == "adcm" and not adcm_: + return err("INVALID_OBJECT_DEFINITION", f'Invalid type "{def_type}" in object definition: {fname}') + check_object_definition(fname, conf, def_type, obj_list) obj = save_prototype(path, conf, def_type, bundle_hash) logger.info("Save definition of %s \"%s\" %s to stage", def_type, conf["name"], conf["version"]) obj_list[cook_obj_id(conf)] = fname + return obj @@ -78,6 +79,28 @@ def check_object_definition(fname, conf, def_type, obj_list): if cook_obj_id(conf) in obj_list: err("INVALID_OBJECT_DEFINITION", f"Duplicate definition of {ref} (file {fname})") + for action_name, action_data in conf.get("actions", {}).items(): + if action_name in { + settings.ADCM_HOST_TURN_ON_MM_ACTION_NAME, + settings.ADCM_HOST_TURN_OFF_MM_ACTION_NAME, + }: + if def_type != "cluster": + err("INVALID_OBJECT_DEFINITION", f'Action named "{action_name}" can be started only in cluster context') + + if not action_data.get("host_action"): + err( + "INVALID_OBJECT_DEFINITION", + f'Action named "{action_name}" should have "host_action: true" property', + ) + + if action_name in settings.ADCM_SERVICE_ACTION_NAMES_SET and set(action_data).intersection( + settings.ADCM_MM_ACTION_FORBIDDEN_PROPS_SET + ): + err( + "INVALID_OBJECT_DEFINITION", + f'Maintenance mode actions shouldn\'t have "{settings.ADCM_MM_ACTION_FORBIDDEN_PROPS_SET}" properties', + ) + def get_config_files(path, bundle_hash): conf_list = [] @@ -102,11 +125,11 @@ def get_config_files(path, bundle_hash): def check_adcm_config(conf_file): warnings.simplefilter("error", ruyaml.error.ReusedAnchorWarning) - schema_file = os.path.join(config.CODE_DIR, "cm", "adcm_schema.yaml") - with open(schema_file, encoding="utf_8") as fd: + schema_file = settings.CODE_DIR / "cm" / "adcm_schema.yaml" + with open(schema_file, encoding=settings.ENCODING_UTF_8) as fd: rules = ruyaml.round_trip_load(fd) try: - with open(conf_file, encoding="utf_8") as fd: + with open(conf_file, encoding=settings.ENCODING_UTF_8) as fd: data = cm.checker.round_trip_load(fd, version="1.1", allow_duplicate_keys=True) except (ruyaml.parser.ParserError, ruyaml.scanner.ScannerError, NotImplementedError) as e: err("STACK_LOAD_ERROR", f"YAML decode \"{conf_file}\" error: {e}") @@ -145,7 +168,7 @@ def get_license_hash(proto, conf, bundle_hash): return None body = read_bundle_file(proto, conf["license"], bundle_hash, "license file") sha1 = hashlib.sha256() - sha1.update(body.encode("utf-8")) + sha1.update(body.encode(settings.ENCODING_UTF_8)) return sha1.hexdigest() @@ -182,6 +205,12 @@ def save_prototype(path, conf, def_type, bundle_hash): fix_display_name(conf, proto) license_hash = get_license_hash(proto, conf, bundle_hash) if license_hash: + if def_type not in ["cluster", "service", "provider"]: + err( + "INVALID_OBJECT_DEFINITION", + f"Invalid license definition in {proto_ref(proto)}." + f" License can be placed in cluster, service or provider", + ) proto.license_path = conf["license"] proto.license_hash = license_hash proto.save() @@ -239,10 +268,10 @@ def save_components(proto, conf, bundle_hash): def check_upgrade(proto, conf): label = f"upgrade \"{conf['name']}\"" check_versions(proto, conf, label) - check_scripts(proto, conf, label) + check_upgrade_scripts(proto, conf, label) -def check_scripts(proto, conf, label): +def check_upgrade_scripts(proto, conf, label): ref = proto_ref(proto) count = 0 if "scripts" in conf: @@ -258,6 +287,10 @@ def check_scripts(proto, conf, label): if count == 0: msg = "Scripts block in {} of {} must contain exact one block with script \"bundle_switch\"" err("INVALID_UPGRADE_DEFINITION", msg.format(label, ref)) + else: + if "masking" in conf or "on_success" in conf or "on_fail" in conf: + msg = "{} of {} couldn't contain `masking`, `on_success` or `on_fail` without `scripts` block" + err("INVALID_UPGRADE_DEFINITION", msg.format(label, ref)) def check_versions(proto, conf, label): @@ -266,21 +299,13 @@ def check_versions(proto, conf, label): if "min" in conf["versions"] and "min_strict" in conf["versions"]: msg = "min and min_strict can not be used simultaneously in versions of {} ({})" err("INVALID_VERSION_DEFINITION", msg.format(label, ref)) - if ( - "min" not in conf["versions"] - and "min_strict" not in conf["versions"] - and "import" not in label - ): + if "min" not in conf["versions"] and "min_strict" not in conf["versions"] and "import" not in label: msg = "min or min_strict should be present in versions of {} ({})" err("INVALID_VERSION_DEFINITION", msg.format(label, ref)) if "max" in conf["versions"] and "max_strict" in conf["versions"]: msg = "max and max_strict can not be used simultaneously in versions of {} ({})" err("INVALID_VERSION_DEFINITION", msg.format(label, ref)) - if ( - "max" not in conf["versions"] - and "max_strict" not in conf["versions"] - and "import" not in label - ): + if "max" not in conf["versions"] and "max_strict" not in conf["versions"] and "import" not in label: msg = "max and max_strict should be present in versions of {} ({})" err("INVALID_VERSION_DEFINITION", msg.format(label, ref)) for name in ("min", "min_strict", "max", "max_strict"): @@ -330,14 +355,17 @@ def save_export(proto, conf): ref = proto_ref(proto) if not in_dict(conf, "export"): return + + export = {} if isinstance(conf["export"], str): export = [conf["export"]] elif isinstance(conf["export"], list): export = conf["export"] - msg = "{} does not has \"{}\" config group" + for key in export: if not StagePrototypeConfig.objects.filter(prototype=proto, name=key): - err("INVALID_OBJECT_DEFINITION", msg.format(ref, key)) + err("INVALID_OBJECT_DEFINITION", f'{ref} does not has "{key}" config group') + se = StagePrototypeExport(prototype=proto, name=key) se.save() @@ -413,9 +441,7 @@ def save_sub_actions(conf, action): elif isinstance(on_fail, dict): sub_action.state_on_fail = _deep_get(on_fail, STATE, default="") sub_action.multi_state_on_fail_set = _deep_get(on_fail, MULTI_STATE, SET, default=[]) - sub_action.multi_state_on_fail_unset = _deep_get( - on_fail, MULTI_STATE, UNSET, default=[] - ) + sub_action.multi_state_on_fail_unset = _deep_get(on_fail, MULTI_STATE, UNSET, default=[]) sub_action.save() @@ -465,7 +491,6 @@ def save_action(proto, ac, bundle_hash, action_name): if ac["type"] == "job": action.script = ac["script"] action.script_type = ac["script_type"] - dict_to_obj(ac, "button", action) dict_to_obj(ac, "display_name", action) dict_to_obj(ac, "description", action) dict_to_obj(ac, "allow_to_terminate", action) @@ -492,13 +517,9 @@ def save_action(proto, ac, bundle_hash, action_name): action.state_on_fail = _deep_get(ac, ON_FAIL, STATE, default="") action.multi_state_available = _deep_get(ac, MASKING, MULTI_STATE, AVAILABLE, default=ANY) - action.multi_state_unavailable = _deep_get( - ac, MASKING, MULTI_STATE, UNAVAILABLE, default=[] - ) + action.multi_state_unavailable = _deep_get(ac, MASKING, MULTI_STATE, UNAVAILABLE, default=[]) action.multi_state_on_success_set = _deep_get(ac, ON_SUCCESS, MULTI_STATE, SET, default=[]) - action.multi_state_on_success_unset = _deep_get( - ac, ON_SUCCESS, MULTI_STATE, UNSET, default=[] - ) + action.multi_state_on_success_unset = _deep_get(ac, ON_SUCCESS, MULTI_STATE, UNSET, default=[]) action.multi_state_on_fail_set = _deep_get(ac, ON_FAIL, MULTI_STATE, SET, default=[]) action.multi_state_on_fail_unset = _deep_get(ac, ON_FAIL, MULTI_STATE, UNSET, default=[]) else: @@ -537,17 +558,17 @@ def is_group(conf): def get_yspec(proto, ref, bundle_hash, conf, name, subname): - msg = f"yspec file of config key \"{name}/{subname}\":" - yspec_body = read_bundle_file(proto, conf["yspec"], bundle_hash, msg) + schema = None + yspec_body = read_bundle_file(proto, conf["yspec"], bundle_hash, f'yspec file of config key "{name}/{subname}":') try: schema = yaml.safe_load(yspec_body) except (yaml.parser.ParserError, yaml.scanner.ScannerError) as e: - msg = "yspec file of config key \"{}/{}\" yaml decode error: {}" - err("CONFIG_TYPE_ERROR", msg.format(name, subname, e)) + err("CONFIG_TYPE_ERROR", f'yspec file of config key "{name}/{subname}" yaml decode error: {e}') + ok, error = yspec.checker.check_rule(schema) if not ok: - msg = "yspec file of config key \"{}/{}\" error: {}" - err("CONFIG_TYPE_ERROR", msg.format(name, subname, error)) + err("CONFIG_TYPE_ERROR", f'yspec file of config key "{name}/{subname}" error: {error}') + return schema @@ -666,10 +687,7 @@ def validate_name(value, err_msg): if not isinstance(value, str): err("WRONG_NAME", f"{err_msg} should be string") p = re.compile(NAME_REGEX) - msg1 = ( - "{} is incorrect. Only latin characters, digits," - " dots (.), dashes (-), and underscores (_) are allowed." - ) + msg1 = "{} is incorrect. Only latin characters, digits, dots (.), dashes (-), and underscores (_) are allowed." if p.fullmatch(value) is None: err("WRONG_NAME", msg1.format(err_msg)) return value diff --git a/python/cm/status_api.py b/python/cm/status_api.py index 8ce702f7b2..c7119311c1 100644 --- a/python/cm/status_api.py +++ b/python/cm/status_api.py @@ -15,8 +15,8 @@ from typing import Iterable import requests +from django.conf import settings -from cm.config import STATUS_SECRET_KEY from cm.logger import logger from cm.models import ( ADCMEntity, @@ -62,14 +62,14 @@ def set_task_status(self, task_id, status): def api_request(method, url, data=None): url = API_URL + url kwargs = { - 'headers': { - 'Content-Type': 'application/json', - 'Authorization': 'Token ' + STATUS_SECRET_KEY, + "headers": { + "Content-Type": "application/json", + "Authorization": f"Token {settings.ADCM_TOKEN}", }, - 'timeout': TIMEOUT, + "timeout": TIMEOUT, } if data is not None: - kwargs['data'] = json.dumps(data) + kwargs["data"] = json.dumps(data) try: request = requests.request(method, url, **kwargs) if request.status_code not in (200, 201): @@ -84,96 +84,96 @@ def api_request(method, url, data=None): def post_event(event, obj_type, obj_id, det_type=None, det_val=None): - details = {'type': det_type, 'value': det_val} + details = {"type": det_type, "value": det_val} if det_type and not det_val: details = det_type data = { - 'event': event, - 'object': { - 'type': obj_type, - 'id': int(obj_id), - 'details': details, + "event": event, + "object": { + "type": obj_type, + "id": int(obj_id), + "details": details, }, } - logger.debug('post_event %s', data) - return api_request('post', '/event/', data) + logger.debug("post_event %s", data) + return api_request("post", "/event/", data) def set_job_status(job_id, status): - return post_event('change_job_status', 'job', job_id, 'status', status) + return post_event("change_job_status", "job", job_id, "status", status) def set_task_status(task_id, status): - return post_event('change_job_status', 'task', task_id, 'status', status) + return post_event("change_job_status", "task", task_id, "status", status) def set_obj_state(obj_type, obj_id, state): - if obj_type == 'adcm': + if obj_type == "adcm": return None - if obj_type not in ('cluster', 'service', 'host', 'provider', 'component'): - logger.error('Unknown object type: "%s"', obj_type) + if obj_type not in ("cluster", "service", "host", "provider", "component"): + logger.error("Unknown object type: '%s'", obj_type) return None - return post_event('change_state', obj_type, obj_id, 'state', state) + return post_event("change_state", obj_type, obj_id, "state", state) def change_obj_multi_state(obj_type, obj_id, multi_state): - if obj_type == 'adcm': + if obj_type == "adcm": return None - if obj_type not in ('cluster', 'service', 'host', 'provider', 'component'): - logger.error('Unknown object type: "%s"', obj_type) + if obj_type not in ("cluster", "service", "host", "provider", "component"): + logger.error("Unknown object type: '%s'", obj_type) return None - return post_event('change_state', obj_type, obj_id, 'multi_state', multi_state) + return post_event("change_state", obj_type, obj_id, "multi_state", multi_state) def get_raw_status(url): - r = api_request('get', url) + r = api_request("get", url) if r is None: return 32 try: js = r.json() except ValueError: return 8 - if 'status' in js: - return js['status'] + if "status" in js: + return js["status"] else: return 4 def get_status(obj: ADCMEntity, url: str): - if obj.prototype.monitoring == 'passive': + if obj.prototype.monitoring == "passive": return 0 return get_raw_status(url) def get_cluster_status(cluster): - return get_raw_status(f'/cluster/{cluster.id}/') + return get_raw_status(f"/cluster/{cluster.id}/") def get_service_status(service): - return get_status(service, f'/cluster/{service.cluster.id}/service/{service.id}/') + return get_status(service, f"/cluster/{service.cluster.id}/service/{service.id}/") def get_host_status(host): - return get_status(host, f'/host/{host.id}/') + return get_status(host, f"/host/{host.id}/") def get_hc_status(hc): - return get_status(hc.component, f'/host/{hc.host_id}/component/{hc.component_id}/') + return get_status(hc.component, f"/host/{hc.host_id}/component/{hc.component_id}/") def get_host_comp_status(host, component): - return get_status(component, f'/host/{host.id}/component/{component.id}/') + return get_status(component, f"/host/{host.id}/component/{component.id}/") def get_component_status(comp: ServiceComponent): - return get_status(comp, f'/component/{comp.id}/') + return get_status(comp, f"/component/{comp.id}/") def get_object_map(obj: ADCMEntity, url_type: str): - if url_type == 'service': - r = api_request('get', f'/cluster/{obj.cluster.id}/service/{obj.id}/?view=interface') + if url_type == "service": + r = api_request("get", f"/cluster/{obj.cluster.id}/service/{obj.id}/?view=interface") else: - r = api_request('get', f'/{url_type}/{obj.id}/?view=interface') + r = api_request("get", f"/{url_type}/{obj.id}/?view=interface") if r is None: return None return r.json() @@ -181,36 +181,32 @@ def get_object_map(obj: ADCMEntity, url_type: str): def make_ui_single_host_status(host: Host) -> dict: return { - 'id': host.id, - 'name': host.fqdn, - 'status': get_host_status(host), + "id": host.id, + "name": host.fqdn, + "status": get_host_status(host), } -def make_ui_component_status( - component: ServiceComponent, host_components: Iterable[HostComponent] -) -> dict: +def make_ui_component_status(component: ServiceComponent, host_components: Iterable[HostComponent]) -> dict: """Make UI representation of component's status per host""" host_list = [] for hc in host_components: host_list.append( { - 'id': hc.host.id, - 'name': hc.host.fqdn, - 'status': get_host_comp_status(hc.host, hc.component), + "id": hc.host.id, + "name": hc.host.fqdn, + "status": get_host_comp_status(hc.host, hc.component), } ) return { - 'id': component.id, - 'name': component.display_name, - 'status': get_component_status(component), - 'hosts': host_list, + "id": component.id, + "name": component.display_name, + "status": get_component_status(component), + "hosts": host_list, } -def make_ui_service_status( - service: ClusterObject, host_components: Iterable[HostComponent] -) -> dict: +def make_ui_service_status(service: ClusterObject, host_components: Iterable[HostComponent]) -> dict: """Make UI representation of service and its children statuses""" component_hc_map = defaultdict(list) for hc in host_components: @@ -220,12 +216,12 @@ def make_ui_service_status( for component, hc_list in component_hc_map.items(): comp_list.append(make_ui_component_status(component, hc_list)) - service_map = get_object_map(service, 'service') + service_map = get_object_map(service, "service") return { - 'id': service.id, - 'name': service.display_name, - 'status': 32 if service_map is None else service_map.get('status', 0), - 'hc': comp_list, + "id": service.id, + "name": service.display_name, + "status": 32 if service_map is None else service_map.get("status", 0), + "hc": comp_list, } @@ -243,13 +239,13 @@ def make_ui_cluster_status(cluster: Cluster, host_components: Iterable[HostCompo for host in Host.obj.filter(cluster=cluster): host_list.append(make_ui_single_host_status(host)) - cluster_map = get_object_map(cluster, 'cluster') + cluster_map = get_object_map(cluster, "cluster") return { - 'name': cluster.name, - 'status': 32 if cluster_map is None else cluster_map.get('status', 0), - 'chilren': { # backward compatibility typo - 'hosts': host_list, - 'services': service_list, + "name": cluster.name, + "status": 32 if cluster_map is None else cluster_map.get("status", 0), + "chilren": { # backward compatibility typo + "hosts": host_list, + "services": service_list, }, } @@ -260,17 +256,17 @@ def make_ui_host_status(host: Host, host_components: Iterable[HostComponent]) -> for hc in host_components: comp_list.append( { - 'id': hc.component.id, - 'name': hc.component.display_name, - 'status': get_component_status(hc.component), - 'service_id': hc.service.id, + "id": hc.component.id, + "name": hc.component.display_name, + "status": get_component_status(hc.component), + "service_id": hc.service.id, } ) - host_map = get_object_map(host, 'host') + host_map = get_object_map(host, "host") return { - 'id': host.id, - 'name': host.fqdn, - 'status': 32 if host_map is None else host_map.get('status', 0), - 'hc': comp_list, + "id": host.id, + "name": host.fqdn, + "status": 32 if host_map is None else host_map.get("status", 0), + "hc": comp_list, } diff --git a/python/cm/tests/files/config_cluster_secretfile_secretmap.tar b/python/cm/tests/files/config_cluster_secretfile_secretmap.tar new file mode 100644 index 0000000000..9ac2c6bbc4 Binary files /dev/null and b/python/cm/tests/files/config_cluster_secretfile_secretmap.tar differ diff --git a/python/cm/tests/test_adcm_entity.py b/python/cm/tests/test_adcm_entity.py index c2ade59d66..fdd93cb5a2 100644 --- a/python/cm/tests/test_adcm_entity.py +++ b/python/cm/tests/test_adcm_entity.py @@ -96,13 +96,9 @@ def test_get_own_issue__empty(self): def test_get_own_issue__others(self): cluster = self.hierarchy["cluster"] service = self.hierarchy["service"] - reason = MessageTemplate.get_message_from_template( - MessageTemplate.KnownNames.ConfigIssue.value, source=cluster - ) + reason = MessageTemplate.get_message_from_template(MessageTemplate.KnownNames.ConfigIssue.value, source=cluster) issue_type = ConcernCause.Config - issue = ConcernItem.objects.create( - type=ConcernType.Issue, reason=reason, owner=cluster, cause=issue_type - ) + issue = ConcernItem.objects.create(type=ConcernType.Issue, reason=reason, owner=cluster, cause=issue_type) cluster.add_to_concerns(issue) service.add_to_concerns(issue) @@ -110,13 +106,9 @@ def test_get_own_issue__others(self): def test_get_own_issue__exists(self): cluster = self.hierarchy["cluster"] - reason = MessageTemplate.get_message_from_template( - MessageTemplate.KnownNames.ConfigIssue.value, source=cluster - ) + reason = MessageTemplate.get_message_from_template(MessageTemplate.KnownNames.ConfigIssue.value, source=cluster) issue_type = ConcernCause.Config - issue = ConcernItem.objects.create( - type=ConcernType.Issue, reason=reason, owner=cluster, cause=issue_type - ) + issue = ConcernItem.objects.create(type=ConcernType.Issue, reason=reason, owner=cluster, cause=issue_type) cluster.add_to_concerns(issue) self.assertIsNotNone(cluster.get_own_issue(issue_type)) diff --git a/python/cm/tests/test_bundle.py b/python/cm/tests/test_bundle.py index c23ff1c89a..d4d6b7e106 100644 --- a/python/cm/tests/test_bundle.py +++ b/python/cm/tests/test_bundle.py @@ -9,62 +9,28 @@ # 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. - -# Since this module is beyond QA responsibility we will not fix docstrings here -# pylint: disable=missing-function-docstring, missing-class-docstring - +import json import os from contextlib import contextmanager +from pathlib import Path from tarfile import TarFile from django.conf import settings from django.db import transaction -from django.test import Client, TestCase from django.urls import reverse from rest_framework import status +from rest_framework.response import Response +from rest_framework.status import HTTP_201_CREATED -from cm.models import Bundle -from init_db import init as init_adcm -from rbac.upgrade.role import init_roles - - -# TODO: refactor this after merging 1524 (audit) in develop -class TestBase(TestCase): - files_dir = None - - def setUp(self) -> None: - init_adcm() - init_roles() - - self.client = Client(HTTP_USER_AGENT="Mozilla/5.0") - response = self.client.post( - path=reverse("rbac:token"), - data={"username": "admin", "password": "admin"}, - content_type="application/json", - ) - self.client.defaults["Authorization"] = f"Token {response.data['token']}" +from adcm.tests.base import BaseTestCase +from cm.adcm_config import ansible_decrypt +from cm.models import Bundle, Cluster, ConfigLog, Prototype - self.client_unauthorized = Client(HTTP_USER_AGENT="Mozilla/5.0") - - def load_bundle(self, bundle_name: str) -> int: - with open(os.path.join(self.files_dir, bundle_name), encoding="utf-8") as f: - with transaction.atomic(): - response = self.client.post( - path=reverse("upload-bundle"), - data={"file": f}, - ) - self.assertEqual(response.status_code, status.HTTP_201_CREATED) - with transaction.atomic(): - response = self.client.post( - path=reverse("load-bundle"), - data={"bundle_file": bundle_name}, - ) - self.assertEqual(response.status_code, status.HTTP_200_OK) - return response.json()["id"] +# Since this module is beyond QA responsibility we will not fix docstrings here +# pylint: disable=missing-function-docstring, missing-class-docstring -class TestBundle(TestBase): - files_dir = os.path.join(settings.BASE_DIR, "python", "cm", "tests", "files") +class TestBundle(BaseTestCase): bundle_config_template = """ - type: cluster name: Monitoring @@ -120,6 +86,7 @@ class TestBundle(TestBase): def setUp(self) -> None: super().setUp() + self.files_dir = os.path.join(settings.BASE_DIR, "python", "cm", "tests", "files") os.makedirs(self.files_dir, exist_ok=True) self.tar_write_cfg = {} @@ -127,14 +94,14 @@ def setUp(self) -> None: @contextmanager def make_bundle_from_str(self, bundle_content: str, filename: str) -> str: tmp_filepath = os.path.join(self.files_dir, "config.yaml") - with open(tmp_filepath, "wt", encoding="utf-8") as config: + with open(tmp_filepath, "wt", encoding=settings.ENCODING_UTF_8) as config: config.write(bundle_content) bundle_filepath = os.path.join(self.files_dir, filename) with TarFile.open( name=bundle_filepath, mode="w", - encoding="utf-8", + encoding=settings.ENCODING_UTF_8, ) as tar: tar.add(name=tmp_filepath, arcname=os.path.basename(tmp_filepath)) os.remove(tmp_filepath) @@ -144,6 +111,39 @@ def make_bundle_from_str(self, bundle_content: str, filename: str) -> str: finally: os.remove(bundle_filepath) + def load_bundle(self, bundle_name: str) -> int: + with open(Path(self.files_dir, bundle_name), encoding=settings.ENCODING_UTF_8) as f: + with transaction.atomic(): + response = self.client.post( + path=reverse("upload-bundle"), + data={"file": f}, + ) + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + with transaction.atomic(): + response = self.client.post( + path=reverse("load-bundle"), + data={"bundle_file": bundle_name}, + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + return response.json()["id"] + + def upload_bundle_create_cluster_config_log(self) -> tuple[Bundle, Cluster, ConfigLog]: + bundle = self.upload_and_load_bundle( + path=Path( + settings.BASE_DIR, + "python/cm/tests/files/config_cluster_secretfile_secretmap.tar", + ), + ) + + cluster_prototype = Prototype.objects.get(bundle_id=bundle.pk, type="cluster") + cluster_response: Response = self.client.post( + path=reverse("cluster"), + data={"name": "test-cluster", "prototype_id": cluster_prototype.pk}, + ) + cluster = Cluster.objects.get(pk=cluster_response.data["id"]) + + return bundle, cluster, ConfigLog.objects.get(obj_ref=cluster.config) + def test_upload_duplicated_upgrade_script_names(self): same_upgrade_name = "Upgrade name" same_script_name = "Script name" @@ -199,3 +199,57 @@ def test_upload_duplicated_upgrade_script_names(self): ) as bundle: bundle_id = self.load_bundle(bundle) Bundle.objects.get(pk=bundle_id).delete() + + def test_secretfile(self): + bundle, cluster, config_log = self.upload_bundle_create_cluster_config_log() + + with open(Path(settings.BUNDLE_DIR, bundle.hash, "secretfile"), encoding=settings.ENCODING_UTF_8) as f: + secret_file_bundle_content = f.read() + + self.assertNotIn(settings.ANSIBLE_VAULT_HEADER, secret_file_bundle_content) + + with open( + Path(settings.FILE_DIR, f"cluster.{cluster.pk}.secretfile."), + encoding=settings.ENCODING_UTF_8, + ) as f: + secret_file_content = f.read() + + self.assertIn(settings.ANSIBLE_VAULT_HEADER, secret_file_content) + self.assertIn(settings.ANSIBLE_VAULT_HEADER, config_log.config["secretfile"]) + self.assertEqual(secret_file_bundle_content, ansible_decrypt(config_log.config["secretfile"])) + + new_content = "new content" + config_log.config["secretfile"] = "new content" + + response: Response = self.client.post( + path=reverse("config-log-list"), + data={"obj_ref": cluster.pk, "config": json.dumps(config_log.config)}, + ) + + self.assertEqual(response.status_code, HTTP_201_CREATED) + + new_config_log = ConfigLog.objects.filter(obj_ref=cluster.config).order_by("pk").last() + + self.assertIn(settings.ANSIBLE_VAULT_HEADER, new_config_log.config["secretfile"]) + self.assertEqual(new_content, ansible_decrypt(new_config_log.config["secretfile"])) + + def test_secretmap(self): + _, cluster, config_log = self.upload_bundle_create_cluster_config_log() + + self.assertIn(settings.ANSIBLE_VAULT_HEADER, config_log.config["secretmap"]["key"]) + self.assertEqual("value", ansible_decrypt(config_log.config["secretmap"]["key"])) + + new_value = "new value" + config_log.config["secretmap"]["key"] = "new value" + + response: Response = self.client.post( + path=reverse("config-log-list"), + data={"obj_ref": cluster.pk, "config": json.dumps(config_log.config)}, + ) + + self.assertEqual(response.status_code, HTTP_201_CREATED) + + new_config_log = ConfigLog.objects.filter(obj_ref=cluster.config).order_by("pk").last() + + self.assertIn(settings.ANSIBLE_VAULT_HEADER, new_config_log.config["secretmap"]["key"]) + self.assertEqual(new_value, ansible_decrypt(new_config_log.config["secretmap"]["key"])) diff --git a/python/cm/tests/test_cluster.py b/python/cm/tests/test_cluster.py index 0dbb776b9b..e945d496d1 100644 --- a/python/cm/tests/test_cluster.py +++ b/python/cm/tests/test_cluster.py @@ -1,3 +1,14 @@ +# 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 string from django.urls import reverse @@ -32,38 +43,25 @@ def setUp(self) -> None: "Ends with hyphen-", "Ends with dot.", "Ends with underscore_", - ) + tuple( - f"forbidden{c}char" - for c in set(string.punctuation) - set(self.allowed_name_chars_middle) - ) + ) + tuple(f"forbidden{c}char" for c in set(string.punctuation) - set(self.allowed_name_chars_middle)) self.bundle = Bundle.objects.create() - self.prototype = Prototype.objects.create( - name="test_prototype_name", type="cluster", bundle=self.bundle - ) + self.prototype = Prototype.objects.create(name="test_prototype_name", type="cluster", bundle=self.bundle) self.cluster = Cluster.objects.create(name="test_cluster_name", prototype=self.prototype) def test_cluster_update_duplicate_name_fail(self): new_cluster = Cluster.objects.create(name="new_name", prototype=self.prototype) url = reverse("cluster-details", kwargs={"cluster_id": self.cluster.pk}) - response = self.client.patch( - path=url, data={"name": new_cluster.name}, content_type=APPLICATION_JSON - ) + response = self.client.patch(path=url, data={"name": new_cluster.name}, content_type=APPLICATION_JSON) self.assertEqual(response.status_code, status.HTTP_409_CONFLICT) self.assertEqual(response.json()["code"], "CLUSTER_CONFLICT") - self.assertEqual( - response.json()["desc"], f'Cluster with name "{new_cluster.name}" already exists' - ) + self.assertEqual(response.json()["desc"], f'Cluster with name "{new_cluster.name}" already exists') - response = self.client.put( - path=url, data={"name": new_cluster.name}, content_type=APPLICATION_JSON - ) + response = self.client.put(path=url, data={"name": new_cluster.name}, content_type=APPLICATION_JSON) self.assertEqual(response.status_code, status.HTTP_409_CONFLICT) self.assertEqual(response.json()["code"], "CLUSTER_CONFLICT") - self.assertEqual( - response.json()["desc"], f'Cluster with name "{new_cluster.name}" already exists' - ) + self.assertEqual(response.json()["desc"], f'Cluster with name "{new_cluster.name}" already exists') def test_cluster_create_duplicate_name_fail(self): response = self.client.post( @@ -73,9 +71,7 @@ def test_cluster_create_duplicate_name_fail(self): ) self.assertEqual(response.status_code, status.HTTP_409_CONFLICT) self.assertEqual(response.json()["code"], "CLUSTER_CONFLICT") - self.assertEqual( - response.json()["desc"], f'Cluster with name "{self.cluster.name}" already exists' - ) + self.assertEqual(response.json()["desc"], f'Cluster with name "{self.cluster.name}" already exists') def test_cluster_create_name_validation(self): url = reverse("cluster") @@ -106,31 +102,23 @@ def test_cluster_update_name_validation(self): with self.another_user_logged_in(username="admin", password="admin"): for name in self.valid_names: with self.subTest("correct-patch", name=name): - response = self.client.patch( - path=url, data={"name": name}, content_type=APPLICATION_JSON - ) + response = self.client.patch(path=url, data={"name": name}, content_type=APPLICATION_JSON) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.json()["name"], name) with self.subTest("correct-put", name=name): - response = self.client.put( - path=url, data={"name": name}, content_type=APPLICATION_JSON - ) + response = self.client.put(path=url, data={"name": name}, content_type=APPLICATION_JSON) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.json()["name"], name) for name in self.invalid_names: with self.subTest("incorrect-patch", name=name): - response = self.client.patch( - path=url, data={"name": name}, content_type=APPLICATION_JSON - ) + response = self.client.patch(path=url, data={"name": name}, content_type=APPLICATION_JSON) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) self.assertEqual(response.json()["code"], "WRONG_NAME") with self.subTest("incorrect-put", name=name): - response = self.client.put( - path=url, data={"name": name}, content_type=APPLICATION_JSON - ) + response = self.client.put(path=url, data={"name": name}, content_type=APPLICATION_JSON) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) self.assertEqual(response.json()["code"], "WRONG_NAME") diff --git a/python/cm/tests/test_component.py b/python/cm/tests/test_component.py new file mode 100644 index 0000000000..a9522b033e --- /dev/null +++ b/python/cm/tests/test_component.py @@ -0,0 +1,95 @@ +# 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 django.urls import reverse + +from adcm.tests.base import APPLICATION_JSON, BaseTestCase +from cm.models import ( + Bundle, + Cluster, + ClusterObject, + Host, + HostComponent, + MaintenanceMode, + Prototype, + ServiceComponent, +) + + +class TestComponent(BaseTestCase): + def setUp(self) -> None: + super().setUp() + + self.bundle = Bundle.objects.create() + self.cluster = Cluster.objects.create( + prototype=Prototype.objects.create(bundle=self.bundle, type="cluster"), + name="test_cluster", + ) + self.service = ClusterObject.objects.create( + prototype=Prototype.objects.create( + bundle=self.bundle, + type="service", + display_name="test_service", + ), + cluster=self.cluster, + ) + self.component = ServiceComponent.objects.create( + prototype=Prototype.objects.create( + bundle=self.bundle, + type="component", + display_name="test_component", + ), + cluster=self.cluster, + service=self.service, + ) + + def test_maintenance_mode_by_hosts(self): + host_1 = Host.objects.create( + fqdn="test_host_1", + prototype=Prototype.objects.create(bundle=self.bundle, type="host"), + maintenance_mode=MaintenanceMode.ON, + ) + host_2 = Host.objects.create( + fqdn="test_host_2", + prototype=Prototype.objects.create(bundle=self.bundle, type="host"), + maintenance_mode=MaintenanceMode.ON, + ) + HostComponent.objects.create( + cluster=self.cluster, + host=host_1, + service=self.service, + component=self.component, + ) + HostComponent.objects.create( + cluster=self.cluster, + host=host_2, + service=self.service, + component=self.component, + ) + + self.assertEqual(self.component.maintenance_mode, MaintenanceMode.ON) + + host_2.maintenance_mode = MaintenanceMode.OFF + host_2.save(update_fields=["maintenance_mode"]) + + self.assertEqual(self.component.maintenance_mode, MaintenanceMode.OFF) + + def test_maintenance_mode_by_service(self): + self.client.post( + path=reverse("service-maintenance-mode", kwargs={"service_id": self.service.pk}), + data={"maintenance_mode": MaintenanceMode.ON}, + content_type=APPLICATION_JSON, + ) + + self.service.refresh_from_db() + + self.assertEqual(self.component.maintenance_mode, MaintenanceMode.ON) diff --git a/python/cm/tests/test_config_group.py b/python/cm/tests/test_config_group.py index 070c762bd6..ec5e18c810 100644 --- a/python/cm/tests/test_config_group.py +++ b/python/cm/tests/test_config_group.py @@ -236,9 +236,7 @@ def test_update_parent_config(self): group.refresh_from_db() cl = ConfigLog.objects.get(id=group.config.current) - self.assertDictEqual( - cl.config, {"group": {"string": "str"}, "activatable_group": {"integer": 100}} - ) + self.assertDictEqual(cl.config, {"group": {"string": "str"}, "activatable_group": {"integer": 100}}) parent_cl.config = {"group": {"string": "string"}, "activatable_group": {"integer": 100}} parent_cl.attr = {"activatable_group": {"active": False}} @@ -246,9 +244,7 @@ def test_update_parent_config(self): group.refresh_from_db() cl = ConfigLog.objects.get(id=group.config.current) - self.assertDictEqual( - cl.config, {"group": {"string": "str"}, "activatable_group": {"integer": 100}} - ) + self.assertDictEqual(cl.config, {"group": {"string": "str"}, "activatable_group": {"integer": 100}}) self.assertDictEqual( cl.attr, { @@ -309,9 +305,7 @@ def test_create_config_for_group(self): } } ) - cl_new = ConfigLog.objects.create( - obj_ref=cl_current.obj_ref, config=cl_current.config, attr=attr - ) + cl_new = ConfigLog.objects.create(obj_ref=cl_current.obj_ref, config=cl_current.config, attr=attr) self.assertDictEqual(cl_current.attr, cl_new.attr) diff --git a/python/cm/tests/test_group_config_unselected.py b/python/cm/tests/test_group_config_unselected.py index d7cd029874..49b4de2c81 100644 --- a/python/cm/tests/test_group_config_unselected.py +++ b/python/cm/tests/test_group_config_unselected.py @@ -1,3 +1,14 @@ +# 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 django.contrib.contenttypes.models import ContentType from django.test import TestCase @@ -95,9 +106,7 @@ def test_empty_list_string_map(self): self.cluster, ) new_config = {"list": None, "string": None, "map": None, "structure": None, "json": {}} - with self.assertRaisesRegex( - AdcmEx, r"Value of `json` field is different in current and new config." - ): + with self.assertRaisesRegex(AdcmEx, r"Value of `json` field is different in current and new config."): check_value_unselected_field( self.cluster_config, new_config, @@ -115,9 +124,7 @@ def test_empty_list_string_map(self): "structure": {"test": []}, "json": None, } - with self.assertRaisesRegex( - AdcmEx, r"Value of `structure` field is different in current and new config." - ): + with self.assertRaisesRegex(AdcmEx, r"Value of `structure` field is different in current and new config."): check_value_unselected_field( self.cluster_config, new_config, @@ -134,9 +141,7 @@ def test_unequal_values(self): self.cluster.save() new_config = {"list": [], "string": "wow", "map": {}, "structure": {}, "json": {}} - with self.assertRaisesRegex( - AdcmEx, r"Value of `string` field is different in current and new config." - ): + with self.assertRaisesRegex(AdcmEx, r"Value of `string` field is different in current and new config."): check_value_unselected_field( self.cluster_config, new_config, @@ -148,9 +153,7 @@ def test_unequal_values(self): ) new_config = {"list": [1, 2, 3], "string": "", "map": {}, "structure": {}, "json": {}} - with self.assertRaisesRegex( - AdcmEx, r"Value of `list` field is different in current and new config." - ): + with self.assertRaisesRegex(AdcmEx, r"Value of `list` field is different in current and new config."): check_value_unselected_field( self.cluster_config, new_config, @@ -162,9 +165,7 @@ def test_unequal_values(self): ) new_config = {"list": [], "string": "", "map": {"key": 1}, "structure": {}, "json": {}} - with self.assertRaisesRegex( - AdcmEx, r"Value of `map` field is different in current and new config." - ): + with self.assertRaisesRegex(AdcmEx, r"Value of `map` field is different in current and new config."): check_value_unselected_field( self.cluster_config, new_config, @@ -176,9 +177,7 @@ def test_unequal_values(self): ) new_config = {"list": [], "string": "", "map": {}, "structure": {"key": 1}, "json": {}} - with self.assertRaisesRegex( - AdcmEx, r"Value of `structure` field is different in current and new config." - ): + with self.assertRaisesRegex(AdcmEx, r"Value of `structure` field is different in current and new config."): check_value_unselected_field( self.cluster_config, new_config, @@ -190,9 +189,7 @@ def test_unequal_values(self): ) new_config = {"list": [], "string": "", "map": {}, "structure": {}, "json": {"key": 1}} - with self.assertRaisesRegex( - AdcmEx, r"Value of `json` field is different in current and new config." - ): + with self.assertRaisesRegex(AdcmEx, r"Value of `json` field is different in current and new config."): check_value_unselected_field( self.cluster_config, new_config, diff --git a/python/cm/tests/test_hc.py b/python/cm/tests/test_hc.py index bad0a2c75e..2b709b0ad8 100644 --- a/python/cm/tests/test_hc.py +++ b/python/cm/tests/test_hc.py @@ -142,7 +142,7 @@ def test_empty_hostcomponent(self): "python/cm/tests/files", test_bundle_filename, ) - with open(test_bundle_path, encoding="utf-8") as f: + with open(test_bundle_path, encoding=settings.ENCODING_UTF_8) as f: response: Response = self.client.post( path=reverse("upload-bundle"), data={"file": f}, diff --git a/python/cm/tests/test_hierarchy.py b/python/cm/tests/test_hierarchy.py index bb6763b94d..51e1310bda 100644 --- a/python/cm/tests/test_hierarchy.py +++ b/python/cm/tests/test_hierarchy.py @@ -297,9 +297,7 @@ def test_get_directly_affected(self): for target_object, affected_objects in expected.items(): target_node = tree.get_node(hierarchy_objects[target_object]) - expected_affected = { - tree.get_node(hierarchy_objects[name]) for name in affected_objects - } + expected_affected = {tree.get_node(hierarchy_objects[name]) for name in affected_objects} got_affected = set(tree.get_directly_affected(target_node)) self.assertSetEqual(expected_affected, got_affected) @@ -422,9 +420,7 @@ def test_get_all_affected(self): for target_object, affected_objects in expected.items(): target_node = tree.get_node(hierarchy_objects[target_object]) - expected_affected = { - tree.get_node(hierarchy_objects[name]) for name in affected_objects - } + expected_affected = {tree.get_node(hierarchy_objects[name]) for name in affected_objects} got_affected = set(tree.get_all_affected(target_node)) self.assertSetEqual(expected_affected, got_affected) diff --git a/python/cm/tests/test_host.py b/python/cm/tests/test_host.py index 74ffc974f7..2e3b746eec 100644 --- a/python/cm/tests/test_host.py +++ b/python/cm/tests/test_host.py @@ -11,7 +11,6 @@ # limitations under the License. # pylint: disable=too-many-lines -import os import string from django.conf import settings @@ -27,10 +26,10 @@ ) from adcm.tests.base import APPLICATION_JSON, BaseTestCase -from cm.models import Bundle, Cluster, Host, HostProvider, Prototype +from cm.models import Bundle, Cluster, Host, HostProvider, MaintenanceMode, Prototype -class TestHostAPI(BaseTestCase): +class TestHostAPI(BaseTestCase): # pylint: disable=too-many-public-methods def setUp(self) -> None: super().setUp() @@ -61,11 +60,11 @@ def setUp(self) -> None: fqdn="test-fqdn", prototype=Prototype.objects.all()[0], provider=self.provider, - maintenance_mode="on", + maintenance_mode=MaintenanceMode.ON, ) def load_bundle(self, bundle_name): - with open(os.path.join(self.files_dir, bundle_name), encoding="utf-8") as f: + with open(self.files_dir / bundle_name, encoding=settings.ENCODING_UTF_8) as f: response: Response = self.client.post( path=reverse("upload-bundle"), data={"file": f}, @@ -81,19 +80,19 @@ def load_bundle(self, bundle_name): self.assertEqual(response.status_code, HTTP_200_OK) def get_host_proto_id(self): - response: Response = self.client.get(reverse("host-type")) + response: Response = self.client.get(reverse("host-prototype-list")) self.assertEqual(response.status_code, HTTP_200_OK) - for host in response.json(): + for host in response.json()["results"]: return host["bundle_id"], host["id"] def get_host_provider_proto_id(self): - response: Response = self.client.get(reverse("provider-type")) + response: Response = self.client.get(reverse("provider-prototype-list")) self.assertEqual(response.status_code, HTTP_200_OK) - for provider in response.json(): + for provider in response.json()["results"]: return provider["bundle_id"], provider["id"] def check_incorrect_fqdn_update(self, response: Response, expected_fqdn: str): @@ -112,11 +111,35 @@ def check_success_fqdn_update(self, response: Response, expected_fqdn: str): self.host.refresh_from_db() self.assertEqual(self.host.fqdn, expected_fqdn) + def check_maintenance_mode_can_be_changed(self, host: Host): + new_mm = MaintenanceMode.ON if host.maintenance_mode == MaintenanceMode.OFF else MaintenanceMode.OFF + response = self.client.put( + path=reverse("host-details", args=[host.pk]), + data={ + "fqdn": host.fqdn, + "maintenance_mode": new_mm, + "description": host.description, + "provider_id": host.provider_id, + "prototype_id": host.prototype_id, + }, + content_type=APPLICATION_JSON, + ) + self.assertEqual(response.status_code, HTTP_200_OK) + self.assertEqual(response.json()["maintenance_mode"], new_mm) + + new_mm = MaintenanceMode.ON if new_mm == MaintenanceMode.OFF else MaintenanceMode.OFF + response = self.client.patch( + path=reverse("host-details", args=[host.pk]), + data={"maintenance_mode": new_mm}, + content_type=APPLICATION_JSON, + ) + self.assertEqual(response.status_code, HTTP_200_OK) + self.assertEqual(response.json()["maintenance_mode"], new_mm) + def test_host(self): # pylint: disable=too-many-statements host = "test.server.net" host_url = reverse("host") - # self.load_bundle(self.bundle_ssh_name) ssh_bundle_id, host_proto = self.get_host_proto_id() response: Response = self.client.post(host_url, {}) @@ -124,9 +147,7 @@ def test_host(self): # pylint: disable=too-many-statements self.assertEqual(response.status_code, HTTP_400_BAD_REQUEST) self.assertEqual(response.json()["fqdn"], ["This field is required."]) - response: Response = self.client.post( - host_url, {"fqdn": host, "prototype_id": host_proto, "provider_id": 0} - ) + response: Response = self.client.post(host_url, {"fqdn": host, "prototype_id": host_proto, "provider_id": 0}) self.assertEqual(response.status_code, HTTP_404_NOT_FOUND) self.assertEqual(response.json()["code"], "PROVIDER_NOT_FOUND") @@ -140,9 +161,7 @@ def test_host(self): # pylint: disable=too-many-statements provider_id = response.json()["id"] - response: Response = self.client.post( - host_url, {"fqdn": host, "prototype_id": 42, "provider_id": provider_id} - ) + response: Response = self.client.post(host_url, {"fqdn": host, "prototype_id": 42, "provider_id": provider_id}) self.assertEqual(response.status_code, HTTP_404_NOT_FOUND) self.assertEqual(response.json()["code"], "PROTOTYPE_NOT_FOUND") @@ -204,7 +223,6 @@ def test_host(self): # pylint: disable=too-many-statements "prototype_id": ["This field is required."], "provider_id": ["This field is required."], "fqdn": ["This field is required."], - "maintenance_mode": ["This field is required."], }, ) @@ -229,23 +247,17 @@ def test_host(self): # pylint: disable=too-many-statements self.assertEqual(response.status_code, HTTP_404_NOT_FOUND) self.assertEqual(response.json()["code"], "HOST_NOT_FOUND") - response: Response = self.client.delete( - path=reverse("bundle-details", kwargs={"bundle_id": ssh_bundle_id}) - ) + response: Response = self.client.delete(path=reverse("bundle-detail", kwargs={"bundle_pk": ssh_bundle_id})) self.assertEqual(response.status_code, HTTP_409_CONFLICT) self.assertEqual(response.json()["code"], "BUNDLE_CONFLICT") - response: Response = self.client.delete( - path=reverse("provider-details", kwargs={"provider_id": provider_id}) - ) + response: Response = self.client.delete(path=reverse("provider-details", kwargs={"provider_id": provider_id})) self.assertEqual(response.status_code, HTTP_204_NO_CONTENT) self.provider.delete() - response: Response = self.client.delete( - path=reverse("bundle-details", kwargs={"bundle_id": ssh_bundle_id}) - ) + response: Response = self.client.delete(path=reverse("bundle-detail", kwargs={"bundle_pk": ssh_bundle_id})) self.assertEqual(response.status_code, HTTP_204_NO_CONTENT) @@ -254,7 +266,7 @@ def test_host_update_fqdn_success(self): response: Response = self.client.patch( path=reverse("host-details", kwargs={"host_id": self.host.pk}), - data={"fqdn": new_test_fqdn, "maintenance_mode": "on"}, + data={"fqdn": new_test_fqdn, "maintenance_mode": MaintenanceMode.ON}, content_type=APPLICATION_JSON, ) self.host.refresh_from_db() @@ -268,7 +280,7 @@ def test_host_update_same_fqdn_success(self): response: Response = self.client.patch( path=reverse("host-details", kwargs={"host_id": self.host.pk}), - data={"fqdn": self.host.fqdn, "maintenance_mode": "on"}, + data={"fqdn": self.host.fqdn, "maintenance_mode": MaintenanceMode.ON}, content_type=APPLICATION_JSON, ) @@ -280,7 +292,7 @@ def test_host_update_fqdn_not_created_state_fail(self): response: Response = self.client.patch( path=reverse("host-details", kwargs={"host_id": self.host.pk}), - data={"fqdn": "new-test-fqdn", "maintenance_mode": "on"}, + data={"fqdn": "new-test-fqdn", "maintenance_mode": MaintenanceMode.ON}, content_type=APPLICATION_JSON, ) @@ -301,7 +313,7 @@ def test_host_update_fqdn_has_cluster_fail(self): response: Response = self.client.patch( path=reverse("host-details", kwargs={"host_id": self.host.pk}), - data={"fqdn": "new-test-fqdn", "maintenance_mode": "on"}, + data={"fqdn": "new-test-fqdn", "maintenance_mode": MaintenanceMode.ON}, content_type=APPLICATION_JSON, ) @@ -352,7 +364,7 @@ def test_host_update_duplicated_fqdn_fail(self): fqdn=fqdn, prototype=Prototype.objects.all()[0], provider=self.provider, - maintenance_mode="disabled", + maintenance_mode=MaintenanceMode.OFF, ) response = self.client.put( @@ -404,9 +416,7 @@ def test_host_create_fqdn_validation(self): self.assertEqual(err["code"], "WRONG_NAME") else: self.assertIn("fqdn", err) - self.assertEqual( - err["fqdn"], ["Ensure this field has no more than 253 characters."] - ) + self.assertEqual(err["fqdn"], ["Ensure this field has no more than 253 characters."]) self.assertEqual(Host.objects.count(), amount_of_hosts) for value in self.correct_values: @@ -420,7 +430,7 @@ def test_host_create_fqdn_validation(self): self.assertEqual(response.json()["fqdn"], value) def test_host_update_fqdn_validation(self): - self.host.maintenance_mode = "disabled" + self.host.maintenance_mode = MaintenanceMode.OFF self.host.save(update_fields=["maintenance_mode"]) fqdn = self.host.fqdn default_values = { diff --git a/python/cm/tests/test_inventory.py b/python/cm/tests/test_inventory.py index f6d42726bc..c06449cba6 100644 --- a/python/cm/tests/test_inventory.py +++ b/python/cm/tests/test_inventory.py @@ -79,9 +79,7 @@ def test_process_config_and_attr(self, mock_get_prototype_config, mock_process_c def test_get_obj_config(self, mock_process_config_and_attr): get_obj_config(self.cluster) config_log = ConfigLog.objects.get(id=self.cluster.config.current) - mock_process_config_and_attr.assert_called_once_with( - self.cluster, config_log.config, config_log.attr - ) + mock_process_config_and_attr.assert_called_once_with(self.cluster, config_log.config, config_log.attr) @patch("cm.inventory.get_import") @patch("cm.inventory.get_obj_config") @@ -193,13 +191,9 @@ def test_get_host(self, mock_get_hosts, mock_get_provider_hosts): def test_prepare_job_inventory(self, mock_open, mock_dump): # pylint: disable=too-many-locals - host2 = Host.objects.create( - prototype=self.host_pt, fqdn="h2", cluster=self.cluster, provider=self.provider - ) + host2 = Host.objects.create(prototype=self.host_pt, fqdn="h2", cluster=self.cluster, provider=self.provider) action = Action.objects.create(prototype=self.cluster_pt) - job = JobLog.objects.create( - action=action, start_date=timezone.now(), finish_date=timezone.now() - ) + job = JobLog.objects.create(action=action, start_date=timezone.now(), finish_date=timezone.now()) fd = Mock() mock_open.return_value = fd @@ -360,21 +354,13 @@ def test_host_vars(self): groups.append(gen_group("component_1", component_11.id, "servicecomponent")) for group in groups: group.hosts.add(self.host) - update_obj_config( - group.config, {"some_string": group.name}, {"group_keys": {"some_string": True}} - ) + update_obj_config(group.config, {"some_string": group.name}, {"group_keys": {"some_string": True}}) - self.assertDictEqual( - get_host_vars(self.host, self.cluster)["cluster"]["config"], {"some_string": "cluster"} - ) + self.assertDictEqual(get_host_vars(self.host, self.cluster)["cluster"]["config"], {"some_string": "cluster"}) service_1_host_vars = get_host_vars(self.host, service_1) - self.assertDictEqual( - service_1_host_vars["services"]["service_1"]["config"], {"some_string": "service_1"} - ) - self.assertDictEqual( - service_1_host_vars["services"]["service_2"]["config"], {"some_string": "service_2"} - ) + self.assertDictEqual(service_1_host_vars["services"]["service_1"]["config"], {"some_string": "service_1"}) + self.assertDictEqual(service_1_host_vars["services"]["service_2"]["config"], {"some_string": "service_2"}) self.assertDictEqual( service_1_host_vars["services"]["service_1"]["component_11"]["config"], {"some_string": "component_1"}, @@ -389,9 +375,7 @@ def test_host_vars(self): ) component_11_host_vars = get_host_vars(self.host, component_11) - self.assertDictEqual( - component_11_host_vars["services"]["service_1"]["config"], {"some_string": "service_1"} - ) + self.assertDictEqual(component_11_host_vars["services"]["service_1"]["config"], {"some_string": "service_1"}) self.assertDictEqual( component_11_host_vars["services"]["service_1"]["component_11"]["config"], {"some_string": "component_1"}, diff --git a/python/cm/tests/test_issue.py b/python/cm/tests/test_issue.py index 307a729c51..2174d6ecb2 100644 --- a/python/cm/tests/test_issue.py +++ b/python/cm/tests/test_issue.py @@ -194,9 +194,7 @@ def test_service_imported(self): b2, _, cluster2 = self.cook_cluster("Monitoring", "Cluster2") proto3 = Prototype.objects.create(type="service", name="Graphana", bundle=b2) service = add_service_to_cluster(cluster2, proto3) - ClusterBind.objects.create( - cluster=cluster1, source_cluster=cluster2, source_service=service - ) + ClusterBind.objects.create(cluster=cluster1, source_cluster=cluster2, source_service=service) self.assertEqual(do_check_import(cluster1), (True, "SERVICE_IMPORTED")) @@ -220,9 +218,7 @@ def test_import_service_to_service(self): b2, _, cluster2 = self.cook_cluster("Monitoring", "Cluster2") proto3 = Prototype.objects.create(type="service", name="Graphana", bundle=b2) service2 = add_service_to_cluster(cluster2, proto3) - ClusterBind.objects.create( - cluster=cluster1, service=service1, source_cluster=cluster2, source_service=service2 - ) + ClusterBind.objects.create(cluster=cluster1, service=service1, source_cluster=cluster2, source_service=service2) self.assertEqual(do_check_import(cluster1, service1), (True, "SERVICE_IMPORTED")) diff --git a/python/cm/tests/test_job.py b/python/cm/tests/test_job.py index dca20e76be..50d2204f3a 100644 --- a/python/cm/tests/test_job.py +++ b/python/cm/tests/test_job.py @@ -15,11 +15,11 @@ from pathlib import Path from unittest.mock import Mock, patch +from django.conf import settings from django.utils import timezone from adcm.tests.base import BaseTestCase from cm.api import add_cluster, add_service_to_cluster -from cm.config import BASE_DIR, BUNDLE_DIR, RUN_DIR, Job from cm.job import ( check_cluster, check_service_task, @@ -46,6 +46,7 @@ Host, HostProvider, JobLog, + JobStatus, Prototype, ServiceComponent, SubAction, @@ -72,10 +73,8 @@ def test_set_job_status(self): bundle = Bundle.objects.create() prototype = Prototype.objects.create(bundle=bundle) action = Action.objects.create(prototype=prototype) - job = JobLog.objects.create( - action=action, start_date=timezone.now(), finish_date=timezone.now() - ) - status = Job.RUNNING + job = JobLog.objects.create(action=action, start_date=timezone.now(), finish_date=timezone.now()) + status = JobStatus.RUNNING pid = 10 event = Mock() @@ -92,14 +91,12 @@ def test_set_task_status(self): bundle = Bundle.objects.create() prototype = Prototype.objects.create(bundle=bundle) action = Action.objects.create(prototype=prototype) - task = TaskLog.objects.create( - action=action, object_id=1, start_date=timezone.now(), finish_date=timezone.now() - ) + task = TaskLog.objects.create(action=action, object_id=1, start_date=timezone.now(), finish_date=timezone.now()) - set_task_status(task, Job.RUNNING, event) + set_task_status(task, JobStatus.RUNNING, event) - self.assertEqual(task.status, Job.RUNNING) - event.set_task_status.assert_called_once_with(task.id, Job.RUNNING) + self.assertEqual(task.status, JobStatus.RUNNING) + event.set_task_status.assert_called_once_with(task.id, JobStatus.RUNNING) def test_get_state_single_job(self): bundle = gen_bundle() @@ -118,9 +115,9 @@ def test_get_state_single_job(self): # status: expected state, expected multi_state set, expected multi_state unset test_data = [ - [Job.SUCCESS, "success", ["success"], ["success unset"]], - [Job.FAILED, "fail", ["fail"], ["fail unset"]], - [Job.ABORTED, None, [], []], + [JobStatus.SUCCESS, "success", ["success"], ["success unset"]], + [JobStatus.FAILED, "fail", ["fail"], ["fail unset"]], + [JobStatus.ABORTED, None, [], []], ] for status, exp_state, exp_m_state_set, exp_m_state_unset in test_data: state, m_state_set, m_state_unset = get_state(action, job, status) @@ -147,9 +144,9 @@ def test_get_state_multi_job(self): # status: expected state, expected multi_state set, expected multi_state unset test_data = [ - [Job.SUCCESS, "success", ["success"], ["success unset"]], - [Job.FAILED, "sub_action fail", ["fail"], ["fail unset"]], - [Job.ABORTED, None, [], []], + [JobStatus.SUCCESS, "success", ["success"], ["success unset"]], + [JobStatus.FAILED, "sub_action fail", ["fail"], ["fail unset"]], + [JobStatus.ABORTED, None, [], []], ] for status, exp_state, exp_m_state_set, exp_m_state_unset in test_data: state, m_state_set, m_state_unset = get_state(action, job, status) @@ -169,9 +166,7 @@ def test_set_action_state(self): host_provider = HostProvider.objects.create(prototype=prototype) adcm = ADCM.objects.create(prototype=prototype) action = Action.objects.create(prototype=prototype) - task = TaskLog.objects.create( - action=action, object_id=1, start_date=timezone.now(), finish_date=timezone.now() - ) + task = TaskLog.objects.create(action=action, object_id=1, start_date=timezone.now(), finish_date=timezone.now()) to_set = "to set" to_unset = "to unset" for obj in (adcm, cluster, cluster_object, host_provider, host): @@ -193,7 +188,7 @@ def test_set_action_state(self): self.assertIn(to_set, obj.multi_state) self.assertNotIn(to_unset, obj.multi_state) - @patch("cm.job.api.save_hc") + @patch("cm.job.save_hc") def test_restore_hc(self, mock_save_hc): bundle = Bundle.objects.create() prototype = Prototype.objects.create(bundle=bundle) @@ -221,10 +216,10 @@ def test_restore_hc(self, mock_save_hc): hostcomponentmap=hostcomponentmap, ) - restore_hc(task, action, Job.FAILED) + restore_hc(task, action, JobStatus.FAILED) mock_save_hc.assert_called_once_with(cluster, [(cluster_object, host, service_component)]) - @patch("cm.job.err") + @patch("cm.job.raise_adcm_ex") def test_check_service_task(self, mock_err): bundle = Bundle.objects.create() prototype = Prototype.objects.create(bundle=bundle) @@ -237,7 +232,7 @@ def test_check_service_task(self, mock_err): self.assertEqual(cluster_object, service) self.assertEqual(mock_err.call_count, 0) - @patch("cm.job.err") + @patch("cm.job.raise_adcm_ex") def test_check_cluster(self, mock_err): bundle = Bundle.objects.create() prototype = Prototype.objects.create(bundle=bundle) @@ -250,17 +245,13 @@ def test_check_cluster(self, mock_err): @patch("cm.job.prepare_ansible_config") @patch("cm.job.prepare_job_config") - @patch("cm.job.inventory.prepare_job_inventory") - def test_prepare_job( - self, mock_prepare_job_inventory, mock_prepare_job_config, mock_prepare_ansible_config - ): + @patch("cm.job.prepare_job_inventory") + def test_prepare_job(self, mock_prepare_job_inventory, mock_prepare_job_config, mock_prepare_ansible_config): bundle = Bundle.objects.create() prototype = Prototype.objects.create(bundle=bundle) cluster = Cluster.objects.create(prototype=prototype) action = Action.objects.create(prototype=prototype) - job = JobLog.objects.create( - action=action, start_date=timezone.now(), finish_date=timezone.now() - ) + job = JobLog.objects.create(action=action, start_date=timezone.now(), finish_date=timezone.now()) prepare_job(action, None, job.id, cluster, "", {}, None, False) @@ -295,16 +286,14 @@ def test_prepare_context(self): service = add_service_to_cluster(cluster, proto2) context = prepare_context(action2, service) - self.assertDictEqual( - context, {"type": "service", "service_id": service.id, "cluster_id": cluster.id} - ) + self.assertDictEqual(context, {"type": "service", "service_id": service.id, "cluster_id": cluster.id}) def test_get_bundle_root(self): bundle = Bundle.objects.create() prototype = Prototype.objects.create(bundle=bundle) action = Action.objects.create(prototype=prototype) - data = [("adcm", str(Path(BASE_DIR, "conf"))), ("", BUNDLE_DIR)] + data = [("adcm", str(Path(settings.BASE_DIR, "conf"))), ("", str(settings.BUNDLE_DIR))] for prototype_type, test_path in data: prototype.type = prototype_type @@ -320,23 +309,23 @@ def test_cook_script(self, mock_get_bundle_root): prototype = Prototype.objects.create(bundle=bundle) action = Action.objects.create(prototype=prototype) sub_action = SubAction.objects.create(action=action, script="ansible/sleep.yaml") - mock_get_bundle_root.return_value = BUNDLE_DIR + mock_get_bundle_root.return_value = str(settings.BUNDLE_DIR) data = [ ( sub_action, "main.yaml", - str(Path(BUNDLE_DIR, action.prototype.bundle.hash, "ansible/sleep.yaml")), + str(Path(settings.BUNDLE_DIR, action.prototype.bundle.hash, "ansible/sleep.yaml")), ), ( None, "main.yaml", - str(Path(BUNDLE_DIR, action.prototype.bundle.hash, "main.yaml")), + str(Path(settings.BUNDLE_DIR, action.prototype.bundle.hash, "main.yaml")), ), ( None, "./main.yaml", - str(Path(BUNDLE_DIR, action.prototype.bundle.hash, "main.yaml")), + str(Path(settings.BUNDLE_DIR, action.prototype.bundle.hash, "main.yaml")), ), ] @@ -383,14 +372,12 @@ def test_prepare_job_config( mock_open.return_value = fd mock_get_adcm_config.return_value = {} mock_prepare_context.return_value = {"type": "cluster", "cluster_id": 1} - mock_get_bundle_root.return_value = BUNDLE_DIR + mock_get_bundle_root.return_value = str(settings.BUNDLE_DIR) mock_cook_script.return_value = str( - Path(BUNDLE_DIR, cluster_action.prototype.bundle.hash, cluster_action.script) + Path(settings.BUNDLE_DIR, cluster_action.prototype.bundle.hash, cluster_action.script) ) - job = JobLog.objects.create( - action=cluster_action, start_date=timezone.now(), finish_date=timezone.now() - ) + job = JobLog.objects.create(action=cluster_action, start_date=timezone.now(), finish_date=timezone.now()) conf = "test" proto4 = Prototype.objects.create(bundle=bundle, type="provider") @@ -418,7 +405,7 @@ def test_prepare_job_config( "env": { "run_dir": mock_dump.call_args[0][0]["env"]["run_dir"], "log_dir": mock_dump.call_args[0][0]["env"]["log_dir"], - "tmp_dir": str(Path(RUN_DIR, f"{job.id}", "tmp")), + "tmp_dir": str(Path(settings.RUN_DIR, f"{job.id}", "tmp")), "stack_dir": mock_dump.call_args[0][0]["env"]["stack_dir"], "status_api_token": mock_dump.call_args[0][0]["env"]["status_api_token"], }, @@ -462,7 +449,9 @@ def test_prepare_job_config( job_config["job"]["hostgroup"] = "127.0.0.1" mock_open.assert_called_with( - Path(f"{RUN_DIR}", f"{job.id}", "config.json"), "w", encoding="utf_8" + settings.RUN_DIR / f"{job.id}" / "config.json", + "w", + encoding=settings.ENCODING_UTF_8, ) mock_dump.assert_called_with(job_config, fd, indent=3, sort_keys=True) mock_get_adcm_config.assert_called() @@ -474,9 +463,7 @@ def test_prepare_job_config( @patch("cm.job.get_old_hc") @patch("cm.job.get_actual_hc") @patch("cm.job.prepare_job") - def test_re_prepare_job( - self, mock_prepare_job, mock_get_actual_hc, mock_get_old_hc, mock_cook_delta - ): + def test_re_prepare_job(self, mock_prepare_job, mock_get_actual_hc, mock_get_old_hc, mock_cook_delta): # pylint: disable=too-many-locals new_hc = Mock() @@ -527,6 +514,4 @@ def test_re_prepare_job( mock_get_actual_hc.assert_called_once_with(cluster) mock_get_old_hc.assert_called_once_with(task.hostcomponentmap) mock_cook_delta.assert_called_once_with(cluster, new_hc, action.hostcomponentmap, old_hc) - mock_prepare_job.assert_called_once_with( - action, sub_action, job.id, cluster, task.config, delta, None, False - ) + mock_prepare_job.assert_called_once_with(action, sub_action, job.id, cluster, task.config, delta, None, False) diff --git a/python/cm/tests/test_service.py b/python/cm/tests/test_service.py index 0ff8a4d6cc..acebffdb09 100644 --- a/python/cm/tests/test_service.py +++ b/python/cm/tests/test_service.py @@ -1,35 +1,144 @@ +# 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 django.urls import reverse -from rest_framework import status +from rest_framework.response import Response +from rest_framework.status import HTTP_204_NO_CONTENT, HTTP_409_CONFLICT from adcm.tests.base import APPLICATION_JSON, BaseTestCase -from cm.models import Bundle, Cluster, ClusterObject, Prototype +from cm.models import ( + Bundle, + Cluster, + ClusterObject, + Host, + HostComponent, + MaintenanceMode, + Prototype, + ServiceComponent, +) -class TestCluster(BaseTestCase): +class TestService(BaseTestCase): def setUp(self) -> None: super().setUp() self.bundle = Bundle.objects.create() - self.prototype = Prototype.objects.create( - name="test_prototype_name", type="cluster", bundle=self.bundle - ) + self.prototype = Prototype.objects.create(name="test_prototype_name", type="cluster", bundle=self.bundle) self.prototype_service = Prototype.objects.create(type="service", bundle=self.bundle) self.cluster = Cluster.objects.create(name="test_cluster_name", prototype=self.prototype) - self.service = ClusterObject.objects.create( - cluster=self.cluster, prototype=self.prototype_service - ) + self.service = ClusterObject.objects.create(cluster=self.cluster, prototype=self.prototype_service) - def test_service_deletion(self): + def test_delete(self): self.service.state = "updated" self.service.save(update_fields=["state"]) - url = reverse( - "service-details", kwargs={"cluster_id": self.cluster.pk, "service_id": self.service.pk} - ) - response = self.client.delete(path=url, content_type=APPLICATION_JSON) - self.assertEqual(response.status_code, status.HTTP_409_CONFLICT) + url = reverse("service-details", kwargs={"cluster_id": self.cluster.pk, "service_id": self.service.pk}) + + response: Response = self.client.delete(path=url, content_type=APPLICATION_JSON) + + self.assertEqual(response.status_code, HTTP_409_CONFLICT) self.assertEqual(response.json()["code"], "SERVICE_DELETE_ERROR") self.service.state = "created" self.service.save(update_fields=["state"]) - response = self.client.delete(path=url, content_type=APPLICATION_JSON) - self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) + + response: Response = self.client.delete(path=url, content_type=APPLICATION_JSON) + + self.assertEqual(response.status_code, HTTP_204_NO_CONTENT) + + def test_maintenance_mode_by_hosts(self): + host_1 = Host.objects.create( + fqdn="test_host_1", + prototype=Prototype.objects.create(bundle=self.bundle, type="host"), + maintenance_mode=MaintenanceMode.ON, + ) + host_2 = Host.objects.create( + fqdn="test_host_2", + prototype=Prototype.objects.create(bundle=self.bundle, type="host"), + maintenance_mode=MaintenanceMode.ON, + ) + component = ServiceComponent.objects.create( + prototype=Prototype.objects.create( + bundle=self.bundle, + type="component", + display_name="test_component", + ), + cluster=self.cluster, + service=self.service, + ) + HostComponent.objects.create( + cluster=self.cluster, + host=host_1, + service=self.service, + component=component, + ) + HostComponent.objects.create( + cluster=self.cluster, + host=host_2, + service=self.service, + component=component, + ) + + self.assertEqual(self.service.maintenance_mode, MaintenanceMode.ON) + + host_2.maintenance_mode = MaintenanceMode.OFF + host_2.save(update_fields=["maintenance_mode"]) + + self.assertEqual(self.service.maintenance_mode, MaintenanceMode.OFF) + + def test_maintenance_mode_by_components(self): + component_1 = ServiceComponent.objects.create( + prototype=Prototype.objects.create( + bundle=self.bundle, + type="component", + display_name="test_component_1", + ), + cluster=self.cluster, + service=self.service, + _maintenance_mode=MaintenanceMode.ON, + ) + component_2 = ServiceComponent.objects.create( + prototype=Prototype.objects.create( + bundle=self.bundle, + type="component", + display_name="test_component_2", + ), + cluster=self.cluster, + service=self.service, + _maintenance_mode=MaintenanceMode.ON, + ) + host = Host.objects.create( + fqdn="test_host", + prototype=Prototype.objects.create(bundle=self.bundle, type="host"), + maintenance_mode=MaintenanceMode.OFF, + ) + HostComponent.objects.create( + cluster=self.cluster, + host=host, + service=self.service, + component=component_1, + ) + HostComponent.objects.create( + cluster=self.cluster, + host=host, + service=self.service, + component=component_2, + ) + + self.assertEqual(self.service.maintenance_mode, MaintenanceMode.ON) + + self.client.post( + path=reverse("component-maintenance-mode", kwargs={"component_id": component_2.pk}), + data={"maintenance_mode": MaintenanceMode.OFF}, + content_type=APPLICATION_JSON, + ) + + self.assertEqual(self.service.maintenance_mode, MaintenanceMode.OFF) diff --git a/python/cm/tests/test_signals.py b/python/cm/tests/test_signals.py new file mode 100644 index 0000000000..bb141da55c --- /dev/null +++ b/python/cm/tests/test_signals.py @@ -0,0 +1,51 @@ +# 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 unittest.mock import patch + +from django.contrib.contenttypes.models import ContentType +from django.db.models.signals import m2m_changed, post_delete, post_save + +from adcm.tests.base import BaseTestCase +from cm.models import Bundle, Cluster, GroupConfig, Host, Prototype +from rbac.models import User + + +class SignalsTest(BaseTestCase): + """Tests for `cm.models.MessageTemplate` methods""" + + @patch("cm.signals.model_delete") + @patch("cm.signals.model_change") + def test_update_delete_signals(self, model_change, model_delete): + post_save.connect(model_change, sender=User) + post_delete.connect(model_delete, sender=User) + User.objects.create_user("test_user_2", "", "") + User.objects.filter(username="test_user_2").delete() + model_change.assert_called_once() + model_delete.assert_called_once() + + @patch("cm.signals.m2m_change") + def test_m2m_signals(self, m2m_change): + m2m_changed.connect(m2m_change, sender=GroupConfig.hosts.through) + bundle = Bundle.objects.create() + cluster = Cluster.objects.create( + prototype=Prototype.objects.create(type="cluster", name="prototype", bundle=bundle) + ) + gc = GroupConfig.objects.create( + object_id=cluster.id, object_type=ContentType.objects.get(model="cluster"), name="group" + ) + host = Host.objects.create( + cluster=cluster, prototype=Prototype.objects.create(type="host", name="prototype_2", bundle=bundle) + ) + + gc.hosts.add(host) + self.assertEqual(2, m2m_change.call_count) diff --git a/python/cm/tests/test_task_log.py b/python/cm/tests/test_task_log.py index 68ccda98d6..45f7795c0f 100644 --- a/python/cm/tests/test_task_log.py +++ b/python/cm/tests/test_task_log.py @@ -84,9 +84,7 @@ def test_unlock_affected(self): self.assertFalse(cluster.locked) self.assertIsNone(task.lock) - @override_settings( - RUN_DIR=settings.BASE_DIR / "python" / "cm" / "tests" / "files" / "task_log_download" - ) + @override_settings(RUN_DIR=settings.BASE_DIR / "python" / "cm" / "tests" / "files" / "task_log_download") def test_download(self): bundle = Bundle.objects.create() cluster = Cluster.objects.create( @@ -197,14 +195,12 @@ def test_download(self): LogStorage.objects.create(job=job_no_files, body="stderr db", type="stderr", format="txt") response: Response = self.client.get( - path=reverse("task-download", kwargs={"task_id": task.pk}), + path=reverse("tasklog-download", kwargs={"task_pk": task.pk}), ) self.assertEqual(response.status_code, HTTP_200_OK) - @override_settings( - RUN_DIR=settings.BASE_DIR / "python" / "cm" / "tests" / "files" / "task_log_download" - ) + @override_settings(RUN_DIR=settings.BASE_DIR / "python" / "cm" / "tests" / "files" / "task_log_download") def test_download_negative(self): bundle = Bundle.objects.create() cluster = Cluster.objects.create( diff --git a/python/cm/tests/test_upgrade.py b/python/cm/tests/test_upgrade.py index 656423794f..b649830b18 100644 --- a/python/cm/tests/test_upgrade.py +++ b/python/cm/tests/test_upgrade.py @@ -84,9 +84,7 @@ def cook_provider(bundle, name): def cook_upgrade(bundle): - return Upgrade.objects.create( - bundle=bundle, min_version="1.0", max_version="2.0", state_available=["created"] - ) + return Upgrade.objects.create(bundle=bundle, min_version="1.0", max_version="2.0", state_available=["created"]) def get_config(obj): diff --git a/python/cm/tests/utils.py b/python/cm/tests/utils.py index c364e79c69..aacfef7e35 100644 --- a/python/cm/tests/utils.py +++ b/python/cm/tests/utils.py @@ -165,14 +165,10 @@ def gen_host_component(component: ServiceComponent, host: Host) -> HostComponent ) -def gen_concern_item( - concern_type, name: str | None = None, reason=None, blocking=True, owner=None -) -> ConcernItem: +def gen_concern_item(concern_type, name: str | None = None, reason=None, blocking=True, owner=None) -> ConcernItem: """Generate ConcernItem object""" reason = reason or {"message": "Test", "placeholder": {}} - return ConcernItem.objects.create( - type=concern_type, name=name, reason=reason, blocking=blocking, owner=owner - ) + return ConcernItem.objects.create(type=concern_type, name=name, reason=reason, blocking=blocking, owner=owner) def gen_action(name: str | None = None, bundle=None, prototype=None) -> Action: diff --git a/python/cm/upgrade.py b/python/cm/upgrade.py index caf2a41d5c..44fc15b619 100644 --- a/python/cm/upgrade.py +++ b/python/cm/upgrade.py @@ -1,6 +1,6 @@ -# 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 +# 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 # @@ -16,9 +16,6 @@ from django.db import transaction from version_utils import rpm -import cm.issue -import cm.job -import cm.status_api from cm.adcm_config import make_object_config, obj_ref, proto_ref, switch_config from cm.api import ( add_components_to_service, @@ -26,7 +23,9 @@ check_license, version_in, ) -from cm.errors import raise_adcm_ex as err +from cm.errors import raise_adcm_ex +from cm.issue import update_hierarchy_issues +from cm.job import start_task from cm.logger import logger from cm.models import ( Cluster, @@ -35,17 +34,17 @@ Host, HostComponent, HostProvider, - MaintenanceModeType, + MaintenanceMode, Prototype, PrototypeImport, ServiceComponent, Upgrade, ) +from cm.status_api import post_event def switch_object(obj: Union[Host, ClusterObject], new_prototype: Prototype) -> None: - """Upgrade object""" - logger.info('upgrade switch from %s to %s', proto_ref(obj.prototype), proto_ref(new_prototype)) + logger.info("upgrade switch from %s to %s", proto_ref(obj.prototype), proto_ref(new_prototype)) old_prototype = obj.prototype obj.prototype = new_prototype obj.save() @@ -53,13 +52,9 @@ def switch_object(obj: Union[Host, ClusterObject], new_prototype: Prototype) -> def switch_services(upgrade: Upgrade, cluster: Cluster) -> None: - """Upgrade services and component""" - for service in ClusterObject.objects.filter(cluster=cluster): try: - prototype = Prototype.objects.get( - bundle=upgrade.bundle, type='service', name=service.prototype.name - ) + prototype = Prototype.objects.get(bundle=upgrade.bundle, type="service", name=service.prototype.name) switch_object(service, prototype) switch_components(cluster, service, prototype) except Prototype.DoesNotExist: @@ -69,17 +64,14 @@ def switch_services(upgrade: Upgrade, cluster: Cluster) -> None: def switch_components(cluster: Cluster, co: ClusterObject, new_co_proto: Prototype) -> None: - """Upgrade components""" for sc in ServiceComponent.objects.filter(cluster=cluster, service=co): try: - new_sc_prototype = Prototype.objects.get( - parent=new_co_proto, type='component', name=sc.prototype.name - ) + new_sc_prototype = Prototype.objects.get(parent=new_co_proto, type="component", name=sc.prototype.name) switch_object(sc, new_sc_prototype) except Prototype.DoesNotExist: sc.delete() - for sc_proto in Prototype.objects.filter(parent=new_co_proto, type='component'): + for sc_proto in Prototype.objects.filter(parent=new_co_proto, type="component"): kwargs = dict(cluster=cluster, service=co, prototype=sc_proto) if not ServiceComponent.objects.filter(**kwargs).exists(): sc = ServiceComponent.objects.create(**kwargs) @@ -87,81 +79,91 @@ def switch_components(cluster: Cluster, co: ClusterObject, new_co_proto: Prototy def switch_hosts(upgrade: Upgrade, provider: HostProvider) -> None: - """Upgrade hosts""" - for prototype in Prototype.objects.filter(bundle=upgrade.bundle, type='host'): + for prototype in Prototype.objects.filter(bundle=upgrade.bundle, type="host"): for host in Host.objects.filter(provider=provider, prototype__name=prototype.name): switch_object(host, prototype) def check_upgrade_version(obj: Union[Cluster, HostProvider], upgrade: Upgrade) -> Tuple[bool, str]: proto = obj.prototype - # log.debug('check %s < %s > %s', upgrade.min_version, proto.version, upgrade.max_version) if upgrade.min_strict: if rpm.compare_versions(proto.version, upgrade.min_version) <= 0: - msg = '{} version {} is less than or equal to upgrade min version {}' + msg = "{} version {} is less than or equal to upgrade min version {}" + return False, msg.format(proto.type, proto.version, upgrade.min_version) else: if rpm.compare_versions(proto.version, upgrade.min_version) < 0: - msg = '{} version {} is less than upgrade min version {}' + msg = "{} version {} is less than upgrade min version {}" + return False, msg.format(proto.type, proto.version, upgrade.min_version) + if upgrade.max_strict: if rpm.compare_versions(proto.version, upgrade.max_version) >= 0: - msg = '{} version {} is more than or equal to upgrade max version {}' + msg = "{} version {} is more than or equal to upgrade max version {}" + return False, msg.format(proto.type, proto.version, upgrade.max_version) else: if rpm.compare_versions(proto.version, upgrade.max_version) > 0: - msg = '{} version {} is more than upgrade max version {}' + msg = "{} version {} is more than upgrade max version {}" + return False, msg.format(proto.type, proto.version, upgrade.max_version) - return True, '' + + return True, "" def check_upgrade_edition(obj: Union[Cluster, HostProvider], upgrade: Upgrade) -> Tuple[bool, str]: if not upgrade.from_edition: - return True, '' + return True, "" + from_edition = upgrade.from_edition if obj.prototype.bundle.edition not in from_edition: msg = 'bundle edition "{}" is not in upgrade list: {}' + return False, msg.format(obj.prototype.bundle.edition, from_edition) - return True, '' + + return True, "" def check_upgrade_state(obj: Union[Cluster, HostProvider], upgrade: Upgrade) -> Tuple[bool, str]: if obj.locked: - return False, 'object is locked' + return False, "object is locked" + if upgrade.allowed(obj): - return True, '' + return True, "" else: - return False, 'no available states' + return False, "no available states" def check_upgrade_import( obj: Union[Cluster, HostProvider], upgrade: Upgrade ) -> Tuple[bool, str]: # pylint: disable=too-many-branches - def get_export(cbind): - if cbind.source_service: - return cbind.source_service + def get_export(_cbind): + if _cbind.source_service: + return _cbind.source_service else: - return cbind.source_cluster + return _cbind.source_cluster - def get_import(cbind): # pylint: disable=redefined-outer-name - if cbind.service: - return cbind.service + def get_import(_cbind): + if _cbind.service: + return _cbind.service else: - return cbind.cluster + return _cbind.cluster - if obj.prototype.type != 'cluster': - return True, '' + if obj.prototype.type != "cluster": + return True, "" for cbind in ClusterBind.objects.filter(cluster=obj): export = get_export(cbind) - impr_obj = get_import(cbind) + import_obj = get_import(cbind) try: proto = Prototype.objects.get( - bundle=upgrade.bundle, name=impr_obj.prototype.name, type=impr_obj.prototype.type + bundle=upgrade.bundle, + name=import_obj.prototype.name, + type=import_obj.prototype.type, ) except Prototype.DoesNotExist: - msg = 'Upgrade does not have new version of {} required for import' - return False, msg.format(proto_ref(impr_obj.prototype)) + msg = "Upgrade does not have new version of {} required for import" + return False, msg.format(proto_ref(import_obj.prototype)) try: pi = PrototypeImport.objects.get(prototype=proto, name=export.prototype.name) if not version_in(export.prototype.version, pi): @@ -178,35 +180,33 @@ def get_import(cbind): # pylint: disable=redefined-outer-name ), ) except PrototypeImport.DoesNotExist: - # msg = 'New version of {} does not have import "{}"' # ADCM-1507 - # return False, msg.format(proto_ref(proto), export.prototype.name) cbind.delete() for cbind in ClusterBind.objects.filter(source_cluster=obj): export = get_export(cbind) try: - proto = Prototype.objects.get( - bundle=upgrade.bundle, name=export.prototype.name, type=export.prototype.type - ) + proto = Prototype.objects.get(bundle=upgrade.bundle, name=export.prototype.name, type=export.prototype.type) except Prototype.DoesNotExist: - msg = 'Upgrade does not have new version of {} required for export' + msg = "Upgrade does not have new version of {} required for export" return False, msg.format(proto_ref(export.prototype)) + import_obj = get_import(cbind) pi = PrototypeImport.objects.get(prototype=import_obj.prototype, name=export.prototype.name) if not version_in(proto.version, pi): - msg = 'Export of {} does not match import versions: ({}, {}) ({})' + msg = "Export of {} does not match import versions: ({}, {}) ({})" return ( False, msg.format(proto_ref(proto), pi.min_version, pi.max_version, obj_ref(import_obj)), ) - return True, '' + return True, "" def check_upgrade(obj: Union[Cluster, HostProvider], upgrade: Upgrade) -> Tuple[bool, str]: if obj.locked: - concerns = [i.name or 'Action lock' for i in obj.concerns.all()] - return False, f'{obj} has blocking concerns to address: {concerns}' + concerns = [i.name or "Action lock" for i in obj.concerns.all()] + + return False, f"{obj} has blocking concerns to address: {concerns}" check_list = [ check_upgrade_version, @@ -218,34 +218,36 @@ def check_upgrade(obj: Union[Cluster, HostProvider], upgrade: Upgrade) -> Tuple[ ok, msg = func(obj, upgrade) if not ok: return False, msg - return True, '' + + return True, "" def switch_hc(obj: Cluster, upgrade: Upgrade) -> None: def find_service(service, bundle): try: - return Prototype.objects.get(bundle=bundle, type='service', name=service.prototype.name) + return Prototype.objects.get(bundle=bundle, type="service", name=service.prototype.name) except Prototype.DoesNotExist: return None def find_component(component, proto): try: - return Prototype.objects.get( - parent=proto, type='component', name=component.prototype.name - ) + return Prototype.objects.get(parent=proto, type="component", name=component.prototype.name) except Prototype.DoesNotExist: return None - if obj.prototype.type != 'cluster': + if obj.prototype.type != "cluster": return for hc in HostComponent.objects.filter(cluster=obj): service_proto = find_service(hc.service, upgrade.bundle) if not service_proto: hc.delete() + continue + if not find_component(hc.component, service_proto): hc.delete() + continue @@ -261,19 +263,22 @@ def rpm_cmp_reverse(obj1, obj2): ok, _msg = check_upgrade_version(obj, upg) if not ok: continue + ok, _msg = check_upgrade_edition(obj, upg) if not ok: continue + ok, _msg = check_upgrade_state(obj, upg) upg.upgradable = bool(ok) - upg.license = upg.bundle.license + upgrade_proto = Prototype.objects.filter(bundle=upg.bundle, name=upg.bundle.name).first() + upg.license = upgrade_proto.license if upg.upgradable: res.append(upg) if order: - if 'name' in order: + if "name" in order: return sorted(res, key=functools.cmp_to_key(rpm_cmp)) - elif '-name' in order: + elif "-name" in order: return sorted(res, key=functools.cmp_to_key(rpm_cmp_reverse)) else: return res @@ -283,13 +288,14 @@ def rpm_cmp_reverse(obj1, obj2): def update_components_after_bundle_switch(cluster, upgrade): if upgrade.action and upgrade.action.hostcomponentmap: - logger.info('update component from %s after upgrade with hc_acl', cluster) + logger.info("update component from %s after upgrade with hc_acl", cluster) for hc_acl in upgrade.action.hostcomponentmap: proto_service = Prototype.objects.filter( - type='service', bundle=upgrade.bundle, name=hc_acl['service'] + type="service", bundle=upgrade.bundle, name=hc_acl["service"] ).first() if not proto_service: continue + try: service = ClusterObject.objects.get(cluster=cluster, prototype=proto_service) if not ServiceComponent.objects.filter(cluster=cluster, service=service).exists(): @@ -306,57 +312,55 @@ def do_upgrade( hc: list, ) -> dict: old_proto = obj.prototype - check_license(obj.prototype.bundle) - check_license(upgrade.bundle) + check_license(obj.prototype) + upgrade_proto = Prototype.objects.filter(bundle=upgrade.bundle, name=upgrade.bundle.name).first() + check_license(upgrade_proto) ok, msg = check_upgrade(obj, upgrade) if not ok: - return err('UPGRADE_ERROR', msg) - logger.info('upgrade %s version %s (upgrade #%s)', obj_ref(obj), old_proto.version, upgrade.id) + return raise_adcm_ex("UPGRADE_ERROR", msg) + logger.info("upgrade %s version %s (upgrade #%s)", obj_ref(obj), old_proto.version, upgrade.id) task_id = None if not upgrade.action: bundle_switch(obj, upgrade) if upgrade.state_on_success: - obj.before_upgrade['state'] = obj.state + obj.before_upgrade["state"] = obj.state obj.state = upgrade.state_on_success obj.save() else: - task = cm.job.start_task(upgrade.action, obj, config, attr, hc, [], False) + task = start_task(upgrade.action, obj, config, attr, hc, [], False) task_id = task.id obj.refresh_from_db() - return {'id': obj.id, 'upgradable': bool(get_upgrade(obj)), 'task_id': task_id} + return {"id": obj.id, "upgradable": bool(get_upgrade(obj)), "task_id": task_id} def bundle_switch(obj: Union[Cluster, HostProvider], upgrade: Upgrade): + new_proto = None old_proto = obj.prototype - if old_proto.type == 'cluster': - new_proto = Prototype.objects.get(bundle=upgrade.bundle, type='cluster') - elif old_proto.type == 'provider': - new_proto = Prototype.objects.get(bundle=upgrade.bundle, type='provider') + if old_proto.type == "cluster": + new_proto = Prototype.objects.get(bundle=upgrade.bundle, type="cluster") + elif old_proto.type == "provider": + new_proto = Prototype.objects.get(bundle=upgrade.bundle, type="provider") else: - err('UPGRADE_ERROR', 'can upgrade only cluster or host provider') + raise_adcm_ex("UPGRADE_ERROR", "can upgrade only cluster or host provider") with transaction.atomic(): obj.prototype = new_proto obj.save() switch_config(obj, new_proto, old_proto) - if obj.prototype.type == 'cluster': + if obj.prototype.type == "cluster": switch_services(upgrade, obj) - if not old_proto.allow_maintenance_mode and new_proto.allow_maintenance_mode: - Host.objects.filter(cluster=obj).update(maintenance_mode=MaintenanceModeType.Off) - elif old_proto.allow_maintenance_mode and not new_proto.allow_maintenance_mode: - Host.objects.filter(cluster=obj).update( - maintenance_mode=MaintenanceModeType.Disabled - ) - elif obj.prototype.type == 'provider': + if old_proto.allow_maintenance_mode != new_proto.allow_maintenance_mode: + Host.objects.filter(cluster=obj).update(maintenance_mode=MaintenanceMode.OFF) + elif obj.prototype.type == "provider": switch_hosts(upgrade, obj) - cm.issue.update_hierarchy_issues(obj) + + update_hierarchy_issues(obj) if isinstance(obj, Cluster): update_components_after_bundle_switch(obj, upgrade) - logger.info('upgrade %s OK to version %s', obj_ref(obj), obj.prototype.version) - cm.status_api.post_event( - 'upgrade', obj.prototype.type, obj.id, 'version', str(obj.prototype.version) - ) + + logger.info("upgrade %s OK to version %s", obj_ref(obj), obj.prototype.version) + post_event("upgrade", obj.prototype.type, obj.id, "version", str(obj.prototype.version)) diff --git a/python/cm/utils.py b/python/cm/utils.py new file mode 100644 index 0000000000..6d3ffe5e22 --- /dev/null +++ b/python/cm/utils.py @@ -0,0 +1,42 @@ +# 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 json +from pathlib import Path +from secrets import token_hex +from typing import Any + +from django.conf import settings + + +def dict_json_get_or_create(path: str | Path, field: str, value: Any = None) -> Any: + with open(path, encoding=settings.ENCODING_UTF_8) as f: + data = json.load(f) + + if field not in data: + data[field] = value + with open(path, "w", encoding=settings.ENCODING_UTF_8) as f: + json.dump(data, f) + + return data[field] + + +def get_adcm_token(): + if not settings.ADCM_TOKEN_FILE.is_file(): + settings.ADCM_TOKEN_FILE.parent.mkdir(parents=True, exist_ok=True) + with open(settings.ADCM_TOKEN_FILE, mode="w", encoding="utf-8") as f: + f.write(token_hex(20)) + + with open(settings.ADCM_TOKEN_FILE, encoding="utf-8") as f: + adcm_token = f.read().strip() + + return adcm_token diff --git a/python/cm/variant.py b/python/cm/variant.py index b5ba2b16eb..d4c5a5ff95 100644 --- a/python/cm/variant.py +++ b/python/cm/variant.py @@ -88,9 +88,7 @@ def var_host_get_service(cluster, args, func): def var_host_get_component(cluster, args, service, func): if 'component' not in args: err('CONFIG_VARIANT_ERROR', f'no "component" argument for predicate "{func}"') - return ServiceComponent.obj.get( - cluster=cluster, service=service, prototype__name=args['component'] - ) + return ServiceComponent.obj.get(cluster=cluster, service=service, prototype__name=args['component']) def var_host_in_service(cluster, args): @@ -122,9 +120,7 @@ def var_host_in_component(cluster, args): out = [] service = var_host_get_service(cluster, args, 'in_component') comp = var_host_get_component(cluster, args, service, 'in_component') - for hc in HostComponent.objects.filter( - cluster=cluster, service=service, component=comp - ).order_by('host__fqdn'): + for hc in HostComponent.objects.filter(cluster=cluster, service=service, component=comp).order_by('host__fqdn'): out.append(hc.host.fqdn) return out @@ -237,20 +233,16 @@ def variant_host_in_cluster(obj, args=None): return [] if 'component' in args: try: - comp = ServiceComponent.objects.get( - cluster=cluster, service=service, prototype__name=args['component'] - ) + comp = ServiceComponent.objects.get(cluster=cluster, service=service, prototype__name=args['component']) except ServiceComponent.DoesNotExist: return [] - for hc in HostComponent.objects.filter( - cluster=cluster, service=service, component=comp - ).order_by('host__fqdn'): + for hc in HostComponent.objects.filter(cluster=cluster, service=service, component=comp).order_by( + 'host__fqdn' + ): out.append(hc.host.fqdn) return out else: - for hc in HostComponent.objects.filter(cluster=cluster, service=service).order_by( - 'host__fqdn' - ): + for hc in HostComponent.objects.filter(cluster=cluster, service=service).order_by('host__fqdn'): out.append(hc.host.fqdn) return out diff --git a/python/init_db.py b/python/init_db.py index 5c45fd19fd..44ca4b13be 100755 --- a/python/init_db.py +++ b/python/init_db.py @@ -11,14 +11,17 @@ # See the License for the specific language governing permissions and # limitations under the License. +# pylint: disable=unused-import + import json -import random -import string from itertools import chain +from secrets import token_hex +from typing import Optional, Tuple + +from django.conf import settings -import adcm.init_django # pylint: disable=unused-import +import adcm.init_django from cm.bundle import load_adcm -from cm.config import SECRETS_FILE from cm.issue import update_hierarchy_issues from cm.job import abort_all from cm.logger import logger @@ -34,22 +37,34 @@ from cm.status_api import Event from rbac.models import User +TOKEN_LENGTH = 20 + -def random_string(strlen=10): - return "".join([random.choice(string.ascii_letters) for _ in range(strlen)]) +def prepare_secrets_json(status_user_username: str, status_user_password: Optional[str]) -> None: + # we need to know status user's password to write it to secrets.json [old implementation] + if not settings.SECRETS_FILE.is_file() and status_user_username is not None: + with open(settings.SECRETS_FILE, "w", encoding=settings.ENCODING_UTF_8) as f: + json.dump( + { + "adcmuser": {"user": status_user_username, "password": status_user_password}, + "token": token_hex(TOKEN_LENGTH), + "adcm_internal_token": settings.ADCM_TOKEN, + }, + f, + ) + logger.info("Update secret file %s OK", settings.SECRETS_FILE) + else: + logger.info("Secret file %s is not updated", settings.SECRETS_FILE) -def create_status_user(): +def create_status_user() -> Tuple[str, Optional[str]]: username = "status" if User.objects.filter(username=username).exists(): - return + return username, None - password = random_string(40) - token = random_string(40) + password = token_hex(TOKEN_LENGTH) User.objects.create_superuser(username, "", password, built_in=True) - with open(SECRETS_FILE, 'w', encoding='utf_8') as f: - json.dump({'adcmuser': {'user': username, 'password': password}, 'token': token}, f) - logger.info('Update secret file %s OK', SECRETS_FILE) + return username, password def create_dummy_data(): @@ -79,12 +94,13 @@ def recheck_issues(): def init(): logger.info("Start initializing ADCM DB...") - if not User.objects.filter(username='admin').exists(): - User.objects.create_superuser('admin', 'admin@example.com', 'admin', built_in=True) - create_status_user() - if not User.objects.filter(username='system').exists(): - User.objects.create_superuser('system', '', None, built_in=True) - logger.info('Create system user') + if not User.objects.filter(username="admin").exists(): + User.objects.create_superuser("admin", "admin@example.com", "admin", built_in=True) + status_user_username, status_user_password = create_status_user() + prepare_secrets_json(status_user_username, status_user_password) + if not User.objects.filter(username="system").exists(): + User.objects.create_superuser("system", "", None, built_in=True) + logger.info("Create system user") event = Event() abort_all(event) clear_temp_tables() @@ -96,5 +112,5 @@ def init(): logger.info("ADCM DB is initialized") -if __name__ == '__main__': +if __name__ == "__main__": init() diff --git a/python/job_runner.py b/python/job_runner.py index b579d36e28..8617e7866a 100755 --- a/python/job_runner.py +++ b/python/job_runner.py @@ -17,29 +17,30 @@ import os import subprocess import sys +from pathlib import Path +from django.conf import settings from django.db import transaction -import adcm.init_django # pylint: disable=unused-import +import adcm.init_django import cm.job -from cm import config from cm.ansible_plugin import finish_check from cm.api import get_hc, save_hc from cm.errors import AdcmEx from cm.logger import logger -from cm.models import JobLog, LogStorage, Prototype, ServiceComponent +from cm.models import JobLog, JobStatus, LogStorage, Prototype, ServiceComponent from cm.status_api import Event, post_event from cm.upgrade import bundle_switch def open_file(root, tag, job_id): - fname = f'{root}/{job_id}/{tag}.txt' - f = open(fname, 'w', encoding='utf_8') + fname = f"{root}/{job_id}/{tag}.txt" + f = open(fname, "w", encoding=settings.ENCODING_UTF_8) return f def read_config(job_id): - fd = open(f'{config.RUN_DIR}/{job_id}/config.json', encoding='utf_8') + fd = open(f"{settings.RUN_DIR}/{job_id}/config.json", encoding=settings.ENCODING_UTF_8) conf = json.load(fd) fd.close() return conf @@ -47,40 +48,41 @@ def read_config(job_id): def set_job_status(job_id, ret, pid, event): if ret == 0: - cm.job.set_job_status(job_id, config.Job.SUCCESS, event, pid) + cm.job.set_job_status(job_id, JobStatus.SUCCESS, event, pid) return 0 elif ret == -15: - cm.job.set_job_status(job_id, config.Job.ABORTED, event, pid) + cm.job.set_job_status(job_id, JobStatus.ABORTED, event, pid) return 15 else: - cm.job.set_job_status(job_id, config.Job.FAILED, event, pid) + cm.job.set_job_status(job_id, JobStatus.FAILED, event, pid) return ret def set_pythonpath(env, stack_dir): - pmod_path = f'./pmod:{stack_dir}/pmod' + pmod_path = f"./pmod:{stack_dir}/pmod" if "PYTHONPATH" in env: - env["PYTHONPATH"] = pmod_path + ':' + env["PYTHONPATH"] + env["PYTHONPATH"] = pmod_path + ":" + env["PYTHONPATH"] else: env["PYTHONPATH"] = pmod_path return env def set_ansible_config(env, job_id): - env['ANSIBLE_CONFIG'] = os.path.join(config.RUN_DIR, f'{job_id}/ansible.cfg') + env["ANSIBLE_CONFIG"] = str(settings.RUN_DIR / f"{job_id}/ansible.cfg") return env def env_configuration(job_config): - job_id = job_config['job']['id'] - stack_dir = job_config['env']['stack_dir'] + job_id = job_config["job"]["id"] + stack_dir = job_config["env"]["stack_dir"] env = os.environ.copy() env = set_pythonpath(env, stack_dir) + # This condition is intended to support compatibility. # Since older bundle versions may contain their own ansible.cfg - if not os.path.exists(os.path.join(stack_dir, 'ansible.cfg')): + if not Path(stack_dir, "ansible.cfg").is_file(): env = set_ansible_config(env, job_id) - logger.info('set ansible config for job:%s', job_id) + logger.info("set ansible config for job:%s", job_id) return env @@ -88,14 +90,14 @@ def post_log(job_id, log_type, log_name): l1 = LogStorage.objects.filter(job__id=job_id, type=log_type, name=log_name).first() if l1: post_event( - 'add_job_log', - 'job', + "add_job_log", + "job", job_id, { - 'id': l1.id, - 'type': l1.type, - 'name': l1.name, - 'format': l1.format, + "id": l1.id, + "type": l1.type, + "name": l1.name, + "format": l1.format, }, ) @@ -105,18 +107,18 @@ def get_venv(job_id: int) -> str: def process_err_out_file(job_id, job_type): - out_file = open_file(config.RUN_DIR, f'{job_type}-stdout', job_id) - err_file = open_file(config.RUN_DIR, f'{job_type}-stderr', job_id) - post_log(job_id, 'stdout', f'{job_type}') - post_log(job_id, 'stderr', f'{job_type}') + out_file = open_file(settings.RUN_DIR, f"{job_type}-stdout", job_id) + err_file = open_file(settings.RUN_DIR, f"{job_type}-stderr", job_id) + post_log(job_id, "stdout", f"{job_type}") + post_log(job_id, "stderr", f"{job_type}") return out_file, err_file def start_subprocess(job_id, cmd, conf, out_file, err_file): event = Event() - logger.info("job run cmd: %s", ' '.join(cmd)) + logger.info("job run cmd: %s", " ".join(cmd)) proc = subprocess.Popen(cmd, env=env_configuration(conf), stdout=out_file, stderr=err_file) - cm.job.set_job_status(job_id, config.Job.RUNNING, event, proc.pid) + cm.job.set_job_status(job_id, JobStatus.RUNNING, event, proc.pid) event.send_state() logger.info("run job #%s, pid %s", job_id, proc.pid) ret = proc.wait() @@ -134,46 +136,46 @@ def start_subprocess(job_id, cmd, conf, out_file, err_file): def run_ansible(job_id): logger.debug("job_runner.py starts to run ansible job %s", job_id) conf = read_config(job_id) - playbook = conf['job']['playbook'] - out_file, err_file = process_err_out_file(job_id, 'ansible') + playbook = conf["job"]["playbook"] + out_file, err_file = process_err_out_file(job_id, "ansible") - os.chdir(conf['env']['stack_dir']) + os.chdir(conf["env"]["stack_dir"]) cmd = [ - '/adcm/python/job_venv_wrapper.sh', + "/adcm/python/job_venv_wrapper.sh", get_venv(int(job_id)), - 'ansible-playbook', - '--vault-password-file', - f'{config.CODE_DIR}/ansible_secret.py', - '-e', - f'@{config.RUN_DIR}/{job_id}/config.json', - '-i', - f'{config.RUN_DIR}/{job_id}/inventory.json', + "ansible-playbook", + "--vault-password-file", + f"{settings.CODE_DIR}/ansible_secret.py", + "-e", + f"@{settings.RUN_DIR}/{job_id}/config.json", + "-i", + f"{settings.RUN_DIR}/{job_id}/inventory.json", playbook, ] - if 'params' in conf['job']: - if 'ansible_tags' in conf['job']['params']: - cmd.append('--tags=' + conf['job']['params']['ansible_tags']) - if 'verbose' in conf['job'] and conf['job']['verbose']: - cmd.append('-vvvv') + if "params" in conf["job"]: + if "ansible_tags" in conf["job"]["params"]: + cmd.append("--tags=" + conf["job"]["params"]["ansible_tags"]) + if "verbose" in conf["job"] and conf["job"]["verbose"]: + cmd.append("-vvvv") ret = start_subprocess(job_id, cmd, conf, out_file, err_file) sys.exit(ret) def run_upgrade(job): event = Event() - cm.job.set_job_status(job.id, config.Job.RUNNING, event) - out_file, err_file = process_err_out_file(job.id, 'internal') + cm.job.set_job_status(job.id, JobStatus.RUNNING, event) + out_file, err_file = process_err_out_file(job.id, "internal") try: with transaction.atomic(): bundle_switch(job.task.task_object, job.action.upgrade) switch_hc(job.task, job.action) except AdcmEx as e: err_file.write(e.msg) - cm.job.set_job_status(job.id, config.Job.FAILED, event) + cm.job.set_job_status(job.id, JobStatus.FAILED, event) out_file.close() err_file.close() sys.exit(1) - cm.job.set_job_status(job.id, config.Job.SUCCESS, event) + cm.job.set_job_status(job.id, JobStatus.SUCCESS, event) event.send_state() out_file.close() err_file.close() @@ -181,11 +183,11 @@ def run_upgrade(job): def run_python(job): - out_file, err_file = process_err_out_file(job.id, 'python') + out_file, err_file = process_err_out_file(job.id, "python") conf = read_config(job.id) - script_path = conf['job']['playbook'] - os.chdir(conf['env']['stack_dir']) - cmd = ['python', script_path] + script_path = conf["job"]["playbook"] + os.chdir(conf["env"]["stack_dir"]) + cmd = ["python", script_path] ret = start_subprocess(job.id, cmd, conf, out_file, err_file) sys.exit(ret) @@ -204,10 +206,10 @@ def switch_hc(task, action): task.save() for hc in new_hc: if "component_prototype_id" in hc: - proto = Prototype.objects.get(type='component', id=hc.pop('component_prototype_id')) + proto = Prototype.objects.get(type="component", id=hc.pop("component_prototype_id")) comp = ServiceComponent.objects.get(cluster=cluster, prototype=proto) - hc['component_id'] = comp.id - hc['service_id'] = comp.service.id + hc["component_id"] = comp.id + hc["service_id"] = comp.service.id host_map, _ = cm.job.check_hostcomponentmap(cluster, action, new_hc) if host_map is not None: save_hc(cluster, host_map) @@ -217,9 +219,9 @@ def main(job_id): logger.debug("job_runner.py called as: %s", sys.argv) job = JobLog.objects.get(id=job_id) job_type = job.sub_action.script_type if job.sub_action else job.action.script_type - if job_type == 'internal': + if job_type == "internal": run_upgrade(job) - elif job_type == 'python': + elif job_type == "python": run_python(job) else: run_ansible(job_id) @@ -233,5 +235,5 @@ def do(): main(sys.argv[1]) -if __name__ == '__main__': +if __name__ == "__main__": do() diff --git a/python/job_venv_wrapper.sh b/python/job_venv_wrapper.sh index 5a49a791a2..3d14539065 100755 --- a/python/job_venv_wrapper.sh +++ b/python/job_venv_wrapper.sh @@ -1,13 +1,10 @@ #!/usr/bin/env bash -# Source virtualenv if it is required if [[ "${1}" != "none" ]]; then venv="/adcm/venv/${1}/bin/activate" - # shellcheck disable=SC1090 - source "${venv}" + . "${venv}" fi shift -# run command "$@" diff --git a/python/rbac/endpoints/group/views.py b/python/rbac/endpoints/group/views.py index 340234ab43..eb91d9099a 100644 --- a/python/rbac/endpoints/group/views.py +++ b/python/rbac/endpoints/group/views.py @@ -10,15 +10,14 @@ # See the License for the specific language governing permissions and # limitations under the License. -from adwp_base.errors import AdwpEx from django_filters.rest_framework import CharFilter, DjangoFilterBackend, FilterSet from guardian.mixins import PermissionListMixin from rest_framework.filters import OrderingFilter -from rest_framework.status import HTTP_405_METHOD_NOT_ALLOWED from rest_framework.viewsets import ModelViewSet from adcm.permissions import DjangoModelPermissionsAudit from audit.utils import audit +from cm.errors import raise_adcm_ex from rbac.endpoints.group.serializers import GroupSerializer from rbac.models import Group @@ -76,10 +75,6 @@ def update(self, request, *args, **kwargs): def destroy(self, request, *args, **kwargs): instance = self.get_object() if instance.built_in: - raise AdwpEx( - "GROUP_DELETE_ERROR", - msg="Built-in group could not be deleted", - http_code=HTTP_405_METHOD_NOT_ALLOWED, - ) + raise_adcm_ex("GROUP_DELETE_ERROR") return super().destroy(request, args, kwargs) diff --git a/python/rbac/endpoints/token.py b/python/rbac/endpoints/token.py index 78dadefdc1..adfcce3742 100644 --- a/python/rbac/endpoints/token.py +++ b/python/rbac/endpoints/token.py @@ -14,23 +14,21 @@ import django.contrib.auth import rest_framework.authtoken.serializers -from adwp_base.errors import AdwpEx from rest_framework import authentication, permissions from rest_framework.authtoken.models import Token from rest_framework.generics import GenericAPIView from rest_framework.response import Response -from rest_framework.status import HTTP_401_UNAUTHORIZED + +from cm.errors import raise_adcm_ex class AuthSerializer(rest_framework.authtoken.serializers.AuthTokenSerializer): """Authentication token serializer""" def validate(self, attrs): - user = django.contrib.auth.authenticate( - username=attrs.get('username'), password=attrs.get('password') - ) + user = django.contrib.auth.authenticate(username=attrs.get('username'), password=attrs.get('password')) if not user: - raise AdwpEx('AUTH_ERROR', 'Wrong user or password', http_code=HTTP_401_UNAUTHORIZED) + raise_adcm_ex('AUTH_ERROR') attrs['user'] = user return attrs @@ -60,7 +58,5 @@ def post(self, request, *args, **kwargs): serializer.is_valid(raise_exception=True) user = serializer.validated_data['user'] token, _created = Token.objects.get_or_create(user=user) - django.contrib.auth.login( - request, user, backend='django.contrib.auth.backends.ModelBackend' - ) + django.contrib.auth.login(request, user, backend='django.contrib.auth.backends.ModelBackend') return Response({'token': token.key}) diff --git a/python/rbac/endpoints/user/serializers.py b/python/rbac/endpoints/user/serializers.py index 52991b9415..432cc110f5 100644 --- a/python/rbac/endpoints/user/serializers.py +++ b/python/rbac/endpoints/user/serializers.py @@ -52,10 +52,11 @@ class ExpandedGroupSerializer(FlexFieldsSerializerMixin, ModelSerializer): user = GroupUserSerializer(many=True, source="user_set") url = HyperlinkedIdentityField(view_name="rbac:group-detail") name = CharField(max_length=150, source="group.display_name") + type = CharField(read_only=True, source="group.type") class Meta: model = Group - fields = ("id", "name", "user", "url") + fields = ("id", "name", "type", "user", "url") expandable_fields = { "user": ( "rbac.endpoints.user.views.UserSerializer", @@ -73,12 +74,8 @@ class UserSerializer(FlexFieldsSerializerMixin, Serializer): id = IntegerField(read_only=True) username = RegexField(r"^[^\s]+$", max_length=150) - first_name = RegexField( - r"^[^\n]*$", max_length=150, allow_blank=True, required=False, default="" - ) - last_name = RegexField( - r"^[^\n]*$", max_length=150, allow_blank=True, required=False, default="" - ) + first_name = RegexField(r"^[^\n]*$", max_length=150, allow_blank=True, required=False, default="") + last_name = RegexField(r"^[^\n]*$", max_length=150, allow_blank=True, required=False, default="") email = EmailField( allow_blank=True, required=False, @@ -98,6 +95,7 @@ class Meta: def update(self, instance, validated_data): context_user = self.context["request"].user + return user_services.update(instance, context_user, partial=self.partial, **validated_data) def create(self, validated_data): diff --git a/python/rbac/endpoints/user/urls.py b/python/rbac/endpoints/user/urls.py index 269f38ff97..88b7c4bb3b 100644 --- a/python/rbac/endpoints/user/urls.py +++ b/python/rbac/endpoints/user/urls.py @@ -10,12 +10,10 @@ # See the License for the specific language governing permissions and # limitations under the License. -"""User URLs""" - from rest_framework.routers import SimpleRouter -from .views import UserViewSet +from rbac.endpoints.user.views import UserViewSet router = SimpleRouter() -router.register('', UserViewSet, basename='user') +router.register("", UserViewSet) urlpatterns = router.urls diff --git a/python/rbac/endpoints/user/views.py b/python/rbac/endpoints/user/views.py index 1bfc647413..6af0cf120d 100644 --- a/python/rbac/endpoints/user/views.py +++ b/python/rbac/endpoints/user/views.py @@ -10,13 +10,12 @@ # See the License for the specific language governing permissions and # limitations under the License. -from adwp_base.errors import AdwpEx from guardian.mixins import PermissionListMixin -from rest_framework.status import HTTP_405_METHOD_NOT_ALLOWED from rest_framework.viewsets import ModelViewSet from adcm.permissions import DjangoModelPermissionsAudit from audit.utils import audit +from cm.errors import raise_adcm_ex from rbac import models from rbac.endpoints.user.serializers import UserSerializer @@ -35,6 +34,7 @@ class UserViewSet(PermissionListMixin, ModelViewSet): # pylint: disable=too-man "is_superuser", "built_in", "is_active", + "type", ) ordering_fields = ("id", "username", "first_name", "last_name", "email", "is_superuser") search_fields = ("username", "first_name", "last_name", "email") @@ -51,9 +51,5 @@ def update(self, request, *args, **kwargs): def destroy(self, request, *args, **kwargs): instance = self.get_object() if instance.built_in: - raise AdwpEx( - "USER_DELETE_ERROR", - msg="Built-in user could not be deleted", - http_code=HTTP_405_METHOD_NOT_ALLOWED, - ) + raise_adcm_ex("USER_DELETE_ERROR") return super().destroy(request, args, kwargs) diff --git a/python/rbac/endpoints_ui/role/views.py b/python/rbac/endpoints_ui/role/views.py index 34caf39f0b..ddf023fd14 100644 --- a/python/rbac/endpoints_ui/role/views.py +++ b/python/rbac/endpoints_ui/role/views.py @@ -25,6 +25,7 @@ Serializer, ) +from adcm.permissions import DjangoObjectPermissionsAudit from api.base_view import GenericUIViewSet from cm import models as cm_models from rbac import models @@ -40,6 +41,7 @@ class RoleSerializer(Serializer): class RoleViewSet(ListModelMixin, GenericUIViewSet): queryset = models.Role.objects.all() serializer_class = RoleSerializer + permission_classes = (DjangoObjectPermissionsAudit,) @action(methods=["get"], detail=True) def object_candidate(self, request, **kwargs): diff --git a/python/rbac/ldap.py b/python/rbac/ldap.py index b9ea975c94..13b8884f1c 100644 --- a/python/rbac/ldap.py +++ b/python/rbac/ldap.py @@ -80,9 +80,7 @@ def is_tls(ldap_uri: str) -> bool: def get_ldap_config() -> Optional[dict]: adcm_object = ADCM.objects.first() - current_configlog = ConfigLog.objects.get( - obj_ref=adcm_object.config, id=adcm_object.config.current - ) + current_configlog = ConfigLog.objects.get(obj_ref=adcm_object.config, id=adcm_object.config.current) if current_configlog.attr["ldap_integration"]["active"]: return current_configlog.config["ldap_integration"] return None @@ -141,8 +139,8 @@ def get_ldap_default_settings() -> Tuple[dict, Optional[str]]: group_search = LDAPSearch( base_dn=ldap_config["group_search_base"], scope=ldap.SCOPE_SUBTREE, - filterstr=f"(objectClass={ldap_config.get('group_object_class') or '*'})" - f"{_process_extra_filter(ldap_config.get('group_search_filter'))}", + filterstr=f"(&(objectClass={ldap_config.get('group_object_class') or '*'})" + f"{_process_extra_filter(ldap_config.get('group_search_filter'))})", ) user_attr_map = { "username": ldap_config["user_name_attribute"], @@ -165,7 +163,7 @@ def get_ldap_default_settings() -> Tuple[dict, Optional[str]]: "GROUP_FILTER": _process_extra_filter(ldap_config.get("group_search_filter", "")), "USER_ATTR_MAP": user_attr_map, "ALWAYS_UPDATE_USER": True, - "CACHE_TIMEOUT": 3600, + "CACHE_TIMEOUT": 0, } if group_search: default_settings.update( @@ -197,9 +195,7 @@ def __init__(self): self.default_settings = {} self.is_tls = False - def authenticate_ldap_user( - self, ldap_user: User | _LDAPUser, password: str - ) -> Optional[_LDAPUser]: + def authenticate_ldap_user(self, ldap_user: User | _LDAPUser, password: str) -> Optional[_LDAPUser]: self.default_settings, _ = get_ldap_default_settings() if not self.default_settings: return None @@ -217,6 +213,7 @@ def authenticate_ldap_user( if isinstance(user_or_none, User): user_or_none.type = OriginType.LDAP + user_or_none.is_active = True user_or_none.save() self._process_groups(user_or_none, ldap_user.dn, user_local_groups) @@ -224,9 +221,7 @@ def authenticate_ldap_user( @property def _group_search_enabled(self) -> bool: - return "GROUP_SEARCH" in self.default_settings and bool( - self.default_settings.get("GROUP_SEARCH") - ) + return "GROUP_SEARCH" in self.default_settings and bool(self.default_settings.get("GROUP_SEARCH")) @staticmethod def _get_local_groups_by_username(username: str) -> List[Group]: @@ -257,9 +252,7 @@ def _get_groups_by_group_search(self) -> List[Tuple[str, dict]]: logger.debug("Found %s groups: %s", len(groups), [i[0] for i in groups]) return groups - def _process_groups( - self, user: User | _LDAPUser, user_dn: str, additional_groups: List[Group] = () - ) -> None: + def _process_groups(self, user: User | _LDAPUser, user_dn: str, additional_groups: List[Group] = ()) -> None: if not self._group_search_enabled: logger.warning("Group search is disabled. Getting all user groups") with self._ldap_connection() as conn: @@ -323,9 +316,7 @@ def _get_rbac_group(group: Group | DjangoGroup, ldap_group_dn: str) -> Group: elif isinstance(group, DjangoGroup): try: # maybe we'll need more accurate filtering here - return Group.objects.get( - name=f"{group.name} [{OriginType.LDAP.value}]", type=OriginType.LDAP.value - ) + return Group.objects.get(name=f"{group.name} [{OriginType.LDAP.value}]", type=OriginType.LDAP.value) except Group.DoesNotExist: with atomic(): rbac_group = Group.objects.create( diff --git a/python/rbac/management/commands/role_map.py b/python/rbac/management/commands/role_map.py index d6878adac0..4fe4c39725 100644 --- a/python/rbac/management/commands/role_map.py +++ b/python/rbac/management/commands/role_map.py @@ -11,6 +11,7 @@ # limitations under the License. import json +from django.conf import settings from django.core.management.base import BaseCommand from django.db.models import Subquery @@ -19,45 +20,43 @@ def read_role(role: Role) -> dict: data = { - 'name': role.name, - 'type': role.type, - 'parametrized_by_type': role.parametrized_by_type, - 'category': [c.value for c in role.category.all()], - 'child': [read_role(r) for r in role.child.all()], + "name": role.name, + "type": role.type, + "parametrized_by_type": role.parametrized_by_type, + "category": [c.value for c in role.category.all()], + "child": [read_role(r) for r in role.child.all()], } return data class Command(BaseCommand): - help = 'Return role map to json file' + help = "Return role map to json file" def add_arguments(self, parser): parser.add_argument( - '--indent', + "--indent", type=int, default=2, - help='Specifies the indent level to use when pretty-printing output.', + help="Specifies the indent level to use when pretty-printing output.", ) parser.add_argument( - '-o', - '--output', + "-o", + "--output", required=True, - help='Specifies file to which the output is written.', + help="Specifies file to which the output is written.", ) def handle(self, *args, **options): - indent = options['indent'] - output = options['output'] + indent = options["indent"] + output = options["output"] data = [] # We should start from root of the forest, so we filter out # everything that is not mentioned as a child. - for role in Role.objects.exclude( - id__in=Subquery(Role.objects.filter(child__isnull=False).values('child__id')) - ): + for role in Role.objects.exclude(id__in=Subquery(Role.objects.filter(child__isnull=False).values("child__id"))): data.append(read_role(role)) - with open(output, 'w', encoding='utf_8') as f: + with open(output, "w", encoding=settings.ENCODING_UTF_8) as f: json.dump(data, f, indent=indent) - self.stdout.write(self.style.SUCCESS(f'Result file: {output}')) + self.stdout.write(self.style.SUCCESS(f"Result file: {output}")) diff --git a/python/rbac/management/commands/upgraderole.py b/python/rbac/management/commands/upgraderole.py index 24d260579b..85faf2b579 100644 --- a/python/rbac/management/commands/upgraderole.py +++ b/python/rbac/management/commands/upgraderole.py @@ -12,9 +12,9 @@ """UpgradeRole command for Django manage.py""" -from adwp_base.errors import AdwpEx from django.core.management.base import BaseCommand, CommandError +from cm.errors import AdcmEx from rbac.upgrade.role import init_roles @@ -33,5 +33,5 @@ def handle(self, *args, **options): try: msg = init_roles() self.stdout.write(self.style.SUCCESS(msg)) - except AdwpEx as e: - raise CommandError(e.message) from None + except AdcmEx as e: + raise CommandError(e.msg) from None diff --git a/python/rbac/migrations/0001_initial.py b/python/rbac/migrations/0001_initial.py index 326504eb96..9702a63bb0 100644 --- a/python/rbac/migrations/0001_initial.py +++ b/python/rbac/migrations/0001_initial.py @@ -115,9 +115,7 @@ class Migration(migrations.Migration): fields=[ ( 'id', - models.AutoField( - auto_created=True, primary_key=True, serialize=False, verbose_name='ID' - ), + models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), ), ('name', models.CharField(max_length=160)), ('description', models.TextField(blank=True)), @@ -158,9 +156,7 @@ class Migration(migrations.Migration): fields=[ ( 'id', - models.AutoField( - auto_created=True, primary_key=True, serialize=False, verbose_name='ID' - ), + models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), ), ( 'group', @@ -196,16 +192,12 @@ class Migration(migrations.Migration): fields=[ ( 'id', - models.AutoField( - auto_created=True, primary_key=True, serialize=False, verbose_name='ID' - ), + models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), ), ('object_id', models.PositiveIntegerField()), ( 'content_type', - models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, to='contenttypes.contenttype' - ), + models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.contenttype'), ), ], ), @@ -214,9 +206,7 @@ class Migration(migrations.Migration): fields=[ ( 'id', - models.AutoField( - auto_created=True, primary_key=True, serialize=False, verbose_name='ID' - ), + models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), ), ('name', models.CharField(max_length=255, unique=True)), ('description', models.TextField(blank=True)), @@ -230,9 +220,7 @@ class Migration(migrations.Migration): ('object', models.ManyToManyField(blank=True, to='rbac.PolicyObject')), ( 'role', - models.ForeignKey( - null=True, on_delete=django.db.models.deletion.SET_NULL, to='rbac.role' - ), + models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='rbac.role'), ), ('user', models.ManyToManyField(blank=True, to='rbac.User')), ( @@ -263,14 +251,10 @@ class Migration(migrations.Migration): ), migrations.AddConstraint( model_name='role', - constraint=models.UniqueConstraint( - fields=('display_name', 'built_in'), name='unique_display_name' - ), + constraint=models.UniqueConstraint(fields=('display_name', 'built_in'), name='unique_display_name'), ), migrations.AddConstraint( model_name='policyobject', - constraint=models.UniqueConstraint( - fields=('content_type', 'object_id'), name='unique_policy_object' - ), + constraint=models.UniqueConstraint(fields=('content_type', 'object_id'), name='unique_policy_object'), ), ] diff --git a/python/rbac/migrations/0002_rm_default_policy.py b/python/rbac/migrations/0002_rm_default_policy.py index e04c4d4915..9b27051b5c 100644 --- a/python/rbac/migrations/0002_rm_default_policy.py +++ b/python/rbac/migrations/0002_rm_default_policy.py @@ -17,12 +17,7 @@ def remove_permissions(policy, policy_model): for pp in policy.model_perm.all(): - if ( - policy_model.objects.filter( - user=pp.user, group=pp.group, model_perm__permission=pp.permission - ).count() - > 1 - ): + if policy_model.objects.filter(user=pp.user, group=pp.group, model_perm__permission=pp.permission).count() > 1: continue if pp.user: pp.user.user_permissions.remove(pp.permission) diff --git a/python/rbac/migrations/0003_user_group_origin_type.py b/python/rbac/migrations/0003_user_group_origin_type.py index 1338c21fd3..827a6dea40 100755 --- a/python/rbac/migrations/0003_user_group_origin_type.py +++ b/python/rbac/migrations/0003_user_group_origin_type.py @@ -25,15 +25,11 @@ class Migration(migrations.Migration): migrations.AddField( model_name='group', name='type', - field=models.CharField( - choices=[('local', 'local'), ('ldap', 'ldap')], default='local', max_length=16 - ), + field=models.CharField(choices=[('local', 'local'), ('ldap', 'ldap')], default='local', max_length=16), ), migrations.AddField( model_name='user', name='type', - field=models.CharField( - choices=[('local', 'local'), ('ldap', 'ldap')], default='local', max_length=16 - ), + field=models.CharField(choices=[('local', 'local'), ('ldap', 'ldap')], default='local', max_length=16), ), ] diff --git a/python/rbac/migrations/0004_fill_group_name_display_name.py b/python/rbac/migrations/0004_fill_group_name_display_name.py index 532668394d..edd01e5ead 100755 --- a/python/rbac/migrations/0004_fill_group_name_display_name.py +++ b/python/rbac/migrations/0004_fill_group_name_display_name.py @@ -1,3 +1,15 @@ +# 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. + # Generated by Django 3.2.13 on 2022-06-29 09:01 from django.db import migrations, models diff --git a/python/rbac/migrations/0005_rbac_group_unique_display_name_type_constraint.py b/python/rbac/migrations/0005_rbac_group_unique_display_name_type_constraint.py index 0fc76efd1d..db9f4012b0 100755 --- a/python/rbac/migrations/0005_rbac_group_unique_display_name_type_constraint.py +++ b/python/rbac/migrations/0005_rbac_group_unique_display_name_type_constraint.py @@ -1,3 +1,15 @@ +# 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. + # Generated by Django 3.2.13 on 2022-07-14 10:18 from django.db import migrations, models @@ -12,8 +24,6 @@ class Migration(migrations.Migration): operations = [ migrations.AddConstraint( model_name='group', - constraint=models.UniqueConstraint( - fields=('display_name', 'type'), name='unique_display_name_type' - ), + constraint=models.UniqueConstraint(fields=('display_name', 'type'), name='unique_display_name_type'), ), ] diff --git a/python/rbac/migrations/0006_auto_20220928_0455.py b/python/rbac/migrations/0006_auto_20220928_0455.py index 4602ff568b..f37272d5ae 100644 --- a/python/rbac/migrations/0006_auto_20220928_0455.py +++ b/python/rbac/migrations/0006_auto_20220928_0455.py @@ -1,3 +1,15 @@ +# 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. + # Generated by Django 3.2.15 on 2022-09-28 04:55 from django.db import migrations, models diff --git a/python/rbac/models.py b/python/rbac/models.py index 33a4e9baa9..aae135aca4 100644 --- a/python/rbac/models.py +++ b/python/rbac/models.py @@ -15,7 +15,6 @@ import importlib import re -from adwp_base.errors import raise_AdwpEx as err from django.contrib.auth.models import Group as AuthGroup from django.contrib.auth.models import Permission from django.contrib.auth.models import User as AuthUser @@ -29,27 +28,28 @@ from guardian.models import GroupObjectPermission, UserObjectPermission from rest_framework.exceptions import ValidationError +from cm.errors import raise_adcm_ex from cm.models import Bundle, DummyData, HostComponent, ProductCategory class ObjectType(models.TextChoices): - cluster = 'cluster', 'cluster' - service = 'service', 'service' - component = 'component', 'component' - provider = 'provider', 'provider' - host = 'host', 'host' + cluster = "cluster", "cluster" + service = "service", "service" + component = "component", "component" + provider = "provider", "provider" + host = "host", "host" def validate_object_type(value): if not isinstance(value, list): - raise ValidationError('Not a valid list.') + raise ValidationError("Not a valid list.") if not all((v in ObjectType.values for v in value)): - raise ValidationError('Not a valid object type.') + raise ValidationError("Not a valid object type.") class OriginType(models.TextChoices): - Local = 'local', 'local' - LDAP = 'ldap', 'ldap' + Local = "local", "local" + LDAP = "ldap", "ldap" class User(AuthUser): @@ -65,9 +65,7 @@ def delete(self, using=None, keep_parents=False): self.is_active = False self.save() - type = models.CharField( - max_length=16, choices=OriginType.choices, null=False, default=OriginType.Local - ) + type = models.CharField(max_length=16, choices=OriginType.choices, null=False, default=OriginType.Local) @property def name(self): @@ -82,43 +80,37 @@ class Group(AuthGroup): description = models.CharField(max_length=255, null=True) built_in = models.BooleanField(default=False, null=False) - type = models.CharField( - max_length=16, choices=OriginType.choices, null=False, default=OriginType.Local - ) + type = models.CharField(max_length=16, choices=OriginType.choices, null=False, default=OriginType.Local) # works as `name` field because `name` field now contains name and type # to bypass unique constraint on `AuthGroup` base table display_name = models.CharField(max_length=150, null=True) class Meta: constraints = [ - models.UniqueConstraint( - fields=['display_name', 'type'], name='unique_display_name_type' - ), + models.UniqueConstraint(fields=["display_name", "type"], name="unique_display_name_type"), ] def name_to_display(self): return self.display_name -BASE_GROUP_NAME_PATTERN = re.compile( - rf'(?P.*?)(?: \[(?:{"|".join(OriginType.values)})\]|$)' -) +BASE_GROUP_NAME_PATTERN = re.compile(rf'(?P.*?)(?: \[(?:{"|".join(OriginType.values)})\]|$)') @receiver(pre_save, sender=Group) def handle_name_type_display_name(sender, instance, **kwargs): match = BASE_GROUP_NAME_PATTERN.match(instance.name) - if match and match.group('base_name'): - instance.name = f'{match.group("base_name")} [{instance.type}]' + if match and match.group("base_name"): + instance.name = f"{match.group('base_name')} [{instance.type}]" instance.display_name = match.group("base_name") else: - raise RuntimeError(f'Check regex. Data: `{instance.name}`') + raise RuntimeError(f"Check regex. Data: `{instance.name}`") class RoleTypes(models.TextChoices): - business = 'business', 'business' - role = 'role', 'role' - hidden = 'hidden', 'hidden' + business = "business", "business" + role = "role", "role" + hidden = "hidden", "hidden" class Role(models.Model): # pylint: disable=too-many-instance-attributes @@ -138,25 +130,19 @@ class Role(models.Model): # pylint: disable=too-many-instance-attributes init_params = models.JSONField(default=dict) bundle = models.ForeignKey(Bundle, on_delete=models.CASCADE, null=True, default=None) built_in = models.BooleanField(default=True, null=False) - type = models.CharField( - max_length=32, choices=RoleTypes.choices, null=False, default=RoleTypes.role - ) + type = models.CharField(max_length=32, choices=RoleTypes.choices, null=False, default=RoleTypes.role) category = models.ManyToManyField(ProductCategory) any_category = models.BooleanField(default=False) - parametrized_by_type = models.JSONField( - default=list, null=False, validators=[validate_object_type] - ) + parametrized_by_type = models.JSONField(default=list, null=False, validators=[validate_object_type]) __obj__ = None class Meta: constraints = [ - models.UniqueConstraint(fields=['name', 'built_in'], name='unique_name'), - models.UniqueConstraint( - fields=['display_name', 'built_in'], name='unique_display_name' - ), + models.UniqueConstraint(fields=["name", "built_in"], name="unique_name"), + models.UniqueConstraint(fields=["display_name", "built_in"], name="unique_display_name"), ] indexes = [ - models.Index(fields=['name', 'display_name']), + models.Index(fields=["name", "display_name"]), ] def get_role_obj(self): @@ -164,12 +150,12 @@ def get_role_obj(self): try: role_module = importlib.import_module(self.module_name) except ModuleNotFoundError: - err('ROLE_MODULE_ERROR', f'No module named "{self.module_name}"') + raise_adcm_ex("ROLE_MODULE_ERROR", f'No module named "{self.module_name}"') try: role_class = getattr(role_module, self.class_name) except AttributeError: msg = f'No class named "{self.class_name}" in module "{self.module_name}"' - err('ROLE_CLASS_ERROR', msg) + raise_adcm_ex("ROLE_CLASS_ERROR", msg) return role_class(**self.init_params) # pylint: disable=E1134 @@ -218,14 +204,10 @@ class PolicyObject(models.Model): content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE) object_id = models.PositiveIntegerField() - object = GenericForeignKey('content_type', 'object_id') + object = GenericForeignKey("content_type", "object_id") class Meta: - constraints = [ - models.UniqueConstraint( - fields=['content_type', 'object_id'], name='unique_policy_object' - ) - ] + constraints = [models.UniqueConstraint(fields=["content_type", "object_id"], name="unique_policy_object")] class PolicyPermission(models.Model): @@ -310,11 +292,11 @@ def apply(self): def get_objects_for_policy(obj): obj_type_map = {} obj_type = obj.prototype.type - if obj_type == 'component': + if obj_type == "component": object_list = [obj, obj.service, obj.cluster] - elif obj_type == 'service': + elif obj_type == "service": object_list = [obj, obj.cluster] - elif obj_type == 'host': + elif obj_type == "host": if obj.cluster: object_list = [obj, obj.provider, obj.cluster] for hc in HostComponent.objects.filter(cluster=obj.cluster, host=obj): diff --git a/python/rbac/roles.py b/python/rbac/roles.py index e49b878b7c..7404cbd737 100644 --- a/python/rbac/roles.py +++ b/python/rbac/roles.py @@ -12,13 +12,13 @@ """RBAC Role classes""" -from adwp_base.errors import raise_AdwpEx as err from django.apps import apps from django.contrib.contenttypes.models import ContentType from django.db import transaction from django.utils import timezone from guardian.models import GroupObjectPermission, UserObjectPermission +from cm.errors import raise_adcm_ex from cm.models import ( Action, ClusterObject, @@ -96,13 +96,13 @@ class ObjectRole(AbstractRole): """This Role apply django-guardian object level permissions""" def filter(self): - if 'model' not in self.params: + if "model" not in self.params: return None try: - model = apps.get_model(self.params['app_name'], self.params['model']) + model = apps.get_model(self.params["app_name"], self.params["model"]) except LookupError as e: - err('ROLE_FILTER_ERROR', str(e)) - return model.objects.filter(**self.params['filter']) + raise_adcm_ex("ROLE_FILTER_ERROR", str(e)) + return model.objects.filter(**self.params["filter"]) def apply(self, policy: Policy, role: Role, user: User, group: Group, param_obj=None): """Apply Role to User and/or Group""" @@ -114,16 +114,16 @@ def apply(self, policy: Policy, role: Role, user: User, group: Group, param_obj= def get_host_objects(obj): object_type = obj.prototype.type host_list = [] - if object_type == 'cluster': + if object_type == "cluster": for host in Host.obj.filter(cluster=obj): host_list.append(host) - elif object_type == 'service': + elif object_type == "service": for hc in HostComponent.obj.filter(cluster=obj.cluster, service=obj): host_list.append(hc.host) - elif object_type == 'component': + elif object_type == "component": for hc in HostComponent.obj.filter(cluster=obj.cluster, service=obj.service, component=obj): host_list.append(hc.host) - elif object_type == 'provider': + elif object_type == "provider": for host in Host.obj.filter(provider=obj): host_list.append(host) @@ -134,23 +134,21 @@ class ActionRole(AbstractRole): """This Role apply permissions to run ADCM action""" def filter(self): - if 'model' not in self.params: + if "model" not in self.params: return None try: - model = apps.get_model(self.params['app_name'], self.params['model']) + model = apps.get_model(self.params["app_name"], self.params["model"]) except LookupError as e: - err('ROLE_FILTER_ERROR', str(e)) - return model.objects.filter(**self.params['filter']) + raise_adcm_ex("ROLE_FILTER_ERROR", str(e)) + return model.objects.filter(**self.params["filter"]) def apply(self, policy: Policy, role: Role, user: User, group: Group = None, param_obj=None): """Apply Role to User and/or Group""" - action = Action.obj.get(id=self.params['action_id']) + action = Action.obj.get(id=self.params["action_id"]) assign_user_or_group_perm(user, group, policy, get_perm_for_model(Action), action) for obj in policy.get_objects(param_obj): for perm in role.get_permissions(): - if action.host_action and perm.content_type == ContentType.objects.get_for_model( - Host - ): + if action.host_action and perm.content_type == ContentType.objects.get_for_model(Host): hosts = get_host_objects(obj) for host in hosts: assign_user_or_group_perm(user, group, policy, perm, host) @@ -160,22 +158,22 @@ def apply(self, policy: Policy, role: Role, user: User, group: Group = None, par class TaskRole(AbstractRole): def apply(self, policy, role, user, group, param_obj=None): - task = TaskLog.obj.get(id=self.params['task_id']) + task = TaskLog.obj.get(id=self.params["task_id"]) for obj in policy.get_objects(param_obj): if obj == task.task_object: apply_jobs(task, policy, user, group) -def get_perm_for_model(model, action='view'): +def get_perm_for_model(model, action="view"): ct = ContentType.objects.get_for_model(model) - codename = f'{action}_{model.__name__.lower()}' + codename = f"{action}_{model.__name__.lower()}" perm, _ = Permission.objects.get_or_create(content_type=ct, codename=codename) return perm def apply_jobs(task: TaskLog, policy: Policy, user: User, group: Group = None): assign_user_or_group_perm(user, group, policy, get_perm_for_model(TaskLog), task) - assign_user_or_group_perm(user, group, policy, get_perm_for_model(TaskLog, 'change'), task) + assign_user_or_group_perm(user, group, policy, get_perm_for_model(TaskLog, "change"), task) for job in JobLog.objects.filter(task=task): assign_user_or_group_perm(user, group, policy, get_perm_for_model(JobLog), job) for log in LogStorage.objects.filter(job=job): @@ -186,14 +184,14 @@ def re_apply_policy_for_jobs(action_object, task): obj_type_map = get_objects_for_policy(action_object) object_model = action_object.__class__.__name__.lower() task_role, _ = Role.objects.get_or_create( - name=f'View role for task {task.id}', - display_name=f'View role for task {task.id}', - description='View tasklog object with following joblog and logstorage', + name=f"View role for task {task.id}", + display_name=f"View role for task {task.id}", + description="View tasklog object with following joblog and logstorage", type=RoleTypes.hidden, - module_name='rbac.roles', - class_name='TaskRole', + module_name="rbac.roles", + class_name="TaskRole", init_params={ - 'task_id': task.id, + "task_id": task.id, }, parametrized_by_type=[task.task_object.prototype.type], ) @@ -202,23 +200,21 @@ def re_apply_policy_for_jobs(action_object, task): for user in policy.user.all(): try: uop = UserObjectPermission.objects.get( - user=user, permission__codename='view_action', object_pk=task.action.pk + user=user, permission__codename="view_action", object_pk=task.action.pk ) except UserObjectPermission.DoesNotExist: continue - if uop in policy.user_object_perm.all() and user.has_perm( - f'view_{object_model}', action_object - ): + if uop in policy.user_object_perm.all() and user.has_perm(f"view_{object_model}", action_object): policy.role.child.add(task_role) apply_jobs(task, policy, user, None) for group in policy.group.all(): try: gop = GroupObjectPermission.objects.get( - group=group, permission__codename='view_action', object_pk=task.action.pk + group=group, permission__codename="view_action", object_pk=task.action.pk ) model_view_gop = GroupObjectPermission.objects.get( group=group, - permission__codename=f'view_{object_model}', + permission__codename=f"view_{object_model}", object_pk=action_object.pk, ) except GroupObjectPermission.DoesNotExist: @@ -241,19 +237,19 @@ def apply(self, policy: Policy, role: Role, user: User, group: Group, param_obj= for perm in role.get_permissions(): - if perm.content_type.model == 'objectconfig': + if perm.content_type.model == "objectconfig": assign_user_or_group_perm(user, group, policy, perm, obj.config) for cg in config_groups: assign_user_or_group_perm(user, group, policy, perm, cg.config) - if perm.content_type.model == 'configlog': + if perm.content_type.model == "configlog": for config in obj.config.configlog_set.all(): assign_user_or_group_perm(user, group, policy, perm, config) for cg in config_groups: for config in cg.config.configlog_set.all(): assign_user_or_group_perm(user, group, policy, perm, config) - if perm.content_type.model == 'groupconfig': + if perm.content_type.model == "groupconfig": for cg in config_groups: assign_user_or_group_perm(user, group, policy, perm, cg) @@ -263,9 +259,7 @@ class ParentRole(AbstractRole): def find_and_apply(self, obj, policy, role, user, group=None): """Find Role of appropriate type and apply it to specified object""" - for r in role.child.filter( - class_name__in=['ObjectRole', 'ActionRole', 'TaskRole', 'ConfigRole'] - ): + for r in role.child.filter(class_name__in=["ObjectRole", "ActionRole", "TaskRole", "ConfigRole"]): if obj.prototype.type in r.parametrized_by_type: r.apply(policy, user, group, obj) @@ -273,7 +267,7 @@ def apply( self, policy: Policy, role: Role, user: User, group: Group = None, param_obj=None ): # pylint: disable=too-many-branches, too-many-nested-blocks """Apply Role to User and/or Group""" - for r in role.child.filter(class_name__in=['ModelRole', 'ParentRole']): + for r in role.child.filter(class_name__in=["ModelRole", "ParentRole"]): r.apply(policy, user, group, param_obj) parametrized_by = set() @@ -281,41 +275,39 @@ def apply( parametrized_by.update(set(r.parametrized_by_type)) for obj in policy.get_objects(param_obj): - view_cluster_perm = Permission.objects.get(codename='view_cluster') - view_service_perm = Permission.objects.get(codename='view_clusterobject') + view_cluster_perm = Permission.objects.get(codename="view_cluster") + view_service_perm = Permission.objects.get(codename="view_clusterobject") self.find_and_apply(obj, policy, role, user, group) - if obj.prototype.type == 'cluster': - if 'service' in parametrized_by or 'component' in parametrized_by: + if obj.prototype.type == "cluster": + if "service" in parametrized_by or "component" in parametrized_by: for service in ClusterObject.obj.filter(cluster=obj): self.find_and_apply(service, policy, role, user, group) - if 'component' in parametrized_by: + if "component" in parametrized_by: for comp in ServiceComponent.obj.filter(service=service): self.find_and_apply(comp, policy, role, user, group) - if 'host' in parametrized_by: + if "host" in parametrized_by: for host in Host.obj.filter(cluster=obj): self.find_and_apply(host, policy, role, user, group) - elif obj.prototype.type == 'service': - if 'component' in parametrized_by: + elif obj.prototype.type == "service": + if "component" in parametrized_by: for comp in ServiceComponent.obj.filter(service=obj): self.find_and_apply(comp, policy, role, user, group) - if 'host' in parametrized_by: + if "host" in parametrized_by: for hc in HostComponent.obj.filter(cluster=obj.cluster, service=obj): self.find_and_apply(hc.host, policy, role, user, group) assign_user_or_group_perm(user, group, policy, view_cluster_perm, obj.cluster) - elif obj.prototype.type == 'component': - if 'host' in parametrized_by: - for hc in HostComponent.obj.filter( - cluster=obj.cluster, service=obj.service, component=obj - ): + elif obj.prototype.type == "component": + if "host" in parametrized_by: + for hc in HostComponent.obj.filter(cluster=obj.cluster, service=obj.service, component=obj): self.find_and_apply(hc.host, policy, role, user, group) assign_user_or_group_perm(user, group, policy, view_cluster_perm, obj.cluster) assign_user_or_group_perm(user, group, policy, view_service_perm, obj.service) - elif obj.prototype.type == 'provider': - if 'host' in parametrized_by: + elif obj.prototype.type == "provider": + if "host" in parametrized_by: for host in Host.obj.filter(provider=obj): self.find_and_apply(host, policy, role, user, group) diff --git a/python/rbac/services/group.py b/python/rbac/services/group.py index a0f32352ce..ecab4f510f 100644 --- a/python/rbac/services/group.py +++ b/python/rbac/services/group.py @@ -14,11 +14,10 @@ from typing import List -from adwp_base.errors import AdwpEx from django.core.exceptions import ObjectDoesNotExist from django.db import IntegrityError, transaction -from rest_framework import status +from cm.errors import raise_adcm_ex from rbac import models from rbac.utils import Empty, set_not_empty_attr @@ -27,20 +26,18 @@ def _update_users(group: models.Group, users: [Empty, List[dict]]) -> None: if users is Empty: return if group.type == models.OriginType.LDAP: - raise AdwpEx('GROUP_UPDATE_ERROR', msg="You can\'t change users in LDAP group") + raise_adcm_ex("GROUP_CONFLICT", msg="You can\'t change users in LDAP group") group_users = {u.id: u for u in group.user_set.all()} - new_users = [u['id'] for u in users] + new_users = [u["id"] for u in users] for user_id in new_users: if user_id in group_users: continue try: user = models.User.objects.get(id=user_id) - except ObjectDoesNotExist as exc: - msg = f'User with ID {user_id} was not found' - raise AdwpEx( - 'GROUP_UPDATE_ERROR', msg=msg, http_code=status.HTTP_400_BAD_REQUEST - ) from exc + except ObjectDoesNotExist: + msg = f"User with ID {user_id} was not found" + raise_adcm_ex("GROUP_UPDATE_ERROR", msg=msg) group.user_set.add(user) group_users[user_id] = user @@ -61,7 +58,7 @@ def create( try: group = models.Group.objects.create(name=name_to_display, description=description) except IntegrityError as exc: - raise AdwpEx('GROUP_CREATE_ERROR', msg=f'Group creation failed with error {exc}') from exc + raise_adcm_ex("GROUP_CREATE_ERROR", msg=f"Group creation failed with error {exc}") _update_users(group, user_set or []) return group @@ -77,12 +74,12 @@ def update( ) -> models.Group: """Full or partial Group object update""" if group.type == models.OriginType.LDAP: - raise AdwpEx('GROUP_UPDATE_ERROR', msg='You cannot change LDAP type group') - set_not_empty_attr(group, partial, 'name', name_to_display) - set_not_empty_attr(group, partial, 'description', description, '') + raise_adcm_ex("GROUP_CONFLICT", msg="You cannot change LDAP type group") + set_not_empty_attr(group, partial, "name", name_to_display) + set_not_empty_attr(group, partial, "description", description, "") try: group.save() except IntegrityError as exc: - raise AdwpEx('GROUP_UPDATE_ERROR', msg=f'Group update failed with error {exc}') from exc + raise_adcm_ex("GROUP_CONFLICT", msg=f"Group update failed with error {exc}") _update_users(group, user_set) return group diff --git a/python/rbac/services/policy.py b/python/rbac/services/policy.py index ee01f14cc3..090b42b739 100644 --- a/python/rbac/services/policy.py +++ b/python/rbac/services/policy.py @@ -13,13 +13,12 @@ from typing import List -from adwp_base.errors import AdwpEx from django.contrib.contenttypes.models import ContentType from django.db import IntegrityError from django.db.transaction import atomic from django.utils import timezone -from rest_framework import status +from cm.errors import raise_adcm_ex from cm.models import ADCMEntity, DummyData from rbac.models import Group, Policy, PolicyObject, Role, User from rbac.utils import update_m2m_field @@ -28,19 +27,16 @@ def _get_policy_object(obj: ADCMEntity) -> PolicyObject: """Get PolicyObject for ADCM entity""" content_type = ContentType.objects.get_for_model(obj) - policy_object, _ = PolicyObject.objects.get_or_create( - object_id=obj.id, content_type=content_type - ) + policy_object, _ = PolicyObject.objects.get_or_create(object_id=obj.id, content_type=content_type) return policy_object def _check_subjects(users: List[User], groups: List[Group]) -> None: """Check if policy has at least one subject""" if not users and not groups: - raise AdwpEx( - 'POLICY_INTEGRITY_ERROR', - msg='Role should be bind with some users or groups', - http_code=status.HTTP_400_BAD_REQUEST, + raise_adcm_ex( + "POLICY_INTEGRITY_ERROR", + msg="Role should be bind with some users or groups", ) @@ -48,26 +44,23 @@ def _check_objects(role: Role, objects: List[ADCMEntity]) -> None: """Check if objects are complies with role parametrization""" if role.parametrized_by_type: if not objects: - raise AdwpEx( - 'POLICY_INTEGRITY_ERROR', - msg='Parametrized role should be applied to some objects', - http_code=status.HTTP_400_BAD_REQUEST, + raise_adcm_ex( + "POLICY_INTEGRITY_ERROR", + msg="Parametrized role should be applied to some objects", ) for obj in objects: if obj.prototype.type not in role.parametrized_by_type: - raise AdwpEx( - 'POLICY_INTEGRITY_ERROR', + raise_adcm_ex( + "POLICY_INTEGRITY_ERROR", msg=( - f'Role parametrized by {role.parametrized_by_type} ' - f'could not be applied to {obj.prototype.type}' + f"Role parametrized by {role.parametrized_by_type} " + f"could not be applied to {obj.prototype.type}" ), - http_code=status.HTTP_400_BAD_REQUEST, ) elif objects: - raise AdwpEx( - 'POLICY_INTEGRITY_ERROR', - msg='Not-parametrized role should not be applied to any objects', - http_code=status.HTTP_400_BAD_REQUEST, + raise_adcm_ex( + "POLICY_INTEGRITY_ERROR", + msg="Not-parametrized role should not be applied to any objects", ) @@ -87,20 +80,18 @@ def policy_create(name: str, role: Role, built_in: bool = False, **kwargs): :return: Policy :rtype: Policy """ - users = kwargs.get('user', []) - groups = kwargs.get('group', []) + users = kwargs.get("user", []) + groups = kwargs.get("group", []) _check_subjects(users, groups) - objects = kwargs.get('object', []) + objects = kwargs.get("object", []) DummyData.objects.filter(id=1).update(date=timezone.now()) _check_objects(role, objects) - description = kwargs.get('description', '') + description = kwargs.get("description", "") try: - policy = Policy.objects.create( - name=name, role=role, built_in=built_in, description=description - ) + policy = Policy.objects.create(name=name, role=role, built_in=built_in, description=description) except IntegrityError as exc: - raise AdwpEx('POLICY_CREATE_ERROR', msg=f'Policy creation failed with error {exc}') from exc + raise_adcm_ex("POLICY_CREATE_ERROR", msg=f"Policy creation failed with error {exc}") for obj in objects: policy.object.add(_get_policy_object(obj)) @@ -124,22 +115,22 @@ def policy_update(policy: Policy, **kwargs) -> Policy: :rtype: Policy """ - users = kwargs.get('user') - groups = kwargs.get('group') + users = kwargs.get("user") + groups = kwargs.get("group") DummyData.objects.filter(id=1).update(date=timezone.now()) _check_subjects( users if users is not None else policy.user.all(), groups if groups is not None else policy.group.all(), ) - role = kwargs.get('role') - objects = kwargs.get('object') + role = kwargs.get("role") + objects = kwargs.get("object") policy_old_objects = [po.object for po in policy.object.all()] _check_objects(role or policy.role, objects if objects is not None else policy_old_objects) - if 'name' in kwargs: - policy.name = kwargs['name'] - if 'description' in kwargs: - policy.description = kwargs['description'] + if "name" in kwargs: + policy.name = kwargs["name"] + if "description" in kwargs: + policy.description = kwargs["description"] if role is not None: policy.role = role if users is not None: @@ -151,6 +142,6 @@ def policy_update(policy: Policy, **kwargs) -> Policy: try: policy.save() except IntegrityError as exc: - raise AdwpEx('POLICY_UPDATE_ERROR', msg=f'Policy update failed with error {exc}') from exc + raise_adcm_ex("POLICY_UPDATE_ERROR", msg=f"Policy update failed with error {exc}") policy.apply() return policy diff --git a/python/rbac/services/role.py b/python/rbac/services/role.py index 54b55e1f87..dc51c8b1b0 100644 --- a/python/rbac/services/role.py +++ b/python/rbac/services/role.py @@ -12,57 +12,56 @@ from typing import List -from adwp_base.errors import AdwpEx from django.db import IntegrityError from django.db.transaction import atomic from rest_framework.exceptions import ValidationError +from cm.errors import raise_adcm_ex from rbac.models import Role, RoleTypes from rbac.utils import update_m2m_field def check_role_child(child: List[Role], partial=False): param_set = set() - cluster_hierarchy = {'cluster', 'service', 'component'} - provider_hierarchy = {'provider'} + cluster_hierarchy = {"cluster", "service", "component"} + provider_hierarchy = {"provider"} if not child and not partial: - errors = {'child': ['Roles without children make not sense']} + errors = {"child": ["Roles without children make not sense"]} raise ValidationError(errors) for item in child: if not item.built_in: - errors = {'child': ['Only built-in roles allowed to be included as children.']} + errors = {"child": ["Only built-in roles allowed to be included as children."]} raise ValidationError(errors) if item.type != RoleTypes.business: - errors = {'child': ['Only business roles allowed to be included as children.']} + errors = {"child": ["Only business roles allowed to be included as children."]} raise ValidationError(errors) param_set.update(item.parametrized_by_type) if param_set.intersection(provider_hierarchy) and param_set.intersection(cluster_hierarchy): - msg = 'Combination of cluster/service/component and provider permissions is not allowed' - raise AdwpEx('ROLE_CONFLICT', msg) + raise_adcm_ex("ROLE_CONFLICT") return list(param_set) @atomic def role_create(built_in=False, type_of_role=RoleTypes.role, **kwargs) -> Role: """Creating Role object""" - child = kwargs.pop('child', []) + child = kwargs.pop("child", []) parametrized_by = check_role_child(child) - name = kwargs.pop('name', '') - if name == '': - name = kwargs['display_name'] + name = kwargs.pop("name", "") + if name == "": + name = kwargs["display_name"] try: role = Role.objects.create( name=name, built_in=built_in, type=type_of_role, - module_name='rbac.roles', - class_name='ParentRole', + module_name="rbac.roles", + class_name="ParentRole", parametrized_by_type=parametrized_by, **kwargs, ) except IntegrityError as exc: - raise AdwpEx('ROLE_CREATE_ERROR', msg=f'Role creation failed with error {exc}') from exc + raise_adcm_ex("ROLE_CREATE_ERROR", msg=f"Role creation failed with error {exc}") role.child.add(*child) return role @@ -70,16 +69,16 @@ def role_create(built_in=False, type_of_role=RoleTypes.role, **kwargs) -> Role: @atomic def role_update(role: Role, partial, **kwargs) -> Role: """Updating Role object""" - child = kwargs.pop('child', []) + child = kwargs.pop("child", []) parametrized_by = check_role_child(child, partial) - kwargs['parametrized_by_type'] = parametrized_by - kwargs.pop('name', None) + kwargs["parametrized_by_type"] = parametrized_by + kwargs.pop("name", None) for key, value in kwargs.items(): setattr(role, key, value) try: role.save() except IntegrityError as exc: - raise AdwpEx('ROLE_UPDATE_ERROR', msg=f'Role update failed with error {exc}') from exc + raise_adcm_ex("ROLE_UPDATE_ERROR", msg=f"Role update failed with error {exc}") if child: update_m2m_field(role.child, child) diff --git a/python/rbac/services/user.py b/python/rbac/services/user.py index e6e26dc234..b23fa8ca30 100644 --- a/python/rbac/services/user.py +++ b/python/rbac/services/user.py @@ -14,13 +14,12 @@ from typing import List -from adwp_base.errors import AdwpEx from django.contrib.auth.hashers import make_password from django.core.exceptions import ObjectDoesNotExist from django.db import IntegrityError, transaction -from rest_framework import status from rest_framework.authtoken.models import Token +from cm.errors import raise_adcm_ex from rbac import models from rbac.utils import Empty, set_not_empty_attr @@ -32,7 +31,7 @@ def _set_password(user: models.User, value: str) -> None: return if not value: - raise AdwpEx("USER_UPDATE_ERROR", msg="Password could not be empty") + raise_adcm_ex("USER_UPDATE_ERROR", msg="Password could not be empty") new_password = make_password(value) if user.password == new_password: @@ -54,13 +53,11 @@ def _update_groups(user: models.User, groups: [Empty, List[dict]]) -> None: continue try: group = models.Group.objects.get(id=group_id) - except ObjectDoesNotExist as exc: + except ObjectDoesNotExist: msg = f"Group with ID {group_id} was not found" - raise AdwpEx( - "USER_UPDATE_ERROR", msg=msg, http_code=status.HTTP_400_BAD_REQUEST - ) from exc + raise_adcm_ex("USER_UPDATE_ERROR", msg=msg) if group.type == models.OriginType.LDAP: - raise AdwpEx("USER_UPDATE_ERROR", msg="You cannot add user to LDAP group") + raise_adcm_ex("USER_CONFLICT", msg="You cannot add user to LDAP group") user.groups.add(group) user_groups[group_id] = group @@ -68,7 +65,7 @@ def _update_groups(user: models.User, groups: [Empty, List[dict]]) -> None: if group_id in new_groups: continue if group.type == models.OriginType.LDAP: - raise AdwpEx("USER_UPDATE_ERROR", msg="You cannot remove user from original LDAP group") + raise_adcm_ex("USER_CONFLICT", msg="You cannot remove user from original LDAP group") user.groups.remove(group) @@ -98,17 +95,17 @@ def update( # pylint: disable=too-many-locals """Full or partial User update""" if (username is not Empty) and (username != user.username): - raise AdwpEx("USER_UPDATE_ERROR", msg="Username could not be changed") + raise_adcm_ex("USER_CONFLICT", msg="Username could not be changed") args = (username, first_name, last_name, email, is_superuser, is_active) if not partial and not all((arg is not Empty for arg in args)): - raise AdwpEx("USER_UPDATE_ERROR", msg="Full User update with partial argset is forbidden") + raise_adcm_ex("USER_UPDATE_ERROR", msg="Full User update with partial argset is forbidden") user_exist = models.User.objects.filter(email=email).exists() if user_exist and (email != ""): email_user = models.User.objects.get(email=email) if email_user != user: - raise AdwpEx("USER_UPDATE_ERROR", msg="User with the same email already exist") + raise_adcm_ex("USER_CONFLICT", msg="User with the same email already exist") names = { "username": username, "first_name": first_name, @@ -121,7 +118,7 @@ def update( if user.type == models.OriginType.LDAP and any( (value is not Empty and getattr(user, key) != value) for key, value in names.items() ): - raise AdwpEx("USER_UPDATE_ERROR", msg="You cannot change LDAP type user") + raise_adcm_ex("USER_CONFLICT", msg="You cannot change LDAP type user") set_not_empty_attr(user, partial, "first_name", first_name, "") set_not_empty_attr(user, partial, "last_name", last_name, "") set_not_empty_attr(user, partial, "email", email, "") @@ -157,7 +154,7 @@ def create( user_exist = models.User.objects.filter(email=email).exists() if user_exist and (email != ""): - raise AdwpEx("USER_CREATE_ERROR", msg="User with the same email already exist") + raise_adcm_ex("USER_CREATE_ERROR", msg="User with the same email already exist") try: user = func( @@ -170,7 +167,7 @@ def create( is_active=is_active, ) except IntegrityError as exc: - raise AdwpEx("USER_CREATE_ERROR", msg=f"User creation failed with error {exc}") from exc + raise_adcm_ex("USER_CREATE_ERROR", msg=f"User creation failed with error {exc}") _update_groups(user, groups or []) _regenerate_token(user) return user diff --git a/python/rbac/tests/test_api.py b/python/rbac/tests/test_api.py index 908e0a974a..849cbe24be 100644 --- a/python/rbac/tests/test_api.py +++ b/python/rbac/tests/test_api.py @@ -102,9 +102,7 @@ def setUp(self) -> None: ( {"name": "test", "role": []}, { - "role": { - "non_field_errors": ["Invalid data. Expected a dictionary, but got list."] - }, + "role": {"non_field_errors": ["Invalid data. Expected a dictionary, but got list."]}, "object": ["This field is required."], }, ), @@ -379,9 +377,7 @@ def test_create_role(self): self.assertEqual(json.loads(response.content), response_data) def test_patch_empty_role_id(self): - role = Role.objects.create( - name="Test role", module_name="rbac.roles", class_name="ModelRole" - ) + role = Role.objects.create(name="Test role", module_name="rbac.roles", class_name="ModelRole") policy = Policy.objects.create(name="Test policy", role=role, built_in=False) policy.user.add(self.test_user) @@ -399,9 +395,7 @@ def test_patch_empty_role_id(self): self.assertEqual(response.status_code, HTTP_200_OK) - response = self.client.patch( - path=path, data={**data_valid, **{"role": {}}}, content_type=APPLICATION_JSON - ) + response = self.client.patch(path=path, data={**data_valid, **{"role": {}}}, content_type=APPLICATION_JSON) self.assertEqual(response.status_code, HTTP_400_BAD_REQUEST) self.assertEqual(response.json()["role"], ["This field may not be empty."]) diff --git a/python/rbac/tests/test_base.py b/python/rbac/tests/test_base.py index 406fbb0185..8d5b1eca9b 100644 --- a/python/rbac/tests/test_base.py +++ b/python/rbac/tests/test_base.py @@ -51,6 +51,7 @@ def create_bundles_and_prototypes(self): name="sample_cluster", version="1.0", display_name="Sample Cluster", + allow_maintenance_mode=True, ) self.sp_1 = Prototype.objects.create( bundle=self.bundle_1, @@ -58,6 +59,7 @@ def create_bundles_and_prototypes(self): name="service_1", version="1.0", display_name="Service 1", + allow_maintenance_mode=True, ) self.sp_2 = Prototype.objects.create( @@ -75,6 +77,7 @@ def create_bundles_and_prototypes(self): version="1.0", display_name="Component 1 from Service 1", parent=self.sp_1, + allow_maintenance_mode=True, ) self.cop_12 = Prototype.objects.create( bundle=self.bundle_1, @@ -101,8 +104,10 @@ def create_bundles_and_prototypes(self): parent=self.sp_2, ) self.bundle_2 = Bundle.objects.create(name="provider_bundle", version="1.0") - self.pp = Prototype.objects.create(bundle=self.bundle_2, type="provider", name="provider") - self.hp = Prototype.objects.create(bundle=self.bundle_2, type="host", name="host") + self.pp = Prototype.objects.create( + bundle=self.bundle_2, type="provider", name="provider", allow_maintenance_mode=True + ) + self.hp = Prototype.objects.create(bundle=self.bundle_2, type="host", name="host", allow_maintenance_mode=True) def create_permissions(self): self.add_host_perm = cook_perm("add", "host") diff --git a/python/rbac/tests/test_group.py b/python/rbac/tests/test_group.py index 02742efc13..de16e4b4cb 100644 --- a/python/rbac/tests/test_group.py +++ b/python/rbac/tests/test_group.py @@ -16,7 +16,7 @@ from rbac.models import Group, OriginType -class GroupBaseTestCase(BaseTestCase): +class GroupTestCase(BaseTestCase): def test_group_creation_blank(self): with self.assertRaisesRegex( RuntimeError, r"Check regex. Data: ", msg="group creation with no args is not allowed" diff --git a/python/rbac/tests/test_policy.py b/python/rbac/tests/test_policy.py index 71a6fb7ff5..53a0148fa6 100644 --- a/python/rbac/tests/test_policy.py +++ b/python/rbac/tests/test_policy.py @@ -159,24 +159,16 @@ def test_parent_policy4cluster(self): self.assertFalse(self.user.has_perm("cm.change_config_of_cluster", self.cluster)) self.assertFalse(self.user.has_perm("cm.change_config_of_clusterobject", self.service_1)) self.assertFalse(self.user.has_perm("cm.change_config_of_clusterobject", self.service_2)) - self.assertFalse( - self.user.has_perm("cm.change_config_of_servicecomponent", self.component_11) - ) - self.assertFalse( - self.user.has_perm("cm.change_config_of_servicecomponent", self.component_21) - ) + self.assertFalse(self.user.has_perm("cm.change_config_of_servicecomponent", self.component_11)) + self.assertFalse(self.user.has_perm("cm.change_config_of_servicecomponent", self.component_21)) p.apply() self.assertTrue(self.user.has_perm("cm.change_config_of_cluster", self.cluster)) self.assertTrue(self.user.has_perm("cm.change_config_of_clusterobject", self.service_1)) self.assertTrue(self.user.has_perm("cm.change_config_of_clusterobject", self.service_2)) - self.assertTrue( - self.user.has_perm("cm.change_config_of_servicecomponent", self.component_11) - ) - self.assertTrue( - self.user.has_perm("cm.change_config_of_servicecomponent", self.component_21) - ) + self.assertTrue(self.user.has_perm("cm.change_config_of_servicecomponent", self.component_11)) + self.assertTrue(self.user.has_perm("cm.change_config_of_servicecomponent", self.component_21)) def test_parent_policy4service(self): p = Policy.objects.create(role=self.object_role_custom_perm_cluster_service_component()) @@ -186,12 +178,8 @@ def test_parent_policy4service(self): self.assertFalse(self.user.has_perm("cm.change_config_of_cluster", self.cluster)) self.assertFalse(self.user.has_perm("cm.change_config_of_clusterobject", self.service_1)) self.assertFalse(self.user.has_perm("cm.change_config_of_clusterobject", self.service_2)) - self.assertFalse( - self.user.has_perm("cm.change_config_of_servicecomponent", self.component_11) - ) - self.assertFalse( - self.user.has_perm("cm.change_config_of_servicecomponent", self.component_21) - ) + self.assertFalse(self.user.has_perm("cm.change_config_of_servicecomponent", self.component_11)) + self.assertFalse(self.user.has_perm("cm.change_config_of_servicecomponent", self.component_21)) p.apply() @@ -199,12 +187,8 @@ def test_parent_policy4service(self): self.assertFalse(self.user.has_perm("cm.change_config_of_cluster", self.cluster)) self.assertTrue(self.user.has_perm("cm.change_config_of_clusterobject", self.service_1)) self.assertFalse(self.user.has_perm("cm.change_config_of_clusterobject", self.service_2)) - self.assertTrue( - self.user.has_perm("cm.change_config_of_servicecomponent", self.component_11) - ) - self.assertFalse( - self.user.has_perm("cm.change_config_of_servicecomponent", self.component_21) - ) + self.assertTrue(self.user.has_perm("cm.change_config_of_servicecomponent", self.component_11)) + self.assertFalse(self.user.has_perm("cm.change_config_of_servicecomponent", self.component_21)) def test_parent_policy4service2(self): p = Policy.objects.create(role=self.object_role_custom_perm_cluster_service_component()) @@ -215,12 +199,8 @@ def test_parent_policy4service2(self): self.assertFalse(self.user.has_perm("cm.change_config_of_cluster", self.cluster)) self.assertFalse(self.user.has_perm("cm.change_config_of_clusterobject", self.service_1)) self.assertFalse(self.user.has_perm("cm.change_config_of_clusterobject", self.service_2)) - self.assertFalse( - self.user.has_perm("cm.change_config_of_servicecomponent", self.component_11) - ) - self.assertFalse( - self.user.has_perm("cm.change_config_of_servicecomponent", self.component_21) - ) + self.assertFalse(self.user.has_perm("cm.change_config_of_servicecomponent", self.component_11)) + self.assertFalse(self.user.has_perm("cm.change_config_of_servicecomponent", self.component_21)) p.apply() @@ -228,12 +208,8 @@ def test_parent_policy4service2(self): self.assertFalse(self.user.has_perm("cm.change_config_of_cluster", self.cluster)) self.assertFalse(self.user.has_perm("cm.change_config_of_clusterobject", self.service_1)) self.assertTrue(self.user.has_perm("cm.change_config_of_clusterobject", self.service_2)) - self.assertFalse( - self.user.has_perm("cm.change_config_of_servicecomponent", self.component_11) - ) - self.assertTrue( - self.user.has_perm("cm.change_config_of_servicecomponent", self.component_21) - ) + self.assertFalse(self.user.has_perm("cm.change_config_of_servicecomponent", self.component_11)) + self.assertTrue(self.user.has_perm("cm.change_config_of_servicecomponent", self.component_21)) def test_parent_policy4component(self): p = Policy.objects.create(role=self.object_role_custom_perm_cluster_service_component()) @@ -245,12 +221,8 @@ def test_parent_policy4component(self): self.assertFalse(self.user.has_perm("cm.change_config_of_cluster", self.cluster)) self.assertFalse(self.user.has_perm("cm.change_config_of_clusterobject", self.service_1)) self.assertFalse(self.user.has_perm("cm.change_config_of_clusterobject", self.service_2)) - self.assertFalse( - self.user.has_perm("cm.change_config_of_servicecomponent", self.component_11) - ) - self.assertFalse( - self.user.has_perm("cm.change_config_of_servicecomponent", self.component_21) - ) + self.assertFalse(self.user.has_perm("cm.change_config_of_servicecomponent", self.component_11)) + self.assertFalse(self.user.has_perm("cm.change_config_of_servicecomponent", self.component_21)) p.apply() @@ -259,12 +231,8 @@ def test_parent_policy4component(self): self.assertFalse(self.user.has_perm("cm.change_config_of_cluster", self.cluster)) self.assertFalse(self.user.has_perm("cm.change_config_of_clusterobject", self.service_1)) self.assertFalse(self.user.has_perm("cm.change_config_of_clusterobject", self.service_2)) - self.assertTrue( - self.user.has_perm("cm.change_config_of_servicecomponent", self.component_11) - ) - self.assertFalse( - self.user.has_perm("cm.change_config_of_servicecomponent", self.component_21) - ) + self.assertTrue(self.user.has_perm("cm.change_config_of_servicecomponent", self.component_11)) + self.assertFalse(self.user.has_perm("cm.change_config_of_servicecomponent", self.component_21)) def test_parent_policy4host_in_cluster(self): provider, host1, host2 = self.get_hosts_and_provider() @@ -306,21 +274,15 @@ def test_parent_policy4host_in_service(self): }, ], ) - p = Policy.objects.create( - role=self.object_role_custom_perm_cluster_service_component_host() - ) + p = Policy.objects.create(role=self.object_role_custom_perm_cluster_service_component_host()) p.user.add(self.user) p.add_object(self.service_1) self.assertFalse(self.user.has_perm("cm.change_config_of_cluster", self.cluster)) self.assertFalse(self.user.has_perm("cm.change_config_of_clusterobject", self.service_1)) self.assertFalse(self.user.has_perm("cm.change_config_of_clusterobject", self.service_2)) - self.assertFalse( - self.user.has_perm("cm.change_config_of_servicecomponent", self.component_11) - ) - self.assertFalse( - self.user.has_perm("cm.change_config_of_servicecomponent", self.component_21) - ) + self.assertFalse(self.user.has_perm("cm.change_config_of_servicecomponent", self.component_11)) + self.assertFalse(self.user.has_perm("cm.change_config_of_servicecomponent", self.component_21)) self.assertFalse(self.user.has_perm("cm.change_config_of_host", host1)) self.assertFalse(self.user.has_perm("cm.change_config_of_host", host2)) @@ -329,12 +291,8 @@ def test_parent_policy4host_in_service(self): self.assertFalse(self.user.has_perm("cm.change_confing_of_cluster", self.cluster)) self.assertTrue(self.user.has_perm("cm.change_config_of_clusterobject", self.service_1)) self.assertFalse(self.user.has_perm("cm.change_config_of_clusterobject", self.service_2)) - self.assertTrue( - self.user.has_perm("cm.change_config_of_servicecomponent", self.component_11) - ) - self.assertFalse( - self.user.has_perm("cm.change_config_of_servicecomponent", self.component_21) - ) + self.assertTrue(self.user.has_perm("cm.change_config_of_servicecomponent", self.component_11)) + self.assertFalse(self.user.has_perm("cm.change_config_of_servicecomponent", self.component_21)) self.assertTrue(self.user.has_perm("cm.change_config_of_host", host1)) self.assertFalse(self.user.has_perm("cm.change_config_of_host", host2)) @@ -365,21 +323,15 @@ def test_parent_policy4host_in_component(self): ], ) - p = Policy.objects.create( - role=self.object_role_custom_perm_cluster_service_component_host() - ) + p = Policy.objects.create(role=self.object_role_custom_perm_cluster_service_component_host()) p.user.add(self.user) p.add_object(self.component_21) self.assertFalse(self.user.has_perm("cm.change_config_of_cluster", self.cluster)) self.assertFalse(self.user.has_perm("cm.change_config_of_clusterobject", self.service_1)) self.assertFalse(self.user.has_perm("cm.change_config_of_clusterobject", self.service_2)) - self.assertFalse( - self.user.has_perm("cm.change_config_of_servicecomponent", self.component_11) - ) - self.assertFalse( - self.user.has_perm("cm.change_config_of_servicecomponent", self.component_21) - ) + self.assertFalse(self.user.has_perm("cm.change_config_of_servicecomponent", self.component_11)) + self.assertFalse(self.user.has_perm("cm.change_config_of_servicecomponent", self.component_21)) self.assertFalse(self.user.has_perm("cm.change_config_of_host", host1)) self.assertFalse(self.user.has_perm("cm.change_config_of_host", host2)) self.assertFalse(self.user.has_perm("cm.change_config_of_host", host3)) @@ -389,12 +341,8 @@ def test_parent_policy4host_in_component(self): self.assertFalse(self.user.has_perm("cm.change_config_of_cluster", self.cluster)) self.assertFalse(self.user.has_perm("cm.change_config_of_clusterobject", self.service_1)) self.assertFalse(self.user.has_perm("cm.change_config_of_clusterobject", self.service_2)) - self.assertFalse( - self.user.has_perm("cm.change_config_of_servicecomponent", self.component_11) - ) - self.assertTrue( - self.user.has_perm("cm.change_config_of_servicecomponent", self.component_21) - ) + self.assertFalse(self.user.has_perm("cm.change_config_of_servicecomponent", self.component_11)) + self.assertTrue(self.user.has_perm("cm.change_config_of_servicecomponent", self.component_21)) self.assertTrue(self.user.has_perm("cm.change_config_of_host", host1)) self.assertTrue(self.user.has_perm("cm.change_config_of_host", host2)) self.assertFalse(self.user.has_perm("cm.change_config_of_host", host3)) @@ -465,17 +413,13 @@ def test_add_host(self): ], ) - p = Policy.objects.create( - role=self.object_role_custom_perm_cluster_service_component_host() - ) + p = Policy.objects.create(role=self.object_role_custom_perm_cluster_service_component_host()) p.user.add(self.user) p.add_object(self.cluster) self.assertFalse(self.user.has_perm("cm.change_config_of_cluster", self.cluster)) self.assertFalse(self.user.has_perm("cm.change_config_of_clusterobject", self.service_1)) - self.assertFalse( - self.user.has_perm("cm.change_config_of_servicecomponent", self.component_11) - ) + self.assertFalse(self.user.has_perm("cm.change_config_of_servicecomponent", self.component_11)) self.assertFalse(self.user.has_perm("cm.change_config_of_host", host1)) self.assertFalse(self.user.has_perm("cm.change_config_of_host", host2)) @@ -483,9 +427,7 @@ def test_add_host(self): self.assertTrue(self.user.has_perm("cm.change_config_of_cluster", self.cluster)) self.assertTrue(self.user.has_perm("cm.change_config_of_clusterobject", self.service_1)) - self.assertTrue( - self.user.has_perm("cm.change_config_of_servicecomponent", self.component_11) - ) + self.assertTrue(self.user.has_perm("cm.change_config_of_servicecomponent", self.component_11)) self.assertTrue(self.user.has_perm("cm.change_config_of_host", host1)) self.assertFalse(self.user.has_perm("cm.change_config_of_host", host2)) @@ -493,9 +435,7 @@ def test_add_host(self): self.assertTrue(self.user.has_perm("cm.change_config_of_cluster", self.cluster)) self.assertTrue(self.user.has_perm("cm.change_config_of_clusterobject", self.service_1)) - self.assertTrue( - self.user.has_perm("cm.change_config_of_servicecomponent", self.component_11) - ) + self.assertTrue(self.user.has_perm("cm.change_config_of_servicecomponent", self.component_11)) self.assertTrue(self.user.has_perm("cm.change_config_of_host", host1)) self.assertTrue(self.user.has_perm("cm.change_config_of_host", host2)) @@ -518,12 +458,8 @@ def test_add_hc(self): self.assertFalse(self.user.has_perm("cm.change_config_of_cluster", self.cluster)) self.assertFalse(self.user.has_perm("cm.change_config_of_clusterobject", self.service_1)) - self.assertFalse( - self.user.has_perm("cm.change_config_of_servicecomponent", self.component_11) - ) - self.assertFalse( - self.user.has_perm("cm.change_config_of_servicecomponent", self.component_12) - ) + self.assertFalse(self.user.has_perm("cm.change_config_of_servicecomponent", self.component_11)) + self.assertFalse(self.user.has_perm("cm.change_config_of_servicecomponent", self.component_12)) self.assertFalse(self.user.has_perm("cm.change_config_of_host", host1)) self.assertFalse(self.user.has_perm("cm.change_config_of_host", host2)) @@ -531,12 +467,8 @@ def test_add_hc(self): self.assertFalse(self.user.has_perm("cm.change_config_of_cluster", self.cluster)) self.assertTrue(self.user.has_perm("cm.change_config_of_clusterobject", self.service_1)) - self.assertTrue( - self.user.has_perm("cm.change_config_of_servicecomponent", self.component_11) - ) - self.assertTrue( - self.user.has_perm("cm.change_config_of_servicecomponent", self.component_12) - ) + self.assertTrue(self.user.has_perm("cm.change_config_of_servicecomponent", self.component_11)) + self.assertTrue(self.user.has_perm("cm.change_config_of_servicecomponent", self.component_12)) self.assertTrue(self.user.has_perm("cm.change_config_of_host", host1)) self.assertFalse(self.user.has_perm("cm.change_config_of_host", host2)) @@ -559,11 +491,7 @@ def test_add_hc(self): self.assertFalse(self.user.has_perm("cm.change_config_of_cluster", self.cluster)) self.assertTrue(self.user.has_perm("cm.change_config_of_clusterobject", self.service_1)) - self.assertTrue( - self.user.has_perm("cm.change_config_of_servicecomponent", self.component_11) - ) - self.assertTrue( - self.user.has_perm("cm.change_config_of_servicecomponent", self.component_12) - ) + self.assertTrue(self.user.has_perm("cm.change_config_of_servicecomponent", self.component_11)) + self.assertTrue(self.user.has_perm("cm.change_config_of_servicecomponent", self.component_12)) self.assertTrue(self.user.has_perm("cm.change_config_of_host", host1)) self.assertTrue(self.user.has_perm("cm.change_config_of_host", host2)) diff --git a/python/rbac/tests/test_role.py b/python/rbac/tests/test_role.py index f49b8cf21c..ef5c24b539 100644 --- a/python/rbac/tests/test_role.py +++ b/python/rbac/tests/test_role.py @@ -12,24 +12,33 @@ import json -from adwp_base.errors import AdwpEx from django.contrib.auth.models import Permission from django.contrib.contenttypes.models import ContentType +from django.test import Client +from django.urls import reverse -from adcm.tests.base import BaseTestCase +from adcm.tests.base import APPLICATION_JSON, BaseTestCase +from api.utils import PermissionDenied, check_custom_perm +from cm.api import add_host_to_cluster +from cm.errors import AdcmEx from cm.models import ( Action, ActionType, Bundle, Cluster, ClusterObject, + Host, + HostProvider, + MaintenanceMode, ProductCategory, Prototype, ServiceComponent, ) from init_db import init as init_adcm -from rbac.models import Role, RoleTypes +from rbac.models import Role, RoleTypes, User from rbac.roles import ModelRole +from rbac.services.policy import policy_create +from rbac.services.role import role_create from rbac.tests.test_base import RBACBaseTestCase from rbac.upgrade.role import init_roles, prepare_action_roles @@ -37,16 +46,16 @@ class RoleModelTest(BaseTestCase): def test_role_class(self): r = Role(module_name="qwe") - with self.assertRaises(AdwpEx) as context: + with self.assertRaises(AdcmEx) as context: r.get_role_obj() - self.assertEqual(context.exception.error_code, "ROLE_MODULE_ERROR") + self.assertEqual(context.exception.code, "ROLE_MODULE_ERROR") r = Role(module_name="rbac", class_name="qwe") - with self.assertRaises(AdwpEx) as context: + with self.assertRaises(AdcmEx) as context: r.get_role_obj() - self.assertEqual(context.exception.error_code, "ROLE_CLASS_ERROR") + self.assertEqual(context.exception.code, "ROLE_CLASS_ERROR") r = Role(module_name="rbac.roles", class_name="ModelRole") obj = r.get_role_obj() @@ -113,10 +122,10 @@ def test_object_filter_error(self): init_params={"app_name": "cm", "model": "qwe"}, ) r1.save() - with self.assertRaises(AdwpEx) as e: + with self.assertRaises(AdcmEx) as e: r1.filter() - self.assertEqual(e.exception.error_code, "ROLE_FILTER_ERROR") + self.assertEqual(e.exception.code, "ROLE_FILTER_ERROR") r2 = Role( name="add", @@ -126,10 +135,10 @@ def test_object_filter_error(self): init_params={"app_name": "qwe", "model": "qwe"}, ) r2.save() - with self.assertRaises(AdwpEx) as e: + with self.assertRaises(AdcmEx) as e: r1.filter() - self.assertEqual(e.exception.error_code, "ROLE_FILTER_ERROR") + self.assertEqual(e.exception.code, "ROLE_FILTER_ERROR") def test_object_complex_filter(self): r = Role( @@ -179,7 +188,6 @@ def setUp(self): version="1.0", hash="47b820a6d66a90b02b42017269904ab2c954bceb", edition="community", - license="absent", category=category, ) self.bundle_1.refresh_from_db() @@ -535,8 +543,7 @@ def check_roles(self): self.assertEqual( count, 1, - f"Role does not exist or not unique: {count} != 1\n" - f"{json.dumps(role_data, indent=2, default=str)}", + f"Role does not exist or not unique: {count} != 1\n" f"{json.dumps(role_data, indent=2, default=str)}", ) role = Role.objects.filter(**role_data).first() @@ -567,3 +574,149 @@ def check_roles(self): ).count() self.assertEqual(sa_role_count, 6, "Roles missing from base roles") + + +# pylint: disable=too-many-instance-attributes, protected-access +class TestMMRoles(RBACBaseTestCase): + def setUp(self) -> None: + init_adcm() + init_roles() + + self.create_bundles_and_prototypes() + self.cluster = Cluster.objects.create(name="testcluster", prototype=self.clp) + self.provider = HostProvider.objects.create( + name="test_provider", + prototype=self.pp, + ) + self.host = Host.objects.create(fqdn="testhost", prototype=self.hp) + add_host_to_cluster(self.cluster, self.host) + self.service = ClusterObject.objects.create(cluster=self.cluster, prototype=self.sp_1) + self.component = ServiceComponent.objects.create( + cluster=self.cluster, service=self.service, prototype=self.cop_11 + ) + + self.test_user_username = "test_user" + self.test_user_password = "test_user_password" + self.test_user = User.objects.create_user( + username=self.test_user_username, + password=self.test_user_password, + ) + + self.client = Client(HTTP_USER_AGENT='Mozilla/5.0') + self.login() + + self.mm_role_host = role_create( + name="mm role host", + display_name="mm role host", + child=[Role.objects.get(name="Manage Maintenance mode")], + ) + self.mm_role_cluster = role_create( + name="mm role cluster", + display_name="mm role cluster", + child=[Role.objects.get(name="Manage cluster Maintenance mode")], + ) + + def test_no_roles(self): + for view_name, url_kwarg_name, obj in ( + ("host-details", "host_id", self.host), + ("component-details", "component_id", self.component), + ("service-details", "service_id", self.service), + ): + url = reverse(view_name, kwargs={url_kwarg_name: obj.pk}) + response = self.client.get(path=url, content_type=APPLICATION_JSON) + self.assertEqual(response.status_code, 404) + + with self.assertRaises(PermissionDenied): + check_custom_perm(self.test_user, "change_maintenance_mode", obj._meta.model_name, obj) + + def test_mm_host_role(self): + policy_create(name="mm host policy", object=[self.host], role=self.mm_role_host, user=[self.test_user]) + check_custom_perm(self.test_user, "change_maintenance_mode", self.host._meta.model_name, self.host) + + response = self.client.post( + path=reverse("host-maintenance-mode", kwargs={'host_id': self.host.pk}), + data={"maintenance_mode": MaintenanceMode.ON}, + format="json", + content_type=APPLICATION_JSON, + ) + self.assertEqual(response.status_code, 200) + + def test_mm_cluster_role(self): + policy_create( + name="mm cluster policy", + object=[self.cluster], + role=self.mm_role_cluster, + user=[self.test_user], + ) + check_custom_perm(self.test_user, "change_maintenance_mode", self.host._meta.model_name, self.host) + check_custom_perm( + self.test_user, + "change_maintenance_mode", + self.component._meta.model_name, + self.component, + ) + check_custom_perm(self.test_user, "change_maintenance_mode", self.service._meta.model_name, self.service) + + response = self.client.post( + path=reverse("host-maintenance-mode", kwargs={'host_id': self.host.pk}), + data={"maintenance_mode": MaintenanceMode.ON}, + format="json", + content_type=APPLICATION_JSON, + ) + self.assertEqual(response.status_code, 200) + + response = self.client.post( + path=reverse("component-maintenance-mode", kwargs={'component_id': self.component.pk}), + data={"maintenance_mode": MaintenanceMode.ON}, + format="json", + content_type=APPLICATION_JSON, + ) + self.assertEqual(response.status_code, 200) + + response = self.client.post( + path=reverse("service-maintenance-mode", kwargs={'service_id': self.service.pk}), + data={"maintenance_mode": MaintenanceMode.ON}, + format="json", + content_type=APPLICATION_JSON, + ) + self.assertEqual(response.status_code, 200) + + def test_mm_cl_adm_role(self): + policy_create( + name="mm cluster policy", + object=[self.cluster], + role=Role.objects.get(name="Cluster Administrator"), + user=[self.test_user], + ) + check_custom_perm(self.test_user, "change_maintenance_mode", self.host._meta.model_name, self.host) + check_custom_perm( + self.test_user, + "change_maintenance_mode", + self.component._meta.model_name, + self.component, + ) + check_custom_perm(self.test_user, "change_maintenance_mode", self.service._meta.model_name, self.service) + + response = self.client.post( + path=reverse("host-maintenance-mode", kwargs={'host_id': self.host.pk}), + data={"maintenance_mode": MaintenanceMode.ON}, + format="json", + content_type=APPLICATION_JSON, + ) + self.assertEqual(response.status_code, 200) + + response = self.client.post( + path=reverse("component-maintenance-mode", kwargs={'component_id': self.component.pk}), + data={"maintenance_mode": MaintenanceMode.ON}, + format="json", + content_type=APPLICATION_JSON, + ) + self.assertEqual(response.status_code, 200) + + response = self.client.post( + path=reverse("service-maintenance-mode", kwargs={'service_id': self.service.pk}), + data={"maintenance_mode": MaintenanceMode.ON}, + format="json", + content_type=APPLICATION_JSON, + ) + self.assertEqual(response.status_code, 200) diff --git a/python/rbac/tests/test_user.py b/python/rbac/tests/test_user.py new file mode 100644 index 0000000000..22d47bfaf1 --- /dev/null +++ b/python/rbac/tests/test_user.py @@ -0,0 +1,34 @@ +# 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 django.urls import reverse +from rest_framework.response import Response +from rest_framework.status import HTTP_200_OK + +from adcm.tests.base import BaseTestCase +from rbac.models import OriginType + + +class UserTestCase(BaseTestCase): + def test_filter(self): + response: Response = self.client.get(reverse("rbac:user-list"), {"type": OriginType.Local}) + + self.assertEqual(response.status_code, HTTP_200_OK) + self.assertEqual(len(response.data["results"]), 2) + + self.test_user.type = OriginType.LDAP + self.test_user.save(update_fields=["type"]) + + response: Response = self.client.get(reverse("rbac:user-list"), {"type": OriginType.Local}) + + self.assertEqual(response.status_code, HTTP_200_OK) + self.assertEqual(len(response.data["results"]), 1) diff --git a/python/rbac/upgrade/role.py b/python/rbac/upgrade/role.py index 22049df44f..b04a2d73a9 100644 --- a/python/rbac/upgrade/role.py +++ b/python/rbac/upgrade/role.py @@ -1,6 +1,6 @@ -# 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 +# 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 # @@ -14,12 +14,13 @@ from typing import List import ruyaml -from adwp_base.errors import raise_AdwpEx as err +from django.conf import settings from django.contrib.contenttypes.models import ContentType from django.db import transaction from django.utils import timezone import cm.checker +from cm.errors import raise_adcm_ex from cm.models import ( Action, Bundle, @@ -36,19 +37,19 @@ def upgrade(data: dict): """Upgrade roles and user permissions""" new_roles = {} - for role in data['roles']: - new_roles[role['name']] = upgrade_role(role, data) + for role in data["roles"]: + new_roles[role["name"]] = upgrade_role(role, data) - for role in data['roles']: - role_obj = new_roles[role['name']] + for role in data["roles"]: + role_obj = new_roles[role["name"]] task_roles = [] for child in role_obj.child.all(): - if child.class_name == 'TaskRole': + if child.class_name == "TaskRole": task_roles.append(child) role_obj.child.clear() - if 'child' not in role: + if "child" not in role: continue - for child in role['child']: + for child in role["child"]: child_role = new_roles[child] role_obj.child.add(child_role) role_obj.child.add(*task_roles) @@ -58,32 +59,32 @@ def upgrade(data: dict): def find_role(name: str, roles: list): """search role in role list by name""" for role in roles: - if role['name'] == name: + if role["name"] == name: return role - return err('INVALID_ROLE_SPEC', f'child role "{name}" is absent') + return raise_adcm_ex("INVALID_ROLE_SPEC", f'child role "{name}" is absent') def check_roles_child(data: dict): """Check if role child name are exist in specification file""" - for role in data['roles']: - if 'child' in role: - for child in role['child']: - find_role(child, data['roles']) + for role in data["roles"]: + if "child" in role: + for child in role["child"]: + find_role(child, data["roles"]) def get_role_permissions(role: dict, data: dict) -> List[Permission]: """Retrieve all role's permissions""" all_perm = [] - if 'apps' not in role: + if "apps" not in role: return [] - for app in role['apps']: - for model in app['models']: + for app in role["apps"]: + for model in app["models"]: try: - ct = ContentType.objects.get(app_label=app['label'], model=model['name']) + ct = ContentType.objects.get(app_label=app["label"], model=model["name"]) except ContentType.DoesNotExist: msg = 'no model "{}" in application "{}"' - err('INVALID_ROLE_SPEC', msg.format(model['name'], app['label'])) - for code in model['codenames']: + raise_adcm_ex("INVALID_ROLE_SPEC", msg.format(model["name"], app["label"])) + for code in model["codenames"]: codename = f"{code}_{model['name']}" try: perm = Permission.objects.get(content_type=ct, codename=codename) @@ -97,33 +98,33 @@ def get_role_permissions(role: dict, data: dict) -> List[Permission]: def upgrade_role(role: dict, data: dict) -> Role: """Upgrade single role""" - perm_list = get_role_permissions(role, data['roles']) + perm_list = get_role_permissions(role, data["roles"]) try: - new_role = Role.objects.get(name=role['name'], built_in=True) + new_role = Role.objects.get(name=role["name"], built_in=True) new_role.permissions.clear() except Role.DoesNotExist: - new_role = Role(name=role['name']) + new_role = Role(name=role["name"]) new_role.save() - new_role.module_name = role['module_name'] - new_role.class_name = role['class_name'] - if 'init_params' in role: - new_role.init_params = role['init_params'] - if 'description' in role: - new_role.description = role['description'] - if 'display_name' in role: - new_role.display_name = role['display_name'] + new_role.module_name = role["module_name"] + new_role.class_name = role["class_name"] + if "init_params" in role: + new_role.init_params = role["init_params"] + if "description" in role: + new_role.description = role["description"] + if "display_name" in role: + new_role.display_name = role["display_name"] else: - new_role.display_name = role['name'] - if 'parametrized_by' in role: - new_role.parametrized_by_type = role['parametrized_by'] - if 'type' in role: - new_role.type = role['type'] + new_role.display_name = role["name"] + if "parametrized_by" in role: + new_role.parametrized_by_type = role["parametrized_by"] + if "type" in role: + new_role.type = role["type"] for perm in perm_list: new_role.permissions.add(perm) - for category_value in role.get('category', []): + for category_value in role.get("category", []): category = ProductCategory.objects.get(value=category_value) new_role.category.add(category) - new_role.any_category = role.get('any_category', False) + new_role.any_category = role.get("any_category", False) new_role.save() return new_role @@ -135,26 +136,26 @@ def get_role_spec(data: str, schema: str) -> dict: (see https://github.com/arenadata/yspec for details about schema syntaxis) """ try: - with open(data, encoding='utf_8') as fd: + with open(data, encoding=settings.ENCODING_UTF_8) as fd: data = ruyaml.round_trip_load(fd) except FileNotFoundError: - err('INVALID_ROLE_SPEC', f'Can not open role file "{data}"') + raise_adcm_ex("INVALID_ROLE_SPEC", f'Can not open role file "{data}"') except (ruyaml.parser.ParserError, ruyaml.scanner.ScannerError, NotImplementedError) as e: - err('INVALID_ROLE_SPEC', f'YAML decode "{data}" error: {e}') + raise_adcm_ex("INVALID_ROLE_SPEC", f'YAML decode "{data}" error: {e}') - with open(schema, encoding='utf_8') as fd: + with open(schema, encoding=settings.ENCODING_UTF_8) as fd: rules = ruyaml.round_trip_load(fd) try: cm.checker.check(data, rules) except cm.checker.FormatError as e: - args = '' + args = "" if e.errors: for ee in e.errors: - if 'Input data for' in ee.message: + if "Input data for" in ee.message: continue - args += f'line {ee.line}: {ee}\n' - err('INVALID_ROLE_SPEC', f'line {e.line} error: {e}', args) + args += f"line {ee.line}: {ee}\n" + raise_adcm_ex("INVALID_ROLE_SPEC", f"line {e.line} error: {e}", args) return data @@ -178,35 +179,33 @@ def prepare_hidden_roles(bundle: Bundle): """Prepares hidden roles""" hidden_roles = {} for act in Action.objects.filter(prototype__bundle=bundle): - name_prefix = f'{act.prototype.type} action:'.title() - name = f'{name_prefix} {act.display_name}' + name_prefix = f"{act.prototype.type} action:".title() + name = f"{name_prefix} {act.display_name}" model = get_model_by_type(act.prototype.type) - if act.prototype.type == 'component': - serv_name = f'service_{act.prototype.parent.name}_' + if act.prototype.type == "component": + serv_name = f"service_{act.prototype.parent.name}_" else: - serv_name = '' + serv_name = "" role_name = ( - f'{bundle.name}_{bundle.version}_{bundle.edition}_{serv_name}' - f'{act.prototype.type}_{act.prototype.display_name}_{act.name}' + f"{bundle.name}_{bundle.version}_{bundle.edition}_{serv_name}" + f"{act.prototype.type}_{act.prototype.display_name}_{act.name}" ) role, _ = Role.objects.get_or_create( name=role_name, display_name=role_name, - description=( - f'run action {act.name} of {act.prototype.type} {act.prototype.display_name}' - ), + description=(f"run action {act.name} of {act.prototype.type} {act.prototype.display_name}"), bundle=bundle, type=RoleTypes.hidden, - module_name='rbac.roles', - class_name='ActionRole', + module_name="rbac.roles", + class_name="ActionRole", init_params={ - 'action_id': act.id, - 'app_name': 'cm', - 'model': model.__name__, - 'filter': { - 'prototype__name': act.prototype.name, - 'prototype__type': act.prototype.type, - 'prototype__bundle_id': bundle.id, + "action_id": act.id, + "app_name": "cm", + "model": model.__name__, + "filter": { + "prototype__name": act.prototype.name, + "prototype__type": act.prototype.type, + "prototype__bundle_id": bundle.id, }, }, parametrized_by_type=[act.prototype.type], @@ -216,46 +215,38 @@ def prepare_hidden_roles(bundle: Bundle): role.category.add(bundle.category) ct = ContentType.objects.get_for_model(model) model_name = model.__name__.lower() - role.permissions.add(get_perm(ct, f'view_{model_name}')) + role.permissions.add(get_perm(ct, f"view_{model_name}")) if name not in hidden_roles: - hidden_roles[name] = {'parametrized_by_type': act.prototype.type, 'children': []} - hidden_roles[name]['children'].append(role) + hidden_roles[name] = {"parametrized_by_type": act.prototype.type, "children": []} + hidden_roles[name]["children"].append(role) if act.host_action: ct_host = ContentType.objects.get_for_model(Host) - role.permissions.add(get_perm(ct_host, 'view_host')) + role.permissions.add(get_perm(ct_host, "view_host")) role.permissions.add( - get_perm( - ct_host, f'run_action_{act.display_name}', f'Can run {act.display_name} actions' - ) + get_perm(ct_host, f"run_action_{act.display_name}", f"Can run {act.display_name} actions") ) else: - role.permissions.add( - get_perm( - ct, f'run_action_{act.display_name}', f'Can run {act.display_name} actions' - ) - ) + role.permissions.add(get_perm(ct, f"run_action_{act.display_name}", f"Can run {act.display_name} actions")) return hidden_roles -def update_built_in_roles( - bundle: Bundle, business_role: Role, parametrized_by_type: list, built_in_roles: dict -): +def update_built_in_roles(bundle: Bundle, business_role: Role, parametrized_by_type: list, built_in_roles: dict): """Add action role to built-in roles""" - if 'cluster' in parametrized_by_type: + if "cluster" in parametrized_by_type: if bundle.category: business_role.category.add(bundle.category) - built_in_roles['Cluster Administrator'].child.add(business_role) - elif 'service' in parametrized_by_type or 'component' in parametrized_by_type: + built_in_roles["Cluster Administrator"].child.add(business_role) + elif "service" in parametrized_by_type or "component" in parametrized_by_type: if bundle.category: business_role.category.add(bundle.category) - built_in_roles['Cluster Administrator'].child.add(business_role) - built_in_roles['Service Administrator'].child.add(business_role) - elif 'provider' in parametrized_by_type: - built_in_roles['Provider Administrator'].child.add(business_role) - elif 'host' in parametrized_by_type: - built_in_roles['Cluster Administrator'].child.add(business_role) - built_in_roles['Provider Administrator'].child.add(business_role) + built_in_roles["Cluster Administrator"].child.add(business_role) + built_in_roles["Service Administrator"].child.add(business_role) + elif "provider" in parametrized_by_type: + built_in_roles["Provider Administrator"].child.add(business_role) + elif "host" in parametrized_by_type: + built_in_roles["Cluster Administrator"].child.add(business_role) + built_in_roles["Provider Administrator"].child.add(business_role) @transaction.atomic @@ -263,36 +254,36 @@ def prepare_action_roles(bundle: Bundle): """Prepares action roles""" DummyData.objects.filter(id=1).update(date=timezone.now()) built_in_roles = { - 'Cluster Administrator': Role.objects.get(name='Cluster Administrator'), - 'Provider Administrator': Role.objects.get(name='Provider Administrator'), - 'Service Administrator': Role.objects.get(name='Service Administrator'), + "Cluster Administrator": Role.objects.get(name="Cluster Administrator"), + "Provider Administrator": Role.objects.get(name="Provider Administrator"), + "Service Administrator": Role.objects.get(name="Service Administrator"), } hidden_roles = prepare_hidden_roles(bundle) for business_role_name, business_role_params in hidden_roles.items(): - if business_role_params['parametrized_by_type'] == 'component': - parametrized_by_type = ['service', 'component'] + if business_role_params["parametrized_by_type"] == "component": + parametrized_by_type = ["service", "component"] else: - parametrized_by_type = [business_role_params['parametrized_by_type']] + parametrized_by_type = [business_role_params["parametrized_by_type"]] business_role, is_created = Role.objects.get_or_create( - name=f'{business_role_name}', - display_name=f'{business_role_name}', - description=f'{business_role_name}', + name=f"{business_role_name}", + display_name=f"{business_role_name}", + description=f"{business_role_name}", type=RoleTypes.business, - module_name='rbac.roles', - class_name='ParentRole', + module_name="rbac.roles", + class_name="ParentRole", parametrized_by_type=parametrized_by_type, ) if is_created: log.info('Create business permission "%s"', business_role_name) - business_role.child.add(*business_role_params['children']) + business_role.child.add(*business_role_params["children"]) update_built_in_roles(bundle, business_role, parametrized_by_type, built_in_roles) def update_all_bundle_roles(): - for bundle in Bundle.objects.exclude(name='ADCM'): + for bundle in Bundle.objects.exclude(name="ADCM"): prepare_action_roles(bundle) msg = f'Prepare roles for "{bundle.name}" bundle.' log.info(msg) @@ -310,16 +301,16 @@ def init_roles(): rm = RoleMigration.objects.last() if rm is None: rm = RoleMigration(version=0) - if role_data['version'] > rm.version: + if role_data["version"] > rm.version: with transaction.atomic(): upgrade(role_data) - rm.version = role_data['version'] + rm.version = role_data["version"] rm.save() update_all_bundle_roles() re_apply_all_polices() - msg = f'Roles are upgraded to version {rm.version}' + msg = f"Roles are upgraded to version {rm.version}" log.info(msg) else: - msg = f'Roles are already at version {rm.version}' + msg = f"Roles are already at version {rm.version}" return msg diff --git a/python/rbac/upgrade/role_spec.yaml b/python/rbac/upgrade/role_spec.yaml index 1302d65a7d..cb8129a644 100644 --- a/python/rbac/upgrade/role_spec.yaml +++ b/python/rbac/upgrade/role_spec.yaml @@ -1,6 +1,6 @@ --- -version: 3 +version: 4 roles: - name: Add host @@ -1463,7 +1463,9 @@ roles: - Create host - Upload bundle - Remove bundle - - Manage Maintenance mode + - Manage host Maintenance mode hidden + - Manage service Maintenance mode + - Manage component Maintenance mode - name: Provider Administrator type: role @@ -1485,7 +1487,20 @@ roles: - Upload bundle - Remove bundle + - name: Manage cluster Maintenance mode + description: the ability to enable / disable the "MM" + type: business + parametrized_by: + - cluster + module_name: rbac.roles + class_name: ParentRole + child: + - Manage host Maintenance mode hidden + - Manage service Maintenance mode + - Manage component Maintenance mode + - name: Manage Maintenance mode + display_name: Manage host Maintenance mode description: the ability to enable / disable the "MM" on the host type: business parametrized_by: @@ -1498,4 +1513,49 @@ roles: - name: host codenames: - view - - change + - change_maintenance_mode + + - name: Manage host Maintenance mode hidden + description: the ability to enable / disable the "MM" on the host + type: hidden + parametrized_by: + - host + module_name: rbac.roles + class_name: ObjectRole + apps: + - label: cm + models: + - name: host + codenames: + - view + - change_maintenance_mode + + - name: Manage service Maintenance mode + description: the ability to enable / disable the "MM" on the service + type: hidden + parametrized_by: + - service + module_name: rbac.roles + class_name: ObjectRole + apps: + - label: cm + models: + - name: clusterobject + codenames: + - view + - change_maintenance_mode + + - name: Manage component Maintenance mode + description: the ability to enable / disable the "MM" on the component + type: hidden + parametrized_by: + - component + module_name: rbac.roles + class_name: ObjectRole + apps: + - label: cm + models: + - name: servicecomponent + codenames: + - view + - change_maintenance_mode diff --git a/python/rbac/upgrade/test_spec.py b/python/rbac/upgrade/test_spec.py index 6ed35eb050..5cc0e3b811 100644 --- a/python/rbac/upgrade/test_spec.py +++ b/python/rbac/upgrade/test_spec.py @@ -15,6 +15,7 @@ from pathlib import Path import ruyaml +from django.conf import settings from django.test import TestCase MANDATORY_KEYS = ["name", "type", "module_name", "class_name"] @@ -36,7 +37,7 @@ class TestRoleSpecification(TestCase): def setUp(self) -> None: - with open(Path(os.path.dirname(__file__), "role_spec.yaml"), encoding="utf-8") as f: + with open(Path(os.path.dirname(__file__), "role_spec.yaml"), encoding=settings.ENCODING_UTF_8) as f: self.spec_data: dict = ruyaml.YAML().load(f) self.role_map: dict = {role["name"]: role for role in self.spec_data["roles"]} self.roots = self.role_map.copy() @@ -72,9 +73,7 @@ def test_allowed_parametrization(self): for v in self.role_map.values(): if "parametrized_by" in v: if v["type"] == "business": - self.assertTrue( - self._is_in_set(BUSINESS_PARAMETRISATION, set(v["parametrized_by"])) - ) + self.assertTrue(self._is_in_set(BUSINESS_PARAMETRISATION, set(v["parametrized_by"]))) def _tree_dive_in(self, roles: dict, visited: dict, path: list, role: dict, root): if role["name"] in visited: diff --git a/python/rbac/urls.py b/python/rbac/urls.py index e0a1cce987..ff4626f557 100644 --- a/python/rbac/urls.py +++ b/python/rbac/urls.py @@ -10,8 +10,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -"""RBAC root URLs""" - from django.urls import include, path from rbac.endpoints.logout import LogOut @@ -19,12 +17,12 @@ from rbac.endpoints.token import GetAuthToken urlpatterns = [ - path('', RBACRoot.as_view(), name='root'), - path('me/', include('rbac.endpoints.me.urls')), - path('user/', include('rbac.endpoints.user.urls')), - path('group/', include('rbac.endpoints.group.urls')), - path('role/', include('rbac.endpoints.role.urls')), - path(r'policy/', include('rbac.endpoints.policy.urls')), - path('logout/', LogOut.as_view(), name='logout'), - path('token/', GetAuthToken.as_view(), name='token'), + path("", RBACRoot.as_view(), name="root"), + path("me/", include("rbac.endpoints.me.urls")), + path("user/", include("rbac.endpoints.user.urls")), + path("group/", include("rbac.endpoints.group.urls")), + path("role/", include("rbac.endpoints.role.urls")), + path("policy/", include("rbac.endpoints.policy.urls")), + path("logout/", LogOut.as_view(), name="logout"), + path("token/", GetAuthToken.as_view(), name="token"), ] diff --git a/python/rbac/utils.py b/python/rbac/utils.py index bc142950f6..ecdd217cef 100644 --- a/python/rbac/utils.py +++ b/python/rbac/utils.py @@ -41,9 +41,7 @@ def update_m2m_field(m2m, instances) -> None: m2m.clear() -def create_model_serializer_class( - name: str, model: Type[Model], meta_fields: Tuple[str, ...], fields: dict = None -): +def create_model_serializer_class(name: str, model: Type[Model], meta_fields: Tuple[str, ...], fields: dict = None): """ Creating serializer class for model diff --git a/python/task_runner.py b/python/task_runner.py index 68bfd6caa1..176b5a88f9 100755 --- a/python/task_runner.py +++ b/python/task_runner.py @@ -11,7 +11,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -# pylint: disable=unused-import, useless-return, protected-access, bare-except, global-statement +# pylint: disable=unused-import,useless-return,protected-access,bare-except,global-statement import os import signal @@ -19,26 +19,26 @@ import sys import time +from django.conf import settings from django.core.exceptions import ObjectDoesNotExist from django.utils import timezone -import adcm.init_django # pylint: disable=unused-import -from cm.config import CODE_DIR, LOG_DIR, RUN_DIR, Job +import adcm.init_django from cm.job import finish_task, re_prepare_job from cm.logger import logger -from cm.models import JobLog, LogStorage, TaskLog +from cm.models import JobLog, JobStatus, LogStorage, TaskLog TASK_ID = 0 def terminate_job(task, jobs): - running_job = jobs.get(status=Job.RUNNING) + running_job = jobs.get(status=JobStatus.RUNNING) if running_job.pid: os.kill(running_job.pid, signal.SIGTERM) - finish_task(task, running_job, Job.ABORTED) + finish_task(task, running_job, JobStatus.ABORTED) else: - finish_task(task, None, Job.ABORTED) + finish_task(task, None, JobStatus.ABORTED) def terminate_task(signum, frame): @@ -48,7 +48,7 @@ def terminate_task(signum, frame): i = 0 while i < 10: - if jobs.filter(status=Job.RUNNING): + if jobs.filter(status=JobStatus.RUNNING): terminate_job(task, jobs) break i += 1 @@ -56,7 +56,7 @@ def terminate_task(signum, frame): if i == 10: logger.warning("no jobs running for task #%s", TASK_ID) - finish_task(task, None, Job.ABORTED) + finish_task(task, None, JobStatus.ABORTED) os._exit(signum) @@ -67,12 +67,12 @@ def terminate_task(signum, frame): def run_job(task_id, job_id, err_file): logger.debug("task run job #%s of task #%s", job_id, task_id) cmd = [ - '/adcm/python/job_venv_wrapper.sh', + "/adcm/python/job_venv_wrapper.sh", TaskLog.objects.get(id=task_id).action.venv, - os.path.join(CODE_DIR, 'job_runner.py'), + str(settings.CODE_DIR / "job_runner.py"), str(job_id), ] - logger.info("task run job cmd: %s", ' '.join(cmd)) + logger.info("task run job cmd: %s", " ".join(cmd)) try: proc = subprocess.Popen(cmd, stderr=err_file) res = proc.wait() @@ -86,10 +86,10 @@ def run_job(task_id, job_id, err_file): def set_log_body(job): name = job.sub_action.script_type if job.sub_action else job.action.script_type - log_storage = LogStorage.objects.filter(job=job, name=name, type__in=['stdout', 'stderr']) + log_storage = LogStorage.objects.filter(job=job, name=name, type__in=["stdout", "stderr"]) for ls in log_storage: - file_path = os.path.join(RUN_DIR, f'{ls.job.id}', f'{ls.name}-{ls.type}.{ls.format}') - with open(file_path, 'r', encoding='utf_8') as f: + file_path = settings.RUN_DIR / f"{ls.job.id}" / f"{ls.name}-{ls.type}.{ls.format}" + with open(file_path, "r", encoding=settings.ENCODING_UTF_8) as f: body = f.read() LogStorage.objects.filter(job=job, name=ls.name, type=ls.type).update(body=body) @@ -106,14 +106,14 @@ def run_task(task_id, args=None): task.pid = os.getpid() task.save() - jobs = JobLog.objects.filter(task_id=task.id).order_by('id') + jobs = JobLog.objects.filter(task_id=task.id).order_by("id") if not jobs: logger.error("no jobs for task %s", task.id) - finish_task(task, None, Job.FAILED) + finish_task(task, None, JobStatus.FAILED) return - err_file = open(os.path.join(LOG_DIR, 'job_runner.err'), 'a+', encoding='utf_8') + err_file = open(settings.LOG_DIR / "job_runner.err", "a+", encoding=settings.ENCODING_UTF_8) logger.info("run task #%s", task_id) @@ -121,7 +121,7 @@ def run_task(task_id, args=None): count = 0 res = 0 for job in jobs: - if args == 'restart' and job.status == Job.SUCCESS: + if args == "restart" and job.status == JobStatus.SUCCESS: logger.info('skip job #%s status "%s" of task #%s', job.id, job.status, task_id) continue @@ -145,9 +145,9 @@ def run_task(task_id, args=None): break if res == 0: - finish_task(task, job, Job.SUCCESS) + finish_task(task, job, JobStatus.SUCCESS) else: - finish_task(task, job, Job.FAILED) + finish_task(task, job, JobStatus.FAILED) err_file.close() @@ -167,5 +167,5 @@ def do(): run_task(sys.argv[1]) -if __name__ == '__main__': +if __name__ == "__main__": do() diff --git a/requirements-test.txt b/requirements-test.txt index 78fa578f56..bbc7209403 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -1,26 +1,25 @@ --extra-index-url https://ci.arenadata.io/artifactory/api/pypi/python-packages/simple -# TODO: adcm-client >= and adcm-pytest-plugin ~= needed to develop -adcm-client~=2022.9.9.12.dev0 -adcm-pytest-plugin~=4.22.0.dev0 -attr==0.3.1 -allure-pytest==2.9.45 -docker==5.0.3 -jsonschema==4.6.1 -Jinja2==3.1.2 -pylint==2.14.4 -pytest==6.2.5 -pytest-asyncio==0.18.3 -pytest-lazy-fixture==0.6.3 -pytest-xdist==2.5.0 -pytest-rerunfailures==10.2 -pytest-timeout==2.1.0 -pytest-check==1.0.5 -python-ldap==3.4.3 -PyYAML==6.0 -requests==2.28.1 -requests-toolbelt==0.9.1 -selenium==3.141.0 -multipledispatch==0.6.0 -rstr==3.2.0 -genson==1.2.2 -websockets==10.3 + +adcm-client>=2022.12.1.10 +adcm-pytest-plugin~=4.23 +attr +autoflake +black +flake8 +flake8-pytest-style +genson +jsonschema +multipledispatch +pylint +pysocks +pytest-asyncio +pytest-check +pytest-lazy-fixture +pytest-rerunfailures +pytest-timeout +pytest-xdist +python-ldap +requests-toolbelt +rstr +selenium +websockets \ No newline at end of file diff --git a/requirements-venv-2.9.txt b/requirements-venv-2.9.txt new file mode 100644 index 0000000000..2d32686b96 --- /dev/null +++ b/requirements-venv-2.9.txt @@ -0,0 +1,3 @@ +-r ./requirements.txt + +git+https://github.com/arenadata/ansible.git@v2.9.27-p1 diff --git a/requirements-venv-default.txt b/requirements-venv-default.txt new file mode 100644 index 0000000000..dd67ace812 --- /dev/null +++ b/requirements-venv-default.txt @@ -0,0 +1,3 @@ +-r ./requirements.txt + +git+https://github.com/arenadata/ansible.git@v2.8.8-p6 diff --git a/requirements.txt b/requirements.txt index 0b1b81277c..58196c0118 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,30 @@ ---extra-index-url https://ci.arenadata.io/artifactory/api/pypi/python-packages/simple -adwp-events==0.1.8 +apache-libcloud==3.6.0 +attr==0.3.1 +casestyle +cryptography==37.0.2 +django-auth-ldap==4.1.0 +django-cors-headers==3.13.0 +django-filter==22.1 +django-generate-secret-key +django-guardian==2.4.0 +django-rest-swagger==2.2.0 +drf-extensions==0.7.1 +drf-flex-fields==0.9.1 +googleapis-common-protos==1.56.3 +grpcio==1.47.0 +Jinja2==2.11.3 +jmespath==1.0.1 +jsonschema==4.6.1 +lxml==4.9.0 +MarkupSafe==1.1.1 +mitogen==0.3.3 +multipledispatch==0.6.0 +pycryptodome==3.15.0 +pyjwt==2.4.0 +requests-toolbelt==0.9.1 +rstr==3.2.0 +ruyaml==0.91.0 +social-auth-app-django==5.0.0 +uwsgi==2.0.20 +version-utils +yspec==0.1.0 diff --git a/tests/api/conftest.py b/tests/api/conftest.py index 05cd6def53..9ef8c4d151 100644 --- a/tests/api/conftest.py +++ b/tests/api/conftest.py @@ -14,12 +14,11 @@ import allure import pytest - -from tests.conftest import DUMMY_DATA_PARAM from tests.api.steps.asserts import BodyAssertionError from tests.api.steps.common import assume_step from tests.api.utils.api_objects import ADCMTestApiWrapper from tests.api.utils.endpoints import Endpoints +from tests.conftest import DUMMY_DATA_PARAM def pytest_generate_tests(metafunc): diff --git a/tests/api/steps/asserts.py b/tests/api/steps/asserts.py index c0962c9105..e535e2bacf 100644 --- a/tests/api/steps/asserts.py +++ b/tests/api/steps/asserts.py @@ -12,14 +12,13 @@ """Some asserts with allure steps""" import json -from dataclasses import field, dataclass +from dataclasses import dataclass, field from http import HTTPStatus from typing import Dict import allure from requests import Response - -from tests.api.utils.tools import NotSet, NotEqual +from tests.api.utils.tools import NotEqual, NotSet @dataclass diff --git a/tests/api/steps/common.py b/tests/api/steps/common.py index f972ba5689..96afb2c88b 100644 --- a/tests/api/steps/common.py +++ b/tests/api/steps/common.py @@ -20,7 +20,6 @@ from _pytest.outcomes import Skipped -# pylint: disable=no-else-return def assume_step(title, exception=None): """ Allows you to suppress exception within the Allure steps. diff --git a/tests/api/test_body/__init__.py b/tests/api/test_body/__init__.py index 89ecaf50a7..1f84f12bc0 100644 --- a/tests/api/test_body/__init__.py +++ b/tests/api/test_body/__init__.py @@ -16,12 +16,11 @@ import allure import pytest - from tests.api.steps.asserts import ExpectedBody from tests.api.testdata.generators import TestDataWithPreparedBody from tests.api.utils.data_classes import AUTO_VALUE from tests.api.utils.methods import Methods -from tests.api.utils.tools import not_set, NotEqual +from tests.api.utils.tools import NotEqual, not_set from tests.api.utils.types import get_fields, is_fk_field, is_password_field pytestmark = [ diff --git a/tests/api/test_body/test_patch.py b/tests/api/test_body/test_patch.py index 583a5a5084..2c011d63c9 100644 --- a/tests/api/test_body/test_patch.py +++ b/tests/api/test_body/test_patch.py @@ -17,18 +17,16 @@ import allure import pytest - from tests.api.test_body import _test_patch_put_body_positive +from tests.api.testdata.db_filler import DbFiller from tests.api.testdata.generators import ( - get_positive_data_for_patch_body_check, - get_negative_data_for_patch_body_check, TestDataWithPreparedBody, + get_negative_data_for_patch_body_check, + get_positive_data_for_patch_body_check, ) -from tests.api.testdata.db_filler import DbFiller from tests.api.utils.api_objects import ADCMTestApiWrapper - -from tests.api.utils.types import get_fields from tests.api.utils.methods import Methods +from tests.api.utils.types import get_fields pytestmark = [ allure.suite("PATCH"), diff --git a/tests/api/test_body/test_post.py b/tests/api/test_body/test_post.py index cab957d40d..f533d914e8 100644 --- a/tests/api/test_body/test_post.py +++ b/tests/api/test_body/test_post.py @@ -18,13 +18,12 @@ import allure import pytest - from tests.api.test_body import generate_body_for_checks from tests.api.testdata.db_filler import DbFiller from tests.api.testdata.generators import ( - get_positive_data_for_post_body_check, - get_negative_data_for_post_body_check, TestDataWithPreparedBody, + get_negative_data_for_post_body_check, + get_positive_data_for_post_body_check, ) from tests.api.utils.api_objects import ADCMTestApiWrapper from tests.api.utils.methods import Methods diff --git a/tests/api/test_body/test_put.py b/tests/api/test_body/test_put.py index 716cc36eb5..494cf2b515 100644 --- a/tests/api/test_body/test_put.py +++ b/tests/api/test_body/test_put.py @@ -17,17 +17,16 @@ import allure import pytest - from tests.api.test_body import _test_patch_put_body_positive +from tests.api.testdata.db_filler import DbFiller from tests.api.testdata.generators import ( - get_positive_data_for_put_body_check, - get_negative_data_for_put_body_check, TestDataWithPreparedBody, + get_negative_data_for_put_body_check, + get_positive_data_for_put_body_check, ) -from tests.api.testdata.db_filler import DbFiller from tests.api.utils.api_objects import ADCMTestApiWrapper -from tests.api.utils.types import get_fields from tests.api.utils.methods import Methods +from tests.api.utils.types import get_fields pytestmark = [ allure.suite("PUT"), diff --git a/tests/api/test_methods.py b/tests/api/test_methods.py index 4a422c4429..935a27cbe8 100644 --- a/tests/api/test_methods.py +++ b/tests/api/test_methods.py @@ -14,11 +14,10 @@ # pylint: disable=redefined-outer-name from typing import List -import pytest import allure - -from tests.api.testdata.generators import TestData, get_data_for_methods_check +import pytest from tests.api.testdata.db_filler import DbFiller +from tests.api.testdata.generators import TestData, get_data_for_methods_check pytestmark = [ allure.suite("API Methods tests"), diff --git a/tests/api/test_urls.py b/tests/api/test_urls.py index c78502ab52..1f65bd75f5 100644 --- a/tests/api/test_urls.py +++ b/tests/api/test_urls.py @@ -16,9 +16,12 @@ import allure import pytest - from tests.api.testdata.db_filler import DbFiller -from tests.api.testdata.generators import get_data_for_urls_check, TestDataWithPreparedPath, TestData +from tests.api.testdata.generators import ( + TestData, + TestDataWithPreparedPath, + get_data_for_urls_check, +) pytestmark = [ allure.suite("API Urls tests"), diff --git a/tests/api/testdata/db_filler.py b/tests/api/testdata/db_filler.py index 71245fdc85..aa8fa0afb4 100644 --- a/tests/api/testdata/db_filler.py +++ b/tests/api/testdata/db_filler.py @@ -15,31 +15,27 @@ import random from collections import defaultdict from copy import deepcopy -from typing import Literal, List, Dict, Any +from typing import Any, Dict, List, Literal import allure - from tests.api.steps.common import assume_step from tests.api.testdata.getters import get_endpoint_data, get_object_data -from tests.api.utils.api_objects import Request, ExpectedResponse +from tests.api.utils.api_objects import ADCMTestApiWrapper, ExpectedResponse, Request from tests.api.utils.endpoints import Endpoints from tests.api.utils.fake_data import build_schema_by_json from tests.api.utils.methods import Methods from tests.api.utils.types import ( - get_fields, Field, - is_fk_field, - ForeignKeyM2M, ForeignKey, - get_field_name_by_fk_dataclass, + ForeignKeyM2M, GenericForeignKeyList, ObjectForeignKey, + get_field_name_by_fk_dataclass, + get_fields, + is_fk_field, ) -from tests.api.utils.api_objects import ADCMTestApiWrapper - - class DbFiller: """Utils to prepare data in DB before test""" diff --git a/tests/api/testdata/generators.py b/tests/api/testdata/generators.py index 06f2218676..c001ddb21b 100644 --- a/tests/api/testdata/generators.py +++ b/tests/api/testdata/generators.py @@ -14,22 +14,17 @@ from collections import ChainMap from http import HTTPStatus -from typing import NamedTuple, List, Optional +from typing import List, NamedTuple, Optional import allure import attr import pytest from _pytest.mark.structures import ParameterSet - -from tests.api.utils.api_objects import Request, ExpectedResponse, ExpectedBody +from tests.api.utils.api_objects import ExpectedBody, ExpectedResponse, Request from tests.api.utils.endpoints import Endpoints from tests.api.utils.methods import Methods from tests.api.utils.tools import fill_lists_by_longest -from tests.api.utils.types import ( - get_fields, - BaseType, - PreparedFieldValue, -) +from tests.api.utils.types import BaseType, PreparedFieldValue, get_fields class MaxRetriesError(Exception): @@ -481,7 +476,21 @@ def get_data_for_body_check(method: Methods, endpoints_with_test_sets: List[tupl for test_group, group_name in test_groups: values: List[TestDataWithPreparedBody] = [] for test_set in test_group: - status_code = method.default_success_code if positive else HTTPStatus.BAD_REQUEST + if positive: + status_code = method.default_success_code + elif ( + method in {Methods.PUT, Methods.PATCH} + and endpoint == Endpoints.RbacUser + and ( + # If there's an attempt to change username, 409 will be the response + # if there's a drop - 400 + "username" in test_group[0].keys() + and not test_group[0]["username"].drop_key + ) + ): + status_code = HTTPStatus.CONFLICT + else: + status_code = HTTPStatus.BAD_REQUEST # It makes no sense to check with all fields if test_set contains only one field if positive or len(test_set) > 1: values.append(_prepare_test_data_with_all_fields(endpoint, method, status_code, test_set)) @@ -575,16 +584,15 @@ def _get_datasets( dataset = {} if "generated_value" in value_properties and "value" in value_properties: raise ValueError("'generated_value', 'value' properties are not compatible") - for field in get_fields(endpoint.data_class): - if field_conditions(field): - dataset[field.name] = PreparedFieldValue( - value=value_properties.get("value", None), - unchanged_value=value_properties.get("unchanged_value", False), - generated_value=value_properties.get("generated_value", False), - error_messages=[value_properties.get("error_message", None)], - drop_key=value_properties.get("drop_key", False), - f_type=field.f_type, - ) + for field in filter(field_conditions, get_fields(endpoint.data_class)): + dataset[field.name] = PreparedFieldValue( + value=value_properties.get("value", None), + unchanged_value=value_properties.get("unchanged_value", False), + generated_value=value_properties.get("generated_value", False), + error_messages=[value_properties.get("error_message", None)], + drop_key=value_properties.get("drop_key", False), + f_type=field.f_type, + ) return [dataset] if dataset else [], desc diff --git a/tests/api/testdata/getters.py b/tests/api/testdata/getters.py index df10e80897..a490ebf14e 100644 --- a/tests/api/testdata/getters.py +++ b/tests/api/testdata/getters.py @@ -12,9 +12,9 @@ """Methods for get endpoints data""" +from tests.api.utils.api_objects import ADCMTestApiWrapper, ExpectedResponse, Request from tests.api.utils.endpoints import Endpoints from tests.api.utils.methods import Methods -from tests.api.utils.api_objects import Request, ExpectedResponse, ADCMTestApiWrapper def get_endpoint_data(adcm: ADCMTestApiWrapper, endpoint: Endpoints) -> list: diff --git a/tests/api/utils/api_objects.py b/tests/api/utils/api_objects.py index f9c5ad9b67..33c847dbb1 100644 --- a/tests/api/utils/api_objects.py +++ b/tests/api/utils/api_objects.py @@ -11,17 +11,16 @@ # limitations under the License. """Module contains api objects for executing and checking requests""" -from dataclasses import field, dataclass +from dataclasses import dataclass, field from typing import Optional, Union from urllib.parse import urlencode import allure from adcm_client.wrappers.api import ADCMApiWrapper - +from tests.api.steps.asserts import ExpectedBody, body_should_be, status_code_should_be from tests.api.utils.endpoints import Endpoints from tests.api.utils.methods import Methods from tests.api.utils.tools import attach_request_log -from tests.api.steps.asserts import status_code_should_be, body_should_be, ExpectedBody @dataclass diff --git a/tests/api/utils/data_classes.py b/tests/api/utils/data_classes.py index 4b968e2a77..112c12c2ca 100644 --- a/tests/api/utils/data_classes.py +++ b/tests/api/utils/data_classes.py @@ -13,35 +13,35 @@ """Endpoint data classes definition""" from abc import ABC -from typing import List, Callable +from typing import Callable, List # there's a local import, but it's not cyclic really from tests.api.utils.data_synchronization import ( # pylint: disable=cyclic-import - sync_object_and_role, sync_child_roles_hierarchy, + sync_object_and_role, ) from tests.api.utils.tools import PARAMETRIZED_BY_LIST from tests.api.utils.types import ( - Field, - PositiveInt, - String, - Text, - Json, - Enum, - ForeignKey, BackReferenceFK, - DateTime, - Relation, Boolean, - ForeignKeyM2M, + DateTime, Email, - ListOf, - Password, EmptyList, + Enum, + Field, + ForeignKey, + ForeignKeyM2M, GenericForeignKeyList, + Json, + ListOf, ObjectForeignKey, - Username, + Password, + PositiveInt, + Relation, SmallIntegerID, + String, + Text, + Username, ) AUTO_VALUE = "auto" diff --git a/tests/api/utils/data_synchronization.py b/tests/api/utils/data_synchronization.py index fe83a571f1..46cf7e3960 100644 --- a/tests/api/utils/data_synchronization.py +++ b/tests/api/utils/data_synchronization.py @@ -21,8 +21,8 @@ def sync_object_and_role(adcm, fields: dict) -> dict: """Sync `object` and `role` fields in Policy data""" - from tests.api.utils.endpoints import Endpoints from tests.api.testdata.getters import get_endpoint_data + from tests.api.utils.endpoints import Endpoints if 'role' not in fields or 'object' not in fields: return fields @@ -49,8 +49,8 @@ def sync_object_and_role(adcm, fields: dict) -> dict: def sync_child_roles_hierarchy(adcm, fields: dict): """Child roles can be only in infrastructure or application hierarchy""" - from tests.api.utils.endpoints import Endpoints from tests.api.testdata.getters import get_endpoint_data + from tests.api.utils.endpoints import Endpoints if "child" not in fields: return fields diff --git a/tests/api/utils/endpoints.py b/tests/api/utils/endpoints.py index b090898264..ac62dd71c6 100644 --- a/tests/api/utils/endpoints.py +++ b/tests/api/utils/endpoints.py @@ -13,37 +13,36 @@ """ADCM Endpoints classes and methods""" from enum import Enum -from typing import List, Type, Optional, Callable +from typing import Callable, List, Optional, Type import attr - -from tests.api.utils.filters import ( - is_business_role, - is_not_business_role, - is_built_in, - is_not_hidden_role, - is_not_built_in, - is_role_type, -) from tests.api.utils.data_classes import ( BaseClass, - GroupConfigFields, - GroupConfigHostsFields, - HostFields, ClusterFields, - ServiceFields, ComponentFields, - ProviderFields, - ObjectConfigFields, ConfigLogFields, + GroupConfigFields, GroupConfigHostCandidatesFields, - RbacNotBuiltInPolicyFields, - RbacUserFields, - RbacGroupFields, - RbacSimpleRoleFields, + GroupConfigHostsFields, + HostFields, + ObjectConfigFields, + ProviderFields, + RbacBuiltInPolicyFields, RbacBuiltInRoleFields, RbacBusinessRoleFields, - RbacBuiltInPolicyFields, + RbacGroupFields, + RbacNotBuiltInPolicyFields, + RbacSimpleRoleFields, + RbacUserFields, + ServiceFields, +) +from tests.api.utils.filters import ( + is_built_in, + is_business_role, + is_not_built_in, + is_not_business_role, + is_not_hidden_role, + is_role_type, ) from tests.api.utils.methods import Methods from tests.api.utils.types import get_fields diff --git a/tests/api/utils/fake_data.py b/tests/api/utils/fake_data.py index 2e4e0b0a84..8ff6a93b89 100644 --- a/tests/api/utils/fake_data.py +++ b/tests/api/utils/fake_data.py @@ -14,12 +14,11 @@ Dummy data generators """ -from random import randint, choice +from random import choice, randint import allure from genson import SchemaBuilder from rstr.xeger import Xeger - from tests.api.utils.tools import random_string @@ -56,7 +55,6 @@ def gen_string(prop=None): return random_string(strlen=randint(min_length, max_length)) -# pylint: disable=unused-argument def gen_bool(prop=None): """ Generate Boolean value diff --git a/tests/api/utils/tools.py b/tests/api/utils/tools.py index e864bdca19..be01db9855 100644 --- a/tests/api/utils/tools.py +++ b/tests/api/utils/tools.py @@ -11,20 +11,19 @@ # limitations under the License. """Some useful methods""" -from dataclasses import dataclass import random import socket import string +from dataclasses import dataclass from itertools import repeat from json import JSONEncoder from time import sleep +import allure import ifaddr import requests -import allure from requests_toolbelt.utils import dump - PARAMETRIZED_BY_LIST = ["cluster", "service", "component", "provider", "host"] diff --git a/tests/api/utils/types.py b/tests/api/utils/types.py index 2e27a90d63..2315f0b86f 100644 --- a/tests/api/utils/types.py +++ b/tests/api/utils/types.py @@ -16,8 +16,8 @@ from abc import ABC, abstractmethod from collections.abc import Callable from datetime import datetime, timedelta -from random import randint, choice -from typing import ClassVar, List, Type, Union, NamedTuple, Optional +from random import choice, randint +from typing import ClassVar, List, NamedTuple, Optional, Type, Union import attr from multipledispatch import dispatch @@ -25,7 +25,7 @@ # There is no circular import, because the import of the module is not yet completed at the moment, # and this allows you to resolve the conflict. from tests.api.utils import data_classes # pylint: disable=unused-import,cyclic-import -from tests.api.utils.fake_data import generate_json_from_schema, gen_string +from tests.api.utils.fake_data import gen_string, generate_json_from_schema from tests.api.utils.tools import random_string diff --git a/tests/conftest.py b/tests/conftest.py index fc6d18c567..b206ddf6bb 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -18,24 +18,27 @@ import sys import tarfile from pathlib import PosixPath -from typing import Optional, List, Tuple, Union, Generator +from typing import Generator, List, Optional, Tuple, Union import allure import ldap import pytest import websockets.client import yaml - from _pytest.python import Function, FunctionDefinition, Module -from adcm_client.objects import ADCMClient, User, Provider, Bundle +from adcm_client.objects import ADCMClient, Bundle, Provider, User from adcm_pytest_plugin.utils import random_string -from allure_commons.model2 import TestResult, Parameter +from allure_commons.model2 import Parameter, TestResult from allure_pytest.listener import AllureListener from docker.utils import parse_repository_tag - from tests.library.adcm_websockets import ADCMWebsocket +from tests.library.api.client import APIClient from tests.library.db import QueryExecutioner -from tests.library.ldap_interactions import LDAPEntityManager, LDAPTestConfig, configure_adcm_for_ldap +from tests.library.ldap_interactions import ( + LDAPEntityManager, + LDAPTestConfig, + configure_adcm_for_ldap, +) from tests.library.utils import ConfigError pytest_plugins = "adcm_pytest_plugin" @@ -117,7 +120,7 @@ def pytest_runtest_setup(item: Function): @pytest.hookimpl(trylast=True) -def pytest_collection_modifyitems(session, config, items): # pylint: disable=unused-argument +def pytest_collection_modifyitems(session, config, items): """Run tests with id "adcm_with_dummy_data" after everything else""" items.sort(key=lambda x: 'adcm_with_dummy_data' in x.name) @@ -172,6 +175,16 @@ def _get_listener_by_item_if_present(item: Function) -> Optional[AllureListener] return None +# API Client + + +@pytest.fixture() +def api_client(adcm_fs, adcm_api_credentials) -> APIClient: + return APIClient( + adcm_fs.url, {"username": adcm_api_credentials["user"], "password": adcm_api_credentials["password"]} + ) + + # Generic bundles GENERIC_BUNDLES_DIR = pathlib.Path(__file__).parent / 'generic_bundles' @@ -276,7 +289,7 @@ def user(sdk_client_fs) -> User: @pytest.fixture() -def user_sdk(user, adcm_fs) -> ADCMClient: # pylint: disable=unused-argument +def user_sdk(user, adcm_fs) -> ADCMClient: """Returns ADCMClient object from adcm_client with testing user""" username, password = TEST_USER_CREDENTIALS return ADCMClient(url=adcm_fs.url, user=username, password=password) @@ -330,7 +343,7 @@ def ldap_config(cmd_opts) -> dict: config = yaml.safe_load(file) if not isinstance(config, dict): raise ConfigError('LDAP config file should have root type "dict"') - ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, ldap.OPT_X_TLS_NEVER) # pylint: disable=no-member + ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, ldap.OPT_X_TLS_NEVER) return config @@ -460,4 +473,8 @@ def configure_adcm_ldap_ad(request, sdk_client_fs: ADCMClient, ldap_basic_ous, a def _create_extra_user_modlist(user: dict) -> dict: - return {'first_name': user['name'], 'last_name': 'Testovich', 'email': f'{user["name"]}@nexistent.ru'} + return { + 'first_name': user['name'], + 'last_name': 'Testovich', + 'email': f'{user["name"]}@nexistent.ru', + } diff --git a/tests/functional/audit/bundles/cluster_mm_allowed/config.yaml b/tests/functional/audit/bundles/cluster_mm_allowed/config.yaml new file mode 100644 index 0000000000..f7c34f8868 --- /dev/null +++ b/tests/functional/audit/bundles/cluster_mm_allowed/config.yaml @@ -0,0 +1,50 @@ +# 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. + + +- type: cluster + name: maintenance_mode_allowed_cluster + version: 5.4 + allow_maintenance_mode: true + + actions: &actions + default_action: + type: job + script: ./actions.yaml + script_type: ansible + states: + available: any + + config: &config + - name: some_param + type: integer + default: 12 + group_customization: true + +- type: service + name: test_service + version: 4.3 + + actions: *actions + config: *config + + components: &components + first_component: + actions: *actions + config: *config + +- type: service + name: another_service + version: 6.5 + + actions: *actions + components: *components diff --git a/tests/functional/audit/bundles/incorrect_import_export/export/config.yaml b/tests/functional/audit/bundles/incorrect_import_export/export/config.yaml new file mode 100644 index 0000000000..8292be8241 --- /dev/null +++ b/tests/functional/audit/bundles/incorrect_import_export/export/config.yaml @@ -0,0 +1,33 @@ +# 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. + +- type: cluster + version: 2.3 + name: exporter_cluster + display_name: Exporter Cluster + + config: &config + - name: param + type: string + required: false + + export: &export + - param + +- type: service + version: 1.2 + name: exporter_service + display_name: Exporter Service + + config: *config + + export: *export diff --git a/tests/functional/audit/bundles/incorrect_import_export/import/config.yaml b/tests/functional/audit/bundles/incorrect_import_export/import/config.yaml new file mode 100644 index 0000000000..428c76fe50 --- /dev/null +++ b/tests/functional/audit/bundles/incorrect_import_export/import/config.yaml @@ -0,0 +1,30 @@ +# 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. + +- type: cluster + version: 2.3 + name: importer_cluster + display_name: Importer Cluster + + import: + exporter_cluster: + versions: + min: 1.0 + max: 3.0 + +- type: service + version: 1.2 + name: importer_service + display_name: Importer Service + + import: + exporter_service: {} diff --git a/tests/functional/audit/checks.py b/tests/functional/audit/checks.py index fd81e28901..716bce004d 100644 --- a/tests/functional/audit/checks.py +++ b/tests/functional/audit/checks.py @@ -51,14 +51,21 @@ def check_audit_cef_logs(client: ADCMClient, adcm_container: Container): operations = client.audit_operation_list(paging={"limit": 200}) logins = client.audit_login_list() logs: List[Union[AuditOperation, AuditLogin]] = list(operations) + list(logins) - logs.sort(key=lambda l: l.operation_time if isinstance(l, AuditOperation) else l.login_time) + logs.sort( + key=lambda log_operation: log_operation.operation_time + if isinstance(log_operation, AuditOperation) + else log_operation.login_time + ) exit_code, out = adcm_container.exec_run(["cat", "/adcm/data/log/audit.log"]) logfile_content = out.decode("utf-8") if exit_code != 0: raise ValueError(f"Failed to get audit logfile content: {logfile_content}") # filter out empty cef_records: Tuple[CEFRecord, ...] = tuple( - map(lambda r: CEFRecord(*r.split("|")), filter(lambda l: 'CEF' in l, logfile_content.split("\n"))) + map( + lambda r: CEFRecord(*r.split("|")), + filter(lambda log_operation: 'CEF' in log_operation, logfile_content.split("\n")), + ) ) with allure.step("Check all logs have correct CEF version, vendor, product name and version"): for param, expected in ( @@ -84,7 +91,11 @@ def check_audit_cef_logs(client: ADCMClient, adcm_container: Container): with allure.step(f"Check CEF log #{i} is corresponding to {log.id} '{name}' with result '{result}'"): corresponding_cef_log: CEFRecord = cef_records[i] expected_severity = "3" if result == OperationResult.DENIED.value else "1" - for param, expected in (("name", name), ("severity", expected_severity), ("extension", extension)): + for param, expected in ( + ("name", name), + ("severity", expected_severity), + ("extension", extension), + ): if getattr(corresponding_cef_log, param) != expected: _attach_api_log(log) @@ -130,7 +141,11 @@ def _format_time(time: datetime): def _attach_cef_logs(cef_logs: Collection[CEFRecord]) -> None: - allure.attach(pformat(cef_logs), name="Parsed CEF logs from container", attachment_type=allure.attachment_type.TEXT) + allure.attach( + pformat(cef_logs), + name="Parsed CEF logs from container", + attachment_type=allure.attachment_type.TEXT, + ) def _attach_api_log(api_log: Union[AuditOperation, AuditLogin]) -> None: diff --git a/tests/functional/audit/conftest.py b/tests/functional/audit/conftest.py index 5d6662a5e1..28db625f74 100644 --- a/tests/functional/audit/conftest.py +++ b/tests/functional/audit/conftest.py @@ -22,10 +22,14 @@ import allure import pytest import requests -from adcm_client.audit import AuditLogin, AuditLoginList, AuditOperation, AuditOperationList +from adcm_client.audit import ( + AuditLogin, + AuditLoginList, + AuditOperation, + AuditOperationList, +) from adcm_client.base import ObjectNotFound from adcm_client.objects import ADCM, ADCMClient, Policy - from tests.functional.conftest import only_clean_adcm from tests.functional.rbac.conftest import BusinessRoles, create_policy from tests.functional.tools import ClusterRelatedObject, ProviderRelatedObject @@ -153,7 +157,7 @@ def rbac_create_data(sdk_client_fs) -> OrderedDictType[str, dict]: @pytest.fixture() -def prepare_settings(sdk_client_fs): +def _prepare_settings(sdk_client_fs): """Prepare settings for correct log rotation / cleanup AND LDAP""" sdk_client_fs.adcm().config_set_diff( { @@ -213,7 +217,13 @@ def delete(sdk_client_fs) -> Callable: base_url = sdk_client_fs.url auth_header = make_auth_header(sdk_client_fs) - def _delete(path: str, *suffixes, headers: Optional[dict] = None, path_fmt: Optional[dict] = None, **kwargs): + def _delete( + path: str, + *suffixes, + headers: Optional[dict] = None, + path_fmt: Optional[dict] = None, + **kwargs, + ): headers = {**auth_header, **({} if headers is None else headers)} path_fmt = {} if path_fmt is None else path_fmt url = f'{base_url}/api/v1/{path.format(**path_fmt)}/{"/".join(map(str,suffixes))}/' @@ -301,7 +311,9 @@ def format_date_for_db(date: datetime) -> str: def set_operations_date( - adcm_db: QueryExecutioner, new_date: datetime, operation_records: Union[AuditOperationList, List[AuditOperation]] + adcm_db: QueryExecutioner, + new_date: datetime, + operation_records: Union[AuditOperationList, List[AuditOperation]], ): """Set date for given operation audit records directly in ADCM database""" adcm_db.exec( @@ -312,7 +324,9 @@ def set_operations_date( def set_logins_date( - adcm_db: QueryExecutioner, new_date: datetime, login_records: Union[AuditLoginList, List[AuditLogin]] + adcm_db: QueryExecutioner, + new_date: datetime, + login_records: Union[AuditLoginList, List[AuditLogin]], ): """Set date for given login audit records directly in ADCM database""" adcm_db.exec( diff --git a/tests/functional/audit/scenarios/import_audit.yaml b/tests/functional/audit/scenarios/import_audit.yaml new file mode 100644 index 0000000000..8fd04ff03f --- /dev/null +++ b/tests/functional/audit/scenarios/import_audit.yaml @@ -0,0 +1,58 @@ +# 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. + +settings: + process-type: sequence + start-from-first: matched + +operations: + - update: + what: &import-service + type: service + name: "Import/Importer Service" + how: &correct-bind + operation: bind + name: "Export/Exporter Service" + - update: + what: *import-service + how: *correct-bind + - update: + what: *import-service + how: *correct-bind + result: denied + username: {{ username }} + - update: + what: *import-service + how: *correct-bind + result: denied + username: {{ username }} + + - update: + what: *import-service + how: &incorrect-bind + operation: bind + name: "Export" + result: fail + - update: + what: *import-service + how: *incorrect-bind + result: denied + username: {{ username }} + - update: + what: *import-service + how: *incorrect-bind + result: fail + - update: + what: *import-service + how: *incorrect-bind + result: denied + username: {{ username }} diff --git a/tests/functional/audit/scenarios/mm_audit.yaml b/tests/functional/audit/scenarios/mm_audit.yaml new file mode 100644 index 0000000000..19c50bd55d --- /dev/null +++ b/tests/functional/audit/scenarios/mm_audit.yaml @@ -0,0 +1,207 @@ +# 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. + +settings: + process-type: sequence + start-from-first: matched + +operations: + # service + - update: &change-service + what: &service + type: service + name: "Test Cluster WITH Maintenance Mode/another_service" + how: change-properties + result: fail + - update: + what: *service + how: change-properties + changes: + current: + maintenance_mode: "ON" + previous: + maintenance_mode: "OFF" + - update: *change-service + result: denied + username: {{ username }} + - update: + what: *service + how: change-properties + changes: + current: + maintenance_mode: "OFF" + previous: + maintenance_mode: "ON" + + - update: *change-service + result: fail + - update: + what: *service + how: change-properties + changes: + current: + maintenance_mode: "ON" + previous: + maintenance_mode: "OFF" + - update: *change-service + result: denied + username: {{ username }} + - update: + what: *service + how: change-properties + changes: + current: + maintenance_mode: "OFF" + previous: + maintenance_mode: "ON" + + # component + - update: &change-component + what: &component + type: component + name: "Test Cluster WITH Maintenance Mode/test_service/first_component" + how: change-properties + result: fail + - update: + what: *component + how: change-properties + changes: + current: + maintenance_mode: "ON" + previous: + maintenance_mode: "OFF" + - update: *change-component + result: denied + username: {{ username }} + - update: + what: *component + how: change-properties + changes: + current: + maintenance_mode: "OFF" + previous: + maintenance_mode: "ON" + + - update: *change-component + result: fail + - update: + what: *component + how: change-properties + changes: + current: + maintenance_mode: "ON" + previous: + maintenance_mode: "OFF" + - update: *change-component + result: denied + username: {{ username }} + - update: + what: *component + how: change-properties + changes: + current: + maintenance_mode: "OFF" + previous: + maintenance_mode: "ON" + + - update: *change-component + result: fail + - update: + what: *component + how: change-properties + changes: + current: + maintenance_mode: "ON" + previous: + maintenance_mode: "OFF" + - update: *change-component + result: denied + username: {{ username }} + - update: + what: *component + how: change-properties + changes: + current: + maintenance_mode: "OFF" + previous: + maintenance_mode: "ON" + + # host + - update: &change-host + what: &host + type: host + name: "test-host-0" + how: change-properties + result: fail + - update: + what: *host + how: change-properties + changes: + current: + maintenance_mode: "ON" + previous: + maintenance_mode: "OFF" + - update: *change-host + result: denied + username: {{ username }} + - update: + what: *host + how: change-properties + changes: + current: + maintenance_mode: "OFF" + previous: + maintenance_mode: "ON" + + - update: *change-host + result: fail + - update: + what: *host + how: change-properties + changes: + current: + maintenance_mode: "ON" + previous: + maintenance_mode: "OFF" + - update: *change-host + result: denied + username: {{ username }} + - update: + what: *host + how: change-properties + changes: + current: + maintenance_mode: "OFF" + previous: + maintenance_mode: "ON" + + - update: *change-host + result: fail + - update: + what: *host + how: change-properties + changes: + current: + maintenance_mode: "ON" + previous: + maintenance_mode: "OFF" + - update: *change-host + result: denied + username: {{ username }} + - update: + what: *host + how: change-properties + changes: + current: + maintenance_mode: "OFF" + previous: + maintenance_mode: "ON" diff --git a/tests/functional/audit/scenarios/objects_update.yaml b/tests/functional/audit/scenarios/objects_update.yaml index 8f4ed09b93..836509a949 100644 --- a/tests/functional/audit/scenarios/objects_update.yaml +++ b/tests/functional/audit/scenarios/objects_update.yaml @@ -62,10 +62,5 @@ operations: - update: what: *host how: change-properties - changes: - previous: - maintenance_mode: "off" - current: - maintenance_mode: "on" - update: *change-host result: fail diff --git a/tests/functional/audit/test_actions_audit.py b/tests/functional/audit/test_actions_audit.py index a16a8a53d0..193e70e605 100644 --- a/tests/functional/audit/test_actions_audit.py +++ b/tests/functional/audit/test_actions_audit.py @@ -20,7 +20,6 @@ from adcm_client.audit import AuditOperation, ObjectType, OperationResult, OperationType from adcm_client.objects import ADCMClient, Bundle, Cluster, Job, Provider, Task from adcm_pytest_plugin.utils import wait_until_step_succeeds - from tests.functional.audit.conftest import ( BUNDLES_DIR, NEW_USER, @@ -63,25 +62,25 @@ def provider(sdk_client_fs) -> Provider: @pytest.fixture() -def grant_view_on_cluster(cluster, build_policy): +def _grant_view_on_cluster(cluster, build_policy): """Grant new user a permission to "view cluster" with permission to view config""" build_policy(BR.ViewClusterConfigurations, cluster) @pytest.fixture() -def grant_view_on_component(cluster, build_policy): +def _grant_view_on_component(cluster, build_policy): """Grant new user a permission to "view component" with permission to view config""" build_policy(BR.ViewComponentConfigurations, cluster.service().component()) @pytest.fixture() -def grant_view_on_provider(provider, build_policy): +def _grant_view_on_provider(provider, build_policy): """Grant new user a permission to "view provider" with permission to view config""" build_policy(BR.ViewProviderConfigurations, provider) @pytest.fixture() -def grant_view_on_host(provider, build_policy): +def _grant_view_on_host(provider, build_policy): """Grant new user a permission to "view host" with permission to view config""" build_policy(BR.ViewHostConfigurations, provider.host()) @@ -125,10 +124,10 @@ def _action_run_test_init(instance: RunActionTestMixin, admin_client: ADCMClient class TestClusterObjectsActions(RunActionTestMixin): """Test on audit of cluster objects' actions""" - pytestmark = [pytest.mark.usefixtures("init", "grant_view_on_component")] + pytestmark = [pytest.mark.usefixtures("_init", "_grant_view_on_component")] @pytest.fixture() - def init(self, sdk_client_fs, new_user_client): + def _init(self, sdk_client_fs, new_user_client): """Fill all required fields""" _action_run_test_init(self, sdk_client_fs, new_user_client) @@ -184,10 +183,10 @@ def _run_component_actions(self, cluster, post): class TestProviderObjectActions(RunActionTestMixin): """Tests on audit of provider objects' actions""" - pytestmark = [pytest.mark.usefixtures("init", "grant_view_on_provider", "grant_view_on_host")] + pytestmark = [pytest.mark.usefixtures("_init", "_grant_view_on_provider", "_grant_view_on_host")] @pytest.fixture() - def init(self, sdk_client_fs, new_user_client): + def _init(self, sdk_client_fs, new_user_client): """Fill all required fields""" _action_run_test_init(self, sdk_client_fs, new_user_client) @@ -196,7 +195,7 @@ def _add_host_to_cluster(self, cluster, provider): cluster.host_add(provider.host()) @parametrize_audit_scenario_parsing("provider_actions.yaml", NEW_USER) - @pytest.mark.usefixtures("grant_view_on_cluster", "_add_host_to_cluster") + @pytest.mark.usefixtures("_grant_view_on_cluster", "_add_host_to_cluster") def test_run_provider_actions(self, provider, audit_log_checker, post): """ Test audit of provider objects' actions from host/provider/cluster's perspective: @@ -248,7 +247,7 @@ class TestUpgrade(RunActionTestMixin): FAIL = "Fail Upgrade" @pytest.fixture() - def init(self, sdk_client_fs, new_user_client): + def _init(self, sdk_client_fs, new_user_client): """Fill all required utilities for audit of actions tests""" _action_run_test_init(self, sdk_client_fs, new_user_client) @@ -261,12 +260,9 @@ def upload_new_bundles(self, sdk_client_fs) -> Tuple[Bundle, Bundle]: ) @pytest.mark.parametrize("parse_with_context", ["upgrade.yaml"], indirect=True) - @pytest.mark.parametrize( - "type_to_pick", - [Cluster, pytest.param(Provider, marks=pytest.mark.skip(reason="https://tracker.yandex.ru/ADCM-3179"))], - ) + @pytest.mark.parametrize("type_to_pick", [Cluster, Provider]) @pytest.mark.usefixtures( - "grant_view_on_cluster", "grant_view_on_provider", "upload_new_bundles", "init" + "_grant_view_on_cluster", "_grant_view_on_provider", "upload_new_bundles", "_init" ) # pylint: disable-next=too-many-locals def test_upgrade(self, type_to_pick: Type, cluster, provider, parse_with_context): """Test audit of cluster/provider simple upgrade/upgrade with action""" @@ -310,7 +306,7 @@ class TestADCMActions: """Test audit of ADCM actions""" @parametrize_audit_scenario_parsing("adcm_actions.yaml", NEW_USER) - @pytest.mark.usefixtures("prepare_settings") + @pytest.mark.usefixtures("_prepare_settings") def test_adcm_actions(self, sdk_client_fs, audit_log_checker, new_user_client, build_policy): """Test audit of ADCM actions""" adcm = sdk_client_fs.adcm() @@ -321,7 +317,11 @@ def test_adcm_actions(self, sdk_client_fs, audit_log_checker, new_user_client, b check_404(requests.post(url, headers=make_auth_header(new_user_client))) with allure.step("Fail to run action"): check_400( - requests.post(url, json={"config": {"i": "doesnotexist"}}, headers=make_auth_header(sdk_client_fs)) + requests.post( + url, + json={"config": {"i": "doesnotexist"}}, + headers=make_auth_header(sdk_client_fs), + ) ) with allure.step("Run action successfuly"): check_succeed(requests.post(url, headers=make_auth_header(sdk_client_fs))) @@ -333,10 +333,10 @@ def test_adcm_actions(self, sdk_client_fs, audit_log_checker, new_user_client, b class TestTaskCancelRestart(RunActionTestMixin): """Test audit of cancelling/restarting tasks with one/multi jobs""" - pytestmark = [pytest.mark.usefixtures("init", "grant_view_on_cluster")] + pytestmark = [pytest.mark.usefixtures("_init", "_grant_view_on_cluster")] @pytest.fixture() - def init(self, sdk_client_fs, new_user_client): + def _init(self, sdk_client_fs, new_user_client): """Fill all utility fields for audit of actions testing""" _action_run_test_init(self, sdk_client_fs, new_user_client) diff --git a/tests/functional/audit/test_api.py b/tests/functional/audit/test_api.py index 38c3efee9c..dc9276cf1b 100644 --- a/tests/functional/audit/test_api.py +++ b/tests/functional/audit/test_api.py @@ -22,7 +22,6 @@ from adcm_client.audit import LoginResult, ObjectType, OperationResult, OperationType from adcm_client.objects import ADCMClient, Group, Policy, Role, User from coreapi.exceptions import ErrorMessage - from tests.functional.audit.conftest import check_failed, make_auth_header from tests.functional.conftest import only_clean_adcm from tests.functional.rbac.conftest import BusinessRoles @@ -36,7 +35,6 @@ NOT_EXISTING_USER = "nosuchuser" -# pylint: disable-next=too-many-arguments def _check_audit_logs( endpoint: str, operation: Callable, @@ -182,7 +180,7 @@ def users(self, sdk_client_fs) -> Tuple[dict, dict]: return user_1_creds, user_2_creds @pytest.fixture() - def successful_logins(self, sdk_client_fs, users) -> None: + def _successful_logins(self, sdk_client_fs, users) -> None: """Make successful logins""" for creds in users: self._login(sdk_client_fs, **creds) @@ -200,7 +198,7 @@ def failed_logins(self, sdk_client_fs, users) -> Tuple[dict, dict]: self._login(sdk_client_fs, **user_does_not_exist) return deactivated_user, user_does_not_exist - @pytest.mark.usefixtures("successful_logins", "failed_logins") + @pytest.mark.usefixtures("_successful_logins", "failed_logins") def test_audit_login_api_filtering(self, sdk_client_fs, users): """Test audit log list filtering: by operation result and username""" self._check_login_list_filtering(sdk_client_fs, 'login_result', LoginResult) diff --git a/tests/functional/audit/test_background_operations.py b/tests/functional/audit/test_background_operations.py index 0a2120aa49..b6414568dd 100644 --- a/tests/functional/audit/test_background_operations.py +++ b/tests/functional/audit/test_background_operations.py @@ -23,8 +23,11 @@ from adcm_client.objects import Cluster, Task from adcm_pytest_plugin.steps.commands import clearaudit, logrotate from adcm_pytest_plugin.utils import wait_until_step_succeeds - -from tests.functional.audit.conftest import BUNDLES_DIR, parametrize_audit_scenario_parsing, set_operations_date +from tests.functional.audit.conftest import ( + BUNDLES_DIR, + parametrize_audit_scenario_parsing, + set_operations_date, +) from tests.functional.conftest import only_clean_adcm from tests.library.db import set_configs_date, set_jobs_date, set_tasks_date @@ -50,7 +53,7 @@ def cluster_with_history(sdk_client_fs) -> Tuple[Cluster, Tuple[dict, ...], Tupl @pytest.fixture() -def make_objects_old(adcm_db, sdk_client_fs, cluster_with_history) -> None: +def _make_objects_old(adcm_db, sdk_client_fs, cluster_with_history) -> None: """Change object's dates (configs, tasks and audit records)""" old_date = datetime.utcnow() - timedelta(days=300) _, configs, tasks = cluster_with_history @@ -58,12 +61,16 @@ def make_objects_old(adcm_db, sdk_client_fs, cluster_with_history) -> None: get_id = attrgetter("id") set_configs_date(adcm_db, old_date, tuple(map(itemgetter("id"), configs[: len(configs) // 2]))) set_tasks_date(adcm_db, old_date, tuple(map(get_id, old_tasks))) - set_jobs_date(adcm_db, old_date, tuple(map(get_id, chain.from_iterable(map(methodcaller("job_list"), old_tasks))))) + set_jobs_date( + adcm_db, + old_date, + tuple(map(get_id, chain.from_iterable(map(methodcaller("job_list"), old_tasks)))), + ) set_operations_date(adcm_db, old_date, sdk_client_fs.audit_operation_list(paging={"limit": 4})) @parametrize_audit_scenario_parsing("background_tasks.yaml") -@pytest.mark.usefixtures("make_objects_old", "prepare_settings") +@pytest.mark.usefixtures("_make_objects_old", "_prepare_settings") def test_background_operations_audit(audit_log_checker, adcm_fs, sdk_client_fs): """Test audit of background operations""" diff --git a/tests/functional/audit/test_create_operations_audit.py b/tests/functional/audit/test_create_operations_audit.py index 9be2820370..e8db0d6b40 100644 --- a/tests/functional/audit/test_create_operations_audit.py +++ b/tests/functional/audit/test_create_operations_audit.py @@ -19,7 +19,6 @@ import allure import pytest from adcm_client.objects import ADCMClient - from tests.functional.audit.conftest import ( BUNDLES_DIR, NEW_USER, @@ -84,26 +83,48 @@ def test_bundle_upload_load(audit_log_checker, post, bundle_archives, sdk_client with allure.step("Upload and load incorrect bundles (as unauthorized and authorized user)"): for bundle_path in (incorrect_cluster_bundle, incorrect_provider_bundle): with bundle_path.open("rb") as f: - check_failed(post(CreateOperation.UPLOAD, files={"file": f}, headers=unauthorized_user_creds), 403) + check_failed( + post(CreateOperation.UPLOAD, files={"file": f}, headers=unauthorized_user_creds), + 403, + ) with bundle_path.open("rb") as f: check_succeed(post(CreateOperation.UPLOAD, files={"file": f})) check_failed( - post(CreateOperation.LOAD, {"bundle_file": bundle_path.name}, headers=unauthorized_user_creds), 403 + post( + CreateOperation.LOAD, + {"bundle_file": bundle_path.name}, + headers=unauthorized_user_creds, + ), + 403, ) check_failed(post(CreateOperation.LOAD, {"bundle_file": bundle_path.name})) with allure.step("Upload and load correct bundles (as unauthorized and authorized user)"): for bundle_path in (cluster_bundle, provider_bundle): with bundle_path.open("rb") as f: - check_failed(post(CreateOperation.UPLOAD, files={"file": f}, headers=unauthorized_user_creds), 403) + check_failed( + post(CreateOperation.UPLOAD, files={"file": f}, headers=unauthorized_user_creds), + 403, + ) with bundle_path.open("rb") as f: check_succeed(post(CreateOperation.UPLOAD, files={"file": f})) check_failed( - post(CreateOperation.LOAD, {"bundle_file": bundle_path.name}, headers=unauthorized_user_creds), 403 + post( + CreateOperation.LOAD, + {"bundle_file": bundle_path.name}, + headers=unauthorized_user_creds, + ), + 403, ) check_succeed(post(CreateOperation.LOAD, {"bundle_file": bundle_path.name})) with allure.step("Load/Upload with incorrect data in request (as unauthorized and authorized user)"): - check_failed(post(CreateOperation.UPLOAD, files={"wrongkey": "sldkj"}, headers=unauthorized_user_creds), 403) - check_failed(post(CreateOperation.LOAD, {"bundle": "somwthign"}, headers=unauthorized_user_creds), 403) + check_failed( + post(CreateOperation.UPLOAD, files={"wrongkey": "sldkj"}, headers=unauthorized_user_creds), + 403, + ) + check_failed( + post(CreateOperation.LOAD, {"bundle": "somwthign"}, headers=unauthorized_user_creds), + 403, + ) check_failed(post(CreateOperation.UPLOAD, files={"wrongkey": "sldkj"})) check_failed(post(CreateOperation.LOAD, {"bundle": "somwthign"})) audit_log_checker.set_user_map(sdk_client_fs) @@ -128,7 +149,12 @@ def test_rbac_create_operations(parse_with_context, rbac_create_data, post, sdk_ check_succeed(post(getattr(CreateOperation, object_type.upper()), create_data)) check_failed(post(getattr(CreateOperation, object_type.upper()), create_data)) check_failed( - post(getattr(CreateOperation, object_type.upper()), create_data, headers=new_user_auth_header), 403 + post( + getattr(CreateOperation, object_type.upper()), + create_data, + headers=new_user_auth_header, + ), + 403, ) audit_checker.set_user_map(sdk_client_fs) audit_checker.check(sdk_client_fs.audit_operation_list()) @@ -149,24 +175,37 @@ def test_create_adcm_objects(audit_log_checker, post, new_user_client, sdk_clien provider_bundle = sdk_client_fs.upload_from_fs(BUNDLES_DIR / "create" / "provider") with allure.step("Create cluster, try to create cluster from incorrect prototype and without permissions"): cluster_proto_id = cluster_bundle.cluster_prototype().id - cluster_create_args = (CreateOperation.CLUSTER, {"prototype_id": cluster_proto_id, "name": "cluster"}) + cluster_create_args = ( + CreateOperation.CLUSTER, + {"prototype_id": cluster_proto_id, "name": "cluster"}, + ) check_succeed(post(*cluster_create_args)) check_failed(post(CreateOperation.CLUSTER, {"prototype_id": 1000, "name": "cluster"}), 404) check_failed(post(*cluster_create_args, headers=new_user_creds), 403) with allure.step("Create provider, try to create provider from incorrect prototype and without permissions"): provider_proto_id = provider_bundle.provider_prototype().id - provider_create_args = (CreateOperation.PROVIDER, {"prototype_id": provider_proto_id, "name": "provider"}) + provider_create_args = ( + CreateOperation.PROVIDER, + {"prototype_id": provider_proto_id, "name": "provider"}, + ) check_succeed(post(*provider_create_args)) check_failed(post(CreateOperation.PROVIDER, {"prototype_id": 1000, "name": "provider"}), 404) check_failed(post(*provider_create_args, headers=new_user_creds), 403) provider = sdk_client_fs.provider() with allure.step("Create host from root and from provider"): - host_from_provider_args = {"data": {"fqdn": "host-from-provider"}, "path_fmt": {"provider_id": provider.id}} + host_from_provider_args = { + "data": {"fqdn": "host-from-provider"}, + "path_fmt": {"provider_id": provider.id}, + } check_succeed(post(CreateOperation.HOST_FROM_PROVIDER, **host_from_provider_args)) host_prototype_id = provider.host().prototype_id host_from_root_args = { - "data": {"fqdn": "host-from-root", "prototype_id": host_prototype_id, "provider_id": provider.id} + "data": { + "fqdn": "host-from-root", + "prototype_id": host_prototype_id, + "provider_id": provider.id, + } } check_succeed(post(CreateOperation.HOST, **host_from_root_args)) with allure.step("Try to incorrectly create host from root and from provider"): @@ -176,7 +215,14 @@ def test_create_adcm_objects(audit_log_checker, post, new_user_client, sdk_clien create_policy( # need it to be able to create host from provider's context sdk_client_fs, BusinessRoles.ViewProviderConfigurations, [provider], [new_user], [] ) - check_failed(post(CreateOperation.HOST_FROM_PROVIDER, **host_from_provider_args, headers=new_user_creds), 403) + check_failed( + post( + CreateOperation.HOST_FROM_PROVIDER, + **host_from_provider_args, + headers=new_user_creds, + ), + 403, + ) check_failed(post(CreateOperation.HOST, **host_from_root_args, headers=new_user_creds), 403) with allure.step( "Create group config for cluster, service and component, " diff --git a/tests/functional/audit/test_delete_operations_audit.py b/tests/functional/audit/test_delete_operations_audit.py index 24b8d19e63..0756bcdd3f 100644 --- a/tests/functional/audit/test_delete_operations_audit.py +++ b/tests/functional/audit/test_delete_operations_audit.py @@ -19,8 +19,16 @@ import allure import pytest from adcm_client.base import ObjectNotFound -from adcm_client.objects import Bundle, Cluster, Group, Host, Policy, Provider, Role, User - +from adcm_client.objects import ( + Bundle, + Cluster, + Group, + Host, + Policy, + Provider, + Role, + User, +) from tests.functional.audit.conftest import BUNDLES_DIR, NEW_USER from tests.functional.audit.conftest import CreateDeleteOperation as Delete from tests.functional.audit.conftest import check_failed, check_succeed @@ -81,13 +89,17 @@ def rbac_objects(sdk_client_fs, rbac_create_data) -> Tuple[User, Group, Role, Po @pytest.fixture() -def grant_view_config_permissions_on_adcm_objects(sdk_client_fs, adcm_objects, new_user_client): +def _grant_view_config_permissions_on_adcm_objects(sdk_client_fs, adcm_objects, new_user_client): """Create policies that allow new user to get ADCM objects (via View Configuration)""" cluster, provider, host_1, host_2 = adcm_objects user = sdk_client_fs.user(id=new_user_client.me().id) create_policy( sdk_client_fs, - [BR.ViewClusterConfigurations, BR.ViewServiceConfigurations, BR.ViewComponentConfigurations], + [ + BR.ViewClusterConfigurations, + BR.ViewServiceConfigurations, + BR.ViewComponentConfigurations, + ], [cluster, (s := cluster.service()), s.component()], users=[user], groups=[], @@ -104,9 +116,7 @@ def grant_view_config_permissions_on_adcm_objects(sdk_client_fs, adcm_objects, n @pytest.mark.parametrize('parse_with_context', ['delete_objects.yaml'], indirect=True) -@pytest.mark.usefixtures( - "grant_view_config_permissions_on_adcm_objects" -) # pylint: disable-next=too-many-locals,too-many-arguments +@pytest.mark.usefixtures("_grant_view_config_permissions_on_adcm_objects") # pylint: disable-next=too-many-locals def test_delete( parse_with_context, sdk_client_fs, diff --git a/tests/functional/audit/test_import_audit.py b/tests/functional/audit/test_import_audit.py new file mode 100644 index 0000000000..11c149e0ef --- /dev/null +++ b/tests/functional/audit/test_import_audit.py @@ -0,0 +1,187 @@ +# 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. + +"""Tests for imports""" +from typing import Optional + +import allure +import pytest +import requests +from adcm_client.objects import ADCMClient, Cluster +from tests.functional.audit.conftest import ( + BUNDLES_DIR, + NEW_USER, + check_failed, + check_succeed, + make_auth_header, + parametrize_audit_scenario_parsing, +) +from tests.functional.audit.test_objects_updates import EXPORT_SERVICE, IMPORT_SERVICE +from tests.functional.conftest import only_clean_adcm +from tests.functional.rbac.conftest import BusinessRoles, create_policy + +pytestmark = [only_clean_adcm] +# pylint: disable=redefined-outer-name + + +@pytest.fixture() +def import_export_clusters(sdk_client_fs) -> tuple[Cluster, Cluster]: + """Create clusters from import and export bundles and add services to them""" + import_bundle = sdk_client_fs.upload_from_fs(BUNDLES_DIR / "incorrect_import_export" / "import") + import_cluster = import_bundle.cluster_create("Import") + import_cluster.service_add(name=IMPORT_SERVICE) + export_bundle = sdk_client_fs.upload_from_fs(BUNDLES_DIR / "incorrect_import_export" / "export") + export_cluster = export_bundle.cluster_create("Export") + export_cluster.service_add(name=EXPORT_SERVICE) + return import_cluster, export_cluster + + +def bind(url: str, cluster_id: Optional[int], service_id: Optional[int], **kwargs) -> requests.Response: + body = { + **({"export_cluster_id": cluster_id} if cluster_id else {}), + **({"export_service_id": service_id} if service_id else {}), + } + + with allure.step(f"Create bind via POST {url} with body: {body}"): + return requests.post(url, json=body, **kwargs) + + +def change_service_id( + admin_client: ADCMClient, user_client: ADCMClient, import_cluster: Cluster, export_cluster: Cluster +) -> None: + """Set different service id and send request""" + export_service = export_cluster.service() + import_service = import_cluster.service() + url = f"{admin_client.url}/api/v1/cluster/{import_cluster.id}/service/{import_service.id}/bind/" + + with allure.step( + f"Bind cluster and service with wrong service id {(export_service.id + 1)}," " with admin header and wait fail" + ): + check_failed( + bind( + url=url, + cluster_id=export_cluster.id, + service_id=(export_service.id + 1), + headers=make_auth_header(admin_client), + ), + exact_code=404, + ) + + with allure.step( + f"Bind cluster and service with wrong service id {(export_service.id + 1)}," " with user header and wait fail" + ): + check_failed( + bind( + url=url, + cluster_id=export_cluster.id, + service_id=(export_service.id + 1), + headers=make_auth_header(user_client), + ), + exact_code=403, + ) + + with allure.step("Bind cluster and service with empty service id, with admin header and wait fail"): + check_failed( + bind(url=url, cluster_id=export_cluster.id, service_id=None, headers=make_auth_header(admin_client)), + exact_code=404, + ) + + with allure.step("Bind cluster and service with empty service id, with user header and wait fail"): + check_failed( + bind(url=url, cluster_id=export_cluster.id, service_id=None, headers=make_auth_header(user_client)), + exact_code=403, + ) + + +def change_import_url( + admin_client: ADCMClient, user_client: ADCMClient, import_cluster: Cluster, export_cluster: Cluster +) -> None: + """Set different url and send request""" + export_service = export_cluster.service() + import_service = import_cluster.service() + + bind_from_cluster_url = f"{admin_client.url}/api/v1/cluster/{import_cluster.id}/service/{import_service.id}/bind/" + bind_from_service_url = f"{admin_client.url}/api/v1/service/{import_service.id}/bind/" + + with allure.step(f"Bind cluster and service with url {bind_from_cluster_url} with admin header and wait success"): + check_succeed( + bind( + url=bind_from_cluster_url, + cluster_id=export_cluster.id, + service_id=export_service.id, + headers=make_auth_header(admin_client), + ) + ) + + with allure.step(f"Unbind service with url {bind_from_service_url}"): + service_bind_id = requests.get(bind_from_cluster_url, headers=make_auth_header(admin_client)).json()[0]["id"] + check_succeed( + requests.delete(f"{bind_from_cluster_url}{service_bind_id}/", headers=make_auth_header(admin_client)) + ) + + with allure.step(f"Bind cluster and service with url {bind_from_service_url} with admin header and wait success"): + check_succeed( + bind( + url=bind_from_service_url, + cluster_id=export_cluster.id, + service_id=export_service.id, + headers=make_auth_header(admin_client), + ) + ) + + with allure.step(f"Bind cluster and service with url {bind_from_cluster_url} with user header and wait fail"): + check_failed( + bind( + url=bind_from_cluster_url, + cluster_id=export_cluster.id, + service_id=export_service.id, + headers=make_auth_header(user_client), + ), + exact_code=403, + ) + + with allure.step(f"Bind cluster and service with url {bind_from_service_url} with user header and wait fail"): + check_failed( + bind( + url=bind_from_service_url, + cluster_id=export_cluster.id, + service_id=export_service.id, + headers=make_auth_header(user_client), + ), + exact_code=403, + ) + + +@parametrize_audit_scenario_parsing("import_audit.yaml", NEW_USER) +def test_negative_service_import(sdk_client_fs: ADCMClient, new_user_client, audit_log_checker, import_export_clusters): + """Test to check params on import""" + import_cluster, export_cluster = import_export_clusters + import_service = import_cluster.service() + new_user = sdk_client_fs.user(id=new_user_client.me().id) + create_policy(sdk_client_fs, [BusinessRoles.ViewServiceConfigurations], [import_service], [new_user], []) + + change_import_url( + admin_client=sdk_client_fs, + user_client=new_user_client, + import_cluster=import_cluster, + export_cluster=export_cluster, + ) + + change_service_id( + admin_client=sdk_client_fs, + user_client=new_user_client, + import_cluster=import_cluster, + export_cluster=export_cluster, + ) + + audit_log_checker.set_user_map(sdk_client_fs) + audit_log_checker.check(list(sdk_client_fs.audit_operation_list())) diff --git a/tests/functional/audit/test_login.py b/tests/functional/audit/test_login.py index 1c2d27bb2a..0eea736d2c 100644 --- a/tests/functional/audit/test_login.py +++ b/tests/functional/audit/test_login.py @@ -18,7 +18,6 @@ import pytest import requests from adcm_client.objects import ADCMClient - from tests.functional.audit.conftest import make_auth_header from tests.functional.conftest import only_clean_adcm @@ -43,14 +42,22 @@ def _viewer_login(client: ADCMClient, username: str, password: str) -> requests. .split(' Provider: + """Upload bundle and create default provider""" + bundle = sdk_client_fs.upload_from_fs(BUNDLES_DIR / 'provider') + return bundle.provider_create(PROVIDER_NAME) + + +@pytest.fixture() +def hosts(provider) -> tuple[Host, ...]: + """Create 6 hosts from the default bundle""" + return tuple(provider.host_create(f'test-host-{i}') for i in range(6)) + + +@pytest.fixture() +def cluster_with_mm(sdk_client_fs: ADCMClient) -> Cluster: + """ + Upload cluster bundle with allowed MM, + create and return cluster with default service + """ + bundle = sdk_client_fs.upload_from_fs(BUNDLES_DIR / 'cluster_mm_allowed') + cluster = bundle.cluster_create(CLUSTER_WITH_MM_NAME) + cluster.service_add(name=DEFAULT_SERVICE_NAME) + return cluster + + +def change_service_mm(admin_client: ADCMClient, user_client: ADCMClient, service: Service) -> None: + """Method to change service to maintenance mode""" + url_list = [ + f'{admin_client.url}/api/v1/cluster/{service.cluster_id}/service/{service.id}/maintenance-mode/', + f'{admin_client.url}/api/v1/service/{service.id}/maintenance-mode/', + ] + + for url in url_list: + body = {"maintenance_mode": True} + with allure.step(f'Fail update service via POST {url} with body: {body}'): + check_failed(requests.post(url, json=body, headers=make_auth_header(admin_client)), 400) + + body = {"maintenance_mode": MM_IS_ON} + with allure.step(f'Success update service via POST {url} with body: {body}'): + check_succeed(requests.post(url, json=body, headers=make_auth_header(admin_client))) + + body = {"maintenance_mode": MM_IS_OFF} + with allure.step(f'Deny update service via POST {url} with body: {body} with wrong user'): + check_failed(requests.post(url, json=body, headers=make_auth_header(user_client)), exact_code=404) + + with allure.step(f'Success update service via POST {url} with body: {body}'): + check_succeed(requests.post(url, json=body, headers=make_auth_header(admin_client))) + + +def change_component_mm(admin_client: ADCMClient, user_client: ADCMClient, component: Component) -> None: + """Method to change component to maintenance mode""" + url_list = [ + ( + f'{admin_client.url}/api/v1/cluster/{component.cluster_id}/' + f'service/{component.service_id}/component/{component.id}/maintenance-mode/' + ), + f'{admin_client.url}/api/v1/service/{component.service_id}/component/{component.id}/maintenance-mode/', + f'{admin_client.url}/api/v1/component/{component.id}/maintenance-mode/', + ] + + for url in url_list: + body = {} + with allure.step(f'Fail update component via POST {url} with body: {body}'): + check_failed(requests.post(url, json=body, headers=make_auth_header(admin_client)), 400) + + body = {"maintenance_mode": MM_IS_ON} + with allure.step(f'Success update component via POST {url} with body: {body}'): + check_succeed(requests.post(url, json=body, headers=make_auth_header(admin_client))) + + body = {"maintenance_mode": MM_IS_OFF} + with allure.step(f'Deny update component via POST {url} with body: {body} with wrong user'): + check_failed(requests.post(url, json=body, headers=make_auth_header(user_client)), exact_code=404) + + with allure.step(f'Success update component via POST {url} with body: {body}'): + check_succeed(requests.post(url, json=body, headers=make_auth_header(admin_client))) + + +def change_host_mm(admin_client: ADCMClient, user_client: ADCMClient, host: Host) -> None: + """Method to change host to maintenance mode""" + host.reread() + url_list = [ + f'{admin_client.url}/api/v1/cluster/{host.cluster_id}/host/{host.id}/maintenance-mode/', + f'{admin_client.url}/api/v1/host/{host.id}/maintenance-mode/', + f'{admin_client.url}/api/v1/provider/{host.provider_id}/host/{host.id}/maintenance-mode/', + ] + + for url in url_list: + + body = {"maintenance_mode": True} + with allure.step(f'Deny update host via POST {url} with body: {body}'): + check_failed(requests.post(url, headers=make_auth_header(admin_client)), 400) + + body = {"maintenance_mode": MM_IS_ON} + with allure.step(f'Success update host via POST {url} with body: {body}'): + check_succeed(requests.post(url, json=body, headers=make_auth_header(admin_client))) + + body = {"maintenance_mode": MM_IS_OFF} + with allure.step(f'Deny update host via POST {url} with body: {body} with wrong user'): + check_failed(requests.post(url, json=body, headers=make_auth_header(user_client)), exact_code=404) + + with allure.step(f'Success update host via POST {url} with body: {body}'): + check_succeed(requests.post(url, json=body, headers=make_auth_header(admin_client))) + + +@parametrize_audit_scenario_parsing("mm_audit.yaml", NEW_USER) # pylint: disable-next=too-many-arguments +def test_mm_audit(sdk_client_fs, audit_log_checker, cluster_with_mm, hosts, new_user_client): + """Test to check audit logs for service and components in maintenance mode""" + first_host, *_ = hosts + first_service = cluster_with_mm.service(name=DEFAULT_SERVICE_NAME) + first_component = first_service.component(name='first_component') + second_service = cluster_with_mm.service_add(name=ANOTHER_SERVICE_NAME) + + add_hosts_to_cluster(cluster_with_mm, (first_host,)) + + change_service_mm(admin_client=sdk_client_fs, user_client=new_user_client, service=second_service) + change_component_mm(admin_client=sdk_client_fs, user_client=new_user_client, component=first_component) + change_host_mm(admin_client=sdk_client_fs, user_client=new_user_client, host=first_host) + + audit_log_checker.set_user_map(sdk_client_fs) + audit_log_checker.check(list(sdk_client_fs.audit_operation_list())) diff --git a/tests/functional/audit/test_objects_updates.py b/tests/functional/audit/test_objects_updates.py index a2c931e13d..05875f374d 100644 --- a/tests/functional/audit/test_objects_updates.py +++ b/tests/functional/audit/test_objects_updates.py @@ -18,7 +18,6 @@ import pytest import requests from adcm_client.objects import ADCMClient, Bundle, Cluster, Host - from tests.functional.audit.conftest import ( BUNDLES_DIR, NEW_USER, @@ -61,7 +60,7 @@ def import_export_clusters(sdk_client_fs, import_bundle) -> Tuple[Cluster, Clust @pytest.fixture() -def grant_cluster_view_permissions(sdk_client_fs, import_export_clusters, new_user_client) -> None: +def _grant_cluster_view_permissions(sdk_client_fs, import_export_clusters, new_user_client) -> None: """Grant view config permissions on import cluster and service""" import_cluster, *_ = import_export_clusters create_policy( @@ -81,18 +80,16 @@ class TestClusterUpdates: new_user_creds: dict admin_creds: dict - pytestmark = [pytest.mark.usefixtures("init")] + pytestmark = [pytest.mark.usefixtures("_init")] @pytest.fixture() - def init(self, sdk_client_fs, unauthorized_creds) -> None: + def _init(self, sdk_client_fs, unauthorized_creds) -> None: """Bind all required "context" to an instance""" self.client = sdk_client_fs self.admin_creds = make_auth_header(sdk_client_fs) self.new_user_creds = unauthorized_creds - @pytest.mark.parametrize( - "parse_with_context", ["plain_service_add.yaml"], indirect=True - ) # pylint: disable-next=too-many-arguments + @pytest.mark.parametrize("parse_with_context", ["plain_service_add.yaml"], indirect=True) def test_plain_service_add(self, import_bundle, parse_with_context, post, delete, new_user_client): """Test adding service from /api/v1/service/""" new_user = self.client.user(id=new_user_client.me().id) @@ -116,15 +113,17 @@ def test_plain_service_add(self, import_bundle, parse_with_context, post, delete check_failed(delete(path, service.id), exact_code=404) checker = AuditLogChecker( parse_with_context( - {"cluster_name": cluster.name, "service_display_name": display_name, "username": NEW_USER["username"]} + { + "cluster_name": cluster.name, + "service_display_name": display_name, + "username": NEW_USER["username"], + } ) ) checker.set_user_map(self.client) checker.check(self.client.audit_operation_list()) - @pytest.mark.parametrize( - "parse_with_context", ["cluster_updates.yaml"], indirect=True - ) # pylint: disable-next=too-many-arguments + @pytest.mark.parametrize("parse_with_context", ["cluster_updates.yaml"], indirect=True) def test_cluster_service_updates( self, bundle_with_license, @@ -149,12 +148,24 @@ def test_cluster_service_updates( host = generic_provider.host_create("first") create_policy(self.client, [BusinessRoles.ViewClusterConfigurations], [cluster], [new_user], []) self._add_service(cluster) - create_policy(self.client, [BusinessRoles.ViewServiceConfigurations], [cluster.service()], [new_user], []) + create_policy( + self.client, + [BusinessRoles.ViewServiceConfigurations], + [cluster.service()], + [new_user], + [], + ) self._remove_service(cluster) self._add_host(cluster, host) with allure.step("Return service back"): service = cluster.service_add(name="service_name") - create_policy(self.client, [BusinessRoles.ViewComponentConfigurations], [service.component()], [new_user], []) + create_policy( + self.client, + [BusinessRoles.ViewComponentConfigurations], + [service.component()], + [new_user], + [], + ) self._set_hostcomponent(cluster, host) new_host = generic_provider.host_create("second") with allure.step("Add another host to a cluster"): @@ -251,14 +262,14 @@ class TestImportAudit: admin_creds: dict @pytest.fixture() - def init(self, sdk_client_fs, unauthorized_creds): + def _init(self, sdk_client_fs, unauthorized_creds): """Bind common stuff to this instance""" self.client = sdk_client_fs self.new_user_creds = unauthorized_creds self.admin_creds = make_auth_header(self.client) @pytest.mark.parametrize("parse_with_context", ["import_updates.yaml"], indirect=True) - @pytest.mark.usefixtures("grant_cluster_view_permissions", "init") + @pytest.mark.usefixtures("_grant_cluster_view_permissions", "_init") def test_import_updates(self, import_export_clusters, parse_with_context): """ Test update operations related to import/exports: @@ -303,14 +314,18 @@ def _update_imports(self, import_cluster, export_cluster): "bind": [ { "import_id": service_import_id, - "export_id": {"cluster_id": export_cluster.id, "service_id": export_service.id}, + "export_id": { + "cluster_id": export_cluster.id, + "service_id": export_service.id, + }, } ] } check_succeed(requests.post(import_path, json=data, headers=self.admin_creds)) with allure.step("Fail to update cluster/service imports"): check_failed( - requests.post(f"{base_url}{cluster_import_path}", json={}, headers=self.admin_creds), exact_code=400 + requests.post(f"{base_url}{cluster_import_path}", json={}, headers=self.admin_creds), + exact_code=400, ) check_failed(requests.post(import_path, json={}, headers=self.admin_creds), exact_code=400) with allure.step("Performed denied cluster/service imports updates"): @@ -344,8 +359,14 @@ def _remove_binds(self, clusters_url, import_cluster): service_bind_id = requests.get(service_bind_url, headers=self.admin_creds).json()[0]["id"] unbind = self._unbind with allure.step("Perform denied cluster/service bind deletion"): - check_failed(unbind(f"{cluster_bind_url}{cluster_bind_id}/", headers=self.new_user_creds), exact_code=403) - check_failed(unbind(f"{service_bind_url}{service_bind_id}/", headers=self.new_user_creds), exact_code=403) + check_failed( + unbind(f"{cluster_bind_url}{cluster_bind_id}/", headers=self.new_user_creds), + exact_code=403, + ) + check_failed( + unbind(f"{service_bind_url}{service_bind_id}/", headers=self.new_user_creds), + exact_code=403, + ) with allure.step("Unbind cluster/service bind deletion"): check_succeed(unbind(f"{cluster_bind_url}{cluster_bind_id}/", headers=self.admin_creds)) check_succeed(unbind(f"{service_bind_url}{service_bind_id}/", headers=self.admin_creds)) @@ -375,19 +396,25 @@ class TestObjectUpdates: new_user_creds: dict admin_creds: dict - pytestmark = [pytest.mark.usefixtures('init')] + pytestmark = [pytest.mark.usefixtures('_init')] @pytest.fixture() - def init(self, sdk_client_fs, unauthorized_creds) -> None: + def _init(self, sdk_client_fs, unauthorized_creds) -> None: """Bind all required "context" to an instance""" self.client = sdk_client_fs self.admin_creds = make_auth_header(sdk_client_fs) self.new_user_creds = unauthorized_creds - @parametrize_audit_scenario_parsing('objects_update.yaml', NEW_USER) - @pytest.mark.parametrize("method", ["put", "patch"]) # pylint: disable-next=too-many-arguments + @parametrize_audit_scenario_parsing("objects_update.yaml", NEW_USER) + @pytest.mark.parametrize("method", ["put", "patch"]) def test_update_objects( - self, method: str, bundle_with_license, build_policy, audit_log_checker, generic_provider, sdk_client_fs + self, + method: str, + bundle_with_license, + build_policy, + audit_log_checker, + generic_provider, + sdk_client_fs, ): """Test update of cluster/host/host in cluster""" old_cluster_name, new_cluster_name = "Cluster Name", "New Cluster Name" @@ -432,7 +459,6 @@ def _update_host_object(self, host: Host, new_fqdn: str, method: str): { "prototype_id": host.prototype_id, "provider_id": host.provider_id, - "maintenance_mode": host.maintenance_mode, } if method == "put" else {} @@ -440,9 +466,11 @@ def _update_host_object(self, host: Host, new_fqdn: str, method: str): } with allure.step(f'Update host via {method.upper()} {url} with body: {body}'): check_succeed(getattr(requests, method)(url, json=body, headers=self.admin_creds)) - body = {**body, "maintenance_mode": "on"} with allure.step(f'Fail updating host via {method.upper()} {url} with body: {body}'): - check_failed(getattr(requests, method)(url, json=body, headers=self.admin_creds), exact_code=409) + check_failed( + getattr(requests, method)(url, json={**body, "provider_id": False}, headers=self.admin_creds), + exact_code=400, + ) def _update_host_in_cluster(self, host: Host, method: str): url = f'{self.client.url}/api/v1/cluster/{host.cluster_id}/host/{host.id}/' diff --git a/tests/functional/audit/test_operation_logs.py b/tests/functional/audit/test_operation_logs.py index 09c5a67a18..278199b02d 100644 --- a/tests/functional/audit/test_operation_logs.py +++ b/tests/functional/audit/test_operation_logs.py @@ -21,7 +21,6 @@ from adcm_pytest_plugin.steps.actions import run_cluster_action_and_assert_result from adcm_pytest_plugin.utils import random_string from docker.models.containers import Container - from tests.functional.audit.conftest import BUNDLES_DIR, ScenarioArg from tests.functional.conftest import only_clean_adcm from tests.functional.rbac.conftest import BusinessRoles, create_policy @@ -63,9 +62,7 @@ def new_user_and_client(sdk_client_fs) -> Tuple[User, ADCMClient]: return user, ADCMClient(url=sdk_client_fs.url, user=credentials["username"], password=credentials["password"]) -@pytest.mark.parametrize( - "parsed_audit_log", [ScenarioArg("simple.yaml", CONTEXT)], indirect=True -) # pylint: disable-next=too-many-arguments +@pytest.mark.parametrize("parsed_audit_log", [ScenarioArg("simple.yaml", CONTEXT)], indirect=True) def test_simple_flow(sdk_client_fs, audit_log_checker, adb_bundle, dummy_host, new_user_and_client): """Test simple from with cluster objects manipulations""" config = {"just_string": "hoho"} @@ -81,7 +78,13 @@ def test_simple_flow(sdk_client_fs, audit_log_checker, adb_bundle, dummy_host, n cluster.hostcomponent_set((dummy_host, component)) run_cluster_action_and_assert_result(cluster, "install", "failed") new_user, new_client = new_user_and_client - create_policy(sdk_client_fs, BusinessRoles.ViewClusterConfigurations, [cluster], users=[new_user], groups=[]) + create_policy( + sdk_client_fs, + BusinessRoles.ViewClusterConfigurations, + [cluster], + users=[new_user], + groups=[], + ) new_client.reread() with allure.step("Try to change config from unauthorized user"): requests.post( @@ -136,7 +139,7 @@ def _exec_django_shell(container: Container, statement: str) -> str: [ "sh", "-c", - "source /adcm/venv/default/bin/activate " f"&& python3 /adcm/python/manage.py shell -c '{script}'", + ". /adcm/venv/default/bin/activate " f"&& python /adcm/python/manage.py shell -c '{script}'", ] ) out = output.decode("utf-8").strip() diff --git a/tests/functional/audit/test_rotation.py b/tests/functional/audit/test_rotation.py index 750ff020f7..239fe5818e 100644 --- a/tests/functional/audit/test_rotation.py +++ b/tests/functional/audit/test_rotation.py @@ -28,8 +28,11 @@ from adcm_pytest_plugin.docker_utils import ADCM, get_file_from_container from adcm_pytest_plugin.steps.commands import clearaudit from adcm_pytest_plugin.utils import random_string - -from tests.functional.audit.conftest import BUNDLES_DIR, set_logins_date, set_operations_date +from tests.functional.audit.conftest import ( + BUNDLES_DIR, + set_logins_date, + set_operations_date, +) from tests.functional.conftest import only_clean_adcm from tests.library.assertions import sets_are_equal from tests.library.db import QueryExecutioner @@ -56,14 +59,22 @@ def logins_to_be_archived( :returns: List of login audit records which dates were changed to the old ones. """ - admin_credentials = {"username": adcm_api_credentials["user"], "password": adcm_api_credentials["password"]} + admin_credentials = { + "username": adcm_api_credentials["user"], + "password": adcm_api_credentials["password"], + } user_creds = {"username": "user1", "password": "password1password1"} not_existing_user = {"username": "user2", "password": "password1password1"} existing_logs: Set[int] = {rec.id for rec in sdk_client_fs.audit_login_list()} with allure.step("Create one more user and try to login with different pairs"): sdk_client_fs.user_create(**user_creds) for _ in range(2): - for creds in (admin_credentials, user_creds, not_existing_user, {**user_creds, "password": "wrongpass"}): + for creds in ( + admin_credentials, + user_creds, + not_existing_user, + {**user_creds, "password": "wrongpass"}, + ): try: ADCMClient(url=sdk_client_fs.url, user=creds["username"], password=creds["password"]) except ADCMApiError: diff --git a/tests/functional/audit/test_update_operations_audit.py b/tests/functional/audit/test_update_operations_audit.py index 57bbddb4d0..1a2e3e0a8c 100644 --- a/tests/functional/audit/test_update_operations_audit.py +++ b/tests/functional/audit/test_update_operations_audit.py @@ -23,9 +23,17 @@ import pytest import requests from adcm_client.audit import OperationResult -from adcm_client.objects import ADCM, ADCMClient, Cluster, Component, GroupConfig, Host, Provider, Service +from adcm_client.objects import ( + ADCM, + ADCMClient, + Cluster, + Component, + GroupConfig, + Host, + Provider, + Service, +) from adcm_pytest_plugin.utils import random_string - from tests.functional.audit.conftest import ( BUNDLES_DIR, NEW_USER, @@ -37,7 +45,11 @@ from tests.functional.conftest import only_clean_adcm from tests.functional.rbac.conftest import BusinessRoles as BR from tests.functional.rbac.conftest import create_policy -from tests.functional.tools import ClusterRelatedObject, ProviderRelatedObject, get_object_represent +from tests.functional.tools import ( + ClusterRelatedObject, + ProviderRelatedObject, + get_object_represent, +) # pylint: disable=redefined-outer-name @@ -64,13 +76,17 @@ def basic_objects(sdk_client_fs) -> Tuple[Cluster, Service, Component, Provider, @pytest.fixture() -def grant_view_config_permissions_on_adcm_objects(sdk_client_fs, basic_objects, new_user_client): +def _grant_view_config_permissions_on_adcm_objects(sdk_client_fs, basic_objects, new_user_client): """Create policies that allow new user to get ADCM objects (via View Configuration) and ADCM itself""" cluster, service, component, provider, host = basic_objects user = sdk_client_fs.user(id=new_user_client.me().id) create_policy( sdk_client_fs, - [BR.ViewClusterConfigurations, BR.ViewServiceConfigurations, BR.ViewComponentConfigurations], + [ + BR.ViewClusterConfigurations, + BR.ViewServiceConfigurations, + BR.ViewComponentConfigurations, + ], [cluster, service, component], users=[user], groups=[], @@ -99,7 +115,7 @@ def group_configs(basic_objects) -> Tuple[GroupConfig, GroupConfig, GroupConfig] @parametrize_audit_scenario_parsing("update_restore_config.yaml", NEW_USER) -@pytest.mark.usefixtures("grant_view_config_permissions_on_adcm_objects") +@pytest.mark.usefixtures("_grant_view_config_permissions_on_adcm_objects") def test_update_config(basic_objects, audit_log_checker, sdk_client_fs, unauthorized_creds): """ Test audit of config updates on (for results: SUCCESS, FAIL, DENIED): @@ -126,7 +142,10 @@ def get_correct_adcm_config(): _check_object_config_update(sdk_client_fs, adcm, unauthorized_creds, get_correct_config=get_correct_adcm_config) _check_object_config_restore( - sdk_client_fs, adcm, unauthorized_creds, get_correct_attrs=lambda: adcm.config(full=True)["attr"] + sdk_client_fs, + adcm, + unauthorized_creds, + get_correct_attrs=lambda: adcm.config(full=True)["attr"], ) for obj in basic_objects: _check_object_config_update(sdk_client_fs, obj, unauthorized_creds) @@ -140,7 +159,7 @@ def get_correct_adcm_config(): @parametrize_audit_scenario_parsing("update_config_of_group_config.yaml", NEW_USER) @pytest.mark.usefixtures( - "grant_view_config_permissions_on_adcm_objects", "basic_objects" + "_grant_view_config_permissions_on_adcm_objects", "basic_objects" ) # pylint: disable-next=too-many-locals def test_update_config_of_group_config(group_configs, audit_log_checker, sdk_client_fs, unauthorized_creds): """ @@ -152,12 +171,21 @@ def test_update_config_of_group_config(group_configs, audit_log_checker, sdk_cli drop_object_id = lambda b: {**b, "object_id": "hello there"} # noqa: E731 with allure.step(f"Update group config info of {group_config.object_type}"): for result, credentials, check_response, change_body in ( - (OperationResult.SUCCESS, admin_creds, check_succeed, lambda b: {**b, "description": "Changed"}), + ( + OperationResult.SUCCESS, + admin_creds, + check_succeed, + lambda b: {**b, "description": "Changed"}, + ), (OperationResult.FAIL, admin_creds, expect_400, drop_object_id), (OperationResult.DENIED, unauthorized_creds, expect_403, drop_object_id), ): update_via = partial( - update_group_config_info, sdk_client_fs, group_config, body_mutator=change_body, headers=credentials + update_group_config_info, + sdk_client_fs, + group_config, + body_mutator=change_body, + headers=credentials, ) with allure.step(f"Change group config info with result: {result.value}"): check_response(update_via(method="PUT")) @@ -169,7 +197,10 @@ def test_update_config_of_group_config(group_configs, audit_log_checker, sdk_cli "config": {**default_config, "param_1": random_string(4)}, "attr": {**default_attr, "group_keys": {**default_attr["group_keys"], "param_1": True}}, } - incorrect_config = {"config": {**default_config, "param_1": random_string(4)}, "attr": {**default_attr}} + incorrect_config = { + "config": {**default_config, "param_1": random_string(4)}, + "attr": {**default_attr}, + } with allure.step( f"Update config of group config of {group_config.object_type} with result: {OperationResult.SUCCESS}" @@ -188,7 +219,7 @@ def test_update_config_of_group_config(group_configs, audit_log_checker, sdk_cli @parametrize_audit_scenario_parsing( "add_delete_host_group_config.yaml", {"username": NEW_USER["username"], "host": FQDN} ) -@pytest.mark.usefixtures("grant_view_config_permissions_on_adcm_objects") # pylint: disable-next=too-many-arguments +@pytest.mark.usefixtures("_grant_view_config_permissions_on_adcm_objects") def test_add_remove_hosts_from_group_config( group_configs, basic_objects, audit_log_checker, sdk_client_fs, post, delete, unauthorized_creds ): @@ -223,7 +254,10 @@ def _check_object_config_update( client: ADCMClient, object_with_config: ObjectWithConfig, unauthorized_creds: dict, - get_correct_config=lambda: {"config": {"param_1": random_string(4), "param_2": None, "param_3": None}, "attr": {}}, + get_correct_config=lambda: { + "config": {"param_1": random_string(4), "param_2": None, "param_3": None}, + "attr": {}, + }, get_incorrect_config=lambda: {"config": {"param_2": randint(0, 50)}, "attr": {}}, ): admin_credentials = make_auth_header(client) @@ -266,7 +300,10 @@ def get_restore_suffix(): config_id = next( map( itemgetter("id"), - filter(lambda c: c["id"] != current_config_id, object_with_config.config_history(full=True)), + filter( + lambda c: c["id"] != current_config_id, + object_with_config.config_history(full=True), + ), ) ) return f"{CONFIG_HISTORY_SUFFIX}{config_id}/restore/" @@ -300,7 +337,11 @@ def update_config_from_root(client: ADCMClient, obj: ObjectWithConfig, config: d `config` should contain both "config" and "attr" keys. """ url = f"{client.url}/api/v1/config-log/" - body = {"obj_ref": _get_obj_ref(client, obj), "description": f"Config {random_string(4)}", **config} + body = { + "obj_ref": _get_obj_ref(client, obj), + "description": f"Config {random_string(4)}", + **config, + } with allure.step(f'Update config from "root" via POST {url} with data: {body}'): return requests.post(url, json=body, **post_kwargs) @@ -378,7 +419,8 @@ def get_host_from_cluster_url(client: ADCMClient, host: Host, suffix: str = "") def _get_obj_ref(client: ADCMClient, obj: ObjectWithConfig): auth_headers = make_auth_header(client) current_config = requests.get( - f"{client.url}/api/v1/{obj.__class__.__name__.lower()}/{obj.id}/config/current/", headers=auth_headers + f"{client.url}/api/v1/{obj.__class__.__name__.lower()}/{obj.id}/config/current/", + headers=auth_headers, ).json() config_log = requests.get(f'{client.url}/api/v1/config-log/{current_config["id"]}', headers=auth_headers).json() return config_log["obj_ref"] diff --git a/tests/functional/audit/test_update_rbac_objects.py b/tests/functional/audit/test_update_rbac_objects.py index 727a3c40da..af8f3885e1 100644 --- a/tests/functional/audit/test_update_rbac_objects.py +++ b/tests/functional/audit/test_update_rbac_objects.py @@ -19,8 +19,11 @@ import pytest import requests from adcm_client.objects import ADCMClient, Group, Policy, Role, User - -from tests.functional.audit.conftest import check_failed, check_succeed, make_auth_header +from tests.functional.audit.conftest import ( + check_failed, + check_succeed, + make_auth_header, +) from tests.functional.conftest import only_clean_adcm from tests.functional.rbac.conftest import BusinessRoles as BR from tests.library.audit.checkers import AuditLogChecker @@ -121,7 +124,7 @@ def _get(key1, key2): @pytest.mark.parametrize("parse_with_context", ["update_rbac.yaml"], indirect=True) -@pytest.mark.parametrize("http_method", ["PATCH", "PUT"]) # pylint: disable-next=too-many-arguments +@pytest.mark.parametrize("http_method", ["PATCH", "PUT"]) def test_update_rbac_objects( http_method: str, rbac_objects, @@ -140,7 +143,8 @@ def test_update_rbac_objects( for obj in rbac_objects: new_info = {**new_rbac_objects_info[obj.__class__.__name__.lower()]} check_succeed(change_as_admin(rbac_object=obj, data=new_info["correct"])) - check_failed(change_as_admin(rbac_object=obj, data=new_info["incorrect"]), exact_code=400) + expected_code = 400 if not isinstance(obj, User) else 409 + check_failed(change_as_admin(rbac_object=obj, data=new_info["incorrect"]), exact_code=expected_code) check_failed(change_as_unauthorized(rbac_object=obj, data=new_info["incorrect"]), exact_code=403) checker = AuditLogChecker(parse_with_context({**rbac_create_data, "changes": {**prepared_changes}})) checker.set_user_map(sdk_client_fs) diff --git a/tests/functional/conftest.py b/tests/functional/conftest.py index b0089d4b68..a79b72408f 100644 --- a/tests/functional/conftest.py +++ b/tests/functional/conftest.py @@ -13,8 +13,11 @@ """Common fixtures for the functional tests""" import pytest - -from tests.conftest import CLEAN_ADCM_PARAM, DUMMY_DATA_FULL_PARAM, marker_in_node_or_its_parent +from tests.conftest import ( + CLEAN_ADCM_PARAM, + DUMMY_DATA_FULL_PARAM, + marker_in_node_or_its_parent, +) only_clean_adcm = pytest.mark.only_clean_adcm diff --git a/tests/functional/ldap_auth/test_access.py b/tests/functional/ldap_auth/test_access.py index ae5c4621d0..42098ee1c6 100644 --- a/tests/functional/ldap_auth/test_access.py +++ b/tests/functional/ldap_auth/test_access.py @@ -20,13 +20,25 @@ import pytest from adcm_client.objects import ADCMClient, Cluster, User from adcm_pytest_plugin.utils import random_string - from tests.functional.conftest import only_clean_adcm from tests.functional.ldap_auth.conftest import BASE_BUNDLES_DIR -from tests.functional.ldap_auth.utils import get_ldap_user_from_adcm, get_ldap_group_from_adcm -from tests.functional.rbac.conftest import is_denied, BusinessRoles, create_policy, is_allowed, delete_policy - -pytestmark = [only_clean_adcm, pytest.mark.usefixtures('configure_adcm_ldap_ad'), pytest.mark.ldap()] +from tests.functional.ldap_auth.utils import ( + get_ldap_group_from_adcm, + get_ldap_user_from_adcm, +) +from tests.functional.rbac.conftest import ( + BusinessRoles, + create_policy, + delete_policy, + is_allowed, + is_denied, +) + +pytestmark = [ + only_clean_adcm, + pytest.mark.usefixtures('configure_adcm_ldap_ad'), + pytest.mark.ldap(), +] # pylint: disable=redefined-outer-name @@ -59,7 +71,7 @@ def test_grant_policy_for_ldap_user(sdk_client_fs, cluster, ldap_user_in_group): is_denied(user_cluster, BusinessRoles.EditClusterConfigurations, client=user_client) -# pylint: disable-next=unused-variable,too-many-arguments,too-many-locals +# pylint: disable-next=unused-variable,too-many-locals def test_grant_policy_for_ldap_group(sdk_client_fs, cluster, ldap_ad, ldap_basic_ous, ldap_group, ldap_user_in_group): """ Test that granting policy for LDAP group in ADCM works the same way as with regular group. diff --git a/tests/functional/ldap_auth/test_basic.py b/tests/functional/ldap_auth/test_basic.py index 1768ffeff5..2100bf17cc 100644 --- a/tests/functional/ldap_auth/test_basic.py +++ b/tests/functional/ldap_auth/test_basic.py @@ -20,10 +20,10 @@ from adcm_pytest_plugin.params import including_https from adcm_pytest_plugin.steps.actions import wait_for_task_and_assert_result from adcm_pytest_plugin.utils import random_string - from tests.functional.conftest import only_clean_adcm from tests.functional.ldap_auth.utils import ( DEFAULT_LOCAL_USERS, + LDAP_ACTION_CAN_NOT_START_REASON, check_existing_groups, check_existing_users, get_ldap_group_from_adcm, @@ -31,7 +31,11 @@ login_should_succeed, ) -pytestmark = [only_clean_adcm, pytest.mark.usefixtures("configure_adcm_ldap_ad"), pytest.mark.ldap()] +pytestmark = [ + only_clean_adcm, + pytest.mark.usefixtures("configure_adcm_ldap_ad"), + pytest.mark.ldap(), +] @pytest.mark.parametrize("configure_adcm_ldap_ad", [False, True], ids=["ssl_off", "ssl_on"], indirect=True) @@ -42,7 +46,10 @@ def test_basic_ldap_auth(sdk_client_fs, ldap_user, ldap_user_in_group): 2. Login of user not in group is not permitted """ login_should_succeed( - "login with LDAP user in group", sdk_client_fs, ldap_user_in_group["name"], ldap_user_in_group["password"] + "login with LDAP user in group", + sdk_client_fs, + ldap_user_in_group["name"], + ldap_user_in_group["password"], ) login_should_fail( "login with LDAP user not in allowed group", @@ -147,7 +154,10 @@ def _alter_user_search_base(client: ADCMClient) -> dict: ("wrong password", {"ldap_integration": {"ldap_password": random_string(6)}}), ) -_deactivate_ldap_integration = ("LDAP config turned off", {"attr": {"ldap_integration": {"active": False}}}) +_deactivate_ldap_integration = ( + "LDAP config turned off", + {"attr": {"ldap_integration": {"active": False}}}, +) @pytest.mark.parametrize( @@ -179,11 +189,12 @@ def test_wrong_ldap_config_fail_actions(action_name: str, sdk_client_fs): task = adcm.action(name=action_name).run() wait_for_task_and_assert_result(task, "failed") adcm.config_set_diff(original_config) + task.wait() with allure.step(f"Deactivate LDAP integration in settings and check action {action_name} is disabled"): adcm.config_set_diff(_deactivate_ldap_integration[1]) assert action_name in [ - a.name for a in adcm.action_list() if a.disabling_cause == "no_ldap_settings" - ], f'Action {action_name} have "no_ldap_settings" disabling cause' + a.name for a in adcm.action_list() if LDAP_ACTION_CAN_NOT_START_REASON in a.start_impossible_reason + ], f'Action {action_name} should have "{LDAP_ACTION_CAN_NOT_START_REASON}" as the reason for disabled launch' def test_login_as_existing_user_is_forbidden(sdk_client_fs, ldap_user_in_group): @@ -222,7 +233,10 @@ def test_login_when_group_itself_is_group_search_base( check_existing_groups(sdk_client_fs) check_existing_users(sdk_client_fs) login_should_succeed( - "login as user in group", sdk_client_fs, ldap_user_in_group["name"], ldap_user_in_group["password"] + "login as user in group", + sdk_client_fs, + ldap_user_in_group["name"], + ldap_user_in_group["password"], ) check_existing_groups(sdk_client_fs, {ldap_group_name}) check_existing_users(sdk_client_fs, {ldap_user_in_group["name"]}) @@ -234,7 +248,10 @@ def test_login_when_group_itself_is_group_search_base( ), f"Incorrect LDAP user name.\nExpected: {expected}\nActual: {actual}" for disallowed_user in (ldap_user, another_ldap_user_in_group): login_should_fail( - f"login as {disallowed_user['name']}", sdk_client_fs, disallowed_user["name"], disallowed_user["password"] + f"login as {disallowed_user['name']}", + sdk_client_fs, + disallowed_user["name"], + disallowed_user["password"], ) check_existing_groups(sdk_client_fs, {ldap_group_name}) check_existing_users(sdk_client_fs, {ldap_user_in_group["name"]}) diff --git a/tests/functional/ldap_auth/test_complex.py b/tests/functional/ldap_auth/test_complex.py index f17e764543..a67fde3d75 100644 --- a/tests/functional/ldap_auth/test_complex.py +++ b/tests/functional/ldap_auth/test_complex.py @@ -19,7 +19,6 @@ from adcm_client.objects import ADCMClient, Group, User from adcm_pytest_plugin.steps.actions import wait_for_task_and_assert_result from adcm_pytest_plugin.utils import random_string - from tests.functional.conftest import only_clean_adcm from tests.functional.ldap_auth.utils import ( DEFAULT_LOCAL_USERS, @@ -85,9 +84,14 @@ def two_adcm_groups(sdk_client_fs) -> Tuple[Group, Group]: @pytest.mark.usefixtures( 'configure_adcm_ldap_ad', 'two_ldap_users' -) # pylint: disable-next=too-many-arguments, too-many-locals, too-many-statements +) # pylint: disable-next=too-many-locals,too-many-statements def test_users_in_groups_sync( - sdk_client_fs, ldap_ad, two_adcm_users, two_adcm_groups, two_ldap_users, two_ldap_groups_with_users + sdk_client_fs, + ldap_ad, + two_adcm_users, + two_adcm_groups, + two_ldap_users, + two_ldap_groups_with_users, ): """ Test ADCM/LDAP users in groups manipulation and sync. @@ -105,7 +109,9 @@ def test_users_in_groups_sync( with allure.step('Sync and add LDAP users to ADCM groups'): _run_sync(sdk_client_fs) check_existing_groups( - sdk_client_fs, {group_info_1['name'], group_info_2['name']}, {adcm_group_1.name, adcm_group_2.name} + sdk_client_fs, + {group_info_1['name'], group_info_2['name']}, + {adcm_group_1.name, adcm_group_2.name}, ) check_existing_users(sdk_client_fs, {user_info_1['name'], user_info_2['name']}, adcm_user_names) ldap_group_1 = get_ldap_group_from_adcm(sdk_client_fs, group_info_1['name']) @@ -129,7 +135,9 @@ def test_users_in_groups_sync( with allure.step('Sync and check groups'): _run_sync(sdk_client_fs) check_existing_groups( - sdk_client_fs, {group_info_1['name'], group_info_2['name']}, {adcm_group_1.name, adcm_group_2.name} + sdk_client_fs, + {group_info_1['name'], group_info_2['name']}, + {adcm_group_1.name, adcm_group_2.name}, ) check_existing_users(sdk_client_fs, {user_info_1['name'], user_info_2['name']}, adcm_user_names) _check_users_in_group(ldap_group_1, ldap_user_2) @@ -144,7 +152,9 @@ def test_users_in_groups_sync( with allure.step('Sync and check user state'): _run_sync(sdk_client_fs) check_existing_groups( - sdk_client_fs, {group_info_1['name'], group_info_2['name']}, {adcm_group_1.name, adcm_group_2.name} + sdk_client_fs, + {group_info_1['name'], group_info_2['name']}, + {adcm_group_1.name, adcm_group_2.name}, ) check_existing_users(sdk_client_fs, {user_info_1['name'], user_info_2['name']}, adcm_user_names) assert not get_ldap_user_from_adcm(sdk_client_fs, user_info_2['name']).is_active, 'User should be deactivated' @@ -155,13 +165,18 @@ def test_users_in_groups_sync( with allure.step('Check login permissions'): _check_login_succeed(sdk_client_fs, user_info_1) login_should_fail( - 'login as user removed from ldap groups', sdk_client_fs, user_info_2['name'], user_info_2['password'] + 'login as user removed from ldap groups', + sdk_client_fs, + user_info_2['name'], + user_info_2['password'], ) with allure.step('Add "free" LDAP user to a group, sync and check results'): ldap_ad.add_user_to_group(user_info_3['dn'], group_info_2['dn']) _run_sync(sdk_client_fs) check_existing_users( - sdk_client_fs, {user_info_1['name'], user_info_2['name'], user_info_3['name']}, adcm_user_names + sdk_client_fs, + {user_info_1['name'], user_info_2['name'], user_info_3['name']}, + adcm_user_names, ) ldap_user_3 = get_ldap_user_from_adcm(sdk_client_fs, user_info_3['name']) _check_users_in_group(ldap_group_1) diff --git a/tests/functional/ldap_auth/test_ldap_filters.py b/tests/functional/ldap_auth/test_ldap_filters.py new file mode 100644 index 0000000000..0656345a2e --- /dev/null +++ b/tests/functional/ldap_auth/test_ldap_filters.py @@ -0,0 +1,438 @@ +# 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. + +"""Test designed to check LDAP filters""" + +from typing import Tuple + +import allure +import pytest +from adcm_client.objects import ADCMClient, Group, User +from adcm_pytest_plugin.utils import random_string +from tests.functional.conftest import only_clean_adcm +from tests.functional.ldap_auth.utils import ( + DEFAULT_LOCAL_USERS, + check_existing_groups, + check_existing_users, + check_users_in_group, + get_ldap_group_from_adcm, + get_ldap_user_from_adcm, + login_should_fail, + login_should_succeed, + turn_off_periodic_ldap_sync, +) +from tests.library.assertions import sets_are_equal +from tests.library.ldap_interactions import change_adcm_ldap_config, sync_adcm_with_ldap + +# pylint: disable=redefined-outer-name + + +pytestmark = [pytest.mark.ldap(), only_clean_adcm] +UserInfo = dict +GroupInfo = dict + + +@pytest.fixture() +def two_adcm_groups_with_users(sdk_client_fs) -> Tuple[Group, User, Group, User]: + """Method to create ADCM users ADCM groups and add users to groups""" + adcm_user_1 = sdk_client_fs.user_create('first-adcm-user', random_string(12)) + adcm_user_2 = sdk_client_fs.user_create('second-adcm-user', random_string(12)) + adcm_group_1 = sdk_client_fs.group_create('first-adcm-group') + adcm_group_2 = sdk_client_fs.group_create('second-adcm-group') + adcm_group_1.add_user(adcm_user_1) + adcm_group_2.add_user(adcm_user_2) + return adcm_group_1, adcm_user_1, adcm_group_2, adcm_user_2 + + +@pytest.fixture() +def two_ldap_groups_with_users(ldap_ad, ldap_basic_ous) -> Tuple[GroupInfo, UserInfo, GroupInfo, UserInfo]: + """Create two ldap users and groups with a user in each one""" + groups_ou, users_ou = ldap_basic_ous + group_1 = {'name': 'group-with-users-1'} + group_2 = {'name': 'group-with-users-2'} + user_1 = {'name': f'user-1-{random_string(4)}-in-group', 'password': random_string(12)} + user_2 = {'name': f'user-2-{random_string(4)}-in-group', 'password': random_string(12)} + user_1['dn'] = ldap_ad.create_user(**user_1, custom_base_dn=users_ou) + user_2['dn'] = ldap_ad.create_user(**user_2, custom_base_dn=users_ou) + group_1['dn'] = ldap_ad.create_group(**group_1, custom_base_dn=groups_ou) + ldap_ad.add_user_to_group(user_1['dn'], group_1['dn']) + group_2['dn'] = ldap_ad.create_group(**group_2, custom_base_dn=groups_ou) + ldap_ad.add_user_to_group(user_2['dn'], group_2['dn']) + return group_1, user_1, group_2, user_2 + + +def check_sync_with_filters( + client: ADCMClient, + user_filter: str, + group_filter: str, + expected_users: set, + expected_groups: set, +) -> None: + """Method to use filter and check result""" + change_adcm_ldap_config( + client, + attach_to_allure=False, + user_search_filter=user_filter, + group_search_filter=group_filter, + ) + sync_adcm_with_ldap(client) + + active_users_records = {u.username for u in client.user_list() if u.type == "ldap" and u.is_active} + groups_records = {g.name for g in client.group_list() if g.type == "ldap"} + sets_are_equal( + actual=active_users_records, + expected=expected_users, + message="Not all filtered active LDAP users are presented in ADCM", + ) + sets_are_equal( + actual=groups_records, + expected=expected_groups, + message="Not all filtered LDAP groups are presented in ADCM", + ) + + +@pytest.mark.usefixtures('configure_adcm_ldap_ad') +# pylint: disable-next=too-many-locals,too-many-statements +def test_search_filters_users(sdk_client_fs, two_ldap_groups_with_users): + """Check LDAP filters for users""" + turn_off_periodic_ldap_sync(client=sdk_client_fs) + group_info_1, user_info_1, group_info_2, user_info_2 = two_ldap_groups_with_users + + check_sync_with_filters( + sdk_client_fs, + user_filter="", + group_filter="", + expected_users={user_info_1['name'], user_info_2['name']}, + expected_groups={group_info_1["name"], group_info_2["name"]}, + ) + + ldap_user_1 = get_ldap_user_from_adcm(sdk_client_fs, user_info_1['name']) + ldap_user_2 = get_ldap_user_from_adcm(sdk_client_fs, user_info_2['name']) + + with allure.step('Check filter for one user and check'): + search_filter = f"(&(objectcategory=person)(objectclass=person)(name={ldap_user_1.username}))" + check_sync_with_filters( + sdk_client_fs, + user_filter=search_filter, + group_filter="", + expected_users={ldap_user_1.username}, + expected_groups={group_info_1["name"], group_info_2["name"]}, + ) + + with allure.step('Check filter for incorrect name user and check'): + search_filter = "(&(objectcategory=person)(objectclass=person)(name=user))" + check_sync_with_filters( + sdk_client_fs, + user_filter=search_filter, + group_filter="", + expected_users=set(), + expected_groups={group_info_1["name"], group_info_2["name"]}, + ) + + with allure.step('Check filter for all users and check'): + search_filter = "(&(objectcategory=person)(objectclass=person)(name=*))" + check_sync_with_filters( + sdk_client_fs, + user_filter=search_filter, + group_filter="", + expected_users={ldap_user_1.username, ldap_user_2.username}, + expected_groups={group_info_1["name"], group_info_2["name"]}, + ) + + with allure.step('Check existing LDAP and ADCM users'): + check_sync_with_filters( + sdk_client_fs, + user_filter="", + group_filter="", + expected_users={ldap_user_1.username, ldap_user_2.username}, + expected_groups={group_info_1["name"], group_info_2["name"]}, + ) + + with allure.step('Check both LDAP users can login'): + for user_info in (user_info_1, user_info_2): + login_should_succeed( + f'login as {user_info["name"]}', + sdk_client_fs, + user_info['name'], + user_info['password'], + ) + + +@pytest.mark.usefixtures('configure_adcm_ldap_ad') +# pylint: disable-next=too-many-arguments, too-many-locals, too-many-statements +def test_search_filters_groups(sdk_client_fs, two_adcm_groups_with_users, two_ldap_groups_with_users): + """Check LDAP filters for groups""" + turn_off_periodic_ldap_sync(client=sdk_client_fs) + adcm_group_1, adcm_user_1, adcm_group_2, adcm_user_2 = two_adcm_groups_with_users + group_info_1, user_info_1, group_info_2, user_info_2 = two_ldap_groups_with_users + + with allure.step('Sync and add LDAP users to ADCM groups'): + check_sync_with_filters( + sdk_client_fs, + user_filter="", + group_filter="", + expected_users={user_info_1['name'], user_info_2['name']}, + expected_groups={group_info_1["name"], group_info_2["name"]}, + ) + + ldap_group_1 = get_ldap_group_from_adcm(sdk_client_fs, group_info_1['name']) + ldap_group_2 = get_ldap_group_from_adcm(sdk_client_fs, group_info_2['name']) + ldap_user_1 = get_ldap_user_from_adcm(sdk_client_fs, user_info_1['name']) + ldap_user_2 = get_ldap_user_from_adcm(sdk_client_fs, user_info_2['name']) + + adcm_group_1.add_user(ldap_user_1) + adcm_group_2.add_user(ldap_user_2) + + adcm_user_names = {adcm_user_1.username, adcm_user_2.username, *DEFAULT_LOCAL_USERS} + + with allure.step('Check that users are in groups'): + check_users_in_group(ldap_group_1, ldap_user_1) + check_users_in_group(ldap_group_2, ldap_user_2) + check_users_in_group(adcm_group_1, adcm_user_1, ldap_user_1) + check_users_in_group(adcm_group_2, adcm_user_2, ldap_user_2) + + with allure.step('Check filter for one group and check'): + search_filter = f"(&(objectclass=group)(name={ldap_group_1.name}))" + check_sync_with_filters( + sdk_client_fs, + user_filter="", + group_filter=search_filter, + expected_users={ldap_user_1.username}, + expected_groups={ldap_group_1.name}, + ) + + with allure.step('Check filter for all groups and check'): + search_filter = "(&(objectclass=group)(name=*))" + check_sync_with_filters( + sdk_client_fs, + user_filter="", + group_filter=search_filter, + expected_users={ldap_user_1.username, ldap_user_2.username}, + expected_groups={ldap_group_1.name, ldap_group_2.name}, + ) + + with allure.step('Check existing LDAP and ADCM groups'): + check_sync_with_filters( + sdk_client_fs, + user_filter="", + group_filter="", + expected_users={ldap_user_1.username, ldap_user_2.username}, + expected_groups={group_info_1["name"], group_info_2["name"]}, + ) + + check_existing_groups( + sdk_client_fs, + {group_info_1['name'], group_info_2['name']}, + {adcm_group_1.name, adcm_group_2.name}, + ) + check_existing_users(sdk_client_fs, {user_info_1['name'], user_info_2['name']}, adcm_user_names) + + ldap_group_1 = get_ldap_group_from_adcm(sdk_client_fs, group_info_1['name']) + ldap_group_2 = get_ldap_group_from_adcm(sdk_client_fs, group_info_2['name']) + ldap_user_1 = get_ldap_user_from_adcm(sdk_client_fs, user_info_1['name']) + ldap_user_2 = get_ldap_user_from_adcm(sdk_client_fs, user_info_2['name']) + + check_users_in_group(ldap_group_1, ldap_user_1) + check_users_in_group(ldap_group_2, ldap_user_2) + check_users_in_group(adcm_group_1, adcm_user_1, ldap_user_1) + check_users_in_group(adcm_group_2, adcm_user_2, ldap_user_2) + + +@pytest.mark.usefixtures('configure_adcm_ldap_ad') +# pylint: disable-next=too-many-locals,too-many-statements +def test_search_filters_groups_with_symbols(sdk_client_fs, two_adcm_groups_with_users, two_ldap_groups_with_users): + """Check LDAP filters for users and groups""" + turn_off_periodic_ldap_sync(client=sdk_client_fs) + + adcm_group_1, adcm_user_1, adcm_group_2, adcm_user_2 = two_adcm_groups_with_users + group_info_1, user_info_1, group_info_2, user_info_2 = two_ldap_groups_with_users + + with allure.step('Sync and add LDAP users to ADCM groups'): + check_sync_with_filters( + sdk_client_fs, + user_filter="", + group_filter="", + expected_users={user_info_1['name'], user_info_2['name']}, + expected_groups={group_info_1["name"], group_info_2["name"]}, + ) + + ldap_group_1 = get_ldap_group_from_adcm(sdk_client_fs, group_info_1['name']) + ldap_group_2 = get_ldap_group_from_adcm(sdk_client_fs, group_info_2['name']) + ldap_user_1 = get_ldap_user_from_adcm(sdk_client_fs, user_info_1['name']) + ldap_user_2 = get_ldap_user_from_adcm(sdk_client_fs, user_info_2['name']) + + adcm_group_1.add_user(ldap_user_1) + adcm_group_2.add_user(ldap_user_2) + + adcm_user_names = {adcm_user_1.username, adcm_user_2.username, *DEFAULT_LOCAL_USERS} + + with allure.step('Check that users are in groups'): + check_users_in_group(ldap_group_1, ldap_user_1) + check_users_in_group(ldap_group_2, ldap_user_2) + check_users_in_group(adcm_group_1, adcm_user_1, ldap_user_1) + check_users_in_group(adcm_group_2, adcm_user_2, ldap_user_2) + + with allure.step('Check filter with filter symbol !'): + search_filter = "(&(name=*user*)(!(name=*2*)))" + check_sync_with_filters( + sdk_client_fs, + user_filter="", + group_filter=search_filter, + expected_users={ldap_user_1.username}, + expected_groups={ldap_group_1.name}, + ) + + with allure.step('Check filter with filter symbols >='): + turn_off_periodic_ldap_sync(client=sdk_client_fs) + search_filter = "(&(name>=t))" + check_sync_with_filters( + sdk_client_fs, + user_filter="", + group_filter=search_filter, + expected_users=set(), + expected_groups=set(), + ) + + with allure.step('Check filter with filter symbols <='): + turn_off_periodic_ldap_sync(client=sdk_client_fs) + search_filter = "(&(name<=t))" + check_sync_with_filters( + sdk_client_fs, + user_filter="", + group_filter=search_filter, + expected_users={ldap_user_1.username, ldap_user_2.username}, + expected_groups={group_info_1["name"], group_info_2["name"]}, + ) + + with allure.step('Check existing LDAP and ADCM users and groups'): + check_sync_with_filters( + sdk_client_fs, + user_filter="", + group_filter="", + expected_users={ldap_user_1.username, ldap_user_2.username}, + expected_groups={group_info_1["name"], group_info_2["name"]}, + ) + + with allure.step('Check that users are in groups'): + check_existing_groups( + sdk_client_fs, + {group_info_1['name'], group_info_2['name']}, + {adcm_group_1.name, adcm_group_2.name}, + ) + check_existing_users(sdk_client_fs, {user_info_1['name'], user_info_2['name']}, adcm_user_names) + + ldap_group_1 = get_ldap_group_from_adcm(sdk_client_fs, group_info_1['name']) + ldap_group_2 = get_ldap_group_from_adcm(sdk_client_fs, group_info_2['name']) + ldap_user_1 = get_ldap_user_from_adcm(sdk_client_fs, user_info_1['name']) + ldap_user_2 = get_ldap_user_from_adcm(sdk_client_fs, user_info_2['name']) + + check_users_in_group(ldap_group_1, ldap_user_1) + check_users_in_group(ldap_group_2, ldap_user_2) + check_users_in_group(adcm_group_1, adcm_user_1, ldap_user_1) + check_users_in_group(adcm_group_2, adcm_user_2, ldap_user_2) + + with allure.step('Check both LDAP users can login'): + for user_info in (user_info_1, user_info_2): + login_should_succeed( + f'login as {user_info["name"]}', + sdk_client_fs, + user_info['name'], + user_info['password'], + ) + + +@pytest.mark.usefixtures('configure_adcm_ldap_ad') +# pylint: disable-next=too-many-arguments, too-many-locals, too-many-statements +def test_search_filters_login_users(sdk_client_fs, two_adcm_groups_with_users, two_ldap_groups_with_users): + """Check LDAP filters for users login""" + turn_off_periodic_ldap_sync(client=sdk_client_fs) + + adcm_group_1, adcm_user_1, adcm_group_2, adcm_user_2 = two_adcm_groups_with_users + group_info_1, user_info_1, group_info_2, user_info_2 = two_ldap_groups_with_users + + with allure.step('Sync and add LDAP users to ADCM groups'): + check_sync_with_filters( + sdk_client_fs, + user_filter="", + group_filter="", + expected_users={user_info_1['name'], user_info_2['name']}, + expected_groups={group_info_1["name"], group_info_2["name"]}, + ) + + ldap_group_1 = get_ldap_group_from_adcm(sdk_client_fs, group_info_1['name']) + ldap_group_2 = get_ldap_group_from_adcm(sdk_client_fs, group_info_2['name']) + ldap_user_1 = get_ldap_user_from_adcm(sdk_client_fs, user_info_1['name']) + ldap_user_2 = get_ldap_user_from_adcm(sdk_client_fs, user_info_2['name']) + + adcm_group_1.add_user(ldap_user_1) + adcm_group_2.add_user(ldap_user_2) + + with allure.step('Check that users are in groups'): + check_users_in_group(ldap_group_1, ldap_user_1) + check_users_in_group(ldap_group_2, ldap_user_2) + check_users_in_group(adcm_group_1, adcm_user_1, ldap_user_1) + check_users_in_group(adcm_group_2, adcm_user_2, ldap_user_2) + + with allure.step('Check filter for one user and check'): + search_filter = f"(&(objectcategory=person)(objectclass=person)(name={ldap_user_1.username}))" + change_adcm_ldap_config( + sdk_client_fs, + attach_to_allure=False, + user_search_filter=search_filter, + group_search_filter="", + ) + with allure.step('Check LDAP user_1 can login and LDAP user_2 login fail'): + login_should_succeed( + f'login as {user_info_1["name"]}', + sdk_client_fs, + user_info_1['name'], + user_info_1['password'], + ) + login_should_fail( + f'login as {user_info_2["name"]}', + sdk_client_fs, + user_info_2['name'], + user_info_2['password'], + ) + with allure.step('Check filter for one group and check'): + search_filter = f"(&(objectclass=group)(name={ldap_group_2.name}))" + change_adcm_ldap_config( + sdk_client_fs, + attach_to_allure=False, + user_search_filter="", + group_search_filter=search_filter, + ) + + with allure.step('Check that users are in groups'): + get_ldap_group_from_adcm(sdk_client_fs, group_info_1['name']) + get_ldap_group_from_adcm(sdk_client_fs, group_info_2['name']) + get_ldap_user_from_adcm(sdk_client_fs, user_info_1['name']) + get_ldap_user_from_adcm(sdk_client_fs, user_info_2['name']) + + check_users_in_group(ldap_group_1, ldap_user_1) + check_users_in_group(ldap_group_2, ldap_user_2) + check_users_in_group(adcm_group_1, adcm_user_1, ldap_user_1) + check_users_in_group(adcm_group_2, adcm_user_2, ldap_user_2) + + with allure.step('Check LDAP user from filtered group can login and LDAP user_1 login fail'): + login_should_succeed( + f'login as {user_info_2["name"]}', + sdk_client_fs, + user_info_2['name'], + user_info_2['password'], + ) + login_should_fail( + f'login as {user_info_1["name"]}', + sdk_client_fs, + user_info_1['name'], + user_info_1['password'], + ) diff --git a/tests/functional/ldap_auth/test_modification.py b/tests/functional/ldap_auth/test_modification.py index ecbbf4639d..f2586f6db8 100644 --- a/tests/functional/ldap_auth/test_modification.py +++ b/tests/functional/ldap_auth/test_modification.py @@ -16,22 +16,25 @@ import allure import pytest -from adcm_client.objects import ADCMClient, User, Group +from adcm_client.objects import ADCMClient, Group, User from adcm_pytest_plugin.steps.actions import wait_for_task_and_assert_result - from tests.functional.conftest import only_clean_adcm from tests.functional.ldap_auth.utils import ( - get_ldap_user_from_adcm, SYNC_ACTION_NAME, - get_ldap_group_from_adcm, TEST_CONNECTION_ACTION, + get_ldap_group_from_adcm, + get_ldap_user_from_adcm, ) from tests.library.assertions import expect_api_error, expect_no_api_error -from tests.library.errorcodes import USER_UPDATE_ERROR, GROUP_UPDATE_ERROR +from tests.library.errorcodes import GROUP_UPDATE_ERROR, USER_UPDATE_ERROR # pylint: disable=redefined-outer-name -pytestmark = [only_clean_adcm, pytest.mark.usefixtures('configure_adcm_ldap_ad'), pytest.mark.ldap()] +pytestmark = [ + only_clean_adcm, + pytest.mark.usefixtures('configure_adcm_ldap_ad'), + pytest.mark.ldap(), +] @pytest.fixture() @@ -61,7 +64,11 @@ def test_ldap_user_manual_modification_is_forbidden(sdk_client_fs, ldap_user_in_ new_password = f'px-{ldap_user_in_group["password"]}' expect_api_error('change password of a user', user.change_password, new_password, err_=USER_UPDATE_ERROR) expect_api_error( - 'login with "new" password', ADCMClient, url=sdk_client_fs.url, user=user.username, password=new_password + 'login with "new" password', + ADCMClient, + url=sdk_client_fs.url, + user=user.username, + password=new_password, ) expect_no_api_error( 'login with "original LDAP" password', @@ -85,8 +92,14 @@ def test_ldap_group_manual_modification_is_forbidden(sdk_client_fs, ldap_group): _check_change_is_forbidden(group, attr) -# pylint: disable-next=too-many-arguments -def test_membership(sdk_client_fs, local_user, local_group, ldap_group, ldap_user_in_group, another_ldap_user_in_group): +def test_membership( + sdk_client_fs, + local_user, + local_group, + ldap_group, + ldap_user_in_group, + another_ldap_user_in_group, +): """ Test that LDAP user can be added to local groups, but not to LDAP ones in ADCM. And that no user can be added to an LDAP group in ADCM. diff --git a/tests/functional/ldap_auth/test_no_group_base.py b/tests/functional/ldap_auth/test_no_group_base.py index 715b829610..6a557c05dc 100644 --- a/tests/functional/ldap_auth/test_no_group_base.py +++ b/tests/functional/ldap_auth/test_no_group_base.py @@ -15,7 +15,6 @@ import allure import pytest from adcm_pytest_plugin.steps.actions import wait_for_task_and_assert_result - from tests.functional.conftest import only_clean_adcm from tests.functional.ldap_auth.utils import ( SYNC_ACTION_NAME, @@ -30,14 +29,14 @@ @pytest.fixture() -def configure_adcm(sdk_client_fs, ad_config, ldap_basic_ous): +def _configure_adcm(sdk_client_fs, ad_config, ldap_basic_ous): """Configure LDAP settings in ADCM and turn off LDAP sync""" _, users_ou = ldap_basic_ous configure_adcm_for_ldap(sdk_client_fs, ad_config, False, None, users_ou, None) sdk_client_fs.adcm().config_set_diff({'ldap_integration': {'sync_interval': 0}}) -@pytest.mark.usefixtures("configure_adcm", "another_ldap_group") +@pytest.mark.usefixtures("_configure_adcm", "another_ldap_group") def test_login_no_group_base(sdk_client_fs, ldap_user, ldap_user_in_group, ldap_group): """ Test that users with or without LDAP group can log in and their groups are created @@ -46,12 +45,15 @@ def test_login_no_group_base(sdk_client_fs, ldap_user, ldap_user_in_group, ldap_ check_existing_users(sdk_client_fs, [ldap_user["name"]]) check_existing_groups(sdk_client_fs) login_should_succeed( - "login as ldap user in group", sdk_client_fs, ldap_user_in_group["name"], ldap_user_in_group["password"] + "login as ldap user in group", + sdk_client_fs, + ldap_user_in_group["name"], + ldap_user_in_group["password"], ) _check_correct_objects_came_from_ldap(sdk_client_fs, ldap_user, ldap_user_in_group, ldap_group) -@pytest.mark.usefixtures("configure_adcm", "another_ldap_group") +@pytest.mark.usefixtures("_configure_adcm", "another_ldap_group") def test_sync_no_group_base(sdk_client_fs, ldap_user, ldap_user_in_group, ldap_group): """ Test that sync without specified LDAP group_search_base works correctly: diff --git a/tests/functional/ldap_auth/test_relations.py b/tests/functional/ldap_auth/test_relations.py index 6e716e662d..b3cf2f9477 100644 --- a/tests/functional/ldap_auth/test_relations.py +++ b/tests/functional/ldap_auth/test_relations.py @@ -11,21 +11,26 @@ # limitations under the License. """Test relations between LDAP objects """ -from typing import Tuple, Collection, Union +from typing import Collection, Tuple, Union import allure import pytest from adcm_client.objects import ADCMClient - from tests.api.utils.tools import random_string from tests.functional.conftest import only_clean_adcm -from tests.functional.ldap_auth.utils import get_ldap_user_from_adcm, get_ldap_group_from_adcm +from tests.functional.ldap_auth.utils import ( + get_ldap_group_from_adcm, + get_ldap_user_from_adcm, +) from tests.library.ldap_interactions import LDAPEntityManager -pytestmark = [only_clean_adcm, pytest.mark.usefixtures('configure_adcm_ldap_ad'), pytest.mark.ldap()] +pytestmark = [ + only_clean_adcm, + pytest.mark.usefixtures('configure_adcm_ldap_ad'), + pytest.mark.ldap(), +] -# pylint: disable-next=too-few-public-methods class TestLDAPEntitiesRelationsInADCM: """Test that relations from LDAP are correctly integrated to ADCM""" diff --git a/tests/functional/ldap_auth/test_sync.py b/tests/functional/ldap_auth/test_sync.py index a714fbc05e..757bdf886e 100644 --- a/tests/functional/ldap_auth/test_sync.py +++ b/tests/functional/ldap_auth/test_sync.py @@ -13,7 +13,7 @@ """Test synchronization and test connection with LDAP""" import time from contextlib import contextmanager -from typing import Optional, Tuple +from typing import Callable, Optional, Tuple import allure import pytest @@ -22,20 +22,29 @@ from adcm_pytest_plugin.steps.actions import wait_for_task_and_assert_result from adcm_pytest_plugin.utils import random_string, wait_until_step_succeeds from coreapi.exceptions import ErrorMessage - from tests.functional.conftest import only_clean_adcm from tests.functional.ldap_auth.utils import ( DEFAULT_LOCAL_USERS, + LDAP_ACTION_CAN_NOT_START_REASON, SYNC_ACTION_NAME, TEST_CONNECTION_ACTION, check_existing_groups, check_existing_users, get_ldap_group_from_adcm, get_ldap_user_from_adcm, + login_should_fail, ) from tests.functional.rbac.conftest import BusinessRoles, RbacRoles -from tests.library.assertions import expect_api_error, expect_no_api_error -from tests.library.ldap_interactions import LDAPTestConfig, configure_adcm_for_ldap +from tests.library.assertions import ( + expect_api_error, + expect_no_api_error, + sets_are_equal, +) +from tests.library.ldap_interactions import ( + LDAPTestConfig, + configure_adcm_for_ldap, + sync_adcm_with_ldap, +) # pylint: disable=redefined-outer-name @@ -76,30 +85,33 @@ def adcm_superuser_client(sdk_client_fs) -> ADCMClient: class TestDisablingCause: """Test LDAP-related ADCM actions have correct disabling cause""" - DISABLING_CAUSE = "no_ldap_settings" - def test_ldap_connection_test_disabling_cause(self, sdk_client_fs, ad_config, ldap_basic_ous): """Test that disabling cause is set right for "test_ldap_connection" action""" adcm = sdk_client_fs.adcm() with allure.step("Check that with default settings disabling cause is set"): - self._check_disabling_cause(adcm, self.DISABLING_CAUSE) + self._check_disabling_cause(adcm, LDAP_ACTION_CAN_NOT_START_REASON) with allure.step("Set correct LDAP settings and check disabling cause is None"): self._set_ldap_settings(sdk_client_fs, ad_config, ldap_basic_ous) self._check_disabling_cause(adcm, None) with allure.step("Disable LDAP settings and check disabling cause is set"): adcm.config_set_diff({"attr": {"ldap_integration": {"active": False}}}) - self._check_disabling_cause(adcm, self.DISABLING_CAUSE) + self._check_disabling_cause(adcm, LDAP_ACTION_CAN_NOT_START_REASON) def _check_disabling_cause(self, adcm: ADCM, expected: Optional[str]): # retrieve each time to avoid rereading sync = adcm.action(name=SYNC_ACTION_NAME) test_connection = adcm.action(name=TEST_CONNECTION_ACTION) assert ( - sync.disabling_cause == expected - ), f"Sync action has incorrect disabling cause: {sync.disabling_cause}.\nExpected: {expected}" - assert test_connection.disabling_cause == expected, ( - f"Test connection action has incorrect disabling cause: {test_connection.disabling_cause}.\n" + expected in sync.start_impossible_reason if expected is not None else sync.start_impossible_reason is None + ), f"Sync action has incorrect disabling cause: {sync.start_impossible_reason}.\nExpected: {expected}" + assert ( + expected in test_connection.start_impossible_reason + if expected is not None + else test_connection.start_impossible_reason is None + ), ( + "Test connection action has incorrect disabling cause: " + f"{test_connection.start_impossible_reason}.\n" f"Expected: {expected}" ) @@ -117,9 +129,14 @@ def test_ldap_simple_sync(self, sdk_client_fs, ldap_user_in_group, ldap_group): """Test that LDAP sync action pulls users and groups from LDAP""" self._simple_sync(sdk_client_fs, ldap_group, ldap_user_in_group, DEFAULT_LOCAL_USERS) - # pylint: disable-next=too-many-arguments def test_access_to_tasks( - self, adcm_user_client, adcm_admin_client, adcm_superuser_client, sdk_client_fs, ldap_user_in_group, ldap_group + self, + adcm_user_client, + adcm_admin_client, + adcm_superuser_client, + sdk_client_fs, + ldap_user_in_group, + ldap_group, ): """Test that only superusers can see LDAP-related tasks""" superuser_name = adcm_superuser_client.me().username @@ -127,7 +144,12 @@ def test_access_to_tasks( sdk_client_fs, ldap_group, ldap_user_in_group, - (*DEFAULT_LOCAL_USERS, adcm_user_client.me().username, adcm_admin_client.me().username, superuser_name), + ( + *DEFAULT_LOCAL_USERS, + adcm_user_client.me().username, + adcm_admin_client.me().username, + superuser_name, + ), ) wait_for_task_and_assert_result(sdk_client_fs.adcm().action(name=TEST_CONNECTION_ACTION).run(), "success") _check_task_logs_amount(adcm_user_client, 0) @@ -144,7 +166,7 @@ def test_sync_with_already_existing_group(self, sdk_client_fs, ldap_user_in_grou local_group: Group = sdk_client_fs.group_create(name=ldap_group["name"]) check_existing_users(sdk_client_fs) check_existing_groups(sdk_client_fs, expected_local={local_group.name}) - _run_sync(sdk_client_fs) + sync_adcm_with_ldap(sdk_client_fs) check_existing_users(sdk_client_fs, {ldap_user_in_group["name"]}) check_existing_groups(sdk_client_fs, {ldap_group["name"]}, {local_group.name}) local_group.reread() @@ -169,7 +191,7 @@ def test_sync_with_already_existing_user(self, sdk_client_fs, ldap_group, ldap_u group.add_user(local_user) check_existing_users(sdk_client_fs, expected_local=expected_local_users) check_existing_groups(sdk_client_fs, expected_local=[group_name]) - _run_sync(sdk_client_fs) + sync_adcm_with_ldap(sdk_client_fs) local_user.reread() with allure.step("Check user type is local"): assert local_user.type == "local", 'User type should stay "local"' @@ -180,30 +202,28 @@ def test_sync_with_already_existing_user(self, sdk_client_fs, ldap_group, ldap_u check_existing_users(sdk_client_fs, expected_local=expected_local_users) check_existing_groups(sdk_client_fs, expected_ldap=[ldap_group["name"]], expected_local=[group_name]) - # pylint: disable-next=too-many-arguments def test_ldap_group_removed(self, sdk_client_fs, ldap_ad, ldap_group, ldap_user_in_group): """Test LDAP group removed from ADCM after it's removed from LDAP""" - _run_sync(sdk_client_fs) + sync_adcm_with_ldap(sdk_client_fs) check_existing_users(sdk_client_fs, {ldap_user_in_group["name"]}) check_existing_groups(sdk_client_fs, {ldap_group["name"]}) with allure.step("Delete group from LDAP"): ldap_ad.delete(ldap_group["dn"]) - _run_sync(sdk_client_fs) + sync_adcm_with_ldap(sdk_client_fs) check_existing_users(sdk_client_fs, {ldap_user_in_group["name"]}) check_existing_groups(sdk_client_fs) - # pylint: disable-next=too-many-arguments def test_user_removed_from_group(self, sdk_client_fs, ldap_ad, ldap_group, ldap_user_in_group): """Test that when user is removed from group in AD, it is also removed in ADCM's LDAP group""" another_group: Group = sdk_client_fs.group_create("Another group") - _run_sync(sdk_client_fs) + sync_adcm_with_ldap(sdk_client_fs) with allure.step("Add LDAP users to the local group"): user_in_group = get_ldap_user_from_adcm(sdk_client_fs, ldap_user_in_group["name"]) group = get_ldap_group_from_adcm(sdk_client_fs, ldap_group["name"]) another_group.add_user(user_in_group) with allure.step("Remove user in AD from LDAP group and rerun sync"): ldap_ad.remove_user_from_group(ldap_user_in_group["dn"], ldap_group["dn"]) - _run_sync(sdk_client_fs) + sync_adcm_with_ldap(sdk_client_fs) with allure.step('Check user was removed only from LDAP group'): check_existing_users(sdk_client_fs, {ldap_user_in_group['name']}) check_existing_groups(sdk_client_fs, {ldap_group['name']}, {another_group.name}) @@ -215,16 +235,20 @@ def test_user_removed_from_group(self, sdk_client_fs, ldap_ad, ldap_group, ldap_ def test_user_deactivated(self, sdk_client_fs, ldap_ad, ldap_user_in_group): """Test that user is deactivated in ADCM after it's deactivated in AD""" ldap_user = ldap_user_in_group - credentials = {"user": ldap_user["name"], "password": ldap_user["password"], "url": sdk_client_fs.url} + credentials = { + "user": ldap_user["name"], + "password": ldap_user["password"], + "url": sdk_client_fs.url, + } with allure.step("Run sync and check that user is active and can log in"): - _run_sync(sdk_client_fs) + sync_adcm_with_ldap(sdk_client_fs) user = get_ldap_user_from_adcm(sdk_client_fs, ldap_user["name"]) assert user.is_active, "User should be active" expect_no_api_error("login as LDAP active user", ADCMClient, **credentials) with allure.step("Deactivate user in LDAP and check it is deactivated after sync"): with session_should_expire(**credentials): ldap_ad.deactivate_user(ldap_user["dn"]) - _run_sync(sdk_client_fs) + sync_adcm_with_ldap(sdk_client_fs) user.reread() assert not user.is_active, 'User should be deactivated' expect_api_error('login as deactivated user', ADCMClient, **credentials) @@ -237,14 +261,14 @@ def test_user_deleted(self, sdk_client_fs, ldap_ad, ldap_user_in_group): "url": sdk_client_fs.url, } with allure.step("Run sync and check that user is active and can log in"): - _run_sync(sdk_client_fs) + sync_adcm_with_ldap(sdk_client_fs) check_existing_users(sdk_client_fs, {ldap_user_in_group["name"]}) expect_no_api_error("login as LDAP user", ADCMClient, **credentials) with allure.step("Delete user in LDAP and check access denied"): with session_should_expire(**credentials): ldap_ad.delete(ldap_user_in_group["dn"]) - _run_sync(sdk_client_fs) + sync_adcm_with_ldap(sdk_client_fs) check_existing_users(sdk_client_fs, {ldap_user_in_group['name']}) user = get_ldap_user_from_adcm(sdk_client_fs, ldap_user_in_group['name']) assert not user.is_active, 'User should be deactivated' @@ -252,12 +276,16 @@ def test_user_deleted(self, sdk_client_fs, ldap_ad, ldap_user_in_group): def test_name_email_sync_from_ldap(self, sdk_client_fs, ldap_ad, ldap_user_in_group): """Test that first/last name and email are synced with LDAP""" - new_user_info = {"first_name": "Babaika", "last_name": "Labadaika", "email": "doesnt@ex.ist"} - _run_sync(sdk_client_fs) + new_user_info = { + "first_name": "Babaika", + "last_name": "Labadaika", + "email": "doesnt@ex.ist", + } + sync_adcm_with_ldap(sdk_client_fs) user = get_ldap_user_from_adcm(sdk_client_fs, ldap_user_in_group["name"]) self._check_user_info(user, ldap_user_in_group) ldap_ad.update_user(ldap_user_in_group["dn"], **new_user_info) - _run_sync(sdk_client_fs) + sync_adcm_with_ldap(sdk_client_fs) self._check_user_info(user, new_user_info) @allure.issue("https://tracker.yandex.ru/ADCM-3019") @@ -271,7 +299,7 @@ def test_sync_when_group_itself_is_group_search_base(self, sdk_client_fs, ldap_u ) check_existing_groups(sdk_client_fs) check_existing_users(sdk_client_fs) - _run_sync(sdk_client_fs) + sync_adcm_with_ldap(sdk_client_fs) check_existing_groups(sdk_client_fs, {ldap_group_name}) check_existing_users(sdk_client_fs, {ldap_user_in_group["name"]}) with allure.step("Check LDAP user is in LDAP group"): @@ -291,7 +319,7 @@ def _check_user_info(self, user: User, user_ldap_info: dict): def _simple_sync(self, sdk_client_fs, ldap_group, ldap_user_in_group, expected_local_users): check_existing_users(sdk_client_fs, expected_local=expected_local_users) check_existing_groups(sdk_client_fs) - _run_sync(sdk_client_fs) + sync_adcm_with_ldap(sdk_client_fs) check_existing_users(sdk_client_fs, {ldap_user_in_group["name"]}, expected_local=expected_local_users) check_existing_groups(sdk_client_fs, {ldap_group["name"]}) @@ -312,7 +340,11 @@ def test_sync_periodic_launch(self, sdk_client_fs, ad_config, ldap_basic_ous): with allure.step("Check that after 1 more minute the second sync task was launched"): wait_until_step_succeeds( - self._check_sync_task_is_presented, timeout=70, period=5, client=sdk_client_fs, expected_amount=2 + self._check_sync_task_is_presented, + timeout=70, + period=5, + client=sdk_client_fs, + expected_amount=2, ) with allure.step("Disable sync in settings and check no new task was launched"): @@ -325,13 +357,94 @@ def test_sync_periodic_launch(self, sdk_client_fs, ad_config, ldap_basic_ous): def _check_sync_task_is_presented(self, client: ADCMClient, expected_amount: int = 1): with allure.step(f"Check {expected_amount} sync task(s) is presented among tasks"): sync_tasks = [ - task for task in (j.task() for j in client.job_list()) if task.action().name == SYNC_ACTION_NAME + task + for task in (j.task() for j in client.job_list()) + if task.wait() or (task.action().name == SYNC_ACTION_NAME) ] assert ( actual_amount := len(sync_tasks) ) == expected_amount, f"Not enough sync tasks: {actual_amount}.\nExpected: {expected_amount}" +class TestKnownSyncBugs: + """Test known bugs with LDAP sync""" + + @pytest.fixture() + def two_users_wo_group(self, ldap_ad, ldap_basic_ous) -> tuple[dict[str, str], dict[str, str]]: + """Create two users without group""" + _, users_ou = ldap_basic_ous + user_1 = {"name": f"coolguy-{random_string(4)}", "password": random_string(16)} + user_1["dn"] = ldap_ad.create_user(custom_base_dn=users_ou, **user_1) + user_2 = {"name": f"coolguy2-{random_string(4)}", "password": random_string(16)} + user_2["dn"] = ldap_ad.create_user(custom_base_dn=users_ou, **user_2) + return user_1, user_2 + + @pytest.fixture() + def set_adcm_ldap_config(self, sdk_client_fs, ad_config) -> Callable[[str, str], None]: + """Prepare method for settings ADCM LDAP settings passing only user and group bases""" + + def wrapped(users_base, group_base): + configure_adcm_for_ldap( + user_base=users_base, + group_base=group_base, + client=sdk_client_fs, + config=ad_config, + ssl_on=False, + ssl_cert=None, + extra_config={"sync_interval": 0}, + ) + + return wrapped + + @allure.issue(url="https://tracker.yandex.ru/ADCM-3274") + @pytest.mark.usefixtures("configure_adcm_ldap_ad") + def test_sync_no_group_in_group_search_base(self, sdk_client_fs, two_users_wo_group): + """Test LDAP sync when 0 groups found with group search base, so users should not be synced""" + with allure.step("Check no LDAP groups or users exist"): + check_existing_groups(sdk_client_fs) + check_existing_users(sdk_client_fs) + + sync_adcm_with_ldap(sdk_client_fs) + + with allure.step("Check no LDAP groups or users exist"): + check_existing_groups(sdk_client_fs) + check_existing_users(sdk_client_fs) + + with allure.step("Check no LDAP user can login"): + for user in two_users_wo_group: + login_should_fail(f"login as {user['name']}", sdk_client_fs, user["name"], user["password"]) + + def test_sync_add_group_search_base(self, set_adcm_ldap_config, two_users_wo_group, ldap_basic_ous, sdk_client_fs): + """ + Test on bug when users aren't deactivated when no group is found + when group search base provided during the second sync + """ + groups_ou, users_ou = ldap_basic_ous + ldap_usernames = {user["name"] for user in two_users_wo_group} + + with allure.step("Configure LDAP only with user base and run sync"): + set_adcm_ldap_config(users_base=users_ou, group_base="") + sync_adcm_with_ldap(sdk_client_fs) + + with allure.step("Check LDAP users appeared and are active"): + sets_are_equal( + actual={u.username for u in sdk_client_fs.user_list() if u.type == "ldap" and u.is_active}, + expected=ldap_usernames, + message="Incorrect users from LDAP are active", + ) + + with allure.step("Add group search to LDAP settings and "): + set_adcm_ldap_config(users_base=users_ou, group_base=groups_ou) + sync_adcm_with_ldap(sdk_client_fs) + + with allure.step("Check LDAP users stayed, but are inactive"): + sets_are_equal( + actual={u.username for u in sdk_client_fs.user_list() if u.type == "ldap" and not u.is_active}, + expected=ldap_usernames, + message="Incorrect users from LDAP are inactive", + ) + + @contextmanager def session_should_expire(user: str, password: str, url: str): """Check that session expires""" @@ -356,12 +469,6 @@ def session_should_expire(user: str, password: str, url: str): ) from err -@allure.step("Run LDAP sync action") -def _run_sync(client: ADCMClient): - action = client.adcm().action(name=SYNC_ACTION_NAME) - wait_for_task_and_assert_result(action.run(), "success") - - @allure.step("Run successful test connection") def _test_connection(client: ADCMClient): wait_for_task_and_assert_result(client.adcm().action(name=TEST_CONNECTION_ACTION).run(), "success") diff --git a/tests/functional/ldap_auth/utils.py b/tests/functional/ldap_auth/utils.py index 8aa5fc648c..9c641ca0ce 100644 --- a/tests/functional/ldap_auth/utils.py +++ b/tests/functional/ldap_auth/utils.py @@ -12,16 +12,20 @@ """Utilities for LDAP-related tests""" -from typing import Collection +from typing import Collection, Set import allure from adcm_client.base import ObjectNotFound from adcm_client.objects import ADCMClient, Group, User - -from tests.library.assertions import sets_are_equal, expect_api_error, expect_no_api_error +from tests.library.assertions import ( + expect_api_error, + expect_no_api_error, + sets_are_equal, +) SYNC_ACTION_NAME = 'run_ldap_sync' TEST_CONNECTION_ACTION = 'test_ldap_connection' +LDAP_ACTION_CAN_NOT_START_REASON = "You need to fill in the LDAP integration settings" DEFAULT_LOCAL_USERS = ('admin', 'status', 'system') @@ -51,7 +55,9 @@ def get_ldap_group_from_adcm(client: ADCMClient, name: str) -> Group: @allure.step('Check users existing in ADCM') def check_existing_users( - client: ADCMClient, expected_ldap: Collection[str] = (), expected_local: Collection[str] = DEFAULT_LOCAL_USERS + client: ADCMClient, + expected_ldap: Collection[str] = (), + expected_local: Collection[str] = DEFAULT_LOCAL_USERS, ): """Check that only provided users exists (both ldap and local)""" expected_ldap = set(expected_ldap) @@ -102,3 +108,25 @@ def login_should_fail(operation_name: str, client: ADCMClient, username: str, pa user=username, password=password, ) + + +def check_users_in_group(group: Group, *users: User): + """Method to check users in group""" + error_msg = f'Incorrect user list in group {group.name}' + sets_are_equal( + actual=get_usernames_in_group(group), + expected={u.username for u in users}, + message=error_msg, + ) + + +def get_usernames_in_group(group: Group) -> Set: + """Method to get usernames from group""" + group.reread() + return {u.username for u in group.user_list()} + + +@allure.step('Turn off periodic ldap sync') +def turn_off_periodic_ldap_sync(client: ADCMClient) -> None: + """Method to turn off periodic ldap sync""" + client.adcm().config_set_diff({'ldap_integration': {'sync_interval': 0}}) diff --git a/tests/functional/maintenance_mode/bundles/cluster_mm_action/actions.yaml b/tests/functional/maintenance_mode/bundles/cluster_mm_action/actions.yaml new file mode 100644 index 0000000000..1f17534f7e --- /dev/null +++ b/tests/functional/maintenance_mode/bundles/cluster_mm_action/actions.yaml @@ -0,0 +1,21 @@ +# 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. +--- +- name: Dummy action + hosts: all + connection: local + gather_facts: no + + tasks: + - name: Dummy? + debug: + msg: "Some message" diff --git a/tests/functional/maintenance_mode/bundles/cluster_mm_action/config.yaml b/tests/functional/maintenance_mode/bundles/cluster_mm_action/config.yaml new file mode 100644 index 0000000000..d1477f0ebd --- /dev/null +++ b/tests/functional/maintenance_mode/bundles/cluster_mm_action/config.yaml @@ -0,0 +1,61 @@ +# 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. + + +- type: cluster + name: maintenance_mode_allowed_cluster + version: 5.4 + allow_maintenance_mode: true + + actions: &actions + adcm_turn_on_maintenance_mode: + type: job + script: ./actions.yaml + script_type: ansible + states: + available: any + + adcm_turn_off_maintenance_mode: + type: job + script: ./actions.yaml + script_type: ansible + states: + available: any + + config: &config + - name: some_param + type: integer + default: 12 + group_customization: true + +- type: service + name: test_service + version: 4.3 + + actions: *actions + config: *config + + components: &components + first_component: + actions: *actions + config: *config + + second_component: + actions: *actions + config: *config + +- type: service + name: another_service + version: 6.5 + + actions: *actions + components: *components diff --git a/tests/functional/maintenance_mode/bundles/cluster_mm_actions_disallowed/config.yaml b/tests/functional/maintenance_mode/bundles/cluster_mm_actions_disallowed/config.yaml new file mode 100644 index 0000000000..d56b7543fd --- /dev/null +++ b/tests/functional/maintenance_mode/bundles/cluster_mm_actions_disallowed/config.yaml @@ -0,0 +1,60 @@ +# 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. + + +- type: cluster + name: maintenance_mode_host_actions + version: 5.1 + allow_maintenance_mode: true + + actions: + cluster_action_allowed: &job + type: job + script: ./actions.yaml + script_type: ansible + host_action: false + allow_in_maintenance_mode: true + states: + available: any + + cluster_action_disallowed: + <<: *job + allow_in_maintenance_mode: false + + +- type: service + name: first_service + version: 234.4 + + actions: + service_action_allowed: + <<: *job + service_action_disallowed: + <<: *job + allow_in_maintenance_mode: false + + components: + first_component: + actions: + component_action_allowed: + <<: *job + component_action_disallowed: + <<: *job + allow_in_maintenance_mode: false + + second_component: + actions: + component_action_allowed: + <<: *job + component_action_disallowed: + <<: *job + allow_in_maintenance_mode: false diff --git a/tests/functional/maintenance_mode/bundles/cluster_using_plugin/component-mm.yaml b/tests/functional/maintenance_mode/bundles/cluster_using_plugin/component-mm.yaml new file mode 100644 index 0000000000..925608b605 --- /dev/null +++ b/tests/functional/maintenance_mode/bundles/cluster_using_plugin/component-mm.yaml @@ -0,0 +1,43 @@ +# 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. +--- +- name: Change MM of component + hosts: localhost + connection: local + gather_facts: no + + tasks: + - name: debug_info + debug: + msg: "Component turn ON" + tags: + - turn_on + + - name: change mm + adcm_change_maintenance_mode: + type: component + value: True + tags: + - turn_on + + - name: debug_info + debug: + msg: "Component turn OFF" + tags: + - turn_off + + - name: change mm + adcm_change_maintenance_mode: + type: component + value: False + tags: + - turn_off diff --git a/tests/functional/maintenance_mode/bundles/cluster_using_plugin/config.yaml b/tests/functional/maintenance_mode/bundles/cluster_using_plugin/config.yaml new file mode 100644 index 0000000000..105cbcf051 --- /dev/null +++ b/tests/functional/maintenance_mode/bundles/cluster_using_plugin/config.yaml @@ -0,0 +1,74 @@ +# 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. + +- type: cluster + version: 2 + name: cluster_with_mm_plugin + allow_maintenance_mode: true + + actions: + adcm_host_turn_on_maintenance_mode: &host-action + type: job + script: ./host-mm.yaml + script_type: ansible + host_action: true + states: + available: any + params: + ansible_tags: turn_on + + adcm_host_turn_off_maintenance_mode: + <<: *host-action + script: ./host-mm.yaml + params: + ansible_tags: turn_off + +- &service + type: service + version: 3 + name: service_with_mm_plugin + + actions: + adcm_turn_on_maintenance_mode: &action + type: job + script: ./service-mm.yaml + script_type: ansible + states: + available: any + params: + ansible_tags: turn_on + + adcm_turn_off_maintenance_mode: + <<: *action + script: ./service-mm.yaml + params: + ansible_tags: turn_off + + components: + component_with_mm_plugin: + actions: + adcm_turn_on_maintenance_mode: + <<: *action + script: ./component-mm.yaml + params: + ansible_tags: turn_on + + adcm_turn_off_maintenance_mode: + <<: *action + script: ./component-mm.yaml + params: + ansible_tags: turn_off + + component_wo_mm_plugin: + +- <<: *service + name: service_2 diff --git a/python/api/stack/filters.py b/tests/functional/maintenance_mode/bundles/cluster_using_plugin/host-mm.yaml similarity index 50% rename from python/api/stack/filters.py rename to tests/functional/maintenance_mode/bundles/cluster_using_plugin/host-mm.yaml index b1631f7a96..6b7b2d4a95 100644 --- a/python/api/stack/filters.py +++ b/tests/functional/maintenance_mode/bundles/cluster_using_plugin/host-mm.yaml @@ -9,21 +9,35 @@ # 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 django_filters import rest_framework as drf_filters - -from cm.models import Prototype - - -class StringInFilter(drf_filters.BaseInFilter, drf_filters.CharFilter): - pass - - -class PrototypeListFilter(drf_filters.FilterSet): - name = StringInFilter(label='name', field_name='name', lookup_expr='in') - parent_name = StringInFilter(label='parent_name', field_name='parent', lookup_expr='name__in') - - class Meta: - model = Prototype - fields = ['bundle_id', 'type'] +--- +- name: Change MM of host + hosts: localhost + connection: local + gather_facts: no + + tasks: + - name: debug_info + debug: + msg: "Host turn ON" + tags: + - turn_on + + - name: change mm + adcm_change_maintenance_mode: + type: host + value: True + tags: + - turn_on + + - name: debug_info + debug: + msg: "Host turn OFF" + tags: + - turn_off + + - name: change mm + adcm_change_maintenance_mode: + type: host + value: False + tags: + - turn_off diff --git a/tests/functional/maintenance_mode/bundles/cluster_using_plugin/service-mm.yaml b/tests/functional/maintenance_mode/bundles/cluster_using_plugin/service-mm.yaml new file mode 100644 index 0000000000..6a4185b184 --- /dev/null +++ b/tests/functional/maintenance_mode/bundles/cluster_using_plugin/service-mm.yaml @@ -0,0 +1,43 @@ +# 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. +--- +- name: Change MM of service + hosts: localhost + connection: local + gather_facts: no + + tasks: + - name: debug_info + debug: + msg: "Service turn ON" + tags: + - turn_on + + - name: change mm + adcm_change_maintenance_mode: + type: service + value: True + tags: + - turn_on + + - name: debug_info + debug: + msg: "Service turn OFF" + tags: + - turn_off + + - name: change mm + adcm_change_maintenance_mode: + type: service + value: False + tags: + - turn_off diff --git a/tests/functional/maintenance_mode/bundles/cluster_with_concerns/concern_cluster/config.yaml b/tests/functional/maintenance_mode/bundles/cluster_with_concerns/concern_cluster/config.yaml new file mode 100644 index 0000000000..14dbb36f2a --- /dev/null +++ b/tests/functional/maintenance_mode/bundles/cluster_with_concerns/concern_cluster/config.yaml @@ -0,0 +1,36 @@ +# 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. + + +- type: cluster + name: maintenance_mode_allowed_cluster + version: 5.4 + allow_maintenance_mode: true + config: &config_issue + - name: some_param_cluster + type: integer + + +- type: service + name: first_service + version: 4.3 + config: *config_issue + + components: + first_component: + config: + - name: some_param_cluster + type: integer + default: 12 + + second_component: + config: *config_issue diff --git a/tests/functional/maintenance_mode/bundles/cluster_with_concerns/concern_cluster_action/component-mm.yaml b/tests/functional/maintenance_mode/bundles/cluster_with_concerns/concern_cluster_action/component-mm.yaml new file mode 100644 index 0000000000..925608b605 --- /dev/null +++ b/tests/functional/maintenance_mode/bundles/cluster_with_concerns/concern_cluster_action/component-mm.yaml @@ -0,0 +1,43 @@ +# 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. +--- +- name: Change MM of component + hosts: localhost + connection: local + gather_facts: no + + tasks: + - name: debug_info + debug: + msg: "Component turn ON" + tags: + - turn_on + + - name: change mm + adcm_change_maintenance_mode: + type: component + value: True + tags: + - turn_on + + - name: debug_info + debug: + msg: "Component turn OFF" + tags: + - turn_off + + - name: change mm + adcm_change_maintenance_mode: + type: component + value: False + tags: + - turn_off diff --git a/tests/functional/maintenance_mode/bundles/cluster_with_concerns/concern_cluster_action/config.yaml b/tests/functional/maintenance_mode/bundles/cluster_with_concerns/concern_cluster_action/config.yaml new file mode 100644 index 0000000000..856caaa1c5 --- /dev/null +++ b/tests/functional/maintenance_mode/bundles/cluster_with_concerns/concern_cluster_action/config.yaml @@ -0,0 +1,79 @@ +# 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. + + +- type: cluster + name: maintenance_mode_allowed_cluster + version: 5.4 + allow_maintenance_mode: true + config: &config_no_issue + - name: some_param_cluster + type: integer + default: 12 + actions: + cluster_action: + type: job + script: ./sleep.yaml + script_type: ansible + states: + available: any + + +- type: service + name: first_service + version: 4.3 + config: &config_issue + - name: some_param_cluster + type: integer + actions: + adcm_turn_on_maintenance_mode: &action + type: job + script: ./service-mm.yaml + script_type: ansible + states: + available: any + params: + ansible_tags: turn_on + adcm_turn_off_maintenance_mode: + <<: *action + script: ./service-mm.yaml + params: + ansible_tags: turn_off + + components: + first_component: + config: *config_no_issue + actions: + adcm_turn_on_maintenance_mode: + <<: *action + script: ./component-mm.yaml + params: + ansible_tags: turn_on + adcm_turn_off_maintenance_mode: + <<: *action + script: ./component-mm.yaml + params: + ansible_tags: turn_off + + second_component: + config: *config_issue + actions: + adcm_turn_on_maintenance_mode: + <<: *action + script: ./component-mm.yaml + params: + ansible_tags: turn_on + adcm_turn_off_maintenance_mode: + <<: *action + script: ./component-mm.yaml + params: + ansible_tags: turn_off diff --git a/tests/functional/maintenance_mode/bundles/cluster_with_concerns/concern_cluster_action/service-mm.yaml b/tests/functional/maintenance_mode/bundles/cluster_with_concerns/concern_cluster_action/service-mm.yaml new file mode 100644 index 0000000000..6a4185b184 --- /dev/null +++ b/tests/functional/maintenance_mode/bundles/cluster_with_concerns/concern_cluster_action/service-mm.yaml @@ -0,0 +1,43 @@ +# 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. +--- +- name: Change MM of service + hosts: localhost + connection: local + gather_facts: no + + tasks: + - name: debug_info + debug: + msg: "Service turn ON" + tags: + - turn_on + + - name: change mm + adcm_change_maintenance_mode: + type: service + value: True + tags: + - turn_on + + - name: debug_info + debug: + msg: "Service turn OFF" + tags: + - turn_off + + - name: change mm + adcm_change_maintenance_mode: + type: service + value: False + tags: + - turn_off diff --git a/tests/functional/maintenance_mode/bundles/cluster_with_concerns/concern_cluster_action/sleep.yaml b/tests/functional/maintenance_mode/bundles/cluster_with_concerns/concern_cluster_action/sleep.yaml new file mode 100644 index 0000000000..b8617baeb3 --- /dev/null +++ b/tests/functional/maintenance_mode/bundles/cluster_with_concerns/concern_cluster_action/sleep.yaml @@ -0,0 +1,23 @@ +# 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. +--- +- name: sleep + hosts: all + connection: local + gather_facts: no + + tasks: + - name: Sleep + pause: + seconds: 5 + - debug: + msg: "sleep" diff --git a/tests/functional/maintenance_mode/bundles/cluster_with_concerns/concern_upgrade/cluster/config.yaml b/tests/functional/maintenance_mode/bundles/cluster_with_concerns/concern_upgrade/cluster/config.yaml new file mode 100644 index 0000000000..f4bd8d9872 --- /dev/null +++ b/tests/functional/maintenance_mode/bundles/cluster_with_concerns/concern_upgrade/cluster/config.yaml @@ -0,0 +1,33 @@ +# 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. + +- type: cluster + name: test_cluster + version: 1.5 + edition: community + + config: &common_config + - name: check_conf + type: boolean + required: false + default: true + +- type: service + name: test_service + version: 101 + config: *common_config + + components: + test_component: + config: *common_config + new_component: + config: *common_config diff --git a/tests/functional/maintenance_mode/bundles/cluster_with_concerns/concern_upgrade/second_cluster/config.yaml b/tests/functional/maintenance_mode/bundles/cluster_with_concerns/concern_upgrade/second_cluster/config.yaml new file mode 100644 index 0000000000..2374f28731 --- /dev/null +++ b/tests/functional/maintenance_mode/bundles/cluster_with_concerns/concern_upgrade/second_cluster/config.yaml @@ -0,0 +1,46 @@ +# 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. + +- type: cluster + name: test_cluster + version: 1.6 + edition: community + + upgrade: + - versions: + min: 0.4 + max: 1.5 + name: upgrade to 1.6 + states: + available: any + on_success: upgradable + + config: &common_config + - name: boolean + type: boolean + required: false + default: true + +- type: service + name: test_service + version: 101 + config: *common_config + + components: + test_component: + config: *common_config + new_component: + config: + - name: boolean + type: boolean + + diff --git a/tests/functional/maintenance_mode/conftest.py b/tests/functional/maintenance_mode/conftest.py index f400861b71..48347d8a3f 100644 --- a/tests/functional/maintenance_mode/conftest.py +++ b/tests/functional/maintenance_mode/conftest.py @@ -18,24 +18,30 @@ import os from pathlib import Path -from typing import Tuple, Literal, Iterable, Set +from typing import Iterable, Literal, Set, Tuple import allure import pytest -from adcm_client.objects import ADCMClient, Cluster, Provider, Host - -from tests.functional.tools import AnyADCMObject +from adcm_client.objects import ADCMClient, Cluster, Component, Host, Provider, Service +from tests.functional.tools import AnyADCMObject, get_object_represent +from tests.library.api.client import APIClient +from tests.library.assertions import sets_are_equal from tests.library.utils import get_hosts_fqdn_representation BUNDLES_DIR = Path(os.path.dirname(__file__)) / 'bundles' -MM_IS_ON = 'on' -MM_IS_OFF = 'off' -MM_IS_DISABLED = 'disabled' - -MaintenanceModeOnHostValue = Literal['on', 'off', 'disabled'] +MM_IS_ON = "ON" +MM_IS_OFF = "OFF" +MM_IS_CHANGING = "CHANGING" +MM_ALLOWED = True +MM_NOT_ALLOWED = False -DISABLING_CAUSE = 'maintenance_mode' +START_IMPOSSIBLE_REASONS = { + "The Action is not available. One or more hosts in 'Maintenance mode'", + "The Action is not available. Host in 'Maintenance mode'", + "The Action is not available. Service in 'Maintenance mode'", + "The Action is not available. Component in 'Maintenance mode'", +} PROVIDER_NAME = 'Test Default Provider' CLUSTER_WITH_MM_NAME = 'Test Cluster WITH Maintenance Mode' @@ -83,24 +89,64 @@ def cluster_without_mm(request, sdk_client_fs: ADCMClient): return cluster -def turn_mm_on(host: Host): +def set_maintenance_mode( + api_client: APIClient, adcm_object: Host | Service | Component, maintenance_mode: bool +) -> None: + """Change maintenance mode on ADCM objects""" + if isinstance(adcm_object, Service): + client = api_client.service + elif isinstance(adcm_object, Component): + client = api_client.component + else: + client = api_client.host + representation = get_object_represent(adcm_object) + with allure.step(f'Turn MM to mode {maintenance_mode} on object {representation}'): + client.change_maintenance_mode(adcm_object.id, maintenance_mode).check_code(200) + adcm_object.reread() + assert (actual_mm := adcm_object.maintenance_mode) == maintenance_mode, ( + f'Maintenance mode of object {representation} should be {maintenance_mode},' f' not {actual_mm}' + ) + + +def turn_mm_on(api_client: APIClient, host: Host): """Turn maintenance mode "on" on host""" with allure.step(f'Turn MM "on" on host {host.fqdn}'): - host.maintenance_mode_set(MM_IS_ON) + api_client.host.change_maintenance_mode(host.id, MM_IS_ON).check_code(200) + host.reread() assert ( actual_mm := host.maintenance_mode ) == MM_IS_ON, f'Maintenance mode of host {host.fqdn} should be {MM_IS_ON}, not {actual_mm}' -def turn_mm_off(host: Host): +def turn_mm_off(api_client: APIClient, host: Host, expected_code: int = 200): """Turn maintenance mode "off" on host""" with allure.step(f'Turn MM "off" on host {host.fqdn}'): - host.maintenance_mode_set(MM_IS_OFF) + api_client.host.change_maintenance_mode(host.id, MM_IS_OFF).check_code(expected_code) + host.reread() assert ( actual_mm := host.maintenance_mode ) == MM_IS_OFF, f'Maintenance mode of host {host.fqdn} should be {MM_IS_OFF}, not {actual_mm}' +def expect_changing_mm_fail( + api_client: APIClient, object_with_mm: Host | Service | Component, new_mm: Literal["ON", "OFF"] +) -> None: + """ + Check that changing MM is disallowed on object. + Be careful with CHANGING status. + """ + object_with_mm.reread() + previous_mm = object_with_mm.maintenance_mode + object_represent = get_object_represent(object_with_mm) + with allure.step(f'Check setting MM "{new_mm}" on "{object_represent}" will fail'): + api_node = getattr(api_client, object_with_mm.__class__.__name__.lower()) + api_node.change_maintenance_mode(object_with_mm.id, new_mm).check_code(409) + object_with_mm.reread() + assert ( + actual_mm := object_with_mm.maintenance_mode + ) == previous_mm, f'Maintenance mode of "{object_represent}" should stay {previous_mm}, not become {actual_mm}' + + def add_hosts_to_cluster(cluster: Cluster, hosts: Iterable[Host]): """Add hosts to cluster""" with allure.step(f'Add hosts to the cluster "{cluster.name}": {get_hosts_fqdn_representation(hosts)}'): @@ -115,28 +161,115 @@ def remove_hosts_from_cluster(cluster: Cluster, hosts: Iterable[Host]): cluster.host_delete(host) -def check_hosts_mm_is(maintenance_mode: MaintenanceModeOnHostValue, *hosts: Host): - """Check that MM of hosts is equal to the expected one""" +def check_mm_is(maintenance_mode: str, *adcm_object: Host | Service | Component) -> None: + """Check value of maintenance_mode on object""" + representation = [get_object_represent(obj) for obj in adcm_object] with allure.step( - f'Check that "maintenance_mode" is equal to "{maintenance_mode}" ' + f'Check that "maintenance_mode" is equal to "{maintenance_mode}" ' f'on objects: {representation}' + ): + + for obj in adcm_object: + obj.reread() + obj_in_wrong_mode = tuple(obj for obj in adcm_object if obj.maintenance_mode != maintenance_mode) + if len(obj_in_wrong_mode) == 0: + return + raise AssertionError( + f"{', '.join(get_object_represent(obj) for obj in obj_in_wrong_mode)} " + "have incorrect value of 'maintenance_mode' flag.\n" + f"Expected: {maintenance_mode}\nActual: {obj_in_wrong_mode[0].maintenance_mode}" + ) + + +def check_mm_availability(is_mm_available: bool, *hosts: Host): + """Check that MM change is allowed/disallowed for the given hosts""" + with allure.step( + f'Check that "is_maintenance_mode_available" is {is_mm_available} ' f'on hosts: {get_hosts_fqdn_representation(hosts)}' ): for host in hosts: host.reread() - hosts_in_wrong_mode = tuple(host for host in hosts if host.maintenance_mode != maintenance_mode) + hosts_in_wrong_mode = tuple(host for host in hosts if host.is_maintenance_mode_available is not is_mm_available) if len(hosts_in_wrong_mode) == 0: return raise AssertionError( - 'Some hosts have incorrect value of "maintenance_mode" flag.\n' + 'Some hosts have incorrect value of "is_maintenance_mode_available" flag.\n' f'Hosts: {get_hosts_fqdn_representation(hosts_in_wrong_mode)}' ) def get_enabled_actions_names(adcm_object: AnyADCMObject) -> Set[str]: """Get actions that aren't disabled by maintenance mode""" - return {action.name for action in adcm_object.action_list() if action.disabling_cause != DISABLING_CAUSE} + return { + action.name + for action in adcm_object.action_list() + if action.start_impossible_reason not in START_IMPOSSIBLE_REASONS + } def get_disabled_actions_names(adcm_object: AnyADCMObject) -> Set[str]: """Get actions disabled because of maintenance mode""" - return {action.name for action in adcm_object.action_list() if action.disabling_cause == DISABLING_CAUSE} + return { + action.name + for action in adcm_object.action_list() + if action.start_impossible_reason in START_IMPOSSIBLE_REASONS + } + + +def check_concerns_on_object(adcm_object: AnyADCMObject, expected_concerns: set[str]) -> None: + """Check concerns on object""" + with allure.step(f"Check concerns on object {adcm_object}"): + adcm_object.reread() + actual_concerns = {concern.reason["placeholder"]["source"]["name"] for concern in adcm_object.concerns()} + sets_are_equal( + actual_concerns, + expected_concerns, + "Actual concerns must be equal with expected concerns" + f" on: {get_object_represent(adcm_object)}\n" + f"Actual concerns: {actual_concerns}\n" + f"Expected concerns: {expected_concerns}", + ) + + +def check_no_concerns_on_objects(*adcm_object): + """Method to check concerns on adcm_object is absent""" + for obj in adcm_object: + obj.reread() + report = [ + ( + f"{get_object_represent(obj)} have concern:\n" + f"{[issue.name for issue in obj.concerns()]}\n" + "while concern is not expected" + ) + for obj in adcm_object + if len(obj.concerns()) != 0 + ] + if not report: + return + raise AssertionError(f"{', '.join(obj for obj in report)}") + + +def check_actions_availability( + adcm_object: AnyADCMObject, expected_enabled: set[str], expected_disabled: set[str] +) -> None: + """Method to check actual enabled and disabled actions with expected""" + representation = get_object_represent(adcm_object) + actual_enabled = get_enabled_actions_names(adcm_object) + actual_disabled = get_disabled_actions_names(adcm_object) + + with allure.step(f"Compare actual enabled actions with expected enabled actions on object {representation}"): + sets_are_equal( + actual_enabled, + expected_enabled, + f"Incorrect actions are enabled on object {representation}\n" + f"Actual enabled actions: {actual_enabled}\n" + f"Expected enabled actions: {expected_enabled}", + ) + + with allure.step(f"Compare actual disabled actions with expected disabled actions on object {representation}"): + sets_are_equal( + actual_disabled, + expected_disabled, + f"Incorrect actions are disabled on object {representation}\n" + f"Actual disabled actions: {actual_disabled}\n" + f"Expected disabled actions: {expected_disabled}", + ) diff --git a/tests/functional/maintenance_mode/test_change_mm_plugin.py b/tests/functional/maintenance_mode/test_change_mm_plugin.py new file mode 100644 index 0000000000..6456e3a16c --- /dev/null +++ b/tests/functional/maintenance_mode/test_change_mm_plugin.py @@ -0,0 +1,226 @@ +# 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 allure +import pytest +from adcm_client.objects import ADCMClient, Bundle, Cluster, Component, Host, Service +from tests.functional.conftest import only_clean_adcm +from tests.functional.maintenance_mode.conftest import ( + BUNDLES_DIR, + MM_IS_OFF, + MM_IS_ON, + check_mm_is, +) +from tests.functional.tools import AnyADCMObject, get_object_represent +from tests.library.assertions import does_not_intersect + +# pylint: disable=redefined-outer-name + +pytestmark = [only_clean_adcm] + +MM_CHANGE_RELATED_ACTION_NAMES = frozenset( + { + "adcm_host_turn_on_maintenance_mode", + "adcm_host_turn_off_maintenance_mode", + "adcm_turn_on_maintenance_mode", + "adcm_turn_off_maintenance_mode", + } +) + + +@pytest.fixture() +def bundle(sdk_client_fs) -> Bundle: + return sdk_client_fs.upload_from_fs(BUNDLES_DIR / "cluster_using_plugin") + + +@pytest.fixture() +def first_cluster_objects(bundle) -> tuple[Service, Component, Component, Service, Component, Component]: + return _prepare_new_cluster(bundle, "Cluster with MM actions")[1:] + + +@pytest.fixture() +def first_cluster_hosts(first_cluster_objects, hosts) -> tuple[Host, Host, Host]: + host_1, host_2, host_3, *_ = hosts + _, component_1, component_2, _, component_3, component_4 = first_cluster_objects + + _map_components_to_hosts((host_1, host_2, host_3), (component_1, component_2, component_3, component_4)) + + return host_1, host_2, host_3 + + +@pytest.fixture() +def second_cluster_objects(bundle) -> tuple[Service, Component, Component, Service, Component, Component]: + return _prepare_new_cluster(bundle, "Control group cluster")[1:] + + +@pytest.fixture() +def second_cluster_hosts(second_cluster_objects, hosts) -> tuple[Host, Host, Host]: + *_, host_1, host_2, host_3 = hosts + _, component_1, component_2, _, component_3, component_4 = second_cluster_objects + + _map_components_to_hosts((host_1, host_2, host_3), (component_1, component_2, component_3, component_4)) + + return host_1, host_2, host_3 + + +def test_changing_mm_via_plugin( + api_client, sdk_client_fs, first_cluster_objects, second_cluster_objects, first_cluster_hosts, second_cluster_hosts +): + """ + Test changing MM flag of service and component via bonded action with MM changing plugin call + """ + service, component_with_plugin, component_wo_plugin, *another_service_objects = first_cluster_objects + hosts = *first_cluster_hosts, *second_cluster_hosts + + check_mm_related_actions_are_absent_on( + service.cluster(), *first_cluster_objects, second_cluster_objects[0].cluster(), *second_cluster_objects, *hosts + ) + + with allure.step("Change service's MM to 'ON' with action bond to it"): + api_client.service.change_maintenance_mode(service.id, MM_IS_ON).check_code(200) + _wait_all_tasks_succeed(sdk_client_fs, 1) + check_mm_is(MM_IS_ON, service, component_with_plugin, component_wo_plugin) + check_mm_is(MM_IS_OFF, *another_service_objects, *second_cluster_objects, *hosts) + + with allure.step("Change service's MM to 'OFF' with action bond to it"): + api_client.service.change_maintenance_mode(service.id, MM_IS_OFF).check_code(200) + _wait_all_tasks_succeed(sdk_client_fs, 2) + check_mm_is(MM_IS_OFF, *first_cluster_objects, *second_cluster_objects, *hosts) + + with allure.step("Change component's MM to 'ON' with action bond to it"): + api_client.component.change_maintenance_mode(component_with_plugin.id, MM_IS_ON).check_code(200) + _wait_all_tasks_succeed(sdk_client_fs, 3) + check_mm_is(MM_IS_ON, component_with_plugin) + check_mm_is(MM_IS_OFF, service, component_wo_plugin, *another_service_objects, *second_cluster_objects, *hosts) + + with allure.step("Change component's MM to 'OFF' with action bond to it"): + api_client.component.change_maintenance_mode(component_with_plugin.id, MM_IS_OFF).check_code(200) + _wait_all_tasks_succeed(sdk_client_fs, 4) + check_mm_is(MM_IS_OFF, *first_cluster_objects, *second_cluster_objects, *hosts) + + check_mm_related_actions_are_absent_on( + service.cluster(), *first_cluster_objects, second_cluster_objects[0].cluster(), *second_cluster_objects, *hosts + ) + + +def test_changing_host_mm_via_plugin( # pylint: disable=too-many-locals + api_client, sdk_client_fs, first_cluster_objects, second_cluster_objects, second_cluster_hosts, hosts +): + """ + Test changing MM flag of host via bonded action with MM changing plugin call + """ + host_1, host_2, host_3, *_ = hosts + first_cluster_hosts = host_1, host_2, host_3 + second_objects = *second_cluster_objects, *second_cluster_hosts + + with allure.step("Check that MM of host outside of cluster can't be changed via bonded action"): + api_client.host.change_maintenance_mode(host_1.id, MM_IS_ON).check_code(409) + check_mm_is(MM_IS_OFF, *first_cluster_hosts, *first_cluster_objects, *second_objects) + + with allure.step("Map hosts to first cluster"): + service, component_1, component_2, _, component_3, component_4 = first_cluster_objects + _map_components_to_hosts((host_1, host_2, host_3), (component_1, component_2, component_3, component_4)) + _, _, _, *another_service_objects = first_cluster_objects + + with allure.step("Change one host's MM to 'ON'"): + api_client.host.change_maintenance_mode(host_1.id, MM_IS_ON).check_code(200) + _wait_all_tasks_succeed(sdk_client_fs, 1) + check_mm_is(MM_IS_ON, host_1) + check_mm_is(MM_IS_OFF, host_2, host_3, *first_cluster_objects, *second_objects) + + with allure.step("Change second host's MM to 'ON'"): + api_client.host.change_maintenance_mode(host_2.id, MM_IS_ON).check_code(200) + _wait_all_tasks_succeed(sdk_client_fs, 2) + check_mm_is(MM_IS_ON, host_1, host_2, component_1) + check_mm_is(MM_IS_OFF, host_3, service, component_2, *another_service_objects, *second_objects) + + with allure.step("Change third host's MM to 'ON' and check all mapped services and components switched"): + api_client.host.change_maintenance_mode(host_3.id, MM_IS_ON).check_code(200) + _wait_all_tasks_succeed(sdk_client_fs, 3) + check_mm_is(MM_IS_ON, *first_cluster_hosts, *first_cluster_objects) + check_mm_is(MM_IS_OFF, *second_objects) + + with allure.step("Switch second host's MM to 'OFF'"): + api_client.host.change_maintenance_mode(host_2.id, MM_IS_OFF).check_code(200) + _wait_all_tasks_succeed(sdk_client_fs, 4) + check_mm_is(MM_IS_ON, host_1, host_3, component_2) + check_mm_is(MM_IS_OFF, host_2, service, component_1, *another_service_objects, *second_objects) + + with allure.step("Switch third host's MM to 'OFF'"): + api_client.host.change_maintenance_mode(host_3.id, MM_IS_OFF).check_code(200) + _wait_all_tasks_succeed(sdk_client_fs, 5) + check_mm_is(MM_IS_ON, host_1) + check_mm_is(MM_IS_OFF, host_2, host_3, *first_cluster_objects, *second_objects) + + with allure.step("Switch first host's MM to 'OFF' and check that everything's back to normal"): + api_client.host.change_maintenance_mode(host_1.id, MM_IS_OFF).check_code(200) + _wait_all_tasks_succeed(sdk_client_fs, 6) + check_mm_is(MM_IS_OFF, *first_cluster_hosts, *first_cluster_objects, *second_objects) + + +@allure.step("Check MM related actions are not shown in actions list") +def check_mm_related_actions_are_absent_on(*adcm_objects: AnyADCMObject) -> None: + for adcm_object in adcm_objects: + action_names = {action.name for action in adcm_object.action_list()} + does_not_intersect( + action_names, + MM_CHANGE_RELATED_ACTION_NAMES, + f"One or more MM related actions are visible in actions list of {get_object_represent(adcm_object)}", + ) + + +@allure.step("Check amount of jobs is {expected_amount} and all tasks finish successfully") +def _wait_all_tasks_succeed(client: ADCMClient, expected_amount: int): + jobs = client.job_list() + assert len(jobs) == expected_amount + assert all(job.task().wait() == "success" for job in jobs) + + +def _prepare_new_cluster( + cluster_bundle: Bundle, cluster_name: str +) -> tuple[Cluster, Service, Component, Component, Service, Component, Component]: + cluster = cluster_bundle.cluster_create(cluster_name) + service_with_plugin = cluster.service_add(name="service_with_mm_plugin") + service_second = cluster.service_add(name="service_2") + return ( + cluster, + service_with_plugin, + service_with_plugin.component(name="component_with_mm_plugin"), + service_with_plugin.component(name="component_wo_mm_plugin"), + service_second, + service_second.component(name="component_with_mm_plugin"), + service_second.component(name="component_wo_mm_plugin"), + ) + + +def _map_components_to_hosts( + hosts: tuple[Host, Host, Host], components: tuple[Component, Component, Component, Component] +) -> None: + host_1, host_2, host_3 = hosts + component_1, component_2, component_3, component_4 = components + cluster = component_1.cluster() + + for host in (host_1, host_2, host_3): + cluster.host_add(host) + host.reread() + + cluster.hostcomponent_set( + (host_1, component_1), + (host_2, component_1), + (host_3, component_2), + (host_1, component_3), + (host_2, component_3), + (host_3, component_3), + (host_1, component_4), + (host_2, component_4), + (host_3, component_4), + ) diff --git a/tests/functional/maintenance_mode/test_concerns.py b/tests/functional/maintenance_mode/test_concerns.py index 30faf25248..c2acac99f1 100644 --- a/tests/functional/maintenance_mode/test_concerns.py +++ b/tests/functional/maintenance_mode/test_concerns.py @@ -16,10 +16,13 @@ import allure import pytest -from adcm_client.objects import Host, Provider, Cluster - +from adcm_client.objects import Cluster, Host, Provider from tests.functional.conftest import only_clean_adcm -from tests.functional.maintenance_mode.conftest import BUNDLES_DIR, turn_mm_on, turn_mm_off +from tests.functional.maintenance_mode.conftest import ( + BUNDLES_DIR, + turn_mm_off, + turn_mm_on, +) from tests.functional.tools import AnyADCMObject # pylint: disable=redefined-outer-name @@ -48,7 +51,9 @@ def _set_host_config(provider_host_with_concerns) -> None: @pytest.mark.usefixtures('_set_provider_config') -def test_mm_host_with_concern_not_raising_issue_on_cluster_objects(cluster_with_mm, provider_host_with_concerns): +def test_mm_host_with_concern_not_raising_issue_on_cluster_objects( + api_client, cluster_with_mm, provider_host_with_concerns +): """ Test that when there's a concern on host that is in MM and mapped to a cluster, cluster objects don't have issues, but host does @@ -66,19 +71,19 @@ def test_mm_host_with_concern_not_raising_issue_on_cluster_objects(cluster_with_ _check_concerns_are_presented_on_cluster_objects(cluster_with_mm) with allure.step('Turn MM on and check that concerns are gone'): - turn_mm_on(host) + turn_mm_on(api_client, host) _check_concern_is_presented_on_object(host, f'host {host.fqdn}') _check_no_concerns_on_cluster_objects(cluster_with_mm) with allure.step('Turn MM off and check that concerns have returned'): - turn_mm_off(host) + turn_mm_off(api_client, host) _check_concern_is_presented_on_object(host, f'host {host.fqdn}') _check_concerns_are_presented_on_cluster_objects(cluster_with_mm) @pytest.mark.usefixtures('_set_host_config') def test_host_from_provider_with_concern_not_raising_issue_on_cluster_objects( - cluster_with_mm, provider_host_with_concerns + api_client, cluster_with_mm, provider_host_with_concerns ): """ Test that when there's a concern on provider, but not on host, @@ -107,13 +112,13 @@ def test_host_from_provider_with_concern_not_raising_issue_on_cluster_objects( _check_concerns_are_presented_on_cluster_objects(cluster_with_mm) with allure.step('Turn MM on and check that concerns are gone'): - turn_mm_on(host) + turn_mm_on(api_client, host) _check_concern_is_presented_on_object(host, f'host {host.fqdn}') _check_concern_is_presented_on_object(provider, f'provider {provider.name}') _check_no_concerns_on_cluster_objects(cluster_with_mm) with allure.step('Turn MM off and check that concerns have returned'): - turn_mm_off(host) + turn_mm_off(api_client, host) _check_concern_is_presented_on_object(host, f'host {host.fqdn}') _check_concern_is_presented_on_object(provider, f'provider {provider.name}') _check_concerns_are_presented_on_cluster_objects(cluster_with_mm) diff --git a/tests/functional/maintenance_mode/test_hosts_behavior.py b/tests/functional/maintenance_mode/test_hosts_behavior.py index 492c555b36..0f5d569128 100644 --- a/tests/functional/maintenance_mode/test_hosts_behavior.py +++ b/tests/functional/maintenance_mode/test_hosts_behavior.py @@ -14,31 +14,45 @@ Test hosts maintenance mode behaviour """ -from typing import Iterable, Tuple, Set +from typing import Iterable, Set, Tuple import allure import pytest -from adcm_client.objects import Host, Cluster, Component - -from tests.library.assertions import sets_are_equal, is_empty, expect_api_error, expect_no_api_error -from tests.library.errorcodes import MAINTENANCE_MODE_NOT_AVAILABLE, ACTION_ERROR, ADCMError, INVALID_HC_HOST_IN_MM -from tests.functional.tools import AnyADCMObject, get_object_represent, build_hc_for_hc_acl_action +from adcm_client.objects import Cluster, Component, Host from tests.functional.conftest import only_clean_adcm from tests.functional.maintenance_mode.conftest import ( - MM_IS_ON, - MM_IS_OFF, - MM_IS_DISABLED, + ANOTHER_SERVICE_NAME, BUNDLES_DIR, - DISABLING_CAUSE, DEFAULT_SERVICE_NAME, - ANOTHER_SERVICE_NAME, - turn_mm_on, - turn_mm_off, + MM_IS_OFF, + MM_IS_ON, + MM_NOT_ALLOWED, add_hosts_to_cluster, - remove_hosts_from_cluster, - check_hosts_mm_is, + check_mm_availability, + check_mm_is, + expect_changing_mm_fail, get_disabled_actions_names, get_enabled_actions_names, + remove_hosts_from_cluster, + turn_mm_off, + turn_mm_on, +) +from tests.functional.tools import ( + AnyADCMObject, + build_hc_for_hc_acl_action, + get_object_represent, +) +from tests.library.assertions import ( + expect_api_error, + expect_no_api_error, + is_empty, + sets_are_equal, +) +from tests.library.errorcodes import ( + ACTION_ERROR, + INVALID_HC_HOST_IN_MM, + MAINTENANCE_MODE_NOT_AVAILABLE, + ADCMError, ) # pylint: disable=redefined-outer-name @@ -65,7 +79,7 @@ def host_actions_cluster(sdk_client_fs) -> Cluster: ids=lambda x: x.strip('cluster_'), indirect=True, ) -def test_adding_host_to_cluster(cluster_with_mm, cluster_without_mm, hosts): +def test_adding_host_to_cluster(api_client, cluster_with_mm, cluster_without_mm, hosts): """ Test that adding/removing host to/from cluster affects "maintenance_mode" flag on host """ @@ -73,35 +87,31 @@ def test_adding_host_to_cluster(cluster_with_mm, cluster_without_mm, hosts): hosts_to_cluster_without_mm = third_host, _ = hosts[2:4] free_hosts = hosts[-2:] - check_hosts_mm_is(MM_IS_DISABLED, *hosts) + check_mm_availability(MM_NOT_ALLOWED, *hosts) add_hosts_to_cluster(cluster_without_mm, hosts_to_cluster_without_mm) - check_hosts_mm_is(MM_IS_DISABLED, *hosts) + check_mm_availability(MM_NOT_ALLOWED, *hosts) add_hosts_to_cluster(cluster_with_mm, hosts_to_cluster_with_mm) - check_hosts_mm_is(MM_IS_OFF, *hosts_to_cluster_with_mm) - check_hosts_mm_is(MM_IS_DISABLED, *hosts_to_cluster_without_mm, *free_hosts) + check_mm_is(MM_IS_OFF, *hosts_to_cluster_with_mm) + check_mm_availability(MM_NOT_ALLOWED, *hosts_to_cluster_without_mm, *free_hosts) - turn_mm_on(first_host) - check_hosts_mm_is(MM_IS_ON, first_host) - check_hosts_mm_is(MM_IS_OFF, second_host) - check_hosts_mm_is(MM_IS_DISABLED, *hosts_to_cluster_without_mm, *free_hosts) + turn_mm_on(api_client, first_host) + check_mm_is(MM_IS_ON, first_host) + check_mm_is(MM_IS_OFF, second_host) + check_mm_availability(MM_NOT_ALLOWED, *hosts_to_cluster_without_mm, *free_hosts) - expect_api_error( - f'turn MM "on" host {third_host.fqdn}', turn_mm_on, third_host, err_=MAINTENANCE_MODE_NOT_AVAILABLE - ) - check_hosts_mm_is(MM_IS_DISABLED, *hosts_to_cluster_without_mm) - expect_api_error( - f'turn MM "off" host {third_host.fqdn}', turn_mm_off, third_host, err_=MAINTENANCE_MODE_NOT_AVAILABLE - ) - check_hosts_mm_is(MM_IS_DISABLED, *hosts_to_cluster_without_mm) + expect_changing_mm_fail(api_client, third_host, MM_IS_ON) + check_mm_availability(MM_NOT_ALLOWED, *hosts_to_cluster_without_mm) + check_mm_is(MM_IS_OFF, third_host) + check_mm_availability(MM_NOT_ALLOWED, *hosts_to_cluster_without_mm) remove_hosts_from_cluster(cluster_with_mm, hosts_to_cluster_with_mm) remove_hosts_from_cluster(cluster_without_mm, hosts_to_cluster_without_mm) - check_hosts_mm_is(MM_IS_DISABLED, *hosts) + check_mm_availability(MM_NOT_ALLOWED, *hosts) -def test_mm_hosts_not_allowed_in_hc_map(cluster_with_mm, hosts): +def test_mm_hosts_not_allowed_in_hc_map(api_client, cluster_with_mm, hosts): """ Test that hosts in MM aren't allowed to be used in hostcomponent map """ @@ -110,7 +120,7 @@ def test_mm_hosts_not_allowed_in_hc_map(cluster_with_mm, hosts): host_in_mm, regular_host, *_ = hosts add_hosts_to_cluster(cluster, (host_in_mm, regular_host)) - turn_mm_on(host_in_mm) + turn_mm_on(api_client, host_in_mm) with allure.step('Try to set HC map with one of hosts in MM'): _expect_hc_set_to_fail(cluster, [(host_in_mm, first_component)], err_=INVALID_HC_HOST_IN_MM) _expect_hc_set_to_fail( @@ -128,7 +138,7 @@ def test_mm_hosts_not_allowed_in_hc_map(cluster_with_mm, hosts): _check_hostcomponents_are_equal(cluster.hostcomponent(), hc_with_regular_host) -def test_actions_not_allowed_in_mm_are_disabled_due_to_host_in_mm(cluster_with_mm, hosts): +def test_actions_not_allowed_in_mm_are_disabled_due_to_host_in_mm(api_client, cluster_with_mm, hosts): """ Test that actions that aren't allowed in maintenance mode - are disabled when MM host is in vertical hierarchy (cluster-service-component) @@ -160,16 +170,16 @@ def test_actions_not_allowed_in_mm_are_disabled_due_to_host_in_mm(cluster_with_m check_all_actions_are_enabled(*all_objects) with allure.step(f'Turn MM "on" on host {first_host.fqdn} and check actions are disabled correctly'): - turn_mm_on(first_host) + turn_mm_on(api_client, first_host) check_actions_are_disabled_on(cluster, first_service, first_component) check_all_actions_are_enabled(second_component, second_service, *second_service_components) with allure.step(f'Turn MM "off" on host {first_host.fqdn} and expect all objects\' actions to be enabled'): - turn_mm_off(first_host) + turn_mm_off(api_client, first_host) check_all_actions_are_enabled(*all_objects) -def test_provider_and_host_actions_affected_by_mm(cluster_with_mm, provider, hosts): +def test_provider_and_host_actions_affected_by_mm(api_client, cluster_with_mm, provider, hosts): """ Test that host in MM doesn't affect provider's actions, but cleans action list of this host (including `host_action: true`) @@ -189,14 +199,14 @@ def _available_actions_are(on_first_host: set, on_second_host: set, on_provider: cluster.hostcomponent_set((first_host, component), (second_host, component)) _available_actions_are(actions_on_host, actions_on_host, actions_on_provider) - turn_mm_on(first_host) - _available_actions_are(set(), actions_on_host, actions_on_provider) + turn_mm_on(api_client, first_host) + _available_actions_are(actions_on_host, actions_on_host, actions_on_provider) - turn_mm_off(first_host) + turn_mm_off(api_client, first_host) _available_actions_are(actions_on_host, actions_on_host, actions_on_provider) -def test_host_actions_on_another_component_host(host_actions_cluster, hosts): +def test_host_actions_on_another_component_host(api_client, host_actions_cluster, hosts): """ Test host_actions from cluster, service and component are working correctly with regular host with component that is also mapped to an MM host @@ -213,7 +223,7 @@ def test_host_actions_on_another_component_host(host_actions_cluster, hosts): add_hosts_to_cluster(cluster, (host_in_mm, regular_host)) cluster.hostcomponent_set((host_in_mm, component), (regular_host, component)) - turn_mm_on(host_in_mm) + turn_mm_on(api_client, host_in_mm) enabled_actions = get_enabled_actions_names(regular_host) disabled_actions = get_disabled_actions_names(regular_host) @@ -225,7 +235,7 @@ def test_host_actions_on_another_component_host(host_actions_cluster, hosts): ) -def test_running_disabled_actions_is_forbidden(cluster_with_mm, hosts): +def test_running_disabled_actions_is_forbidden(api_client, cluster_with_mm, hosts): """ Test that disabled actions actually can't be launched and that host's filtered action can't be launched directly via API @@ -241,50 +251,55 @@ def test_running_disabled_actions_is_forbidden(cluster_with_mm, hosts): host_action_from_itself = first_host.action(name='default_action') host_action_from_component = first_host.action(name='see_me_on_host') - turn_mm_on(first_host) + turn_mm_on(api_client, first_host) expect_api_error( 'run not allowed in MM action on service', service.action(name=ACTION_NOT_ALLOWED_IN_MM).run, err_=ACTION_ERROR, ) - expect_no_api_error('run allowed in MM action on service', service.action(name=ACTION_ALLOWED_IN_MM).run) + task = expect_no_api_error('run allowed in MM action on service', service.action(name=ACTION_ALLOWED_IN_MM).run) expect_api_error('run action on host in MM', host_action_from_itself.run, err_=ACTION_ERROR) - expect_api_error( - 'run action `host_action: true` on host in MM', - host_action_from_component.run, - err_=ACTION_ERROR, - ) + task.wait() + expect_no_api_error('run action `host_action: true` on host in MM', host_action_from_component.run) @only_clean_adcm -def test_host_actions_with_mm(cluster_with_mm, hosts): +def test_host_actions_with_mm(api_client, cluster_with_mm, hosts): """ Test that host actions (`host_action: true`) are working correctly with `allow_in_maintenance_mode` flag """ allowed_action = 'allowed_in_mm' not_allowed_action = 'not_allowed_in_mm' + default_action_of_host = 'default_action' + all_actions = {allowed_action, not_allowed_action, default_action_of_host} cluster = cluster_with_mm component = cluster.service_add(name='host_actions').component() host_in_mm, regular_host, *_ = hosts add_hosts_to_cluster(cluster, (host_in_mm, regular_host)) cluster.hostcomponent_set((host_in_mm, component), (regular_host, component)) - turn_mm_on(host_in_mm) + turn_mm_on(api_client, host_in_mm) - check_visible_actions(host_in_mm, set()) - check_visible_actions(regular_host, {allowed_action, not_allowed_action, 'default_action'}) + check_visible_actions(host_in_mm, all_actions) + check_visible_actions(regular_host, all_actions) + + expect_no_api_error('run allowed in MM action', host_in_mm.action(name=allowed_action).run).wait() expect_api_error( 'run not allowed in MM action', regular_host.action(name=not_allowed_action).run, err_=ACTION_ERROR ) + expect_api_error('run not allowed in MM action', host_in_mm.action(name=not_allowed_action).run, err_=ACTION_ERROR) + expect_api_error( + 'run not allowed in MM action of host', host_in_mm.action(name=default_action_of_host).run, err_=ACTION_ERROR + ) expect_no_api_error('run allowed in MM action', regular_host.action(name=allowed_action).run) @only_clean_adcm -def test_hc_acl_action_with_mm(cluster_with_mm, hosts): +def test_hc_acl_action_with_mm(api_client, cluster_with_mm, hosts): """ Test behaviour of actions with `hc_acl`: - adding component on host in MM should be forbidden @@ -303,9 +318,9 @@ def test_hc_acl_action_with_mm(cluster_with_mm, hosts): (mm_host_3, first_component), (mm_host_3, second_component), ) - turn_mm_on(mm_host_1) - turn_mm_on(mm_host_2) - turn_mm_on(mm_host_3) + turn_mm_on(api_client, mm_host_1) + turn_mm_on(api_client, mm_host_2) + turn_mm_on(api_client, mm_host_3) with allure.step('Check "adding" component to a host in MM is forbidden'): expect_api_error( @@ -364,7 +379,7 @@ def test_hosts_in_not_blocking_regular_hc_acl(cluster_with_mm, hosts): @only_clean_adcm -def test_state_after_mm_switch(cluster_with_mm, hosts): +def test_state_after_mm_switch(api_client, cluster_with_mm, hosts): """ Test that state stays the same after switch of MM flag """ @@ -373,9 +388,9 @@ def test_state_after_mm_switch(cluster_with_mm, hosts): add_hosts_to_cluster(cluster_with_mm, [host]) check_state(host, expected_state) - turn_mm_on(host) + turn_mm_on(api_client, host) check_state(host, expected_state) - turn_mm_off(host) + turn_mm_off(api_client, host) check_state(host, expected_state) remove_hosts_from_cluster(cluster_with_mm, [host]) check_state(host, expected_state) @@ -395,18 +410,18 @@ def test_set_value_not_in_enum_in_mm(cluster_with_mm, hosts): expect_api_error(f'Set value "{mm_value}" to MM', lambda: host.maintenance_mode_set(mm_value)) -def test_mm_after_cluster_deletion(cluster_with_mm, hosts): +def test_mm_after_cluster_deletion(api_client, cluster_with_mm, hosts): """ Test that MM on hosts from deleted cluster is "disabled" """ host_1, host_2, *_ = hosts add_hosts_to_cluster(cluster_with_mm, [host_1, host_2]) - turn_mm_on(host_2) - check_hosts_mm_is(MM_IS_OFF, host_1) - check_hosts_mm_is(MM_IS_ON, host_2) + turn_mm_on(api_client, host_2) + check_mm_is(MM_IS_OFF, host_1) + check_mm_is(MM_IS_ON, host_2) with allure.step('Delete cluster'): cluster_with_mm.delete() - check_hosts_mm_is(MM_IS_DISABLED, host_1, host_2) + check_mm_availability(MM_NOT_ALLOWED, host_1, host_2) def check_actions_are_disabled_on(*objects) -> None: @@ -473,13 +488,3 @@ def _expect_hc_set_to_fail( def _check_hostcomponents_are_equal(actual_hc, expected_hc) -> None: """Compare hostcomponent maps directly""" assert actual_hc == expected_hc, f'Hostcomponent map has changed.\nExpected:\n{expected_hc}\nActual:\n{actual_hc}' - - -def _get_enabled_actions_names(adcm_object: AnyADCMObject) -> Set[str]: - """Get actions that aren't disabled by maintenance mode""" - return {action.name for action in adcm_object.action_list() if action.disabling_cause != DISABLING_CAUSE} - - -def _get_disabled_actions_names(adcm_object: AnyADCMObject) -> Set[str]: - """Get actions disabled because of maintenance mode""" - return {action.name for action in adcm_object.action_list() if action.disabling_cause == DISABLING_CAUSE} diff --git a/tests/functional/maintenance_mode/test_inventory_modification.py b/tests/functional/maintenance_mode/test_inventory_modification.py index 80b033fdbe..4448b50bd9 100644 --- a/tests/functional/maintenance_mode/test_inventory_modification.py +++ b/tests/functional/maintenance_mode/test_inventory_modification.py @@ -19,21 +19,36 @@ import allure import pytest -from adcm_client.objects import Action, Cluster, Component, GroupConfig, Host +from adcm_client.objects import ( + Action, + ADCMClient, + Cluster, + Component, + GroupConfig, + Host, + Service, +) from adcm_pytest_plugin.docker_utils import ADCM from adcm_pytest_plugin.utils import get_or_add_service - from tests.functional.conftest import only_clean_adcm from tests.functional.maintenance_mode.conftest import ( + BUNDLES_DIR, DEFAULT_SERVICE_NAME, FIRST_COMPONENT, + MM_IS_OFF, MM_IS_ON, SECOND_COMPONENT, add_hosts_to_cluster, + set_maintenance_mode, turn_mm_on, ) from tests.functional.maintenance_mode.test_hosts_behavior import ACTION_ALLOWED_IN_MM -from tests.functional.tools import build_hc_for_hc_acl_action, create_config_group_and_add_host, get_inventory_file +from tests.functional.tools import ( + build_hc_for_hc_acl_action, + create_config_group_and_add_host, + get_inventory_file, + get_object_represent, +) from tests.library.assertions import sets_are_equal # pylint: disable=redefined-outer-name @@ -43,6 +58,8 @@ DEFAULT_ACTION_NAME = "default_action" HC_ACL_SERVICE_NAME = "hc_acl_service" +DEFAULT_SERVICE_COMPONENT_MM_BUNDLE = "cluster_mm_allowed" + @pytest.fixture(params=[pytest.param(DEFAULT_SERVICE_NAME, id="default_service")]) def cluster_with_hc_set(request, cluster_with_mm, hosts) -> Cluster: @@ -65,6 +82,20 @@ def cluster_with_hc_set(request, cluster_with_mm, hosts) -> Cluster: return cluster_with_mm +@pytest.fixture(params=[pytest.param(DEFAULT_SERVICE_COMPONENT_MM_BUNDLE, id="default_cluster")]) +def cluster_with_service_component_mm( + request, sdk_client_fs, hosts +) -> tuple[Cluster, Service, Component, Component, Host, Host]: + bundle = sdk_client_fs.upload_from_fs(BUNDLES_DIR / request.param) + cluster = bundle.cluster_create("Cluster with allowed MM") + service = cluster.service_add(name="test_service") + first_component = service.component(name="first_component") + second_component = service.component(name="second_component") + host_1, host_2, *_ = hosts + cluster.hostcomponent_set((cluster.host_add(host_1), first_component), (cluster.host_add(host_2), second_component)) + return cluster, service, first_component, second_component, host_1, host_2 + + @pytest.fixture() def config_groups(cluster_with_hc_set) -> Tuple[GroupConfig, GroupConfig, GroupConfig]: """Add config group to the cluster, service and one of components""" @@ -88,7 +119,7 @@ def host_not_in_config_group(cluster_with_hc_set, config_groups) -> Host: return [host for host in cluster_with_hc_set.host_list() if host.fqdn not in hosts_in_groups][0] -def test_hosts_in_mm_removed_from_inventory(adcm_fs, cluster_with_hc_set): +def test_hosts_in_mm_removed_from_inventory(api_client, adcm_fs, cluster_with_hc_set): """Test filtering of hosts in inventory file when hosts are in MM""" host, *_ = cluster_with_hc_set.host_list() service = cluster_with_hc_set.service() @@ -97,13 +128,15 @@ def test_hosts_in_mm_removed_from_inventory(adcm_fs, cluster_with_hc_set): inventory = run_action_and_get_inventory(action_on_service, adcm_fs) check_all_hosts_are_present(inventory, cluster_with_hc_set) - turn_mm_on(host) + turn_mm_on(api_client, host) inventory = run_action_and_get_inventory(action_on_service, adcm_fs) check_hosts_in_mm_are_absent(inventory, cluster_with_hc_set) -def test_hosts_in_mm_removed_from_group_config(adcm_fs, cluster_with_hc_set, config_groups, host_not_in_config_group): +def test_hosts_in_mm_removed_from_group_config( + api_client, adcm_fs, cluster_with_hc_set, config_groups, host_not_in_config_group +): """Test filtering of hosts in inventory file when hosts are in MM and in config group""" *_, component_group = config_groups component: Component = cluster_with_hc_set.service().component(name=FIRST_COMPONENT) @@ -112,14 +145,14 @@ def test_hosts_in_mm_removed_from_group_config(adcm_fs, cluster_with_hc_set, con inventory = run_action_and_get_inventory(action_on_component, adcm_fs) check_all_hosts_are_present(inventory, cluster_with_hc_set) - turn_mm_on(component_group.hosts()[0]) - turn_mm_on(host_not_in_config_group) + turn_mm_on(api_client, component_group.hosts()[0]) + turn_mm_on(api_client, host_not_in_config_group) inventory = run_action_and_get_inventory(action_on_component, adcm_fs) check_hosts_in_mm_are_absent(inventory, cluster_with_hc_set) -def test_hosts_filtered_when_added_to_group_config_after_entering_mm(adcm_fs, cluster_with_hc_set): +def test_hosts_filtered_when_added_to_group_config_after_entering_mm(api_client, adcm_fs, cluster_with_hc_set): """Test filtering of hosts in inventory file when host entered MM and then added to config group""" component: Component = cluster_with_hc_set.service().component(name=FIRST_COMPONENT) host = cluster_with_hc_set.host( @@ -127,7 +160,7 @@ def test_hosts_filtered_when_added_to_group_config_after_entering_mm(adcm_fs, cl ) action_on_component = component.action(name=ACTION_ALLOWED_IN_MM) - turn_mm_on(host) + turn_mm_on(api_client, host) create_config_group_and_add_host("Component Group", component, host) inventory = run_action_and_get_inventory(action_on_component, adcm_fs) @@ -135,7 +168,7 @@ def test_hosts_filtered_when_added_to_group_config_after_entering_mm(adcm_fs, cl @pytest.mark.parametrize("cluster_with_hc_set", [HC_ACL_SERVICE_NAME], indirect=True) -def test_host_filtering_with_hc_acl(adcm_fs, cluster_with_hc_set: Cluster, hosts): +def test_host_filtering_with_hc_acl(api_client, adcm_fs, cluster_with_hc_set: Cluster, hosts): """Test filtering of hosts in MM in inventory groups when action have `hc_acl` directive""" cluster = cluster_with_hc_set service = cluster.service(name=HC_ACL_SERVICE_NAME) @@ -145,7 +178,7 @@ def test_host_filtering_with_hc_acl(adcm_fs, cluster_with_hc_set: Cluster, hosts add_hosts_to_cluster(cluster, [free_host]) - turn_mm_on(host) + turn_mm_on(api_client, host) inventory = run_action_and_get_inventory( service.action(name="change"), @@ -156,6 +189,37 @@ def test_host_filtering_with_hc_acl(adcm_fs, cluster_with_hc_set: Cluster, hosts _check_add_remove_groups(inventory, add={free_host.fqdn}, remove=set()) +def test_mm_flag_on_service_and_components(cluster_with_service_component_mm, sdk_client_fs, api_client, adcm_fs: ADCM): + """Test that MM flag is set correctly on services and components in inventory""" + action_name = "allowed_in_mm" + _, service, first_component, second_component, _, second_host = cluster_with_service_component_mm + + set_maintenance_mode(api_client=api_client, adcm_object=service, maintenance_mode=MM_IS_ON) + check_mm_flag_in_inventory( + client=sdk_client_fs, + inventory=run_action_and_get_inventory(service.action(name=action_name), adcm_fs), + expect_on=(service, first_component, second_component), + ) + + set_maintenance_mode(api_client=api_client, adcm_object=service, maintenance_mode=MM_IS_OFF) + set_maintenance_mode(api_client=api_client, adcm_object=first_component, maintenance_mode=MM_IS_ON) + check_mm_flag_in_inventory( + client=sdk_client_fs, + inventory=run_action_and_get_inventory(service.action(name=action_name), adcm_fs), + expect_on=(first_component,), + expect_off=(service, second_component), + ) + + set_maintenance_mode(api_client=api_client, adcm_object=second_host, maintenance_mode=MM_IS_ON) + set_maintenance_mode(api_client=api_client, adcm_object=first_component, maintenance_mode=MM_IS_OFF) + check_mm_flag_in_inventory( + client=sdk_client_fs, + inventory=run_action_and_get_inventory(service.action(name=action_name), adcm_fs), + expect_on=(second_component,), + expect_off=(service, first_component), + ) + + def run_action_and_get_inventory(action: Action, adcm: ADCM, **run_kwargs) -> dict: """Run action and get inventory file contents from container""" with allure.step(f"Run action {action.name}"): @@ -189,6 +253,28 @@ def check_hosts_in_mm_are_absent(inventory: dict, cluster: Cluster, service_name _check_mm_groups(inventory, service_name, component, hostnames_in_mm) +@allure.step("Check MM attributes of services and components in inventory") +def check_mm_flag_in_inventory( + client: ADCMClient, + inventory: dict, + expect_on: tuple[Service | Component, ...] = (), + expect_off: tuple[Service | Component, ...] = (), +): + # pylint: disable=unsupported-membership-test,unsubscriptable-object + for obj in expect_on: + with allure.step(f"Check that MM of {get_object_represent(obj)} is 'ON' in inventory"): + node = _get_object_node(inventory=inventory, client=client, adcm_object=obj) + assert "maintenance_mode" in node, f"No field 'maintenance_mode' found within {node.keys()}" + assert node["maintenance_mode"] is True + + for obj in expect_off: + with allure.step(f"Check that MM of {get_object_represent(obj)} is 'OFF' in inventory"): + node = _get_object_node(inventory=inventory, client=client, adcm_object=obj) + assert "maintenance_mode" in node, f"No field 'maintenance_mode' found within {node.keys()}" + assert node["maintenance_mode"] is False + # pylint: enable=unsupported-membership-test,unsubscriptable-object + + @allure.step("Check that correct hosts are presented in regular groups") def _check_groups( inventory, expected_hosts: set, expected_on_second_component: set, service_name: str = DEFAULT_SERVICE_NAME @@ -245,3 +331,14 @@ def _check_add_remove_groups(inventory, add: Set[str], remove: Set[str]): with allure.step(f"Check node {node_name}"): actual_hosts = set(node_value["hosts"].keys()) sets_are_equal(actual_hosts, remove, '"remove" node is incorrect') + + +def _get_object_node(inventory: dict, client: ADCMClient, adcm_object: Service | Component) -> dict: + services_node = inventory["all"]["children"]["CLUSTER"]["vars"]["services"] + match adcm_object: + case Service(): + return services_node[adcm_object.name] + case Component(): + return services_node[client.service(id=adcm_object.service_id).name][adcm_object.name] + case _: + raise ValueError("`adcm_object` can be only Service or Component") diff --git a/tests/functional/maintenance_mode/test_mm_actions.py b/tests/functional/maintenance_mode/test_mm_actions.py new file mode 100644 index 0000000000..47d9e2f564 --- /dev/null +++ b/tests/functional/maintenance_mode/test_mm_actions.py @@ -0,0 +1,97 @@ +# 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. + +""" +Test designed to check that actions are disallowed when cluster object in MM +""" + +import allure +import pytest +from adcm_client.objects import Cluster +from tests.functional.conftest import only_clean_adcm +from tests.functional.maintenance_mode.conftest import ( + BUNDLES_DIR, + MM_IS_OFF, + MM_IS_ON, + add_hosts_to_cluster, + check_actions_availability, + check_mm_is, + set_maintenance_mode, +) + +# pylint: disable=redefined-outer-name + +CLUSTER_OBJECTS = ("cluster", "service", "component") +pytestmark = [only_clean_adcm] + + +@pytest.fixture() +def cluster_mm_action_disallowed(sdk_client_fs) -> Cluster: + """Upload and create cluster with service and component actions from cluster""" + bundle = sdk_client_fs.upload_from_fs(BUNDLES_DIR / "cluster_mm_actions_disallowed") + cluster = bundle.cluster_create("Cluster with actions") + cluster.service_add(name="first_service") + return cluster + + +def test_mm_action(api_client, cluster_mm_action_disallowed, hosts): + """ + Test to check actions for cluster objects are disallowed when object in MM + """ + host_in_mm, regular_host, *_ = hosts + cluster = cluster_mm_action_disallowed + first_service = cluster.service() + first_component = first_service.component(name="first_component") + second_component = first_service.component(name="second_component") + + add_hosts_to_cluster(cluster, (host_in_mm, regular_host)) + cluster.hostcomponent_set((host_in_mm, first_component), (regular_host, second_component)) + + with allure.step("Switch host MM to 'ON' and check enabled and disabled actions"): + set_maintenance_mode(api_client, host_in_mm, MM_IS_ON) + check_mm_is(MM_IS_ON, first_component, host_in_mm) + + expected_enabled = {f"{obj_type}_action_allowed" for obj_type in CLUSTER_OBJECTS if "component" in obj_type} + expected_disabled = {f"{obj_type}_action_disallowed" for obj_type in CLUSTER_OBJECTS if "component" in obj_type} + + check_actions_availability( + adcm_object=first_component, expected_enabled=expected_enabled, expected_disabled=expected_disabled + ) + + with allure.step( + "Switch host MM to 'OFF', switch component with action to MM 'ON' and check enabled and disabled actions" + ): + set_maintenance_mode(api_client, host_in_mm, MM_IS_OFF) + set_maintenance_mode(api_client, first_component, MM_IS_ON) + check_mm_is(MM_IS_ON, first_component) + + expected_enabled = {f"{obj_type}_action_allowed" for obj_type in CLUSTER_OBJECTS if "component" in obj_type} + expected_disabled = {f"{obj_type}_action_disallowed" for obj_type in CLUSTER_OBJECTS if "component" in obj_type} + + check_actions_availability( + adcm_object=first_component, expected_enabled=expected_enabled, expected_disabled=expected_disabled + ) + + with allure.step("Switch component MM to 'OFF', switch service MM to 'ON' and check enabled and disabled actions"): + set_maintenance_mode(api_client, first_component, MM_IS_OFF) + set_maintenance_mode(api_client, first_service, MM_IS_ON) + check_mm_is(MM_IS_ON, first_service, first_component, second_component) + + expected_enabled = {f"{obj_type}_action_allowed" for obj_type in CLUSTER_OBJECTS if "service" in obj_type} + expected_disabled = {f"{obj_type}_action_disallowed" for obj_type in CLUSTER_OBJECTS if "service" in obj_type} + + check_actions_availability( + adcm_object=first_service, expected_enabled=expected_enabled, expected_disabled=expected_disabled + ) + check_actions_availability( + adcm_object=first_service, expected_enabled=expected_enabled, expected_disabled=expected_disabled + ) diff --git a/tests/functional/maintenance_mode/test_mm_bundle_validation.py b/tests/functional/maintenance_mode/test_mm_bundle_validation.py index cb4ffa1de1..5993b755d6 100644 --- a/tests/functional/maintenance_mode/test_mm_bundle_validation.py +++ b/tests/functional/maintenance_mode/test_mm_bundle_validation.py @@ -15,8 +15,7 @@ import allure import pytest from coreapi.exceptions import ErrorMessage - -from tests.conftest import DUMMY_CLUSTER_BUNDLE, DUMMY_ACTION +from tests.conftest import DUMMY_ACTION, DUMMY_CLUSTER_BUNDLE from tests.functional.conftest import only_clean_adcm from tests.library.errorcodes import INVALID_OBJECT_DEFINITION diff --git a/tests/functional/maintenance_mode/test_mm_concerns.py b/tests/functional/maintenance_mode/test_mm_concerns.py new file mode 100644 index 0000000000..639b1243bc --- /dev/null +++ b/tests/functional/maintenance_mode/test_mm_concerns.py @@ -0,0 +1,360 @@ +# 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. + +"""Test designed to check behaviour of service and components with concern when they switched to MM 'ON'""" + +import allure +import pytest +from adcm_client.objects import ADCMClient, Cluster, Provider +from tests.functional.conftest import only_clean_adcm +from tests.functional.maintenance_mode.conftest import ( + BUNDLES_DIR, + MM_IS_OFF, + MM_IS_ON, + add_hosts_to_cluster, + check_concerns_on_object, + check_mm_is, + check_no_concerns_on_objects, + set_maintenance_mode, +) +from tests.library.api.core import RequestResult + +# pylint: disable=redefined-outer-name + +pytestmark = [only_clean_adcm] +DEFAULT_CLUSTER_PARAM = 12 +EXPECTED_ERROR = "LOCK_ERROR" + + +@pytest.fixture() +def cluster_actions(sdk_client_fs) -> Cluster: + bundle = sdk_client_fs.upload_from_fs(BUNDLES_DIR / "cluster_with_concerns" / "concern_cluster_action") + cluster = bundle.cluster_create("Cluster actions") + cluster.service_add(name="first_service") + return cluster + + +@pytest.fixture() +def cluster_with_concern(sdk_client_fs) -> Cluster: + """Create cluster and add service""" + bundle = sdk_client_fs.upload_from_fs(BUNDLES_DIR / "cluster_with_concerns" / "concern_cluster") + cluster = bundle.cluster_create("Cluster concern") + cluster.service_add(name="first_service") + return cluster + + +@pytest.fixture() +def provider_with_concern(sdk_client_fs) -> Provider: + """Create provider and host""" + bundle = sdk_client_fs.upload_from_fs(BUNDLES_DIR / "provider_with_issues") + provider = bundle.provider_create("Provider with concerns") + return provider + + +def test_mm_concern_cluster(api_client, cluster_with_concern, hosts): + """ + Test to check behaviour cluster objects when cluster have a concern + cluster, service and second_component have a concern + """ + first_host, second_host, *_ = hosts + cluster = cluster_with_concern + first_service = cluster.service(name="first_service") + first_component = first_service.component(name="first_component") + second_component = first_service.component(name="second_component") + + add_hosts_to_cluster(cluster, (first_host, second_host)) + cluster.hostcomponent_set( + (first_host, first_component), + (second_host, second_component), + ) + + for obj in (cluster, first_service, second_component, second_host): + check_concerns_on_object( + adcm_object=obj, expected_concerns={cluster.name, first_service.name, second_component.name} + ) + for obj in (first_component, first_host): + check_concerns_on_object(adcm_object=obj, expected_concerns={cluster.name, first_service.name}) + + with allure.step("Switch service MM 'ON' and check cluster objects"): + set_maintenance_mode(api_client, first_service, MM_IS_ON) + + check_mm_is(MM_IS_ON, first_service, first_component, second_component) + check_mm_is(MM_IS_OFF, first_host, second_host) + + for obj in (cluster, first_component, first_host, second_host): + check_concerns_on_object(adcm_object=obj, expected_concerns={cluster.name}) + check_concerns_on_object(adcm_object=first_service, expected_concerns={cluster.name, first_service.name}) + check_concerns_on_object(adcm_object=second_component, expected_concerns={cluster.name, second_component.name}) + + with allure.step( + "Switch MM 'OFF' on service and component objects," + "switch MM 'ON' on component with concern and check cluster objects" + ): + set_maintenance_mode(api_client, first_service, MM_IS_OFF) + set_maintenance_mode(api_client, second_component, MM_IS_ON) + + check_mm_is(MM_IS_ON, second_component) + check_mm_is(MM_IS_OFF, first_service, first_component, first_host, second_host) + + check_concerns_on_object( + adcm_object=second_component, expected_concerns={cluster.name, first_service.name, second_component.name} + ) + for obj in (cluster, first_service, first_component, first_host, second_host): + check_concerns_on_object(adcm_object=obj, expected_concerns={cluster.name, first_service.name}) + + with allure.step("Switch component to MM 'OFF', switch host to MM 'ON' and check cluster objects"): + set_maintenance_mode(api_client, second_component, MM_IS_OFF) + set_maintenance_mode(api_client, first_host, MM_IS_ON) + + check_mm_is(MM_IS_ON, first_component, first_host) + check_mm_is(MM_IS_OFF, first_service, second_component, second_host) + + for obj in (cluster, first_service, second_component, second_host): + check_concerns_on_object( + adcm_object=obj, expected_concerns={cluster.name, first_service.name, second_component.name} + ) + for obj in (first_component, first_host): + check_concerns_on_object(adcm_object=obj, expected_concerns={cluster.name, first_service.name}) + + with allure.step("Switch another host to MM 'ON' and check cluster objects"): + set_maintenance_mode(api_client, second_host, MM_IS_ON) + + check_mm_is(MM_IS_ON, first_service, first_component, first_host, second_component, second_host) + + for obj in (cluster, first_component, first_host, second_host): + check_concerns_on_object(adcm_object=obj, expected_concerns={cluster.name}) + check_concerns_on_object(adcm_object=first_service, expected_concerns={cluster.name, first_service.name}) + check_concerns_on_object(adcm_object=second_component, expected_concerns={cluster.name, second_component.name}) + + with allure.step("Switch hosts to MM 'OFF' and check cluster objects"): + set_maintenance_mode(api_client, first_host, MM_IS_OFF) + set_maintenance_mode(api_client, second_host, MM_IS_OFF) + + check_mm_is(MM_IS_OFF, first_service, first_component, first_host, second_component, second_host) + + for obj in (cluster, first_service, second_component, second_host): + check_concerns_on_object( + adcm_object=obj, expected_concerns={cluster.name, first_service.name, second_component.name} + ) + for obj in (first_component, first_host): + check_concerns_on_object(adcm_object=obj, expected_concerns={cluster.name, first_service.name}) + + +def test_mm_concern_provider_host(api_client, provider_with_concern, cluster_with_mm, hosts): + """Test to check behaviour provider objects with hosts when provider have a concern""" + first_host, *_ = hosts + cluster = cluster_with_mm + first_service = cluster.service() + first_component = first_service.component() + + provider = provider_with_concern + host_concern = provider.host_create('host-with-concerns') + + add_hosts_to_cluster(cluster, (first_host, host_concern)) + cluster.hostcomponent_set( + (first_host, first_component), + (host_concern, first_component), + ) + for obj in (cluster, first_service, first_component, host_concern): + check_concerns_on_object(adcm_object=obj, expected_concerns={provider.name, host_concern.fqdn}) + + check_concerns_on_object(adcm_object=provider, expected_concerns={provider.name}) + + check_no_concerns_on_objects(first_host) + + with allure.step("Switch service to MM 'ON' and check cluster objects and hosts"): + set_maintenance_mode(api_client=api_client, adcm_object=first_service, maintenance_mode=MM_IS_ON) + check_mm_is(MM_IS_ON, first_service, first_component) + check_mm_is(MM_IS_OFF, first_host, host_concern) + + check_concerns_on_object(adcm_object=provider, expected_concerns={provider.name}) + for obj in (first_component, host_concern): + check_concerns_on_object(adcm_object=obj, expected_concerns={provider.name, host_concern.fqdn}) + + check_no_concerns_on_objects(first_service, first_host) + + with allure.step("Switch component to MM 'ON' and check cluster objects and hosts"): + set_maintenance_mode(api_client=api_client, adcm_object=first_component, maintenance_mode=MM_IS_ON) + check_mm_is(MM_IS_ON, first_service, first_component) + check_mm_is(MM_IS_OFF, first_host, host_concern) + + check_concerns_on_object(adcm_object=provider, expected_concerns={provider.name}) + for obj in (first_component, host_concern): + check_concerns_on_object(adcm_object=obj, expected_concerns={provider.name, host_concern.fqdn}) + + check_no_concerns_on_objects(cluster, first_service, first_host) + + with allure.step( + "Switch service and component to MM 'OFF', " + "switch host without concern to MM 'ON' and check cluster objects and hosts" + ): + set_maintenance_mode(api_client=api_client, adcm_object=first_service, maintenance_mode=MM_IS_OFF) + set_maintenance_mode(api_client=api_client, adcm_object=first_component, maintenance_mode=MM_IS_OFF) + set_maintenance_mode(api_client=api_client, adcm_object=first_host, maintenance_mode=MM_IS_ON) + check_mm_is(MM_IS_ON, first_host) + check_mm_is(MM_IS_OFF, first_service, first_component, host_concern) + + check_concerns_on_object(adcm_object=provider, expected_concerns={provider.name}) + for obj in (cluster, first_service, first_component, host_concern): + check_concerns_on_object(adcm_object=obj, expected_concerns={provider.name, host_concern.fqdn}) + + check_no_concerns_on_objects(first_host) + + with allure.step("Switch host with concern to MM 'ON' and check cluster objects and hosts"): + set_maintenance_mode(api_client=api_client, adcm_object=host_concern, maintenance_mode=MM_IS_ON) + check_mm_is(MM_IS_ON, first_service, first_component, first_host, host_concern) + + check_concerns_on_object(adcm_object=provider, expected_concerns={provider.name}) + check_concerns_on_object(adcm_object=host_concern, expected_concerns={provider.name, host_concern.fqdn}) + check_no_concerns_on_objects(cluster, first_component, first_host) + + with allure.step("Switch all objects to MM 'OFF' and check cluster objects and hosts"): + set_maintenance_mode(api_client=api_client, adcm_object=first_host, maintenance_mode=MM_IS_OFF) + set_maintenance_mode(api_client=api_client, adcm_object=host_concern, maintenance_mode=MM_IS_OFF) + check_mm_is(MM_IS_OFF, first_service, first_component, first_host, host_concern) + + check_concerns_on_object(adcm_object=provider, expected_concerns={provider.name}) + for obj in (cluster, first_service, first_component, host_concern): + check_concerns_on_object(adcm_object=obj, expected_concerns={provider.name, host_concern.fqdn}) + + check_no_concerns_on_objects(first_host) + + +def test_mm_concern_upgrade(api_client, sdk_client_fs, hosts): + """Test to check behaviour objects with concern after upgrade""" + first_host, second_host, *_ = hosts + old_bundle = sdk_client_fs.upload_from_fs(BUNDLES_DIR / "cluster_with_concerns" / "concern_upgrade" / "cluster") + cluster = old_bundle.cluster_create("Old cluster") + + first_service = cluster.service_add(name="test_service") + first_component = first_service.component(name="test_component") + second_component = first_service.component(name="new_component") + + add_hosts_to_cluster(cluster, (first_host, second_host)) + cluster.hostcomponent_set( + (first_host, first_component), + (second_host, second_component), + ) + + cluster_objects = cluster, first_service, first_component, second_component, first_host, second_host + check_no_concerns_on_objects(*cluster_objects) + + with allure.step(f"Switch {second_component.name} without concern to MM 'ON' before upgrade cluster"): + set_maintenance_mode(api_client, second_component, MM_IS_ON) + check_mm_is(MM_IS_ON, second_component) + + with allure.step(f"Upgrade cluster to config with concern on {second_component.name}"): + sdk_client_fs.upload_from_fs(BUNDLES_DIR / "cluster_with_concerns" / "concern_upgrade" / "second_cluster") + upgrade_task = cluster.upgrade().do() + if upgrade_task: + upgrade_task.wait() + cluster.reread() + + with allure.step(f"Check that {second_component.name} have MM 'ON' after upgrade and cluster have concern"): + check_mm_is(MM_IS_ON, second_component) + check_concerns_on_object(adcm_object=second_component, expected_concerns={second_component.name}) + check_no_concerns_on_objects(cluster, first_service, first_component, first_host, second_host) + + with allure.step(f"Switch MM to 'OFF' on {second_component.name} and check concerns on cluster"): + set_maintenance_mode(api_client, second_component, MM_IS_OFF) + check_mm_is(MM_IS_OFF, second_component) + + for obj in (cluster, first_service, second_component, second_host): + check_concerns_on_object(obj, {second_component.name}) + check_no_concerns_on_objects(first_component, first_host) + + +def test_mm_concern_action(api_client, sdk_client_fs, cluster_actions, hosts): + """ + Test to check behaviour objects with concerns and actions + service and second_component have a concern + """ + first_host, second_host, *_ = hosts + cluster = cluster_actions + first_service = cluster.service(name="first_service") + first_component = first_service.component(name="first_component") + second_component = first_service.component(name="second_component") + + add_hosts_to_cluster(cluster, (first_host, second_host)) + cluster.hostcomponent_set( + (first_host, first_component), + (second_host, second_component), + ) + + for obj in (cluster, first_service, second_component, second_host): + check_concerns_on_object(adcm_object=obj, expected_concerns={first_service.name, second_component.name}) + for obj in (first_component, first_host): + check_concerns_on_object(adcm_object=obj, expected_concerns={first_service.name}) + + with allure.step("Switch component with concern and action to MM 'ON' and check cluster objects"): + api_client.component.change_maintenance_mode(second_component.id, MM_IS_ON).check_code(200) + _wait_all_tasks_succeed(sdk_client_fs, 1) + check_mm_is(MM_IS_ON, second_component) + check_mm_is(MM_IS_OFF, first_service, first_component, first_host, second_host) + + check_concerns_on_object( + adcm_object=second_component, expected_concerns={first_service.name, second_component.name} + ) + for obj in (cluster, first_service, first_component, first_host, second_host): + check_concerns_on_object(adcm_object=obj, expected_concerns={first_service.name}) + + with allure.step("Switch service with concern and action to MM 'ON' and check cluster objects"): + api_client.service.change_maintenance_mode(first_service.id, MM_IS_ON).check_code(200) + _wait_all_tasks_succeed(sdk_client_fs, 2) + check_mm_is(MM_IS_ON, first_service, first_component, second_component) + check_mm_is(MM_IS_OFF, first_host, second_host) + + check_concerns_on_object(adcm_object=first_service, expected_concerns={first_service.name}) + check_concerns_on_object(adcm_object=second_component, expected_concerns={second_component.name}) + check_no_concerns_on_objects(cluster, first_host, second_host) + + with allure.step("Switch seervice and component with concern and action to MM 'OFF'"): + api_client.service.change_maintenance_mode(first_service.id, MM_IS_OFF).check_code(200) + _wait_all_tasks_succeed(sdk_client_fs, 3) + api_client.component.change_maintenance_mode(second_component.id, MM_IS_OFF).check_code(200) + _wait_all_tasks_succeed(sdk_client_fs, 4) + check_mm_is(MM_IS_OFF, first_service, first_component, second_component, first_host, second_host) + + for obj in (cluster, first_service, second_component, second_host): + check_concerns_on_object(adcm_object=obj, expected_concerns={first_service.name, second_component.name}) + for obj in (first_component, first_host): + check_concerns_on_object(adcm_object=obj, expected_concerns={first_service.name}) + + with allure.step(f"Fix concern on {first_service.name} and {second_component.name}"): + service_config = first_service.config() + service_config["some_param_cluster"] = DEFAULT_CLUSTER_PARAM + first_service.config_set(service_config) + second_component_config = second_component.config() + second_component_config["some_param_cluster"] = DEFAULT_CLUSTER_PARAM + second_component.config_set(second_component_config) + + with allure.step("Start simple cluster action and switch component to MM 'ON'"): + cluster.action(name="cluster_action").run() + check_response_error( + response=api_client.component.change_maintenance_mode(second_component.id, MM_IS_ON), + expected_error=EXPECTED_ERROR, + ) + + +@allure.step("Check amount of jobs and all tasks finish successfully") +def _wait_all_tasks_succeed(client: ADCMClient, expected_amount: int): + jobs = client.job_list() + assert len(jobs) == expected_amount + assert all(job.task().wait() == "success" for job in jobs) + + +@allure.step("Check response contain correct error") +def check_response_error(response: RequestResult, expected_error: str) -> None: + """Method to check error message in response""" + assert ( + response.data["code"] == expected_error + ), f"Incorrect request data code.\nActual: {response.data['code']}\nExpected: {expected_error}" diff --git a/tests/functional/maintenance_mode/test_mm_get_state_calculation_mode.py b/tests/functional/maintenance_mode/test_mm_get_state_calculation_mode.py new file mode 100644 index 0000000000..9df266fbcb --- /dev/null +++ b/tests/functional/maintenance_mode/test_mm_get_state_calculation_mode.py @@ -0,0 +1,101 @@ +# 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. + +"""Test designed to check valid values for getting MM: ON, OFF, CHANGING""" + +import allure +import pytest +from adcm_client.objects import ADCMClient, Cluster, Component, Service +from tests.functional.conftest import only_clean_adcm +from tests.functional.maintenance_mode.conftest import ( + ANOTHER_SERVICE_NAME, + BUNDLES_DIR, + DEFAULT_SERVICE_NAME, + FIRST_COMPONENT, + MM_IS_CHANGING, + MM_IS_OFF, + MM_IS_ON, + SECOND_COMPONENT, + check_mm_is, +) +from tests.library.api.client import APIClient + +# pylint: disable=redefined-outer-name + +pytestmark = [only_clean_adcm] + + +@pytest.fixture() +def cluster_with_mm(sdk_client_fs: ADCMClient) -> Cluster: + """ + Upload cluster bundle with allowed MM, + create and return cluster with default service + """ + bundle = sdk_client_fs.upload_from_fs(BUNDLES_DIR / "cluster_mm_action") + cluster = bundle.cluster_create("Actions Cluster") + cluster.service_add(name=DEFAULT_SERVICE_NAME) + return cluster + + +def test_state_calculation_mode_service(api_client, cluster_with_mm, hosts): + """Test to check CHANGING mode for service when object changes his maintenance mode""" + first_service = cluster_with_mm.service(name=DEFAULT_SERVICE_NAME) + second_service = cluster_with_mm.service_add(name=ANOTHER_SERVICE_NAME) + first_component = first_service.component(name=FIRST_COMPONENT) + second_component = first_service.component(name=SECOND_COMPONENT) + + cluster_with_mm.hostcomponent_set( + (cluster_with_mm.host_add(hosts[0]), first_component), + (cluster_with_mm.host_add(hosts[1]), second_component), + (cluster_with_mm.host_add(hosts[2]), second_service.component()), + ) + + check_mm_is(MM_IS_OFF, first_service, second_service, first_component, second_component) + with allure.step('Check service action maintenance mode set maintenance mode to CHANGING value'): + _status_changing(api_client, second_service, MM_IS_ON) + check_mm_is(MM_IS_CHANGING, second_service) + check_mm_is(MM_IS_OFF, first_service, first_component, second_component) + + with allure.step('Check CHANGING mode on service does not change other objects on service'): + _status_changing(api_client, first_service, MM_IS_ON) + check_mm_is(MM_IS_CHANGING, first_service, second_service) + check_mm_is(MM_IS_OFF, first_component, second_component) + + +def test_state_calculation_mode_component(api_client, cluster_with_mm, hosts): + """Test to check CHANGING mode for component when object changes his maintenance mode""" + first_service = cluster_with_mm.service(name=DEFAULT_SERVICE_NAME) + second_service = cluster_with_mm.service_add(name=ANOTHER_SERVICE_NAME) + first_component = first_service.component(name=FIRST_COMPONENT) + second_component = first_service.component(name=SECOND_COMPONENT) + + cluster_with_mm.hostcomponent_set( + (cluster_with_mm.host_add(hosts[0]), first_component), (cluster_with_mm.host_add(hosts[1]), second_component) + ) + + check_mm_is(MM_IS_OFF, first_service, second_service, first_component, second_component) + with allure.step('Check component action maintenance mode set maintenance mode to CHANGING value'): + _status_changing(api_client, first_component, MM_IS_ON) + check_mm_is(MM_IS_CHANGING, first_component) + check_mm_is(MM_IS_OFF, first_service, second_service, second_component) + + with allure.step('Check CHANGING mode on component does not change other objects in cluster'): + _status_changing(api_client, second_component, MM_IS_ON) + check_mm_is(MM_IS_CHANGING, first_component, second_component) + check_mm_is(MM_IS_OFF, first_service, second_service) + + +def _status_changing(api_client: APIClient, adcm_object: Service | Component, maintenance_mode: str) -> None: + """Try to change maintenance mode on ADCM objects and catch mode CHANGING""" + client = api_client.service if isinstance(adcm_object, Service) else api_client.component + client.change_maintenance_mode(adcm_object.id, maintenance_mode).check_code(200) + adcm_object.reread() diff --git a/tests/functional/maintenance_mode/test_mm_state_calculation_mode.py b/tests/functional/maintenance_mode/test_mm_state_calculation_mode.py new file mode 100644 index 0000000000..ae6d8f9a24 --- /dev/null +++ b/tests/functional/maintenance_mode/test_mm_state_calculation_mode.py @@ -0,0 +1,126 @@ +# 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. + +""" +Test designed to check MM state calculation logic for services/components +""" + +import allure +from tests.functional.conftest import only_clean_adcm +from tests.functional.maintenance_mode.conftest import ( + ANOTHER_SERVICE_NAME, + DEFAULT_SERVICE_NAME, + MM_IS_OFF, + MM_IS_ON, + add_hosts_to_cluster, + check_mm_is, + set_maintenance_mode, +) + +# pylint: disable=redefined-outer-name + + +@only_clean_adcm +def test_mm_state_service(api_client, cluster_with_mm, hosts): + """Test to check maintenance_mode on services and hosts""" + first_host, second_host, *_ = hosts + first_service = cluster_with_mm.service(name=DEFAULT_SERVICE_NAME) + second_service = cluster_with_mm.service_add(name=ANOTHER_SERVICE_NAME) + first_component = first_service.component(name='first_component') + second_component = first_service.component(name='second_component') + + add_hosts_to_cluster(cluster_with_mm, (first_host, second_host)) + cluster_with_mm.hostcomponent_set( + (first_host, first_component), + (second_host, second_component), + ) + + with allure.step('Check MM state calculation logic for service'): + set_maintenance_mode(api_client=api_client, adcm_object=second_service, maintenance_mode=MM_IS_ON) + check_mm_is(MM_IS_ON, second_service) + check_mm_is(MM_IS_OFF, first_host, second_host, first_component, second_component, first_service) + + set_maintenance_mode(api_client=api_client, adcm_object=first_service, maintenance_mode=MM_IS_ON) + check_mm_is(MM_IS_ON, first_component, second_component, first_service, second_service) + check_mm_is(MM_IS_OFF, first_host, second_host) + + set_maintenance_mode(api_client=api_client, adcm_object=first_service, maintenance_mode=MM_IS_OFF) + set_maintenance_mode(api_client=api_client, adcm_object=second_service, maintenance_mode=MM_IS_OFF) + check_mm_is( + MM_IS_OFF, first_host, second_host, first_component, second_component, first_service, second_service + ) + + set_maintenance_mode(api_client=api_client, adcm_object=first_service, maintenance_mode=MM_IS_ON) + check_mm_is(MM_IS_ON, first_service) + check_mm_is(MM_IS_OFF, first_host, second_host, second_service) + + +@only_clean_adcm +def test_mm_state_component(api_client, cluster_with_mm, hosts): + """Test to check maintenance_mode on components and hosts""" + first_host, second_host, *_ = hosts + first_service = cluster_with_mm.service(name=DEFAULT_SERVICE_NAME) + first_component = first_service.component(name='first_component') + second_component = first_service.component(name='second_component') + + add_hosts_to_cluster(cluster_with_mm, (first_host, second_host)) + cluster_with_mm.hostcomponent_set( + (first_host, first_component), + (second_host, second_component), + ) + + with allure.step('Check MM state calculation logic for components'): + set_maintenance_mode(api_client=api_client, adcm_object=first_component, maintenance_mode=MM_IS_ON) + check_mm_is(MM_IS_ON, first_component) + check_mm_is(MM_IS_OFF, first_host, second_host, second_component, first_service) + + set_maintenance_mode(api_client=api_client, adcm_object=second_component, maintenance_mode=MM_IS_ON) + check_mm_is(MM_IS_ON, first_component, second_component, first_service) + check_mm_is(MM_IS_OFF, first_host, second_host) + + set_maintenance_mode(api_client=api_client, adcm_object=first_component, maintenance_mode=MM_IS_OFF) + set_maintenance_mode(api_client=api_client, adcm_object=second_component, maintenance_mode=MM_IS_OFF) + check_mm_is(MM_IS_OFF, first_host, second_host, first_component, second_component, first_service) + + set_maintenance_mode(api_client=api_client, adcm_object=second_component, maintenance_mode=MM_IS_ON) + check_mm_is(MM_IS_ON, second_component) + check_mm_is(MM_IS_OFF, first_host, second_host, first_component, first_service) + + +@only_clean_adcm +def test_mm_state_host(api_client, cluster_with_mm, hosts): + """Test to check maintenance_mode on components and hosts""" + first_host, second_host, *_ = hosts + first_service = cluster_with_mm.service(name=DEFAULT_SERVICE_NAME) + first_component = first_service.component(name='first_component') + second_component = first_service.component(name='second_component') + + add_hosts_to_cluster(cluster_with_mm, (first_host, second_host)) + cluster_with_mm.hostcomponent_set( + (first_host, first_component), + (second_host, second_component), + ) + + with allure.step('Check MM state calculation logic for hosts'): + set_maintenance_mode(api_client=api_client, adcm_object=first_host, maintenance_mode=MM_IS_ON) + check_mm_is(MM_IS_ON, first_host, first_component) + check_mm_is(MM_IS_OFF, first_service, second_host, second_component) + + set_maintenance_mode(api_client=api_client, adcm_object=second_host, maintenance_mode=MM_IS_ON) + check_mm_is(MM_IS_ON, first_service, first_host, first_component, second_host, second_component) + + set_maintenance_mode(api_client=api_client, adcm_object=first_host, maintenance_mode=MM_IS_OFF) + set_maintenance_mode(api_client=api_client, adcm_object=second_host, maintenance_mode=MM_IS_OFF) + check_mm_is(MM_IS_OFF, first_service, first_host, first_component, second_host, second_component) + + set_maintenance_mode(api_client=api_client, adcm_object=second_host, maintenance_mode=MM_IS_ON) + check_mm_is(MM_IS_ON, second_host, second_component) diff --git a/tests/functional/maintenance_mode/test_statuses.py b/tests/functional/maintenance_mode/test_statuses.py index 4ed21aa0c3..9f78b62c49 100644 --- a/tests/functional/maintenance_mode/test_statuses.py +++ b/tests/functional/maintenance_mode/test_statuses.py @@ -15,32 +15,38 @@ """ from operator import not_, truth -from typing import Tuple, Collection +from typing import Collection, Tuple import allure import pytest import requests -from adcm_client.objects import Cluster, Component, ADCMClient - -from tests.library.assertions import dicts_are_equal -from tests.library.status import ADCMObjectStatusChanger +from adcm_client.objects import ADCMClient, Cluster, Component +from tests.functional.conftest import only_clean_adcm from tests.functional.maintenance_mode.conftest import ( - add_hosts_to_cluster, - FIRST_COMPONENT, - SECOND_COMPONENT, ANOTHER_SERVICE_NAME, DEFAULT_SERVICE_NAME, + FIRST_COMPONENT, + MM_IS_OFF, + MM_IS_ON, + SECOND_COMPONENT, + add_hosts_to_cluster, + set_maintenance_mode, turn_mm_on, ) +from tests.library.assertions import dicts_are_equal +from tests.library.status import ADCMObjectStatusChanger # pylint: disable=redefined-outer-name -CHILDREN_KEY = 'chilren' +CHILDREN_KEY = "chilren" POSITIVE_STATUS = 0 NEGATIVE_STATUS = 16 +pytestmark = [only_clean_adcm] + + @pytest.fixture() def status_changer(sdk_client_fs, adcm_fs) -> ADCMObjectStatusChanger: """Init status changer""" @@ -72,9 +78,8 @@ def deployed_component(cluster_with_mm, hosts) -> Tuple[Component, Component, Co class TestStatusAggregationWithMM: """Test status aggregation with hosts in MM""" - # pylint: disable-next=too-many-arguments def test_turn_mm_after_negative_status( - self, status_changer, sdk_client_fs, cluster_with_mm, deployed_component, hosts + self, api_client, status_changer, sdk_client_fs, cluster_with_mm, deployed_component, hosts ): """ Test status aggregation when components on hosts are turned "off" after MM turned "on" on host @@ -85,12 +90,12 @@ def test_turn_mm_after_negative_status( _, host_2, host_3, *_ = hosts service_name = service.name - component_name = f'{service_name}.{component.name}' - host_on_component_name = f'{component_name}.{host_2.fqdn}' + component_name = f"{service_name}.{component.name}" + host_on_component_name = f"{component_name}.{host_2.fqdn}" self.enable_cluster(status_changer, cluster) - with allure.step(f'Disable component {component_name} on host {host_2.fqdn} and host itself'): + with allure.step(f"Disable component {component_name} on host {host_2.fqdn} and host itself"): status_changer.set_host_negative_status(host_2) status_changer.set_component_negative_status((host_2, component)) check_statuses( @@ -102,20 +107,19 @@ def test_turn_mm_after_negative_status( {host_2.fqdn}, ) - turn_mm_on(host_2) + turn_mm_on(api_client, host_2) - with allure.step('Expect nothing but host and component on it to be disabled'): + with allure.step("Expect nothing but host and component on it to be disabled"): check_statuses( retrieve_status(sdk_client_fs, cluster), - hosts_on_components={host_on_component_name}, + components_on_hosts={host_on_component_name}, hosts={host_2.fqdn}, ) self._turn_off_component_not_on_mm_host(sdk_client_fs, status_changer, cluster, host_2, host_3) - # pylint: disable-next=too-many-arguments def test_turn_mm_before_negative_status( - self, status_changer, sdk_client_fs, cluster_with_mm, deployed_component, hosts + self, api_client, status_changer, sdk_client_fs, cluster_with_mm, deployed_component, hosts ): """ Test status aggregation when components on hosts are turned "off" before MM turned "on" on host @@ -126,24 +130,143 @@ def test_turn_mm_before_negative_status( component, *_ = deployed_component service_name = service.name - component_name = f'{service_name}.{component.name}' - host_on_component_name = f'{component_name}.{host_2.fqdn}' + component_name = f"{service_name}.{component.name}" + host_on_component_name = f"{component_name}.{host_2.fqdn}" - turn_mm_on(host_2) + turn_mm_on(api_client, host_2) self.enable_cluster(status_changer, cluster) - with allure.step(f'Disable component {component_name} on host {host_2.fqdn} and host itself'): + with allure.step(f"Disable component {component_name} on host {host_2.fqdn} and host itself"): status_changer.set_host_negative_status(host_2) status_changer.set_component_negative_status((host_2, component)) check_statuses( retrieve_status(sdk_client_fs, cluster), - hosts_on_components={host_on_component_name}, + components_on_hosts={host_on_component_name}, hosts={host_2.fqdn}, ) self._turn_off_component_not_on_mm_host(sdk_client_fs, status_changer, cluster, host_2, host_3) + def test_status_service_mm_changed( + self, api_client, status_changer, sdk_client_fs, cluster_with_mm, deployed_component, hosts + ): + cluster = cluster_with_mm + service = cluster.service(name=DEFAULT_SERVICE_NAME) + component, *_ = deployed_component + _, host_2, *_ = hosts + + service_name = service.name + component_name = f"{service_name}.{component.name}" + host_on_component_name = f"{component_name}.{host_2.fqdn}" + + self.enable_cluster(status_changer, cluster) + + with allure.step(f"Disable component {component_name} on host {host_2.fqdn}"): + status_changer.set_component_negative_status((host_2, component)) + check_statuses( + retrieve_status(sdk_client_fs, cluster), + cluster=cluster.name, + services={service_name}, + components={component_name}, + components_on_hosts={host_on_component_name}, + hosts=(), + ) + + with allure.step("Turn MM 'ON' on service and check statuses"): + set_maintenance_mode(api_client, service, MM_IS_ON) + check_statuses( + retrieve_status(sdk_client_fs, cluster), + cluster=None, + services=(), + components=(), + components_on_hosts={host_on_component_name}, + hosts=(), + ) + + with allure.step("Turn MM 'OFF' on service and check statuses"): + set_maintenance_mode(api_client, service, MM_IS_OFF) + check_statuses( + retrieve_status(sdk_client_fs, cluster), + cluster=cluster.name, + services={service_name}, + components={component_name}, + components_on_hosts={host_on_component_name}, + hosts=(), + ) + + # pylint: disable=too-many-locals + def test_status_component_mm_changed( + self, api_client, status_changer, sdk_client_fs, cluster_with_mm, deployed_component, hosts + ): + cluster = cluster_with_mm + service = cluster.service(name=DEFAULT_SERVICE_NAME) + component_1, component_2, *_ = deployed_component + _, host_2, *_ = hosts + + service_name = service.name + component_name = f"{service_name}.{component_1.name}" + host_on_component_name = f"{component_name}.{host_2.fqdn}" + component_2_name = f"{service_name}.{component_2.name}" + host_on_component_2_name = f"{component_2_name}.{host_2.fqdn}" + + self.enable_cluster(status_changer, cluster) + + with allure.step(f"Disable component {component_name} on host {host_2.fqdn}"): + status_changer.set_component_negative_status((host_2, component_1)) + check_statuses( + retrieve_status(sdk_client_fs, cluster), + cluster=cluster.name, + services={service_name}, + components={component_name}, + components_on_hosts={host_on_component_name}, + hosts=(), + ) + + with allure.step("Turn 'ON' first component's MM and check statuses"): + set_maintenance_mode(api_client, component_1, MM_IS_ON) + check_statuses( + retrieve_status(sdk_client_fs, cluster), + cluster=None, + services=(), + components=(), + components_on_hosts={host_on_component_name}, + hosts=(), + ) + + with allure.step(f"Disable component {component_2_name} on host {host_2.fqdn}"): + status_changer.set_component_negative_status((host_2, component_2)) + check_statuses( + retrieve_status(sdk_client_fs, cluster), + cluster=cluster.name, + services={service_name}, + components={component_2_name}, + components_on_hosts={host_on_component_name, host_on_component_2_name}, + hosts=(), + ) + + with allure.step("Turn MM 'ON' on second component and check statuses"): + set_maintenance_mode(api_client, component_2, MM_IS_ON) + check_statuses( + retrieve_status(sdk_client_fs, cluster), + cluster=None, + services=(), + components=(), + components_on_hosts={host_on_component_name, host_on_component_2_name}, + hosts=(), + ) + + with allure.step("Turn off MM on second component"): + set_maintenance_mode(api_client, component_2, MM_IS_OFF) + check_statuses( + retrieve_status(sdk_client_fs, cluster), + cluster.name, + services={service_name}, + components={component_2_name}, + components_on_hosts={host_on_component_name, host_on_component_2_name}, + hosts=(), + ) + @allure.step('Turn all components in cluster "on"') def enable_cluster(self, status_changer, cluster) -> None: """Enable all components on all hosts of the cluster""" @@ -155,56 +278,56 @@ def _turn_off_component_not_on_mm_host(self, client, status_changer, cluster, ho component_1 = service.component(name=FIRST_COMPONENT) component_2 = service.component(name=SECOND_COMPONENT) service_name = service.name - component_name = f'{service_name}.{component_1.name}' - host_on_component_name = f'{component_name}.{host_2.fqdn}' + component_name = f"{service_name}.{component_1.name}" + host_on_component_name = f"{component_name}.{host_2.fqdn}" with allure.step( f'Turn "off" component "{component_2.name}" on host {host_3.fqdn} ' - 'and expect it to affect aggregation statuses' + "and expect it to affect aggregation statuses" ): status_changer.set_component_negative_status((host_3, component_2)) check_statuses( retrieve_status(client, cluster), cluster=cluster.name, services={service_name}, - components={f'{service_name}.{component_2.name}'}, - hosts_on_components={host_on_component_name, f'{service_name}.{component_2.name}.{host_3.fqdn}'}, + components={f"{service_name}.{component_2.name}"}, + components_on_hosts={host_on_component_name, f"{service_name}.{component_2.name}.{host_3.fqdn}"}, hosts={host_2.fqdn}, ) def retrieve_status(client: ADCMClient, cluster: Cluster) -> dict: """Get status map for cluster""" - url = f'{client.url}/api/v1/cluster/{cluster.id}/status/?view=interface' - response = requests.get(url, headers={'Authorization': f'Token {client.api_token()}'}) + url = f"{client.url}/api/v1/cluster/{cluster.id}/status/?view=interface" + response = requests.get(url, headers={"Authorization": f"Token {client.api_token()}"}) data = response.json() return { - 'name': data['name'], - 'status': data['status'], - 'hosts': {host['name']: {'status': host['status']} for host in data[CHILDREN_KEY]['hosts']}, - 'services': { - service['name']: { - 'status': service['status'], - 'components': { - hc['name']: { - 'status': hc['status'], - 'hosts': {host_info['name']: {'status': host_info['status']} for host_info in hc['hosts']}, + "name": data["name"], + "status": data["status"], + "hosts": {host["name"]: {"status": host["status"]} for host in data[CHILDREN_KEY]["hosts"]}, + "services": { + service["name"]: { + "status": service["status"], + "components": { + hc["name"]: { + "status": hc["status"], + "hosts": {host_info["name"]: {"status": host_info["status"]} for host_info in hc["hosts"]}, } - for hc in service['hc'] + for hc in service["hc"] }, } - for service in data[CHILDREN_KEY]['services'] + for service in data[CHILDREN_KEY]["services"] }, } -@allure.step("Check statuses of a cluster's objects") # pylint: disable-next=too-many-arguments +@allure.step("Check statuses of a cluster's objects") def check_statuses( statuses: dict, cluster: str = None, services: Collection[str] = (), components: Collection[str] = (), - hosts_on_components: Collection[str] = (), + components_on_hosts: Collection[str] = (), hosts: Collection[str] = (), default_positive: bool = True, ) -> None: @@ -216,30 +339,30 @@ def check_statuses( p = not_ if default_positive else truth expected_statuses = { - 'name': statuses['name'], - 'status': _expected_status(p(bool(cluster))), - 'hosts': { - fqdn: {'status': _expected_status(p(fqdn in hosts))} for fqdn, host_dict in statuses['hosts'].items() + "name": statuses["name"], + "status": _expected_status(p(bool(cluster))), + "hosts": { + fqdn: {"status": _expected_status(p(fqdn in hosts))} for fqdn, host_dict in statuses["hosts"].items() }, - 'services': { + "services": { service_name: { - 'status': _expected_status(p(service_name in services)), - 'components': { + "status": _expected_status(p(service_name in services)), + "components": { component_name: { - 'status': _expected_status(p(f'{service_name}.{component_name}' in components)), - 'hosts': { + "status": _expected_status(p(f"{service_name}.{component_name}" in components)), + "hosts": { fqdn: { - 'status': _expected_status( - p(f'{service_name}.{component_name}.{fqdn}' in hosts_on_components) + "status": _expected_status( + p(f"{service_name}.{component_name}.{fqdn}" in components_on_hosts) ) } - for fqdn, host_dict in component_dict['hosts'].items() + for fqdn, host_dict in component_dict["hosts"].items() }, } - for component_name, component_dict in service_dict['components'].items() + for component_name, component_dict in service_dict["components"].items() }, } - for service_name, service_dict in statuses['services'].items() + for service_name, service_dict in statuses["services"].items() }, } diff --git a/tests/functional/maintenance_mode/test_upgrade_to_mm.py b/tests/functional/maintenance_mode/test_upgrade_to_mm.py index 46ec89f5fe..d5142bce41 100644 --- a/tests/functional/maintenance_mode/test_upgrade_to_mm.py +++ b/tests/functional/maintenance_mode/test_upgrade_to_mm.py @@ -16,15 +16,15 @@ import allure import pytest - from tests.conftest import DUMMY_ACTION from tests.functional.maintenance_mode.conftest import ( MM_IS_OFF, - MM_IS_DISABLED, + MM_NOT_ALLOWED, add_hosts_to_cluster, - check_hosts_mm_is, - get_enabled_actions_names, + check_mm_availability, + check_mm_is, get_disabled_actions_names, + get_enabled_actions_names, turn_mm_on, ) from tests.functional.tools import get_object_represent @@ -81,7 +81,7 @@ ], indirect=True, ) -def test_allow_mm_after_upgrade(sdk_client_fs, create_bundle_archives, hosts): +def test_allow_mm_after_upgrade(api_client, sdk_client_fs, create_bundle_archives, hosts): """ Test that after upgrade to the bundle version where MM is allowed: - hosts in cluster set to correct MM mode @@ -96,17 +96,17 @@ def test_allow_mm_after_upgrade(sdk_client_fs, create_bundle_archives, hosts): add_hosts_to_cluster(old_cluster, hosts_in_cluster) old_cluster.hostcomponent_set(*[(host, component) for host in hosts_in_cluster]) - check_hosts_mm_is(MM_IS_DISABLED, *hosts) + check_mm_availability(MM_NOT_ALLOWED, *hosts) upgrade_task = old_cluster.upgrade().do() if upgrade_task: upgrade_task.wait() - check_hosts_mm_is(MM_IS_OFF, *hosts_in_cluster) - check_hosts_mm_is(MM_IS_DISABLED, *free_hosts) + check_mm_is(MM_IS_OFF, *hosts_in_cluster) + check_mm_availability(MM_NOT_ALLOWED, *free_hosts) check_actions_are_disabled_correctly(set(DUMMY_ACTIONS_WITH_ALLOWED.keys()), set(), old_cluster, service, component) - turn_mm_on(hosts_in_cluster[0]) + turn_mm_on(api_client, hosts_in_cluster[0]) check_actions_are_disabled_correctly( set(ALLOWED_ACTION.keys()), set(TWO_DUMMY_ACTIONS.keys()), old_cluster, service, component ) @@ -128,13 +128,13 @@ def test_upgrade_to_mm_false(sdk_client_fs, create_bundle_archives, hosts): cluster_hosts = [old_cluster.host_add(host) for host in hosts] old_cluster.hostcomponent_set(*[(h, component) for h in cluster_hosts]) - check_hosts_mm_is(MM_IS_DISABLED, *cluster_hosts) + check_mm_availability(MM_NOT_ALLOWED, *cluster_hosts) upgrade_task = old_cluster.upgrade().do() if upgrade_task: upgrade_task.wait() - check_hosts_mm_is(MM_IS_DISABLED, *cluster_hosts) + check_mm_availability(MM_NOT_ALLOWED, *cluster_hosts) check_actions_are_disabled_correctly(set(TWO_DUMMY_ACTIONS.keys()), set(), old_cluster, service, component) @@ -148,7 +148,7 @@ def test_upgrade_to_mm_false(sdk_client_fs, create_bundle_archives, hosts): ], indirect=True, ) -def test_upgrade_from_true_to_false_mm(sdk_client_fs, create_bundle_archives, hosts): +def test_upgrade_from_true_to_false_mm(api_client, sdk_client_fs, create_bundle_archives, hosts): """ Test upgrade from version with `allow_maintenance_mode: true` to `allow_maintenance_mode: false` """ @@ -159,14 +159,14 @@ def test_upgrade_from_true_to_false_mm(sdk_client_fs, create_bundle_archives, ho component = service.component() old_cluster.hostcomponent_set(*[(h, component) for h in cluster_hosts]) - check_hosts_mm_is(MM_IS_OFF, *cluster_hosts) - turn_mm_on(cluster_hosts[0]) + check_mm_is(MM_IS_OFF, *cluster_hosts) + turn_mm_on(api_client, cluster_hosts[0]) upgrade_task = old_cluster.upgrade().do() if upgrade_task: upgrade_task.wait() - check_hosts_mm_is(MM_IS_DISABLED, *cluster_hosts) + check_mm_availability(MM_NOT_ALLOWED, *cluster_hosts) check_actions_are_disabled_correctly(set(TWO_DUMMY_ACTIONS.keys()), set(), old_cluster, service, component) @@ -200,7 +200,7 @@ def test_upgrade_from_true_to_false_mm(sdk_client_fs, create_bundle_archives, ho ], indirect=True, ) -def test_allowed_actions_changed(sdk_client_fs, create_bundle_archives, hosts): +def test_allowed_actions_changed(api_client, sdk_client_fs, create_bundle_archives, hosts): """ Test upgrade when allowed/disallowed in MM actions changed """ @@ -209,7 +209,7 @@ def test_allowed_actions_changed(sdk_client_fs, create_bundle_archives, hosts): add_hosts_to_cluster(old_cluster, hosts) old_cluster.hostcomponent_set((hosts[0], old_cluster.service_add(name='just_service').component())) - turn_mm_on(hosts[0]) + turn_mm_on(api_client, hosts[0]) check_actions_are_disabled_correctly({'enabled_at_first'}, {'disabled_at_first'}, old_cluster) diff --git a/tests/functional/plugin_utils.py b/tests/functional/plugin_utils.py index 31b7ad6b28..ec60c4710e 100644 --- a/tests/functional/plugin_utils.py +++ b/tests/functional/plugin_utils.py @@ -13,34 +13,39 @@ Common functions and helpers for testing plugins (state, multi_state, config) """ -from typing import Callable, TypeVar, Collection, Type, Optional, List, Tuple, Dict, Set from contextlib import contextmanager from operator import methodcaller +from typing import Callable, Collection, Dict, List, Optional, Set, Tuple, Type, TypeVar import allure import pytest - from _pytest.mark.structures import ParameterSet -from adcm_client.objects import Cluster, Service, Component, Provider, Host, ADCMClient, Action +from adcm_client.objects import ( + Action, + ADCMClient, + Cluster, + Component, + Host, + Provider, + Service, +) from adcm_pytest_plugin import utils as plugin_utils from adcm_pytest_plugin.steps.actions import ( - wait_for_task_and_assert_result, run_cluster_action_and_assert_result, - run_service_action_and_assert_result, run_component_action_and_assert_result, - run_provider_action_and_assert_result, run_host_action_and_assert_result, + run_provider_action_and_assert_result, + run_service_action_and_assert_result, + wait_for_task_and_assert_result, ) - from tests.functional.tools import ( - get_objects_via_pagination, + ADCMObjects, + AnyADCMObject, ClusterRelatedObject, ProviderRelatedObject, - AnyADCMObject, - ADCMObjects, + get_objects_via_pagination, ) - # value of object's field (e.g. "created" as value for state) ADCMObjectField = TypeVar('ADCMObjectField') @@ -127,7 +132,10 @@ def generate_cluster_success_params(action_prefix: str, id_template: str) -> Lis from_obj_func, id=id_template.format('service') + f'_from_{from_obj_id}', ) - for from_obj_func, from_obj_id in ((first_service, 'self'), (first_service_first_component, 'component')) + for from_obj_func, from_obj_id in ( + (first_service, 'self'), + (first_service_first_component, 'component'), + ) ], pytest.param( f'{action_prefix}_component', @@ -200,14 +208,27 @@ def generate_provider_success_params(action_prefix: str, id_template: str) -> Li host = (*provider, 'first-first') return [ - pytest.param(f'{action_prefix}_provider', provider, provider, id=id_template.format('provider') + '_from_self'), - pytest.param(f'{action_prefix}_provider', provider, host, id=id_template.format('provider') + '_from_host'), + pytest.param( + f'{action_prefix}_provider', + provider, + provider, + id=id_template.format('provider') + '_from_self', + ), + pytest.param( + f'{action_prefix}_provider', + provider, + host, + id=id_template.format('provider') + '_from_host', + ), pytest.param(f'{action_prefix}_host', host, host, id=id_template.format('host') + '_from_self'), ] def get_cluster_related_object( - client: ADCMClient, cluster: str = 'first', service: Optional[str] = None, component: Optional[str] = None + client: ADCMClient, + cluster: str = 'first', + service: Optional[str] = None, + component: Optional[str] = None, ) -> ClusterRelatedObject: """ Get function to get one of ADCM cluster objects: @@ -441,7 +462,9 @@ def cluster_service_component( return cluster, service, component def run_immediate_change_test( - self, provider_host: Tuple[Provider, Host], cluster_service_component: Tuple[Cluster, Service, Component] + self, + provider_host: Tuple[Provider, Host], + cluster_service_component: Tuple[Cluster, Service, Component], ): """ Run the same action (self._action) for cluster, service, component, provider, host diff --git a/tests/functional/rbac/action_role_utils.py b/tests/functional/rbac/action_role_utils.py index 400d5d2fe4..ab60204fe6 100644 --- a/tests/functional/rbac/action_role_utils.py +++ b/tests/functional/rbac/action_role_utils.py @@ -1,16 +1,46 @@ +# 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. """Checks and utilities for action roles""" from operator import itemgetter -from typing import Iterable, Tuple, Set, List, Iterator, Optional, Union +from typing import Iterable, Iterator, List, Optional, Set, Tuple, Union import allure from adcm_client.base import ObjectNotFound -from adcm_client.objects import ADCMClient, Cluster, Prototype, Service, Role, Bundle, Host, Policy +from adcm_client.objects import ( + ADCMClient, + Bundle, + Cluster, + Host, + Policy, + Prototype, + Role, + Service, +) from adcm_pytest_plugin.utils import catch_failed, random_string - from tests.functional.rbac.checkers import ForbiddenCallChecker -from tests.functional.rbac.conftest import RbacRoles, extract_role_short_info, RoleShortInfo, RoleType, BusinessRole +from tests.functional.rbac.conftest import ( + BusinessRole, + RbacRoles, + RoleShortInfo, + RoleType, + extract_role_short_info, +) from tests.functional.tools import AnyADCMObject -from tests.library.assertions import is_in_collection, is_not_in_collection, is_superset_of, does_not_intersect +from tests.library.assertions import ( + does_not_intersect, + is_in_collection, + is_not_in_collection, + is_superset_of, +) from tests.library.consts import HTTPMethod @@ -105,7 +135,11 @@ def check_cluster_actions_roles_are_created_correctly( full_hidden_prefix = f'{hidden_role_prefix}{get_prototype_prefix_for_action_role(cluster_proto)}' with allure.step('Check that "hidden" roles are created for each action in cluster'): cluster_actions_role_names = get_actions_role_names(full_hidden_prefix, actions) - is_superset_of(hidden_role_names, cluster_actions_role_names, 'Not all expected "hidden" roles were found') + is_superset_of( + hidden_role_names, + cluster_actions_role_names, + 'Not all expected "hidden" roles were found', + ) _, business = check_business_roles_children(client, cluster_proto, actions, cluster_actions_role_names) with allure.step('Check that business roles are applied correctly to RBAC default roles'): @@ -145,7 +179,9 @@ def check_service_and_components_roles_are_created_correctly( with allure.step('Check that "hidden" roles are created for each action in service'): service_actions_role_names = get_actions_role_names(service_full_hidden_prefix, service_actions) is_superset_of( - hidden_role_names, service_actions_role_names, "Some of required roles weren't created for service" + hidden_role_names, + service_actions_role_names, + "Some of required roles weren't created for service", ) _, business = check_business_roles_children(client, service_proto, service_actions, service_actions_role_names) @@ -178,7 +214,8 @@ def _check_components_roles_are_created_correctly(client, service, hidden_role_n component_proto = component.prototype() component_actions = component_proto.actions component_actions_role_names = get_actions_role_names( - f'{prefix_for_component}{get_prototype_prefix_for_action_role(component_proto)}', component_actions + f'{prefix_for_component}{get_prototype_prefix_for_action_role(component_proto)}', + component_actions, ) is_superset_of(hidden_role_names, component_actions_role_names, 'Not all roles were created') @@ -212,7 +249,9 @@ def check_provider_based_object_action_roles_are_created_correctly( _, business = check_business_roles_children(client, prototype, actions, actions_role_names) check_roles_are_not_added_to_rbac_roles( - client, (RbacRoles.ClusterAdministrator, RbacRoles.ServiceAdministrator, RbacRoles.ADCMUser), business + client, + (RbacRoles.ClusterAdministrator, RbacRoles.ServiceAdministrator, RbacRoles.ADCMUser), + business, ) diff --git a/tests/functional/rbac/actions/test_permissions_mixup.py b/tests/functional/rbac/actions/test_permissions_mixup.py index 5425437ec1..5e9ca1b156 100644 --- a/tests/functional/rbac/actions/test_permissions_mixup.py +++ b/tests/functional/rbac/actions/test_permissions_mixup.py @@ -12,20 +12,22 @@ """Test corner cases where permissions got messed up and allows more than they should be""" -from typing import Iterable, Tuple, Callable +from typing import Callable, Iterable, Tuple import allure import pytest from adcm_client.objects import Cluster - -from tests.functional.tools import AnyADCMObject +from tests.functional.rbac.action_role_utils import ( + action_business_role, + create_action_policy, +) from tests.functional.rbac.conftest import ( + BusinessRole, RbacRoles, - is_allowed, as_user_objects, - BusinessRole, + is_allowed, ) -from tests.functional.rbac.action_role_utils import action_business_role, create_action_policy +from tests.functional.tools import AnyADCMObject pytestmark = [pytest.mark.extra_rbac] @@ -43,7 +45,12 @@ def clusters(self, actions_cluster_bundle, simple_cluster_bundle) -> Tuple[Clust second_cluster = actions_cluster_bundle.cluster_create("Second Cluster") first_another_bundle_cluster = simple_cluster_bundle.cluster_create("Another Bundle Cluster") second_another_bundle_cluster = simple_cluster_bundle.cluster_create("One More Another Bundle Cluster") - return first_cluster, second_cluster, first_another_bundle_cluster, second_another_bundle_cluster + return ( + first_cluster, + second_cluster, + first_another_bundle_cluster, + second_another_bundle_cluster, + ) @pytest.fixture() def clusters_with_services(self, actions_cluster_bundle, simple_cluster_bundle) -> Tuple[Cluster, Cluster]: @@ -170,7 +177,10 @@ def test_service_admin(self, clients, clusters_with_services, user, is_denied_to self.check_permissions( "Check that Service Admin role allows actions only on one service in one cluster", - allowed=((user_first_service, do_nothing_role), (user_first_service, same_display_role)), + allowed=( + (user_first_service, do_nothing_role), + (user_first_service, same_display_role), + ), denied=((second_service, do_nothing_role), (another_cluster_service, do_nothing_role)), check_denied=is_denied_to_user, ) @@ -192,7 +202,10 @@ def test_service_admin(self, clients, clusters_with_services, user, is_denied_to (user_first_service, same_display_role), (user_second_service, do_nothing_role), ), - denied=((second_service, same_display_role), (another_cluster_service, do_nothing_role)), + denied=( + (second_service, same_display_role), + (another_cluster_service, do_nothing_role), + ), check_denied=is_denied_to_user, ) @@ -214,7 +227,10 @@ def test_service_admin(self, clients, clusters_with_services, user, is_denied_to (user_second_service, do_nothing_role), (user_another_cluster_service, do_nothing_role), ), - denied=((second_service, same_display_role), (another_cluster_service, same_display_role)), + denied=( + (second_service, same_display_role), + (another_cluster_service, same_display_role), + ), check_denied=is_denied_to_user, ) diff --git a/tests/functional/rbac/actions/test_role_composition.py b/tests/functional/rbac/actions/test_role_composition.py index 344e3c394c..12856020e4 100644 --- a/tests/functional/rbac/actions/test_role_composition.py +++ b/tests/functional/rbac/actions/test_role_composition.py @@ -14,16 +14,15 @@ import allure import pytest +from adcm_client.objects import ADCMClient, Role, User +from adcm_pytest_plugin.utils import catch_failed, random_string from coreapi.exceptions import ErrorMessage -from adcm_client.objects import Role, ADCMClient, User -from adcm_pytest_plugin.utils import random_string, catch_failed - -from tests.library.errorcodes import ADCMError -from tests.functional.tools import AnyADCMObject from tests.functional.rbac.action_role_utils import ( get_bundle_prefix_for_role_name, get_prototype_prefix_for_action_role, ) +from tests.functional.tools import AnyADCMObject +from tests.library.errorcodes import ADCMError pytestmark = [pytest.mark.extra_rbac] diff --git a/tests/functional/rbac/actions/test_role_creation.py b/tests/functional/rbac/actions/test_role_creation.py index edd8b9715e..67116a1100 100644 --- a/tests/functional/rbac/actions/test_role_creation.py +++ b/tests/functional/rbac/actions/test_role_creation.py @@ -13,20 +13,17 @@ """Test policies, roles, permissions created after bundle upload""" import allure - - from tests.functional.rbac.action_role_utils import ( - get_roles_of_type, check_cluster_actions_roles_are_created_correctly, - check_service_and_components_roles_are_created_correctly, check_provider_based_object_action_roles_are_created_correctly, check_roles_does_not_have_category, + check_service_and_components_roles_are_created_correctly, get_bundle_prefix_for_role_name, + get_roles_of_type, ) from tests.functional.rbac.actions.conftest import ALL_SERVICE_NAMES from tests.functional.rbac.conftest import RoleType, extract_role_short_info - # !===== Tests ======! @@ -50,7 +47,10 @@ def test_roles_creation_on_cluster_bundle_upload(sdk_client_fs, actions_cluster_ check_cluster_actions_roles_are_created_correctly(sdk_client_fs, cluster, hidden_role_names, hidden_role_prefix) for service_name in ALL_SERVICE_NAMES: check_service_and_components_roles_are_created_correctly( - sdk_client_fs, cluster.service_add(name=service_name), hidden_role_names, hidden_role_prefix + sdk_client_fs, + cluster.service_add(name=service_name), + hidden_role_names, + hidden_role_prefix, ) diff --git a/tests/functional/rbac/actions/test_run_actions.py b/tests/functional/rbac/actions/test_run_actions.py index 7608d11c1f..9cd82a13b1 100644 --- a/tests/functional/rbac/actions/test_run_actions.py +++ b/tests/functional/rbac/actions/test_run_actions.py @@ -17,17 +17,24 @@ import itertools import os from contextlib import contextmanager -from typing import Optional, Union, Tuple +from typing import Optional, Tuple, Union import allure import pytest -from adcm_client.objects import ADCMClient, Cluster, Service, Component, User, Provider - +from adcm_client.objects import ADCMClient, Cluster, Component, Provider, Service, User +from tests.functional.rbac.action_role_utils import ( + action_business_role, + create_action_policy, +) from tests.functional.rbac.actions.conftest import DATA_DIR +from tests.functional.rbac.conftest import ( + BusinessRole, + as_user_objects, + delete_policy, + is_allowed, + is_denied, +) from tests.functional.tools import AnyADCMObject, get_object_represent -from tests.functional.rbac.conftest import BusinessRole, delete_policy, is_denied, is_allowed, as_user_objects -from tests.functional.rbac.action_role_utils import action_business_role, create_action_policy - DO_NOTHING_ACTION = 'Do nothing' @@ -87,7 +94,8 @@ def _test_basic_action_run_permissions(adcm_object, admin_sdk, user_sdk, user, a with allure.step(f"Check that granted permission doesn't allow running '{DO_NOTHING_ACTION}' on other objects"): for obj in filter( - lambda x: not _is_the_same(x, adcm_object) and not _do_nothing_action_not_presented(x), all_objects + lambda x: not _is_the_same(x, adcm_object) and not _do_nothing_action_not_presented(x), + all_objects, ): is_denied(obj, action_business_role(obj, DO_NOTHING_ACTION), client=user_sdk) @@ -115,7 +123,11 @@ def test_config_change_via_plugin(clients, user, actions_cluster, actions_provid _test_config_change(cluster, (cluster,), user=user, user_client=clients.user, admin_client=clients.admin) _test_config_change(service, (cluster, service), user=user, user_client=clients.user, admin_client=clients.admin) _test_config_change( - component, (cluster, service, component), user=user, user_client=clients.user, admin_client=clients.admin + component, + (cluster, service, component), + user=user, + user_client=clients.user, + admin_client=clients.admin, ) _test_config_change(provider, (provider,), user=user, user_client=clients.user, admin_client=clients.admin) @@ -204,7 +216,9 @@ def test_host_actions(clients, actions_cluster, actions_cluster_bundle, actions_ with allure.step('Grant permission to run host actions on cluster, service and component'): business_roles = [ action_business_role( - obj, host_action_template.format(object_type=obj.__class__.__name__), action_on_host=first_host + obj, + host_action_template.format(object_type=obj.__class__.__name__), + action_on_host=first_host, ) for obj in cluster_objects ] diff --git a/tests/functional/rbac/actions/test_upgrade.py b/tests/functional/rbac/actions/test_upgrade.py index cb82e8ece1..ed8a46637f 100644 --- a/tests/functional/rbac/actions/test_upgrade.py +++ b/tests/functional/rbac/actions/test_upgrade.py @@ -14,24 +14,28 @@ # pylint: disable=redefined-outer-name -from typing import List, Tuple, Dict, Literal, Iterable +from typing import Dict, Iterable, List, Literal, Tuple import allure import pytest from adcm_client.base import NoSuchEndpointOrAccessIsDenied, ObjectNotFound -from adcm_client.objects import Bundle, Cluster, Policy, Component, Service, ADCMClient +from adcm_client.objects import ADCMClient, Bundle, Cluster, Component, Policy, Service from adcm_client.wrappers.api import AccessIsDenied - -from tests.library.consts import HTTPMethod -from tests.functional.tools import ClusterRelatedObject -from tests.functional.rbac.checkers import ForbiddenCallChecker -from tests.functional.rbac.conftest import BusinessRole, is_allowed, is_denied, as_user_objects from tests.functional.rbac.action_role_utils import ( - create_action_policy, action_business_role, + create_action_policy, get_action_display_name_from_role_name, ) from tests.functional.rbac.actions.conftest import DATA_DIR +from tests.functional.rbac.checkers import ForbiddenCallChecker +from tests.functional.rbac.conftest import ( + BusinessRole, + as_user_objects, + is_allowed, + is_denied, +) +from tests.functional.tools import ClusterRelatedObject +from tests.library.consts import HTTPMethod ClusterObjectClassName = Literal['Cluster', 'Service', 'Component'] @@ -109,9 +113,7 @@ def old_cluster_actions_policies(self, clients, user, all_business_roles, old_cl ] @pytest.mark.usefixtures("new_bundle", "old_cluster_actions_policies") - def test_upgrade( - self, clients, user, old_cluster, all_business_roles, old_cluster_objects_map - ): # pylint: disable=too-many-arguments + def test_upgrade(self, clients, user, old_cluster, all_business_roles, old_cluster_objects_map): """ Test that upgrade works correctly considering permissions on actions: 1. Actions with same name and display name are still available after upgrade if permissions were granted. @@ -154,7 +156,8 @@ def check_permissions_after_upgrade(self, user_client: ADCMClient, all_business_ user_object_map, tuple( self._get_roles_filter_exclude_by_action_name( - all_business_roles, (ACTION_NAME_BEFORE_CHANGE, ACTION_TO_BE_DELETED, *self.NOT_ALLOWED_ACTIONS) + all_business_roles, + (ACTION_NAME_BEFORE_CHANGE, ACTION_TO_BE_DELETED, *self.NOT_ALLOWED_ACTIONS), ) ), ) @@ -202,7 +205,8 @@ def check_roles_are_allowed( """Check that given roles are allowed to be launched""" for role in business_roles: adcm_object, *_ = as_user_objects( - user_client, self._get_object_from_map_by_role_name(role.role_name, cluster_object_map) + user_client, + self._get_object_from_map_by_role_name(role.role_name, cluster_object_map), ) is_allowed(adcm_object, role).wait() diff --git a/tests/functional/rbac/checkers.py b/tests/functional/rbac/checkers.py index 451240c61e..8cee1ff74c 100644 --- a/tests/functional/rbac/checkers.py +++ b/tests/functional/rbac/checkers.py @@ -15,29 +15,28 @@ import json import re from functools import partial -from typing import Type, Collection, Union, Callable, Optional +from typing import Callable, Collection, Optional, Type, Union from urllib import parse import allure import requests from adcm_client.base import ObjectNotFound from adcm_client.objects import ( + ADCM, ADCMClient, + Bundle, Cluster, - Service, Component, - Provider, - Host, - User, Group, - Role, + Host, Policy, - Bundle, - ADCM, + Provider, + Role, + Service, + User, ) - -from tests.library.consts import HTTPMethod from tests.functional.tools import AnyADCMObject, AnyRBACObject +from tests.library.consts import HTTPMethod RoleTargetObject = Union[AnyADCMObject, AnyRBACObject, Bundle, ADCM] RoleTargetType = Type[RoleTargetObject] diff --git a/tests/functional/rbac/conftest.py b/tests/functional/rbac/conftest.py index 4a716bab54..68134ce7c8 100644 --- a/tests/functional/rbac/conftest.py +++ b/tests/functional/rbac/conftest.py @@ -15,33 +15,37 @@ from enum import Enum from functools import partial from operator import methodcaller -from typing import Callable, NamedTuple, Union, List, Tuple, Collection, Optional +from typing import Callable, Collection, List, NamedTuple, Optional, Tuple, Union import allure import pytest -from adcm_client.base import NoSuchEndpointOrAccessIsDenied, BaseAPIObject, ObjectNotFound +from adcm_client.base import ( + BaseAPIObject, + NoSuchEndpointOrAccessIsDenied, + ObjectNotFound, +) from adcm_client.objects import ( + ADCM, ADCMClient, - User, - Group, + Bundle, Cluster, - Service, Component, - Provider, + Group, Host, - Bundle, - Role, - ADCM, Policy, + Provider, + Role, + Service, + User, ) from adcm_client.wrappers.api import AccessIsDenied, ADCMApiWrapper from adcm_pytest_plugin.utils import catch_failed, random_string - +from coreapi.exceptions import ErrorMessage +from tests.functional.maintenance_mode.conftest import MM_IS_OFF, MM_IS_ON from tests.functional.rbac.checkers import Deny -from tests.functional.tools import get_object_represent, AnyADCMObject, ADCMObjects - +from tests.functional.tools import ADCMObjects, AnyADCMObject, get_object_represent -# pylint: disable=redefined-outer-name,unused-argument +# pylint: disable=redefined-outer-name # Enum names doesn't conform to UPPER_CASE naming style @@ -188,11 +192,14 @@ class BusinessRoles(Enum): EditHostConfigurations = BusinessRole( "Edit host configurations", methodcaller("config_set_diff", {}), Deny.ChangeConfigOf(Host) ) + # checks for this role won't work, check fixture that creates changing MM Business roles ManageMaintenanceMode = BusinessRole( # to change specific values, pass kwargs to call to denial checker - "Manage Maintenance mode", - lambda host, mm_flag: host.maintenance_mode_set(mm_flag), - Deny.Change(Host), + "Manage cluster Maintenance mode", + lambda host, mm_flag: host.maintenance_mode_set( + mm_flag + ), # it won't work with new MM, check corresponding tests + Deny.Change(Host), # it won't work with new MM, check corresponding tests ) ViewImports = BusinessRole("View imports", methodcaller("imports"), Deny.ViewImportsOf((Cluster, Service))) @@ -357,6 +364,31 @@ def second_objects(sdk_client_fs): return cluster, service, component, provider, host +@pytest.fixture() +def mm_changing_roles(api_client) -> tuple[BusinessRole, BusinessRole, BusinessRole]: + """ + Prepare utility roles for checking changing MM on various objects: service, component, host + """ + + def change_service_mm(*_, object_id: int, value: str, user_token: str) -> None: + with api_client.logged_as_another_user(token=user_token): + api_client.service.change_maintenance_mode(object_id, value) + + def change_component_mm(*_, object_id: int, value: str, user_token: str) -> None: + with api_client.logged_as_another_user(token=user_token): + api_client.component.change_maintenance_mode(object_id, value) + + def change_host_mm(*_, object_id: int, value: str, user_token: str) -> None: + with api_client.logged_as_another_user(token=user_token): + api_client.host.change_maintenance_mode(object_id, value) + + return ( + BusinessRole("Manage service MM", change_service_mm, change_service_mm), + BusinessRole("Manage component MM", change_component_mm, change_component_mm), + BusinessRole("Manage host MM", change_host_mm, change_host_mm), + ) + + def get_as_client_object(api: ADCMApiWrapper, obj: AnyADCMObject, **kwargs): """Get representation of an object from perspective of given user (client)""" return obj.__class__(api, id=obj.id, **kwargs) @@ -378,7 +410,6 @@ def delete_policy(policy): policy.delete() -# pylint: disable-next=too-many-arguments def create_policy( sdk_client, permission: Union[BusinessRoles, List[BusinessRoles]], @@ -498,7 +529,7 @@ def is_denied( else: try: role.method_call(base_object, *args, **kwargs) - except (AccessIsDenied, NoSuchEndpointOrAccessIsDenied, ObjectNotFound): + except (AccessIsDenied, NoSuchEndpointOrAccessIsDenied, ObjectNotFound, ErrorMessage): pass else: raise AssertionError(f"{role.role_name} on {object_represent} should not be allowed") @@ -506,6 +537,46 @@ def is_denied( role.check_denied(client, base_object, **kwargs) +def check_mm_change_is_denied( + obj: Service | Component | Host, + denial_method: Union[BusinessRoles, BusinessRole], + user_client: ADCMClient, + new_mm_value: str = MM_IS_ON, + old_mm_value: str = MM_IS_OFF, +): + """ + Check that change maintenance mode is disallowed to the user + and the value is the same + """ + is_denied( + obj, + denial_method, + client=user_client, + object_id=obj.id, + value=new_mm_value, + user_token=user_client.api_token(), + ) + obj.reread() + assert ( + obj.maintenance_mode == old_mm_value + ), f'{obj.__class__.__name__} maintenance mode should be intact and be equal to "{old_mm_value}"' + + +def check_mm_change_is_allowed( + obj: Service | Component | Host, + allow_method: Union[BusinessRoles, BusinessRole], + user_client: ADCMClient, + new_mm_value: str = MM_IS_ON, +): + """ + Check that change maintenance mode is allowed to the user + and the value changed + """ + is_allowed(obj, allow_method, object_id=obj.id, value=new_mm_value, user_token=user_client.api_token()) + obj.reread() + assert obj.maintenance_mode == new_mm_value, f'{obj.__class__.__name__} maintenance mode should be "{new_mm_value}"' + + def extract_role_short_info(role: Role) -> RoleShortInfo: """Convert API Role object to RoleShortInfo""" return RoleShortInfo(role.id, role.name, tuple(role.category)) diff --git a/tests/functional/rbac/test_access_to_objects.py b/tests/functional/rbac/test_access_to_objects.py index 5c074017e0..2d8c70556a 100644 --- a/tests/functional/rbac/test_access_to_objects.py +++ b/tests/functional/rbac/test_access_to_objects.py @@ -18,26 +18,38 @@ import os.path from contextlib import contextmanager -from typing import Callable, Set, List, Iterable, Union, Dict +from typing import Callable, Dict, Iterable, List, Set, Union import allure import pytest from adcm_client.base import ObjectNotFound -from adcm_client.objects import ADCMClient, Task, Cluster, Service, Component, Provider, User, Group +from adcm_client.objects import ( + ADCMClient, + Cluster, + Component, + Group, + Provider, + Service, + Task, + User, +) from adcm_pytest_plugin.utils import catch_failed - -from tests.library.utils import lower_class_name -from tests.functional.tools import get_object_represent +from coreapi.exceptions import ErrorMessage +from tests.functional.rbac.action_role_utils import ( + action_business_role, + create_action_policy, +) +from tests.functional.rbac.conftest import DATA_DIR +from tests.functional.rbac.conftest import BusinessRoles as BR from tests.functional.rbac.conftest import ( - DATA_DIR, - BusinessRoles as BR, - get_as_client_object, - delete_policy, - create_policy, RbacRoles, SDKClients, + create_policy, + delete_policy, + get_as_client_object, ) -from tests.functional.rbac.action_role_utils import create_action_policy, action_business_role +from tests.functional.tools import get_object_represent +from tests.library.utils import lower_class_name @contextmanager @@ -294,9 +306,7 @@ def user_or_group(self, request, user, clients) -> Dict[str, Union[User, Group]] raise ValueError('param should be either "user" or "group"') # pylint: disable-next=too-many-locals - def test_access_to_tasks( - self, user_or_group: dict, clients, cluster, provider, finished_tasks - ): # pylint: disable=too-many-arguments + def test_access_to_tasks(self, user_or_group: dict, clients, cluster, provider, finished_tasks): """ Test that user: 1. Have no access to task objects that were launched before user got permission to run action @@ -367,7 +377,7 @@ def test_access_to_tasks_on_service_add_remove( self.check_access_granted_for_tasks(clients.user, [task]) self.check_no_access_granted_for_tasks(clients.user, [second_task]) - @pytest.mark.extra_rbac() # pylint: disable-next=too-many-arguments + @pytest.mark.extra_rbac() def test_access_to_tasks_on_cluster_host_add_remove( self, user_or_group: Callable, cluster: Cluster, provider: Provider, clients: SDKClients ): @@ -392,7 +402,7 @@ def test_access_to_tasks_on_cluster_host_add_remove( cluster.host_delete(host) self.check_access_granted_for_tasks(clients.user, [task, second_task]) - @pytest.mark.extra_rbac() # pylint: disable-next=too-many-arguments + @pytest.mark.extra_rbac() def test_access_to_tasks_on_hc_map_change(self, user_or_group: dict, cluster, provider, clients): """ Test that access for task objects is correct after HC map is changed @@ -528,7 +538,7 @@ def check_access_granted_for_tasks(self, user_client: ADCMClient, tasks: Iterabl for job in task.job_list(): with catch_failed(ObjectNotFound, 'Job and log objects should be available directly via client'): get_as_client_object(api, job) - get_as_client_object(api, job.log(), path_args={'job_id': job.id}) + get_as_client_object(api, job.log(), path_args={'job_pk': job.id}) def check_no_access_granted_for_tasks(self, user_client: ADCMClient, tasks: Iterable[Task]): """Check there's no access to tasks, their jobs and logs""" @@ -541,7 +551,7 @@ def check_no_access_granted_for_tasks(self, user_client: ADCMClient, tasks: Iter _expect_not_found(api, job, 'Job object should be available directly via client') log = job.log() _expect_not_found( - api, log, 'Log object should be available directly via client', path_args={'job_id': job.id} + api, log, 'Log object should be available directly via client', path_args={'job_pk': job.id} ) @@ -552,8 +562,9 @@ def _get_objects_id(get_objects_list: Callable) -> Set[int]: def _expect_not_found(api, obj, message, **kwargs): try: get_as_client_object(api, obj, **kwargs) - except ObjectNotFound: - pass + except ErrorMessage as e: + if not hasattr(e.error, "title") or e.error.title != "404 Not Found": + raise AssertionError(message) from e else: raise AssertionError(message) diff --git a/tests/functional/rbac/test_adcm_related_permissions.py b/tests/functional/rbac/test_adcm_related_permissions.py index 8efa14fb23..76011a8f9b 100644 --- a/tests/functional/rbac/test_adcm_related_permissions.py +++ b/tests/functional/rbac/test_adcm_related_permissions.py @@ -12,17 +12,14 @@ """Test business permissions related to ADCM""" -# pylint: disable=too-many-arguments,unused-argument - import pytest from adcm_client.objects import ADCMClient - from tests.functional.rbac.conftest import ( - use_role, BusinessRoles, - is_denied, - is_allowed, delete_policy, + is_allowed, + is_denied, + use_role, ) pytestmark = [pytest.mark.extra_rbac] diff --git a/tests/functional/rbac/test_built_in_roles_composition.py b/tests/functional/rbac/test_built_in_roles_composition.py index cb6f88ed21..6cd18a7a74 100644 --- a/tests/functional/rbac/test_built_in_roles_composition.py +++ b/tests/functional/rbac/test_built_in_roles_composition.py @@ -20,9 +20,8 @@ from adcm_client.objects import ADCMClient, Role from adcm_client.wrappers.api import AccessIsDenied from adcm_pytest_plugin.utils import catch_failed - -from tests.library.assertions import is_superset_of from tests.functional.rbac.conftest import BusinessRoles +from tests.library.assertions import is_superset_of pytestmark = [pytest.mark.extra_rbac] @@ -71,7 +70,6 @@ BusinessRoles.RemoveBundle, BusinessRoles.CreateHost, BusinessRoles.RemoveHosts, - BusinessRoles.ManageMaintenanceMode, ) } ) diff --git a/tests/functional/rbac/test_bundle_deletion.py b/tests/functional/rbac/test_bundle_deletion.py index 1bbcf26134..7ba6f6d695 100644 --- a/tests/functional/rbac/test_bundle_deletion.py +++ b/tests/functional/rbac/test_bundle_deletion.py @@ -18,7 +18,6 @@ import pytest from adcm_client.objects import ADCMClient, Bundle, Role from adcm_pytest_plugin.utils import get_data_dir - from tests.functional.rbac.action_role_utils import get_roles_of_type from tests.functional.rbac.conftest import RoleType, extract_role_short_info from tests.library.assertions import sets_are_equal @@ -101,7 +100,10 @@ def check_categories_are_presented(client: ADCMClient, *categories: str): roles = tuple( map( extract_role_short_info, - filter(lambda x: ACTION_BUSINESS_ROLE_INFIX in x.name, get_roles_of_type(RoleType.BUSINESS, client)), + filter( + lambda x: ACTION_BUSINESS_ROLE_INFIX in x.name, + get_roles_of_type(RoleType.BUSINESS, client), + ), ) ) for category in categories: @@ -139,7 +141,9 @@ def check_action_business_roles_have_hidden_roles(client: ADCMClient, bundles: I actual_child_names = _get_children_names(role) expected_child_names = extraction_function(bundles) sets_are_equal( - actual_child_names, expected_child_names, "Children of a business action role aren't correct" + actual_child_names, + expected_child_names, + "Children of a business action role aren't correct", ) diff --git a/tests/functional/rbac/test_categories.py b/tests/functional/rbac/test_categories.py index a63d9d2d35..a830ea7a0b 100644 --- a/tests/functional/rbac/test_categories.py +++ b/tests/functional/rbac/test_categories.py @@ -13,15 +13,18 @@ """Test categories (filters for roles)""" import os -from typing import Set, Generator, Dict +from typing import Dict, Generator, Set from urllib import parse import allure import requests from adcm_client.objects import ADCMClient - -from tests.functional.rbac.conftest import DATA_DIR, RoleShortInfo, extract_role_short_info -from tests.library.assertions import is_superset_of, is_empty +from tests.functional.rbac.conftest import ( + DATA_DIR, + RoleShortInfo, + extract_role_short_info, +) +from tests.library.assertions import is_empty, is_superset_of CATEGORIES_SUFFIX = 'api/v1/rbac/role/category' @@ -106,7 +109,8 @@ def check_categories_after_provider_bundle_upload(client: ADCMClient, bundle_pat def _check_category_list(client: ADCMClient, categories: Set[str]): """Check if category list is the same as expected""" categories_request = requests.get( - parse.urljoin(client.url, CATEGORIES_SUFFIX), headers={'Authorization': f'Token {client.api_token()}'} + parse.urljoin(client.url, CATEGORIES_SUFFIX), + headers={'Authorization': f'Token {client.api_token()}'}, ) categories_request.raise_for_status() category_list = categories_request.json() @@ -114,7 +118,11 @@ def _check_category_list(client: ADCMClient, categories: Set[str]): expected := len(categories) ), f'Amount of categories should be exactly {expected}, not {actual}' # is superset is ok, because length is the same, but if one day we have "is_equal_to", change it - is_superset_of(set(category_list), categories, 'Categories list is incorrect. See attachment for more details.') + is_superset_of( + set(category_list), + categories, + 'Categories list is incorrect. See attachment for more details.', + ) def _get_all_roles_info(client: ADCMClient) -> Generator[RoleShortInfo, None, None]: diff --git a/tests/functional/rbac/test_cluster_related_permissions.py b/tests/functional/rbac/test_cluster_related_permissions.py index ee7f58ffd7..d08926a5e9 100644 --- a/tests/functional/rbac/test_cluster_related_permissions.py +++ b/tests/functional/rbac/test_cluster_related_permissions.py @@ -12,26 +12,28 @@ """Test business permissions related to cluster objects""" -# pylint: disable=too-many-arguments,unused-argument,too-many-locals +# pylint: disable=too-many-locals import itertools import allure import pytest from adcm_client.objects import ADCMClient, Policy - from tests.functional.rbac.conftest import ( - BusinessRoles as BR, - use_role, + CLUSTER_EDIT_CONFIG_ROLES, + CLUSTER_VIEW_CONFIG_ROLES, + PROVIDER_EDIT_CONFIG_ROLES, + PROVIDER_VIEW_CONFIG_ROLES, +) +from tests.functional.rbac.conftest import BusinessRoles as BR +from tests.functional.rbac.conftest import ( + RbacRoles, as_user_objects, + check_mm_change_is_allowed, + create_policy, + delete_policy, is_allowed, is_denied, - delete_policy, - create_policy, - RbacRoles, - CLUSTER_VIEW_CONFIG_ROLES, - PROVIDER_VIEW_CONFIG_ROLES, - CLUSTER_EDIT_CONFIG_ROLES, - PROVIDER_EDIT_CONFIG_ROLES, + use_role, ) pytestmark = [pytest.mark.extra_rbac] @@ -476,20 +478,29 @@ def test_service_administrator(user, user_sdk: ADCMClient, sdk_client_fs, prepar is_denied_to_view(cluster, second_service_on_first_cluster, *second_objects, *provider_objects, client=user_sdk) -def test_cluster_administrator(user, user_sdk: ADCMClient, sdk_client_fs, prepare_objects, second_objects): +def test_cluster_administrator(user, mm_changing_roles, clients, prepare_objects, second_objects): """Test that cluster administrator role grants access to single cluster and related services and components""" - cluster, service, component, *provider_objects = prepare_objects + cluster, service, component, provider, host = prepare_objects - role = sdk_client_fs.role(name=RbacRoles.ClusterAdministrator.value) - sdk_client_fs.policy_create( + cluster.host_add(host) + + role = clients.admin.role(name=RbacRoles.ClusterAdministrator.value) + clients.admin.policy_create( name=f"Policy with role {role.name}", role=role, objects=[cluster], user=[user], group=[] ) - user_sdk.reread() + clients.user.reread() - allowed_user_objects = as_user_objects(user_sdk, cluster, service, component) + allowed_user_objects = *_, user_service, user_component, user_host = as_user_objects( + clients.user, cluster, service, component, host + ) is_allowed_to_view(*allowed_user_objects) is_allowed_to_edit(*allowed_user_objects) - is_denied_to_view(*second_objects, *provider_objects, client=user_sdk) + is_denied_to_view(*second_objects, provider, client=clients.user) + + service_role, component_role, host_role = mm_changing_roles + check_mm_change_is_allowed(user_host, host_role, clients.user) + check_mm_change_is_allowed(user_service, service_role, clients.user) + check_mm_change_is_allowed(user_component, component_role, clients.user) def test_provider_administrator(user, user_sdk: ADCMClient, sdk_client_fs, prepare_objects, second_objects): diff --git a/tests/functional/rbac/test_complex_cases.py b/tests/functional/rbac/test_complex_cases.py index 3afe81de3f..7c5c2eca5d 100644 --- a/tests/functional/rbac/test_complex_cases.py +++ b/tests/functional/rbac/test_complex_cases.py @@ -12,22 +12,20 @@ """Test complex RBAC cases""" -# pylint: disable=too-many-arguments - -from typing import Iterable, Union +from typing import Iterable import allure import pytest from adcm_client.objects import ADCMClient, Host - -from tests.functional.rbac.checkers import Deny +from tests.functional.maintenance_mode.conftest import MM_IS_OFF, MM_IS_ON +from tests.functional.rbac.conftest import BusinessRoles as BR from tests.functional.rbac.conftest import ( - create_policy, - BusinessRoles as BR, as_user_objects, + check_mm_change_is_allowed, + check_mm_change_is_denied, + create_policy, is_allowed, is_denied, - BusinessRole, ) from tests.functional.tools import AnyADCMObject @@ -49,76 +47,45 @@ def second_host_in_cluster_with_mm_allowed(self, second_objects) -> Host: cluster, *_, host = second_objects return cluster.host_add(host) - @pytest.mark.parametrize( - 'business_role', - [ - BR.ManageMaintenanceMode, - BusinessRole( - BR.ManageMaintenanceMode.value.role_name, - BR.ManageMaintenanceMode.value.method_call, - Deny.PartialChange(Host), - ), - ], - ids=['by_put', 'by_patch'], - ) - @pytest.mark.usefixtures('host_in_cluster_with_mm_allowed', 'second_host_in_cluster_with_mm_allowed') - def test_manage_maintenance_mode(self, business_role, clients, user, prepare_objects, second_objects): + @pytest.mark.usefixtures("host_in_cluster_with_mm_allowed", "second_host_in_cluster_with_mm_allowed") + def test_manage_maintenance_mode( # pylint: disable=too-many-locals + self, mm_changing_roles, clients, user, prepare_objects, second_objects + ): """ Test that manage maintenance mode role is working correctly """ - *_, host = prepare_objects + service_role, component_role, host_role = mm_changing_roles + cluster, service, component, _, host = prepare_objects *_, second_host = second_objects with allure.step("Check that user can't change maintenance mode without permission"): - self.check_mm_change_is_denied(host, business_role, clients.user) - self.check_mm_change_is_denied(second_host, business_role, clients.user) + check_mm_change_is_denied(service, service_role, clients.user) + check_mm_change_is_denied(component, component_role, clients.user) + check_mm_change_is_denied(host, host_role, clients.user) + check_mm_change_is_denied(second_host, host_role, clients.user) - policy = create_policy(clients.admin, BR.ManageMaintenanceMode, [host], [user], []) + policy = create_policy(clients.admin, BR.ManageMaintenanceMode, [cluster], [user], []) - with allure.step('Check that user see only host which they can edit'): - user_hosts = [host.fqdn for host in clients.user.host_list()] - assert len(user_hosts) == 1, 'User should see only 1 host' - assert ( - actual_fqdn := user_hosts[0] - ) == host.fqdn, f'User should see only host {host.fqdn}, not {actual_fqdn}' + with allure.step("Check that user see only service, component and host which they can edit"): + assert len(clients.user.service_list()) == 1, "User should see only 1 service" + assert len(clients.user.host_list()) == 1, "User should see only 1 host" + assert len(clients.user.component_list()) == 2, "User should see only 2 components" - with allure.step('Check that user can change maintenance mode after permission is granted'): - user_host, *_ = as_user_objects(clients.user, host) - self.check_mm_change_is_allowed(user_host, business_role) - self.check_mm_change_is_denied(second_host, business_role, clients.user) + with allure.step("Check that user can change maintenance mode after permission is granted"): + user_service, user_component, user_host = as_user_objects(clients.user, service, component, host) + check_mm_change_is_allowed(user_service, service_role, clients.user) + check_mm_change_is_allowed(user_component, component_role, clients.user) + check_mm_change_is_allowed(user_host, host_role, clients.user) + check_mm_change_is_denied(second_host, host_role, clients.user) policy.delete() with allure.step("Check that user can't change maintenance mode when permission is withdrawn"): - self.check_mm_change_is_denied(host, business_role, clients.user, new_mm_value='off', old_mm_value='on') - self.check_mm_change_is_denied(second_host, business_role, clients.user) - - def check_mm_change_is_denied( - self, - host: Host, - denial_method: Union[BR, BusinessRole], - user_client: ADCMClient, - new_mm_value: str = 'on', - old_mm_value: str = 'off', - ): - """ - Check that change maintenance mode is disallowed to the user - and the value is the same - """ - is_denied(host, denial_method, client=user_client, data={'maintenance_mode': new_mm_value}) - host.reread() - assert ( - host.maintenance_mode == old_mm_value - ), f'Host maintenance mode should be intact and be equal to "{old_mm_value}"' - - def check_mm_change_is_allowed(self, host: Host, allow_method: Union[BR, BusinessRole], new_mm_value: str = 'on'): - """ - Check that change maintenance mode is allowed to the user - and the value changed - """ - is_allowed(host, allow_method, new_mm_value) - host.reread() - assert host.maintenance_mode == new_mm_value, f'Host maintenance mode should be "{new_mm_value}"' + kwargs = {"new_mm_value": MM_IS_OFF, "old_mm_value": MM_IS_ON} + check_mm_change_is_denied(service, service_role, clients.user, **kwargs) + check_mm_change_is_denied(component, component_role, clients.user, **kwargs) + check_mm_change_is_denied(host, host_role, clients.user, **kwargs) + check_mm_change_is_denied(second_host, host_role, clients.user) class TestTwoUsers: @@ -137,7 +104,7 @@ def second_user(self, sdk_client_fs): return sdk_client_fs.user_create(*self._SECOND_USER_CREDS) @pytest.fixture() - def second_user_sdk(self, adcm_fs, second_user): # pylint: disable=unused-argument + def second_user_sdk(self, adcm_fs, second_user): """ADCM Client for second non-superuser""" username, password = self._SECOND_USER_CREDS return ADCMClient(url=adcm_fs.url, user=username, password=password) @@ -156,19 +123,19 @@ def test_grant_role_to_users_withdraw_from_one( objects_wo_service = [cluster, component, provider, host, *second_objects] admin_client, first_client, second_client = sdk_client_fs, user_sdk, second_user_sdk - with allure.step('Create policy assigned to both users'): + with allure.step("Create policy assigned to both users"): policy = create_policy(admin_client, BR.EditServiceConfigurations, [service], [first_user, second_user], []) - with allure.step('Check that config of only one service can be edited by both users'): + with allure.step("Check that config of only one service can be edited by both users"): self._edit_is_allowed_to(first_client, [service]) self._edit_is_allowed_to(second_client, [service]) self._edit_is_denied_to(first_client, objects_wo_service) self._edit_is_denied_to(second_client, objects_wo_service) - with allure.step('Remove one of users from policy'): - policy.update(user=[{'id': second_user.id}]) + with allure.step("Remove one of users from policy"): + policy.update(user=[{"id": second_user.id}]) - with allure.step('Check that only one of users can edit config of one service'): + with allure.step("Check that only one of users can edit config of one service"): self._edit_is_allowed_to(second_client, [service]) self._edit_is_denied_to(second_client, objects_wo_service) self._edit_is_denied_to(first_client, [*prepare_objects, *second_objects]) @@ -186,25 +153,25 @@ def test_change_users_in_groups( objects_wo_cluster = [service, component, provider, host, *second_objects] admin_client, first_client, second_client = sdk_client_fs, user_sdk, second_user_sdk - with allure.step('Create two groups with one user in each'): - first_group = admin_client.group_create('First Group', user=[{'id': first_user.id}]) - second_group = admin_client.group_create('Second Group', user=[{'id': second_user.id}]) + with allure.step("Create two groups with one user in each"): + first_group = admin_client.group_create("First Group", user=[{"id": first_user.id}]) + second_group = admin_client.group_create("Second Group", user=[{"id": second_user.id}]) - with allure.step('Create two policies and assign separate group to each'): + with allure.step("Create two policies and assign separate group to each"): create_policy(admin_client, BR.EditServiceConfigurations, [service], [], [first_group]) create_policy(admin_client, BR.EditClusterConfigurations, [cluster], [], [second_group]) - with allure.step('Check that one user have access only to service config and another only to cluster config'): + with allure.step("Check that one user have access only to service config and another only to cluster config"): self._edit_is_allowed_to(first_client, [service]) self._edit_is_allowed_to(second_client, [cluster]) self._edit_is_denied_to(first_client, objects_wo_service) self._edit_is_denied_to(second_client, objects_wo_cluster) - with allure.step('Swap users in groups'): - first_group.update(user=[{'id': second_user.id}]) - second_group.update(user=[{'id': first_user.id}]) + with allure.step("Swap users in groups"): + first_group.update(user=[{"id": second_user.id}]) + second_group.update(user=[{"id": first_user.id}]) - with allure.step('Check that permissions were swapped'): + with allure.step("Check that permissions were swapped"): self._edit_is_allowed_to(first_client, [cluster]) self._edit_is_allowed_to(second_client, [service]) self._edit_is_denied_to(first_client, objects_wo_cluster) @@ -223,23 +190,23 @@ def test_grant_different_permissions_to_two_users( objects_wo_cluster = [service, component, provider, host, *second_objects] admin_client, first_client, second_client = sdk_client_fs, user_sdk, second_user_sdk - with allure.step('Create two policies and assign separate group to each'): + with allure.step("Create two policies and assign separate group to each"): first_policy = create_policy(admin_client, BR.EditServiceConfigurations, [service], [first_user], []) second_policy = create_policy(admin_client, BR.EditClusterConfigurations, [cluster], [second_user], []) with allure.step( - 'Check that one user have access only to cluster config change, another user to service config change' + "Check that one user have access only to cluster config change, another user to service config change" ): self._edit_is_allowed_to(first_client, [service]) self._edit_is_allowed_to(second_client, [cluster]) self._edit_is_denied_to(first_client, objects_wo_service) self._edit_is_denied_to(second_client, objects_wo_cluster) - with allure.step('Change users in policies'): - first_policy.update(user=[{'id': second_user.id}]) - second_policy.update(user=[{'id': first_user.id}]) + with allure.step("Change users in policies"): + first_policy.update(user=[{"id": second_user.id}]) + second_policy.update(user=[{"id": first_user.id}]) - with allure.step('Check that permissions were swapped'): + with allure.step("Check that permissions were swapped"): self._edit_is_allowed_to(first_client, [cluster]) self._edit_is_allowed_to(second_client, [service]) self._edit_is_denied_to(first_client, objects_wo_cluster) @@ -257,27 +224,27 @@ def test_reassign_policies_between_two_users( objects_wo_service = [cluster, component, provider, host, *second_objects] admin_client, first_client, second_client = sdk_client_fs, user_sdk, second_user_sdk - with allure.step('Create policy assigned to one user'): + with allure.step("Create policy assigned to one user"): policy = create_policy(admin_client, BR.EditServiceConfigurations, [service], [first_user], []) - with allure.step('Check that user have access only to service config change'): + with allure.step("Check that user have access only to service config change"): self._edit_is_allowed_to(first_client, [service]) self._edit_is_denied_to(first_client, objects_wo_service) self._edit_is_denied_to(second_client, [*prepare_objects, *second_objects]) - with allure.step('Grant access to both users to edit service config'): - policy.update(user=[{'id': first_user.id}, {'id': second_user.id}]) + with allure.step("Grant access to both users to edit service config"): + policy.update(user=[{"id": first_user.id}, {"id": second_user.id}]) - with allure.step('Check that both users have access only to service config change'): + with allure.step("Check that both users have access only to service config change"): self._edit_is_allowed_to(first_client, [service]) self._edit_is_allowed_to(second_client, [service]) self._edit_is_denied_to(first_client, objects_wo_service) self._edit_is_denied_to(second_client, objects_wo_service) - with allure.step('Remove first user from policy'): - policy.update(user=[{'id': second_user.id}]) + with allure.step("Remove first user from policy"): + policy.update(user=[{"id": second_user.id}]) - with allure.step('Check that another user now have access only to service config edit'): + with allure.step("Check that another user now have access only to service config edit"): self._edit_is_allowed_to(second_client, [service]) self._edit_is_denied_to(second_client, objects_wo_service) self._edit_is_denied_to(first_client, [*prepare_objects, *second_objects]) diff --git a/tests/functional/rbac/test_flat_endpoints.py b/tests/functional/rbac/test_flat_endpoints.py index b96353188b..905da2c217 100644 --- a/tests/functional/rbac/test_flat_endpoints.py +++ b/tests/functional/rbac/test_flat_endpoints.py @@ -18,9 +18,8 @@ import allure import pytest import requests -from adcm_client.objects import Cluster, Host, ADCMClient, ADCM +from adcm_client.objects import ADCM, ADCMClient, Cluster, Host from adcm_pytest_plugin.utils import random_string - from tests.functional.rbac.conftest import RbacRoles from tests.library.assertions import sets_are_equal from tests.library.utils import lower_class_name @@ -33,7 +32,7 @@ @pytest.fixture() -def prepare_configs(prepare_objects, second_objects) -> None: +def _prepare_configs(prepare_objects, second_objects) -> None: """Change configs, crete group configs and change them""" changed_config = {'boolean': False} for first_object, second_object in zip(prepare_objects, second_objects): @@ -44,7 +43,7 @@ def prepare_configs(prepare_objects, second_objects) -> None: _prepare_group_config(second_object) -@pytest.mark.usefixtures('prepare_configs') +@pytest.mark.usefixtures('_prepare_configs') def test_flat_endpoints(user, clients, prepare_objects, second_objects): """ Test "flat" endpoints: @@ -55,7 +54,15 @@ def test_flat_endpoints(user, clients, prepare_objects, second_objects): config/ """ cluster, service, component, provider, host = prepare_objects - all_objects = [*second_objects, cluster, service, provider, host, *service.component_list(), clients.admin.adcm()] + all_objects = [ + *second_objects, + cluster, + service, + provider, + host, + *service.component_list(), + clients.admin.adcm(), + ] clients.admin.policy_create( name=f'Service administrator of {service.name}', @@ -86,11 +93,10 @@ def check_jobs_and_tasks(client: ADCMClient, objects): task_flat_endpoint = "task" with allure.step(f'Check jobs at "{job_flat_endpoint}/" endpoint based on task_id'): - expected_jobs: set = { - job.task_id - for job in itertools.chain.from_iterable([obj.action(name=ACTION_NAME).task_list() for obj in objects]) - } - actual_jobs: set = {job['task_id'] for job in _query_flat_endpoint(client, job_flat_endpoint)} + expected_jobs = set() + for task in itertools.chain.from_iterable([obj.action(name=ACTION_NAME).task_list() for obj in objects]): + expected_jobs |= {job.id for job in task.job_list()} + actual_jobs: set = {job['id'] for job in _query_flat_endpoint(client, job_flat_endpoint)} sets_are_equal( actual_jobs, expected_jobs, @@ -119,7 +125,10 @@ def check_configs(client: ADCMClient, objects): @allure.step(f'Check tasks at "{GROUP_CONFIG_EP}/" endpoint based on object type, object_id and config_id') def _check_group_config_endpoint(client, objects): objects_with_group_config = tuple( - filter(lambda x: not isinstance(x, Host) and not isinstance(x, ADCM) and x.group_config(), objects) + filter( + lambda x: not isinstance(x, Host) and not isinstance(x, ADCM) and x.group_config(), + objects, + ) ) expected_group_configs = { (lower_class_name(obj), obj.id, obj.group_config()[0].config_id) for obj in objects_with_group_config @@ -165,7 +174,9 @@ def _check_configs_endpoint(client, expected_config_logs): } actual_configs = {config['id'] for config in _query_flat_endpoint(client, CONFIG_EP)} sets_are_equal( - actual_configs, expected_configs, f'Configs at flat endpoint "{CONFIG_EP}/" are not the same as expected' + actual_configs, + expected_configs, + f'Configs at flat endpoint "{CONFIG_EP}/" are not the same as expected', ) @@ -191,7 +202,10 @@ def _query_flat_endpoint(client: ADCMClient, endpoint: str): def _prepare_group_config(adcm_object: Cluster): group = adcm_object.group_config_create(f'{adcm_object.name} group {random_string(4)}') group.config_set_diff( - {'config': {'boolean': True}, 'attr': {'group_keys': {'boolean': True}, 'custom_group_keys': {'boolean': True}}} + { + 'config': {'boolean': True}, + 'attr': {'group_keys': {'boolean': True}, 'custom_group_keys': {'boolean': True}}, + } ) diff --git a/tests/functional/rbac/test_is_superuser.py b/tests/functional/rbac/test_is_superuser.py index 72e597d9b0..c953c63821 100644 --- a/tests/functional/rbac/test_is_superuser.py +++ b/tests/functional/rbac/test_is_superuser.py @@ -18,12 +18,11 @@ import allure import pytest from adcm_client.base import NoSuchEndpointOrAccessIsDenied -from adcm_client.objects import User, ADCMClient +from adcm_client.objects import ADCMClient, User from adcm_client.wrappers.api import AccessIsDenied from adcm_pytest_plugin.utils import catch_failed - +from tests.functional.rbac.conftest import BusinessRoles, as_user_objects, is_allowed from tests.functional.tools import get_object_represent -from tests.functional.rbac.conftest import as_user_objects, BusinessRoles, is_allowed SUPERUSER_CREDENTIALS = {'username': 'supausa', 'password': 'youcantcrackme'} @@ -38,7 +37,7 @@ def superuser(sdk_client_fs: ADCMClient) -> User: @pytest.fixture() -def superuser_sdk(superuser, adcm_fs) -> ADCMClient: # pylint: disable=unused-argument +def superuser_sdk(superuser, adcm_fs) -> ADCMClient: """Returns ADCMClient for superuser""" creds = SUPERUSER_CREDENTIALS return ADCMClient(url=adcm_fs.url, user=creds['username'], password=creds['password']) diff --git a/tests/functional/rbac/test_object_parametrization.py b/tests/functional/rbac/test_object_parametrization.py index 7be1e8a617..ceef245e11 100644 --- a/tests/functional/rbac/test_object_parametrization.py +++ b/tests/functional/rbac/test_object_parametrization.py @@ -16,18 +16,17 @@ import pytest from adcm_client.objects import ADCMClient from coreapi.exceptions import ErrorMessage - from tests.api.utils.tools import random_string from tests.functional.rbac.conftest import ( - create_policy, + CLUSTER_VIEW_CONFIG_ROLES, + PROVIDER_VIEW_CONFIG_ROLES, + TEST_USER_CREDENTIALS, BusinessRoles, as_user_objects, - is_allowed, + create_policy, delete_policy, + is_allowed, is_denied, - TEST_USER_CREDENTIALS, - CLUSTER_VIEW_CONFIG_ROLES, - PROVIDER_VIEW_CONFIG_ROLES, ) diff --git a/tests/functional/rbac/test_permissions_deleting.py b/tests/functional/rbac/test_permissions_deleting.py index 9ea26ec9ed..9433acf065 100644 --- a/tests/functional/rbac/test_permissions_deleting.py +++ b/tests/functional/rbac/test_permissions_deleting.py @@ -14,13 +14,12 @@ import allure from adcm_client.objects import ADCMClient - from tests.functional.rbac.conftest import ( + CLUSTER_VIEW_CONFIG_ROLES, BusinessRoles, create_policy, is_allowed, is_denied, - CLUSTER_VIEW_CONFIG_ROLES, ) @@ -30,7 +29,11 @@ def test_remove_user_from_policy(user_sdk: ADCMClient, user, prepare_objects, sd """ cluster_via_admin, *_ = prepare_objects policy = create_policy( - sdk_client_fs, BusinessRoles.ViewClusterConfigurations, objects=[cluster_via_admin], users=[user], groups=[] + sdk_client_fs, + BusinessRoles.ViewClusterConfigurations, + objects=[cluster_via_admin], + users=[user], + groups=[], ) cluster = user_sdk.cluster(id=cluster_via_admin.id) is_allowed(cluster, BusinessRoles.ViewComponentConfigurations) @@ -47,7 +50,11 @@ def test_remove_group_from_policy(user_sdk: ADCMClient, user, prepare_objects, s group = sdk_client_fs.group_create("test_group", user=[{"id": user.id}]) empty_group = sdk_client_fs.group_create("empty_group") policy = create_policy( - sdk_client_fs, BusinessRoles.ViewClusterConfigurations, objects=[cluster_via_admin], users=[], groups=[group] + sdk_client_fs, + BusinessRoles.ViewClusterConfigurations, + objects=[cluster_via_admin], + users=[], + groups=[group], ) cluster = user_sdk.cluster(id=cluster_via_admin.id) is_allowed(cluster, BusinessRoles.ViewClusterConfigurations) @@ -63,7 +70,11 @@ def test_remove_user_from_group(user_sdk: ADCMClient, user, prepare_objects, sdk cluster_via_admin, *_ = prepare_objects group = sdk_client_fs.group_create("test_group", user=[{"id": user.id}]) create_policy( - sdk_client_fs, BusinessRoles.ViewClusterConfigurations, objects=[cluster_via_admin], users=[], groups=[group] + sdk_client_fs, + BusinessRoles.ViewClusterConfigurations, + objects=[cluster_via_admin], + users=[], + groups=[group], ) cluster = user_sdk.cluster(id=cluster_via_admin.id) is_allowed(cluster, BusinessRoles.ViewClusterConfigurations) @@ -100,7 +111,11 @@ def test_change_child_role(user_sdk: ADCMClient, user, prepare_objects, sdk_clie cluster_via_admin, *_ = prepare_objects policy = create_policy( - sdk_client_fs, BusinessRoles.ViewClusterConfigurations, objects=[cluster_via_admin], users=[user], groups=[] + sdk_client_fs, + BusinessRoles.ViewClusterConfigurations, + objects=[cluster_via_admin], + users=[user], + groups=[], ) cluster = user_sdk.cluster(id=cluster_via_admin.id) @@ -186,10 +201,18 @@ def test_remove_policy_but_exists_same_policy(user_sdk: ADCMClient, user, prepar """ cluster_via_admin, *_ = prepare_objects create_policy( - sdk_client_fs, BusinessRoles.ViewClusterConfigurations, objects=[cluster_via_admin], users=[user], groups=[] + sdk_client_fs, + BusinessRoles.ViewClusterConfigurations, + objects=[cluster_via_admin], + users=[user], + groups=[], ) second_policy = create_policy( - sdk_client_fs, BusinessRoles.ViewClusterConfigurations, objects=[cluster_via_admin], users=[user], groups=[] + sdk_client_fs, + BusinessRoles.ViewClusterConfigurations, + objects=[cluster_via_admin], + users=[user], + groups=[], ) cluster = user_sdk.cluster(id=cluster_via_admin.id) is_allowed(cluster, BusinessRoles.ViewClusterConfigurations) diff --git a/tests/functional/rbac/test_reassignment.py b/tests/functional/rbac/test_reassignment.py index 5d13faab78..6ab134f147 100644 --- a/tests/functional/rbac/test_reassignment.py +++ b/tests/functional/rbac/test_reassignment.py @@ -13,20 +13,29 @@ """Test roles reassignment in various situations""" from contextlib import contextmanager -from typing import Dict, List, Generator, Collection, Tuple +from typing import Collection, Dict, Generator, List, Tuple import allure import pytest -from adcm_client.objects import ADCMClient, Bundle, User, Policy, Host, Cluster, Service, Provider, Role +from adcm_client.objects import ( + ADCMClient, + Bundle, + Cluster, + Host, + Policy, + Provider, + Role, + Service, + User, +) from adcm_pytest_plugin.utils import get_data_dir - from tests.functional.rbac.conftest import ( + TEST_USER_CREDENTIALS, BusinessRoles, + RbacRoles, + as_user_objects, is_allowed, is_denied, - as_user_objects, - TEST_USER_CREDENTIALS, - RbacRoles, ) from tests.functional.tools import AnyADCMObject, get_object_represent @@ -152,7 +161,10 @@ def grant_role(self, client: ADCMClient, user: User, role: RbacRoles, *objects: """Grant RBAC default role to a user""" with allure.step(f'Grant role "{role.value}" to user {user.username}'): return client.policy_create( - name=f'{user.username} is {role.value}', role=client.role(name=role.value), objects=objects, user=[user] + name=f'{user.username} is {role.value}', + role=client.role(name=role.value), + objects=objects, + user=[user], ) def test_add_remove_user_from_group_and_policy(self, clients, is_denied_to_user, prepare_objects, user): @@ -395,7 +407,9 @@ def check_role_wo_parametrization(clients, user, cluster_bundle, provider_bundle """Check that update of role without parametrization leads to correct permissions update""" role_name = "Role without parametrization" role = clients.admin.role_create( - name=role_name, display_name=role_name, child=_form_children(clients.admin, BusinessRoles.CreateCluster) + name=role_name, + display_name=role_name, + child=_form_children(clients.admin, BusinessRoles.CreateCluster), ) policy = clients.admin.policy_create(name="User policy", role=role, user=[user]) with new_client_instance(*TEST_USER_CREDENTIALS, clients.user.url) as user_client: diff --git a/tests/functional/rbac/test_upgrade_adcm.py b/tests/functional/rbac/test_upgrade_adcm.py index ba32d653aa..ae0af2a346 100644 --- a/tests/functional/rbac/test_upgrade_adcm.py +++ b/tests/functional/rbac/test_upgrade_adcm.py @@ -18,21 +18,20 @@ import allure import pytest from adcm_client.objects import ADCMClient, Bundle -from adcm_pytest_plugin.utils import random_string, get_data_dir from adcm_pytest_plugin.docker_utils import ADCM - +from adcm_pytest_plugin.utils import get_data_dir, random_string +from tests.functional.conftest import only_clean_adcm from tests.functional.rbac.action_role_utils import ( - get_roles_of_type, - get_bundle_prefix_for_role_name, check_cluster_actions_roles_are_created_correctly, - check_service_and_components_roles_are_created_correctly, check_provider_based_object_action_roles_are_created_correctly, check_roles_does_not_have_category, + check_service_and_components_roles_are_created_correctly, + get_bundle_prefix_for_role_name, + get_roles_of_type, ) +from tests.functional.rbac.conftest import DATA_DIR, RoleType, extract_role_short_info from tests.library.utils import previous_adcm_version_tag from tests.upgrade_utils import upgrade_adcm_version -from tests.functional.conftest import only_clean_adcm -from tests.functional.rbac.conftest import DATA_DIR, RoleType, extract_role_short_info pytestmark = [only_clean_adcm] @@ -50,7 +49,10 @@ indirect=True, ) def test_rbac_init_on_upgrade( - adcm_fs: ADCM, sdk_client_fs: ADCMClient, adcm_api_credentials: dict, adcm_image_tags: Tuple[str, str] + adcm_fs: ADCM, + sdk_client_fs: ADCMClient, + adcm_api_credentials: dict, + adcm_image_tags: Tuple[str, str], ): """ Test that roles are created on bundles uploaded before an upgrade @@ -88,7 +90,10 @@ def check_roles_are_created(client, bundles: Tuple[Bundle, Bundle, Bundle, Bundl check_cluster_actions_roles_are_created_correctly(client, cluster, hidden_role_names, hidden_role_prefix) for service_name in SERVICE_NAMES: check_service_and_components_roles_are_created_correctly( - client, cluster.service_add(name=service_name), hidden_role_names, hidden_role_prefix + client, + cluster.service_add(name=service_name), + hidden_role_names, + hidden_role_prefix, ) with allure.step('Check provider roles were created correctly'): diff --git a/tests/functional/rbac/test_users.py b/tests/functional/rbac/test_users.py index 1761eeef9a..4561e36fac 100644 --- a/tests/functional/rbac/test_users.py +++ b/tests/functional/rbac/test_users.py @@ -14,10 +14,10 @@ import allure import pytest -from coreapi.exceptions import ErrorMessage from adcm_client.base import NoSuchEndpointOrAccessIsDenied from adcm_client.objects import ADCMClient, User from adcm_client.wrappers.api import ADCMApiError, MethodNotAllowed +from coreapi.exceptions import ErrorMessage # pylint: disable=redefined-outer-name diff --git a/tests/functional/test_action_config.py b/tests/functional/test_action_config.py index b536d97c3e..89b52280ad 100644 --- a/tests/functional/test_action_config.py +++ b/tests/functional/test_action_config.py @@ -12,22 +12,19 @@ """Tests for actions config""" -import pytest import allure - -from coreapi.exceptions import ErrorMessage - -from adcm_client.objects import ADCMClient, Cluster, Service, Component, Provider, Host +import pytest +from adcm_client.objects import ADCMClient, Cluster, Component, Host, Provider, Service from adcm_pytest_plugin import utils as plugin_utils from adcm_pytest_plugin.steps.actions import ( run_cluster_action_and_assert_result, - run_service_action_and_assert_result, run_component_action_and_assert_result, - run_provider_action_and_assert_result, run_host_action_and_assert_result, + run_provider_action_and_assert_result, + run_service_action_and_assert_result, ) - -from tests.library.errorcodes import CONFIG_VALUE_ERROR, CONFIG_KEY_ERROR +from coreapi.exceptions import ErrorMessage +from tests.library.errorcodes import CONFIG_KEY_ERROR, CONFIG_VALUE_ERROR # pylint: disable=redefined-outer-name diff --git a/tests/functional/test_actions.py b/tests/functional/test_actions.py index 709eb070db..fb9bd8397e 100644 --- a/tests/functional/test_actions.py +++ b/tests/functional/test_actions.py @@ -15,22 +15,20 @@ # pylint: disable=redefined-outer-name import allure import pytest - from adcm_client.objects import ADCMClient, Cluster, Provider +from adcm_pytest_plugin import utils from adcm_pytest_plugin.steps.actions import ( - run_host_action_and_assert_result, run_cluster_action_and_assert_result, - run_service_action_and_assert_result, run_component_action_and_assert_result, + run_host_action_and_assert_result, run_provider_action_and_assert_result, + run_service_action_and_assert_result, ) -from adcm_pytest_plugin import utils from adcm_pytest_plugin.utils import fixture_parametrized_by_data_subdirs - from tests.functional.tools import ( AnyADCMObject, - actions_in_objects_are_present, actions_in_objects_are_absent, + actions_in_objects_are_present, ) diff --git a/tests/functional/test_actions_on_host.py b/tests/functional/test_actions_on_host.py index bc299ce575..4d34ce1b0c 100644 --- a/tests/functional/test_actions_on_host.py +++ b/tests/functional/test_actions_on_host.py @@ -15,25 +15,27 @@ # pylint: disable=redefined-outer-name import allure import pytest - from adcm_client.objects import Cluster, Provider from adcm_pytest_plugin.steps.actions import ( - run_host_action_and_assert_result, run_cluster_action_and_assert_result, - run_service_action_and_assert_result, run_component_action_and_assert_result, + run_host_action_and_assert_result, + run_service_action_and_assert_result, ) from adcm_pytest_plugin.utils import get_data_dir -from tests.functional.tools import action_in_object_is_absent, action_in_object_is_present from tests.functional.test_actions import ( - FIRST_SERVICE, - SECOND_SERVICE, FIRST_COMPONENT, + FIRST_SERVICE, SECOND_COMPONENT, - SWITCH_SERVICE_STATE, + SECOND_SERVICE, SWITCH_CLUSTER_STATE, - SWITCH_HOST_STATE, SWITCH_COMPONENT_STATE, + SWITCH_HOST_STATE, + SWITCH_SERVICE_STATE, +) +from tests.functional.tools import ( + action_in_object_is_absent, + action_in_object_is_present, ) ACTION_ON_HOST = "action_on_host" diff --git a/tests/functional/test_adcm_check_plugin.py b/tests/functional/test_adcm_check_plugin.py index d65de2e255..0ababc373c 100644 --- a/tests/functional/test_adcm_check_plugin.py +++ b/tests/functional/test_adcm_check_plugin.py @@ -18,9 +18,8 @@ from adcm_pytest_plugin import utils from adcm_pytest_plugin.steps.actions import run_cluster_action_and_assert_result from adcm_pytest_plugin.steps.asserts import assert_action_result - from tests.functional.conftest import only_clean_adcm -from tests.library.consts import States, MessageStates +from tests.library.consts import MessageStates, States NO_FIELD = [ "no_title", @@ -78,8 +77,7 @@ def test_all_fields(sdk_client_fs: ADCMClient, name, result): task = run_cluster_action_and_assert_result(cluster, action=params["action"], status=params["expected_state"]) job = task.job() with allure.step("Check all fields after action execution"): - logs = job.log_list() - content = job.log(job_id=job.id, log_id=logs[2].id).content[0] + content = job.log_list()[2].content[0] assert content["message"] == group_msg, f'Expected message {group_msg}. Current message {content["message"]}' assert content["result"] is group_result assert ( @@ -117,8 +115,7 @@ def test_message_with_other_field(sdk_client_fs: ADCMClient, name): job = task.job() assert_action_result(result=job.status, status=params["expected_state"], name=params["action"]) with allure.step(f"Check if content message is {name}"): - logs = job.log_list() - log = job.log(log_id=logs[2].id) + log = job.log_list()[2] content = log.content[0] assert content["message"] == name, f'Expected content message {name}. Current {content["message"]}' @@ -140,8 +137,7 @@ def test_success_and_fail_msg_on_success(sdk_client_fs: ADCMClient): job = task.job() assert_action_result(result=job.status, status=params["expected_state"], name=params["action"]) with allure.step("Check if success and fail message are in their own fields."): - logs = job.log_list() - log = job.log(job_id=job.id, log_id=logs[2].id) + log = job.log_list()[2] content = log.content[0] assert content["result"], f'Result is {content["result"]} expected True' assert content["message"] == params["expected_message"], ( @@ -166,8 +162,7 @@ def test_success_and_fail_msg_on_fail(sdk_client_fs: ADCMClient): job = task.job() assert_action_result(result=job.status, status=params["expected_state"], name=params["action"]) with allure.step("Check if success and fail message are in their own fields"): - logs = job.log_list() - log = job.log(job_id=job.id, log_id=logs[2].id) + log = job.log_list()[2] content = log.content[0] assert not content["result"], f'Result is {content["result"]} expected True' assert content["message"] == params["expected_message"], ( @@ -201,8 +196,7 @@ def test_multiple_tasks(sdk_client_fs: ADCMClient): action.wait() with allure.step(f'Check if log content is equal {params["logs_amount"]}'): job = action.job() - logs = job.log_list() - log = job.log(job_id=job.id, log_id=logs[2].id) + log = job.log_list()[2] assert len(log.content) == params["logs_amount"], log.content with allure.step("Check log's messages, titles and results."): for result in expected_result: @@ -246,8 +240,7 @@ def test_multiple_group_tasks(sdk_client_fs: ADCMClient): action.wait() with allure.step("Check log content amount"): job = action.job() - logs = job.log_list() - log = job.log(job_id=job.id, log_id=logs[2].id) + log = job.log_list()[2] assert len(log.content) == 2, log.content assert len(log.content[0]["content"]) == 2, log.content[0].content assert len(log.content[1]["content"]) == 1, log.content[1].content @@ -291,8 +284,7 @@ def test_multiple_group_tasks_without_group_title(sdk_client_fs: ADCMClient): action.wait() with allure.step(f'Check log content amount is equal {params["logs_amount"]}'): job = action.job() - logs = job.log_list() - log = job.log(job_id=job.id, log_id=logs[2].id) + log = job.log_list()[2] assert len(log.content) == params["logs_amount"], log.content with allure.step("Check title and result in log content"): for log_entry in log.content: @@ -317,8 +309,7 @@ def test_multiple_tasks_action_with_log_files_check(sdk_client_fs: ADCMClient): job = task.job() assert_action_result(result=job.status, status=params["expected_state"], name=params["action"]) with allure.step("Check if result is True"): - logs = job.log_list() - log = job.log(job_id=job.id, log_id=logs[2].id) + log = job.log_list()[2] content = log.content[0] assert content["result"], f'Result is {content["result"]}, Expected True' @@ -337,8 +328,7 @@ def test_result_no(sdk_client_fs: ADCMClient): job = task.job() assert_action_result(result=job.status, status=params["expected_state"], name=params["action"]) with allure.step("Check if result is False"): - logs = job.log_list() - log = job.log(job_id=job.id, log_id=logs[2].id) + log = job.log_list()[2] content = log.content[0] assert not content["result"], f'Result is {content["result"]}, Expected False' diff --git a/tests/functional/test_adcm_client_compability.py b/tests/functional/test_adcm_client_compability.py index 3591fc24ec..255916c5a3 100644 --- a/tests/functional/test_adcm_client_compability.py +++ b/tests/functional/test_adcm_client_compability.py @@ -17,10 +17,9 @@ from adcm_pytest_plugin.plugin import parametrized_by_adcm_version from adcm_pytest_plugin.steps.actions import ( run_cluster_action_and_assert_result, - run_service_action_and_assert_result, run_host_action_and_assert_result, + run_service_action_and_assert_result, ) - from adcm_pytest_plugin.utils import get_data_dir diff --git a/tests/functional/test_adcm_upgrade.py b/tests/functional/test_adcm_upgrade.py index be82a56baf..3658935c57 100644 --- a/tests/functional/test_adcm_upgrade.py +++ b/tests/functional/test_adcm_upgrade.py @@ -12,47 +12,48 @@ """Tests for ADCM upgrade""" -# pylint:disable=redefined-outer-name, too-many-arguments +# pylint:disable=redefined-outer-name import random from contextlib import contextmanager from pathlib import Path -from typing import Tuple, Union, List, Iterable, Any, Collection +from typing import Any, Collection, Iterable, List, Tuple, Union import allure import pytest from adcm_client.base import ObjectNotFound from adcm_client.objects import ( ADCMClient, - Cluster, - Host, - Service, Bundle, + Cluster, Component, + GroupConfig, + Host, + Job, Provider, + Service, Task, - Job, Upgrade, - GroupConfig, ) from adcm_pytest_plugin import params from adcm_pytest_plugin.docker_utils import ADCM from adcm_pytest_plugin.plugin import parametrized_by_adcm_version from adcm_pytest_plugin.steps.actions import ( run_cluster_action_and_assert_result, - run_service_action_and_assert_result, run_component_action_and_assert_result, run_provider_action_and_assert_result, + run_service_action_and_assert_result, ) from adcm_pytest_plugin.utils import catch_failed, get_data_dir, random_string - -from tests.library.assertions import dicts_are_not_equal, dicts_are_equal -from tests.upgrade_utils import upgrade_adcm_version - from tests.functional.conftest import only_clean_adcm -from tests.functional.plugin_utils import build_objects_checker, build_objects_comparator +from tests.functional.plugin_utils import ( + build_objects_checker, + build_objects_comparator, +) from tests.functional.tools import AnyADCMObject, get_config, get_objects_via_pagination +from tests.library.assertions import dicts_are_equal, dicts_are_not_equal from tests.library.utils import previous_adcm_version_tag +from tests.upgrade_utils import upgrade_adcm_version pytestmark = [only_clean_adcm] @@ -354,9 +355,12 @@ def dirty_adcm(self, sdk_client_fs: ADCMClient) -> dict: :returns: Dictionary with providers, clusters and sometimes bundles. """ dirty_dir = Path(get_data_dir(__file__)) / "dirty_upgrade" - simple_provider_bundle, simple_providers, simple_hosts, all_tasks = self.create_simple_providers( - sdk_client_fs, dirty_dir - ) + ( + simple_provider_bundle, + simple_providers, + simple_hosts, + all_tasks, + ) = self.create_simple_providers(sdk_client_fs, dirty_dir) simple_cluster_bundle, simple_clusters, tasks = self.create_simple_clusters(sdk_client_fs, dirty_dir) complex_objects = self.create_complex_providers_and_clusters(sdk_client_fs, dirty_dir) upgraded_cluster, not_upgraded_cluster = self.create_upgradable_clusters(sdk_client_fs, dirty_dir) @@ -373,7 +377,10 @@ def dirty_adcm(self, sdk_client_fs: ADCMClient) -> dict: 'cluster_bundle': simple_cluster_bundle, }, 'complex': { - 'providers': {'host_supplier': complex_objects[0], 'free_hosts': complex_objects[1]}, + 'providers': { + 'host_supplier': complex_objects[0], + 'free_hosts': complex_objects[1], + }, 'clusters': { 'all_services': complex_objects[2], 'config_history': complex_objects[3], @@ -430,7 +437,9 @@ def extract_job_info(job: Job) -> dict: } comparator = build_objects_comparator( - get_compare_value=extract_job_info, field_name='Job info', name_composer=lambda obj: f"Job with id {obj.id}" + get_compare_value=extract_job_info, + field_name='Job info', + name_composer=lambda obj: f"Job with id {obj.id}", ) jobs: List[Job] = get_objects_via_pagination(adcm_client.job_list) frozen_objects = {job.job_id: extract_job_info(job) for job in jobs} @@ -480,7 +489,11 @@ def create_simple_clusters( """ amount_of_clusters = 34 params = { - 'cluster_altered_config': {'number_of_segments': 2, 'auto_reboot': False, 'textarea': self.LONG_TEXT}, + 'cluster_altered_config': { + 'number_of_segments': 2, + 'auto_reboot': False, + 'textarea': self.LONG_TEXT, + }, 'service_altered_config': {'simple-is-best': False, 'mode': 'fast'}, 'component_altered_config': {'simpler-is-better': True}, 'cluster_action': 'install', @@ -562,7 +575,13 @@ def create_complex_providers_and_clusters( cluster_bundle, tuple(provider.host_list())[:3] ) cluster_with_hosts = self._create_cluster_with_hosts(cluster_bundle, tuple(provider.host_list())[3:]) - return provider, provider_with_free_hosts, cluster_with_all_services, cluster_with_history, cluster_with_hosts + return ( + provider, + provider_with_free_hosts, + cluster_with_all_services, + cluster_with_history, + cluster_with_hosts, + ) @allure.step('Create two upgradable clusters, upgrade one of them') def create_upgradable_clusters(self, adcm_client: ADCMClient, bundles_directory: Path) -> Tuple[Cluster, Cluster]: @@ -578,14 +597,22 @@ def create_upgradable_clusters(self, adcm_client: ADCMClient, bundles_directory: adcm_client.upload_from_fs(bundles_directory / "cluster_greater_version") cluster_to_upgrade = old_version_bundle.cluster_create('I will be upgraded') good_old_cluster = old_version_bundle.cluster_create('I am good the way I am') - _wait_for_tasks((cluster_to_upgrade.action(name='dummy').run(), good_old_cluster.action(name='dummy').run())) + _wait_for_tasks( + ( + cluster_to_upgrade.action(name='dummy').run(), + good_old_cluster.action(name='dummy').run(), + ) + ) upgrade: Upgrade = cluster_to_upgrade.upgrade() upgrade.do() return cluster_to_upgrade, good_old_cluster @allure.step('Run some actions in upgraded ADCM') def run_actions_after_upgrade( - self, cluster_all_services: Cluster, cluster_config_history: Cluster, simple_provider: Provider + self, + cluster_all_services: Cluster, + cluster_config_history: Cluster, + simple_provider: Provider, ) -> None: """ Run successful actions on: cluster, service, component. @@ -632,7 +659,10 @@ def _create_cluster_with_config_history(self, bundle: Bundle) -> Cluster: def get_random_config_map() -> dict: return { - 'a_lot_of_text': {'simple_string': random_string(25), 'file_pass': random_string(16)}, + 'a_lot_of_text': { + 'simple_string': random_string(25), + 'file_pass': random_string(16), + }, 'from_doc': { 'memory_size': random.randint(2, 64), 'person': { @@ -718,7 +748,10 @@ def _run_actions_on_components(self, cluster: Cluster, service: Service, compone def _delete_simple_cluster_with_job(self, simple_clusters: List[Cluster]) -> None: """Delete one of simple clusters where at least one job was ran""" cluster_with_job = next( - filter(lambda cluster: any(len(action.task_list()) for action in cluster.action_list()), simple_clusters), + filter( + lambda cluster: any(len(action.task_list()) for action in cluster.action_list()), + simple_clusters, + ), None, ) if cluster_with_job is None: diff --git a/tests/functional/test_allow_to_terminate.py b/tests/functional/test_allow_to_terminate.py index ec52066b81..6735e1cea4 100644 --- a/tests/functional/test_allow_to_terminate.py +++ b/tests/functional/test_allow_to_terminate.py @@ -16,8 +16,7 @@ import pytest from adcm_client.objects import Cluster, Component from adcm_pytest_plugin.utils import wait_until_step_succeeds - -from tests.library.assertions import expect_no_api_error, expect_api_error +from tests.library.assertions import expect_api_error, expect_no_api_error # pylint: disable=redefined-outer-name @@ -83,7 +82,13 @@ def test_terminate_action_with_hc_acl(cluster, generic_provider): with allure.step('Run action with hc_acl and terminate it right away'): task = cluster.action(name='with_hc_acl').run( - hc=[{'host_id': host.id, 'service_id': component.service_id, 'component_id': component.id}] + hc=[ + { + 'host_id': host.id, + 'service_id': component.service_id, + 'component_id': component.id, + } + ] ) # action need time to "actually" launch wait_until_step_succeeds( diff --git a/tests/functional/test_ansible_venv.py b/tests/functional/test_ansible_venv.py index 8414fc086b..5278c7c580 100644 --- a/tests/functional/test_ansible_venv.py +++ b/tests/functional/test_ansible_venv.py @@ -16,13 +16,13 @@ import allure import pytest -from adcm_client.objects import Cluster, ADCMClient, Provider +from adcm_client.objects import ADCMClient, Cluster, Provider from adcm_pytest_plugin.steps.actions import ( run_cluster_action_and_assert_result, - run_service_action_and_assert_result, run_component_action_and_assert_result, - run_provider_action_and_assert_result, run_host_action_and_assert_result, + run_provider_action_and_assert_result, + run_service_action_and_assert_result, ) from adcm_pytest_plugin.utils import get_data_dir @@ -96,12 +96,16 @@ def test_default_ansible( run_cluster_action_and_assert_result(cluster_no_venv, "no_venv", config=DEFAULT_ANSIBLE_VER) run_service_action_and_assert_result(cluster_no_venv.service(name="no_venv"), "no_venv", config=DEFAULT_ANSIBLE_VER) run_component_action_and_assert_result( - cluster_no_venv.service(name="no_venv").component(name="no_venv"), "no_venv", config=DEFAULT_ANSIBLE_VER + cluster_no_venv.service(name="no_venv").component(name="no_venv"), + "no_venv", + config=DEFAULT_ANSIBLE_VER, ) run_cluster_action_and_assert_result(cluster_obj_venv_default, "obj_venv_default", config=DEFAULT_ANSIBLE_VER) run_service_action_and_assert_result( - cluster_obj_venv_default.service(name="obj_venv_default"), "obj_venv_default", config=DEFAULT_ANSIBLE_VER + cluster_obj_venv_default.service(name="obj_venv_default"), + "obj_venv_default", + config=DEFAULT_ANSIBLE_VER, ) run_component_action_and_assert_result( cluster_obj_venv_default.service(name="obj_venv_default").component(name="obj_venv_default"), @@ -114,7 +118,9 @@ def test_default_ansible( run_provider_action_and_assert_result(provider_obj_venv_default, "obj_venv_default", config=DEFAULT_ANSIBLE_VER) run_host_action_and_assert_result( - provider_obj_venv_default.host(fqdn="obj-venv-default"), "obj_venv_default", config=DEFAULT_ANSIBLE_VER + provider_obj_venv_default.host(fqdn="obj-venv-default"), + "obj_venv_default", + config=DEFAULT_ANSIBLE_VER, ) @@ -142,7 +148,9 @@ def test_ansible_set_on_prototype(cluster_obj_venv_9: Cluster, provider_obj_venv run_cluster_action_and_assert_result(cluster_obj_venv_9, "obj_venv_9", config=ANSIBLE_9) run_service_action_and_assert_result(cluster_obj_venv_9.service(name="obj_venv_9"), "obj_venv_9", config=ANSIBLE_9) run_component_action_and_assert_result( - cluster_obj_venv_9.service(name="obj_venv_9").component(name="obj_venv_9"), "obj_venv_9", config=ANSIBLE_9 + cluster_obj_venv_9.service(name="obj_venv_9").component(name="obj_venv_9"), + "obj_venv_9", + config=ANSIBLE_9, ) run_provider_action_and_assert_result(provider_obj_venv_9, "obj_venv_9", config=ANSIBLE_9) @@ -208,7 +216,9 @@ def test_ansible_set_on_action( run_cluster_action_and_assert_result(cluster_obj_no_venv_action_9, "obj_no_venv_action_9", config=ANSIBLE_9) run_service_action_and_assert_result( - cluster_obj_no_venv_action_9.service(name="obj_no_venv_action_9"), "obj_no_venv_action_9", config=ANSIBLE_9 + cluster_obj_no_venv_action_9.service(name="obj_no_venv_action_9"), + "obj_no_venv_action_9", + config=ANSIBLE_9, ) run_component_action_and_assert_result( cluster_obj_no_venv_action_9.service(name="obj_no_venv_action_9").component(name="obj_no_venv_action_9"), @@ -218,7 +228,9 @@ def test_ansible_set_on_action( run_provider_action_and_assert_result(provider_no_venv_action_9, "no_venv_action_9", config=ANSIBLE_9) run_host_action_and_assert_result( - provider_no_venv_action_9.host(fqdn="no-venv-action-9"), "no_venv_action_9", config=ANSIBLE_9 + provider_no_venv_action_9.host(fqdn="no-venv-action-9"), + "no_venv_action_9", + config=ANSIBLE_9, ) run_provider_action_and_assert_result( diff --git a/tests/functional/test_backend_filtering.py b/tests/functional/test_backend_filtering.py index bb08c6fb37..bb08d58777 100644 --- a/tests/functional/test_backend_filtering.py +++ b/tests/functional/test_backend_filtering.py @@ -12,11 +12,12 @@ """Tests for backend filtering""" -from typing import List, Union, Type +from typing import List, Type, Union import allure import pytest -from adcm_client.base import ResponseTooLong, BaseAPIListObject, BaseAPIObject +import pytest_check as check +from adcm_client.base import BaseAPIObject from adcm_client.objects import ( Action, ADCMClient, @@ -38,12 +39,11 @@ ProviderList, ProviderPrototype, ProviderPrototypeList, + Service, Task, TaskList, - Service, ) from adcm_pytest_plugin.utils import get_data_dir, get_subdirs_iter -import pytest_check as check from pytest_lazyfixture import lazy_fixture # pylint: disable=redefined-outer-name,protected-access @@ -175,6 +175,7 @@ def one_host_provider_id_attr(one_host: Host): return {'provider_id': one_host.provider_id} +@pytest.mark.skip(reason="ADCM-3297") @pytest.mark.parametrize( 'tested_class', [ @@ -214,32 +215,6 @@ def _get_params(link): ) -@pytest.mark.full() -@pytest.mark.parametrize( - ('sdk_client', 'tested_class'), - [ - pytest.param(lazy_fixture('cluster_bundles'), ClusterPrototypeList, id="Cluster Prototype"), - pytest.param(lazy_fixture('cluster_bundles'), PrototypeList, id="Prototype"), - pytest.param(lazy_fixture('provider_bundles'), ProviderPrototypeList, id="Provider Prototype"), - pytest.param(lazy_fixture('provider_bundles'), HostPrototypeList, id="Host Prototype"), - pytest.param(lazy_fixture('provider_bundles'), BundleList, id="Bundle"), - pytest.param(lazy_fixture('clusters'), ClusterList, id="Cluster"), - pytest.param(lazy_fixture('hosts'), HostList, id="Host"), - pytest.param(lazy_fixture('hosts_with_jobs'), TaskList, id="Task"), - pytest.param(lazy_fixture('hosts_with_jobs'), JobList, id="Job"), - ], -) -def test_paging_fail(sdk_client, tested_class: Type[BaseAPIListObject]): - """Scenario: - * Prepare a lot of objects in ADCM - * Call listing api over objects.*List classes - * Expecting to have ResponseTooLong error - """ - with allure.step(f'Prepare a lot of objects: {tested_class.__name__} ' f'in ADCM and check ResponseTooLong error'): - with pytest.raises(ResponseTooLong): - tested_class(sdk_client._api) - - @pytest.mark.parametrize( ('sdk_client', 'tested_class', 'tested_list_class', 'search_args', 'expected_args'), [ @@ -506,7 +481,7 @@ def task_status_attr(): @pytest.fixture() def job_task_id_attr(host_ok_action: Action): """Get task task_id attr""" - return {'task_id': host_ok_action.task().task_id} + return {'task_id': host_ok_action.task().id} @pytest.mark.parametrize( diff --git a/tests/functional/test_bundle_support.py b/tests/functional/test_bundle_support.py index 62d7ff2f77..faca782b4b 100644 --- a/tests/functional/test_bundle_support.py +++ b/tests/functional/test_bundle_support.py @@ -16,11 +16,9 @@ import coreapi import pytest from _pytest.outcomes import Failed - from adcm_client.objects import ADCMClient from adcm_pytest_plugin import utils from adcm_pytest_plugin.utils import catch_failed, random_string - from tests.library import errorcodes as err diff --git a/tests/functional/test_bundle_upgrades.py b/tests/functional/test_bundle_upgrades.py index 103c7b5ac5..c3ad44f778 100644 --- a/tests/functional/test_bundle_upgrades.py +++ b/tests/functional/test_bundle_upgrades.py @@ -17,8 +17,11 @@ import pytest from adcm_client.objects import ADCMClient from adcm_pytest_plugin import utils - -from tests.library.errorcodes import INVALID_VERSION_DEFINITION, UPGRADE_ERROR, UPGRADE_NOT_FOUND +from tests.library.errorcodes import ( + INVALID_VERSION_DEFINITION, + UPGRADE_ERROR, + UPGRADE_NOT_FOUND, +) # pylint: disable=redefined-outer-name diff --git a/tests/functional/test_cluster_functions.py b/tests/functional/test_cluster_functions.py index 3e385e9bd7..a0e5a8a42d 100644 --- a/tests/functional/test_cluster_functions.py +++ b/tests/functional/test_cluster_functions.py @@ -17,11 +17,9 @@ import allure import coreapi import pytest - -from adcm_client.objects import ADCMClient, Bundle, Cluster, Provider, Host -from adcm_pytest_plugin.utils import get_data_dir +from adcm_client.objects import ADCMClient, Bundle, Cluster, Host, Provider from adcm_pytest_plugin import utils - +from adcm_pytest_plugin.utils import get_data_dir from tests.library import errorcodes as err DEFAULT_CLUSTER_BUNDLE_PATH = get_data_dir(__file__, "cluster_simple") @@ -178,7 +176,7 @@ def test_get_cluster_service_list(self, sdk_client_fs: ADCMClient, cluster: Clus expected = [] with allure.step("Create a list of services in the cluster"): for prototype in sdk_client_fs.service_prototype_list(bundle_id=cluster.bundle_id): - service = cluster.service_add(prototype_id=prototype.id) + service = cluster.service_add(name=prototype.name) expected.append(service._data) with allure.step("Get a service list in cluster"): actual = [x._data for x in cluster.service_list()] diff --git a/tests/functional/test_cluster_service_config_functions.py b/tests/functional/test_cluster_service_config_functions.py index b4e19ed657..0c8be9e1e6 100644 --- a/tests/functional/test_cluster_service_config_functions.py +++ b/tests/functional/test_cluster_service_config_functions.py @@ -14,13 +14,13 @@ import json import os -from typing import Tuple, Any +from typing import Any, Tuple import allure import coreapi import pytest from adcm_client.base import BaseAPIObject -from adcm_client.objects import ADCMClient, Cluster, Service, Bundle, Provider +from adcm_client.objects import ADCMClient, Bundle, Cluster, Provider, Service from adcm_pytest_plugin import utils from adcm_pytest_plugin.steps.actions import run_cluster_action_and_assert_result from jsonschema import validate diff --git a/tests/functional/test_concerns.py b/tests/functional/test_concerns.py index cd66e4f58b..adb8af461a 100644 --- a/tests/functional/test_concerns.py +++ b/tests/functional/test_concerns.py @@ -17,17 +17,13 @@ import allure import coreapi import pytest - from adcm_client.base import ActionHasIssues -from adcm_client.objects import ADCMClient, Provider, Host, Service, Cluster +from adcm_client.objects import ADCMClient, Cluster, Host, Provider, Service from adcm_pytest_plugin import utils from adcm_pytest_plugin.utils import catch_failed from coreapi.exceptions import ErrorMessage - from tests.functional.conftest import only_clean_adcm from tests.library.adcm_websockets import ADCMWebsocket, EventMessage -from tests.library.errorcodes import UPGRADE_ERROR - # pylint: disable=redefined-outer-name @@ -77,8 +73,7 @@ def test_action_should_not_be_run_while_hostprovider_has_an_issue( provider.action(name="install").run() -@pytest.mark.xfail(reason="https://tracker.yandex.ru/ADCM-3033") -def test_when_cluster_has_issue_than_upgrade_locked(sdk_client_fs: ADCMClient): +def test_when_cluster_has_issue_then_upgrade_locked(sdk_client_fs: ADCMClient): """Test upgrade should not be run while cluster has an issue""" with allure.step("Create cluster and upload new one bundle"): old_bundle_path = utils.get_data_dir(__file__, "cluster") @@ -86,15 +81,16 @@ def test_when_cluster_has_issue_than_upgrade_locked(sdk_client_fs: ADCMClient): old_bundle = sdk_client_fs.upload_from_fs(old_bundle_path) cluster = old_bundle.cluster_create(name=utils.random_string()) sdk_client_fs.upload_from_fs(new_bundle_path) + with allure.step("Check upgrade isn't listed when concern is presented"): + assert len(cluster.upgrade_list()) == 0, "No upgrade should be available with concern" with allure.step("Upgrade cluster"): - with pytest.raises(coreapi.exceptions.ErrorMessage) as e: + cluster.config_set_diff({"required_param": 11}) + assert len(cluster.upgrade_list()) == 1, "Upgrade should be available after concern is removed" + with catch_failed(coreapi.exceptions.ErrorMessage, "Upgrade should've launched successfuly"): cluster.upgrade().do() - with allure.step("Check if cluster has issues"): - UPGRADE_ERROR.equal(e, "cluster ", " has blocking concerns ") -@pytest.mark.xfail(reason="https://tracker.yandex.ru/ADCM-3033") -def test_when_hostprovider_has_issue_than_upgrade_locked(sdk_client_fs: ADCMClient): +def test_when_hostprovider_has_issue_then_upgrade_locked(sdk_client_fs: ADCMClient): """Test upgrade should not be run while hostprovider has an issue""" with allure.step("Create hostprovider"): old_bundle_path = utils.get_data_dir(__file__, "provider") @@ -102,15 +98,19 @@ def test_when_hostprovider_has_issue_than_upgrade_locked(sdk_client_fs: ADCMClie old_bundle = sdk_client_fs.upload_from_fs(old_bundle_path) provider = old_bundle.provider_create(name=utils.random_string()) sdk_client_fs.upload_from_fs(new_bundle_path) + with allure.step("Check upgrade isn't listed when concern is presented"): + assert len(provider.upgrade_list()) == 0, "No upgrade should be available with concern" with allure.step("Upgrade provider"): - with pytest.raises(coreapi.exceptions.ErrorMessage) as e: + provider.config_set_diff({"required_param": 11}) + assert len(provider.upgrade_list()) == 1, "Upgrade should be available after concern is removed" + with catch_failed(coreapi.exceptions.ErrorMessage, "Upgrade should've launched successfully"): provider.upgrade().do() - with allure.step("Check if upgrade locked"): - UPGRADE_ERROR.equal(e) @allure.link("https://jira.arenadata.io/browse/ADCM-487") -def test_when_component_has_no_constraint_then_cluster_doesnt_have_issues(sdk_client_fs: ADCMClient): +def test_when_component_has_no_constraint_then_cluster_doesnt_have_issues( + sdk_client_fs: ADCMClient, +): """Test no cluster issues if no constraints on components""" with allure.step("Create cluster (component has no constraint)"): bundle_path = utils.get_data_dir(__file__, "cluster_component_hasnt_constraint") @@ -240,7 +240,11 @@ async def _create_cluster_with_concerns_and_add_service( ( EventMessage( 'add', - {'type': 'service', 'id': service.id, 'details': {'type': 'cluster', 'value': str(cluster.id)}}, + { + 'type': 'service', + 'id': service.id, + 'details': {'type': 'cluster', 'value': str(cluster.id)}, + }, ), *[ self._concern_add_msg(type_) diff --git a/tests/functional/test_config.py b/tests/functional/test_config.py index 9f254b483a..7fcf356114 100644 --- a/tests/functional/test_config.py +++ b/tests/functional/test_config.py @@ -18,19 +18,16 @@ from typing import Tuple import allure -import yaml -import pytest import coreapi - - +import pytest +import yaml from _pytest.fixtures import SubRequest -from coreapi.exceptions import ErrorMessage from adcm_client.base import ActionHasIssues -from adcm_client.objects import ADCMClient, Cluster, Service, Provider, Host +from adcm_client.objects import ADCMClient, Cluster, Host, Provider, Service from adcm_pytest_plugin.utils import fixture_parametrized_by_data_subdirs, get_data_dir - +from coreapi.exceptions import ErrorMessage from tests.functional.plugin_utils import AnyADCMObject -from tests.library.errorcodes import CONFIG_KEY_ERROR, ADCMError, CONFIG_NOT_FOUND +from tests.library.errorcodes import CONFIG_KEY_ERROR, CONFIG_NOT_FOUND, ADCMError def get_value(path, entity, value_type): @@ -399,7 +396,6 @@ def assert_option_type(*args): } -# pylint: disable=too-many-arguments def assert_config_type(path, config_type, entities, is_required, is_default, sent_value_type): """ Running test scenario for cluster, service, provider and host @@ -724,7 +720,12 @@ def test_secret_text_field(self, cluster: Cluster, provider: Provider): 'secret_not_required_no_default', 'secret_required_no_default', ) - required_default, not_required_default, not_required_no_default, required_no_default = fields + ( + required_default, + not_required_default, + not_required_no_default, + required_no_default, + ) = fields required_diff = {required_no_default: value_to_set} changed_diff = {field: value_to_set for field in fields if field != required_no_default} default_diff = { @@ -754,7 +755,11 @@ def test_secret_text_field(self, cluster: Cluster, provider: Provider): ) def _change_config_and_check_changed_by_action( - self, objects_to_change: Tuple[AnyADCMObject], config_to_set: dict, action_before: str, action_after: str + self, + objects_to_change: Tuple[AnyADCMObject], + config_to_set: dict, + action_before: str, + action_after: str, ): """ Loop over objects_to_change: diff --git a/tests/functional/test_config_after_upgrade.py b/tests/functional/test_config_after_upgrade.py index 737fe95228..c84460d714 100644 --- a/tests/functional/test_config_after_upgrade.py +++ b/tests/functional/test_config_after_upgrade.py @@ -13,7 +13,7 @@ """Tests for cluster and provider configs after upgrade""" import json from collections import OrderedDict -from typing import Callable, Union, Tuple +from typing import Callable, Tuple, Union import allure import pytest @@ -22,7 +22,6 @@ from allure_commons.types import AttachmentType from tests.functional.plugin_utils import AnyADCMObject - ############################### # Tools ############################### @@ -115,7 +114,10 @@ def _update_config_and_attr_for_changed_types( config["text"] = "DAILY" config["option"] = "text" config["structure"] = {"port": 9200, "transport_port": 9300} - config["group"] = [OrderedDict({"code": 1, "country": "Test1"}), OrderedDict({"code": 2, "country": "Test2"})] + config["group"] = [ + OrderedDict({"code": 1, "country": "Test1"}), + OrderedDict({"code": 2, "country": "Test2"}), + ] attr["structure"] = {"active": True} del attr["group"] @@ -149,7 +151,10 @@ def _update_config_and_attr_for_changed_group_customisation_test( attr["custom_group_keys"]["password"] = False attr["custom_group_keys"]["list"] = False attr["custom_group_keys"]["option"] = False - attr["custom_group_keys"]["group"] = {"value": False, "fields": {"port": False, "transport_port": False}} + attr["custom_group_keys"]["group"] = { + "value": False, + "fields": {"port": False, "transport_port": False}, + } attr["custom_group_keys"]["map"] = False attr["custom_group_keys"]["json"] = False attr["custom_group_keys"]["secrettext"] = True @@ -329,9 +334,17 @@ def _update_and_assert_config(update_func: Callable, ref_config_and_attr, obj: A ref_config, ref_attr = update_func(*ref_config_and_attr, group_config=False) new_config, new_attr = _get_config_and_attr(obj) _assert_configs( - obj_type=obj.__class__.__name__, actual_config=new_config, expected_config=ref_config, group_config=False + obj_type=obj.__class__.__name__, + actual_config=new_config, + expected_config=ref_config, + group_config=False, + ) + _assert_attr( + obj_type=obj.__class__.__name__, + actual_attr=new_attr, + expected_attr=ref_attr, + group_config=False, ) - _assert_attr(obj_type=obj.__class__.__name__, actual_attr=new_attr, expected_attr=ref_attr, group_config=False) ################################## @@ -479,6 +492,14 @@ def _update_and_assert_config(update_func: Callable, ref_config_and_attr, group_ ref_config, ref_attr = update_func(*ref_config_and_attr, group_config=True) new_config, new_attr = _get_config_and_attr(group_config) _assert_configs( - obj_type=group_config.object_type, actual_config=new_config, expected_config=ref_config, group_config=True + obj_type=group_config.object_type, + actual_config=new_config, + expected_config=ref_config, + group_config=True, + ) + _assert_attr( + obj_type=group_config.object_type, + actual_attr=new_attr, + expected_attr=ref_attr, + group_config=True, ) - _assert_attr(obj_type=group_config.object_type, actual_attr=new_attr, expected_attr=ref_attr, group_config=True) diff --git a/tests/functional/test_config_groups.py b/tests/functional/test_config_groups.py index a3378af4b5..06babd34ba 100644 --- a/tests/functional/test_config_groups.py +++ b/tests/functional/test_config_groups.py @@ -9,46 +9,42 @@ # 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. -# pylint: disable=redefined-outer-name, unused-argument, dangerous-default-value + +# pylint: disable=redefined-outer-name,dangerous-default-value,too-many-lines """Tests for config groups""" import json from collections import OrderedDict -from typing import ( - Union, - Tuple, - Type, -) +from typing import Tuple, Type, Union import allure import pytest from adcm_client.objects import ( ADCMClient, Cluster, - Provider, + Component, + GroupConfig, + Host, HostList, + Provider, Service, - Host, - GroupConfig, - Component, ) from adcm_pytest_plugin import utils from adcm_pytest_plugin.steps.actions import ( run_cluster_action_and_assert_result, - run_service_action_and_assert_result, run_component_action_and_assert_result, run_provider_action_and_assert_result, + run_service_action_and_assert_result, ) from adcm_pytest_plugin.utils import get_data_dir from coreapi.exceptions import ErrorMessage from docker.models.containers import Container - from tests.library.errorcodes import ( + ATTRIBUTE_ERROR, + GROUP_CONFIG_CHANGE_UNSELECTED_FIELD, GROUP_CONFIG_HOST_ERROR, GROUP_CONFIG_HOST_EXISTS, - GROUP_CONFIG_CHANGE_UNSELECTED_FIELD, ADCMError, - ATTRIBUTE_ERROR, ) CLUSTER_BUNDLE_PATH = get_data_dir(__file__, "cluster_simple") @@ -402,7 +398,18 @@ def test_host_in_service_group_after_hc_change( class TestChangeGroupsConfig: """Tests for changing group config""" - ASSERT_TYPE = ["float", "boolean", "integer", "string", "list", "option", "text", "group", "structure", "map"] + ASSERT_TYPE = [ + "float", + "boolean", + "integer", + "string", + "list", + "option", + "text", + "group", + "structure", + "map", + ] PARAMS_TO_CHANGE = { "float": 1.1, @@ -466,7 +473,10 @@ class TestChangeGroupsConfig: ] def _add_values_to_group_config_template( - self, custom_group_keys: dict = None, group_keys: dict = None, config_attr: dict = PARAMS_TO_CHANGE + self, + custom_group_keys: dict = None, + group_keys: dict = None, + config_attr: dict = PARAMS_TO_CHANGE, ) -> dict: """ Template for group configuration. @@ -541,13 +551,19 @@ def _check_error_about_group_keys(self, group: GroupConfig, config_before: dict) "config": {param: self.PARAMS_TO_CHANGE[param]}, } self._check_error_with_adding_param_to_group( - group, invalid_config, error_message=GROUP_ERROR_MESSAGE, adcm_error=ATTRIBUTE_ERROR + group, + invalid_config, + error_message=GROUP_ERROR_MESSAGE, + adcm_error=ATTRIBUTE_ERROR, ) @pytest.fixture( params=[ pytest.param(CLUSTER_BUNDLE_WITH_GROUP_PATH, id="cluster_with_group_customization"), - pytest.param(CLUSTER_BUNDLE_WITH_CONFIG_GROUP_CUSTOM_PATH, id="cluster_with_config_group_customization"), + pytest.param( + CLUSTER_BUNDLE_WITH_CONFIG_GROUP_CUSTOM_PATH, + id="cluster_with_config_group_customization", + ), ] ) def cluster_bundle(self, request, sdk_client_fs): @@ -571,7 +587,12 @@ def test_change_group_in_cluster(self, cluster_bundle, cluster_with_two_hosts_on actual_values=config_after, expected_values=config_before, ) - config_previous = {"map": {test_host_1.fqdn: dict(config_before), test_host_2.fqdn: dict(config_before)}} + config_previous = { + "map": { + test_host_1.fqdn: dict(config_before), + test_host_2.fqdn: dict(config_before), + } + } for hosts in self.CLUSTER_HOSTS_VARIANTS: config_previous["hosts"] = hosts with allure.step(f"Assert that config values is fine on inventory hosts: {hosts}"): @@ -588,7 +609,10 @@ def test_change_group_in_cluster(self, cluster_bundle, cluster_with_two_hosts_on expected_values=config_expected_with_groups['config'], ) config_updated = { - "map": {test_host_1.fqdn: config_expected_with_groups['config'], test_host_2.fqdn: dict(config_before)} + "map": { + test_host_1.fqdn: config_expected_with_groups['config'], + test_host_2.fqdn: dict(config_before), + } } for hosts in self.CLUSTER_HOSTS_VARIANTS: config_updated["hosts"] = hosts @@ -614,7 +638,12 @@ def test_change_group_in_service(self, cluster_bundle, cluster_with_components): actual_values=config_after, expected_values=config_before, ) - config_previous = {"map": {test_host_1.fqdn: dict(config_before), test_host_2.fqdn: dict(config_before)}} + config_previous = { + "map": { + test_host_1.fqdn: dict(config_before), + test_host_2.fqdn: dict(config_before), + } + } for hosts in self.CLUSTER_HOSTS_VARIANTS: config_previous["hosts"] = hosts with allure.step(f"Assert that config values is fine on inventory hosts: {hosts}"): @@ -631,7 +660,10 @@ def test_change_group_in_service(self, cluster_bundle, cluster_with_components): expected_values=config_expected_with_groups['config'], ) config_updated = { - "map": {test_host_1.fqdn: config_expected_with_groups['config'], test_host_2.fqdn: dict(config_before)} + "map": { + test_host_1.fqdn: config_expected_with_groups['config'], + test_host_2.fqdn: dict(config_before), + } } for hosts in self.CLUSTER_HOSTS_VARIANTS: config_updated["hosts"] = hosts @@ -658,7 +690,12 @@ def test_change_group_in_component(self, cluster_bundle, cluster_with_components actual_values=config_after, expected_values=config_before, ) - config_previous = {"map": {test_host_1.fqdn: dict(config_before), test_host_2.fqdn: dict(config_before)}} + config_previous = { + "map": { + test_host_1.fqdn: dict(config_before), + test_host_2.fqdn: dict(config_before), + } + } for hosts in self.CLUSTER_HOSTS_VARIANTS: config_previous["hosts"] = hosts with allure.step(f"Assert that config values is fine on inventory hosts: {hosts}"): @@ -677,7 +714,10 @@ def test_change_group_in_component(self, cluster_bundle, cluster_with_components expected_values=config_expected_with_groups['config'], ) config_updated = { - "map": {test_host_1.fqdn: config_expected_with_groups['config'], test_host_2.fqdn: dict(config_before)} + "map": { + test_host_1.fqdn: config_expected_with_groups['config'], + test_host_2.fqdn: dict(config_before), + } } for hosts in self.CLUSTER_HOSTS_VARIANTS: config_updated["hosts"] = hosts @@ -692,7 +732,10 @@ def test_change_group_in_component(self, cluster_bundle, cluster_with_components "provider_bundle", [ pytest.param(PROVIDER_BUNDLE_WITH_GROUP_PATH, id="provider_with_group_customization"), - pytest.param(PROVIDER_BUNDLE_WITH_CONFIG_GROUP_CUSTOM_PATH, id="provider_with_config_group_customization"), + pytest.param( + PROVIDER_BUNDLE_WITH_CONFIG_GROUP_CUSTOM_PATH, + id="provider_with_config_group_customization", + ), ], indirect=True, ) @@ -713,7 +756,12 @@ def test_change_group_in_provider(self, provider_bundle, provider, create_two_ho actual_values=config_after, expected_values=config_before, ) - config_previous = {"map": {test_host_1.fqdn: dict(config_before), test_host_2.fqdn: dict(config_before)}} + config_previous = { + "map": { + test_host_1.fqdn: dict(config_before), + test_host_2.fqdn: dict(config_before), + } + } for hosts in self.CLUSTER_HOSTS_VARIANTS: config_previous["hosts"] = hosts with allure.step(f"Assert that config values is fine on inventory hosts: {hosts}"): @@ -730,7 +778,10 @@ def test_change_group_in_provider(self, provider_bundle, provider, create_two_ho expected_values=config_expected_with_groups['config'], ) config_updated = { - "map": {test_host_1.fqdn: config_expected_with_groups['config'], test_host_2.fqdn: dict(config_before)} + "map": { + test_host_1.fqdn: config_expected_with_groups['config'], + test_host_2.fqdn: dict(config_before), + } } for hosts in self.CLUSTER_HOSTS_VARIANTS: config_updated["hosts"] = hosts @@ -750,7 +801,8 @@ def test_error_with_changing_custom_group_keys_in_cluster_group(self, cluster_bu "cluster_bundle", [ pytest.param( - get_data_dir(__file__, "cluster_with_all_group_keys_false"), id="cluster_with_all_group_keys_false" + get_data_dir(__file__, "cluster_with_all_group_keys_false"), + id="cluster_with_all_group_keys_false", ) ], indirect=True, @@ -776,7 +828,8 @@ def test_error_with_changing_custom_group_keys_in_service_group(self, cluster_bu "cluster_bundle", [ pytest.param( - get_data_dir(__file__, "cluster_with_all_group_keys_false"), id="cluster_with_all_group_keys_false" + get_data_dir(__file__, "cluster_with_all_group_keys_false"), + id="cluster_with_all_group_keys_false", ) ], indirect=True, @@ -804,7 +857,8 @@ def test_error_with_changing_custom_group_keys_in_component_group(self, cluster_ "cluster_bundle", [ pytest.param( - get_data_dir(__file__, "cluster_with_all_group_keys_false"), id="cluster_with_all_group_keys_false" + get_data_dir(__file__, "cluster_with_all_group_keys_false"), + id="cluster_with_all_group_keys_false", ) ], indirect=True, @@ -825,7 +879,10 @@ def test_change_params_in_component_group_without_group_customization( "provider_bundle", [ pytest.param(PROVIDER_BUNDLE_WITH_GROUP_PATH, id="provider_with_group_customization"), - pytest.param(PROVIDER_BUNDLE_WITH_CONFIG_GROUP_CUSTOM_PATH, id="provider_with_config_group_customization"), + pytest.param( + PROVIDER_BUNDLE_WITH_CONFIG_GROUP_CUSTOM_PATH, + id="provider_with_config_group_customization", + ), ], indirect=True, ) @@ -879,7 +936,10 @@ def test_changing_params_in_cluster_group_subs(self, cluster_bundle, cluster_wit config_expected['attr']['group'] = {'active': True} cluster_group.config_set(config_expected) config_updated = { - "map": {test_host_1.fqdn: config_expected['config'], test_host_2.fqdn: dict(config_before)} + "map": { + test_host_1.fqdn: config_expected['config'], + test_host_2.fqdn: dict(config_before), + } } run_cluster_action_and_assert_result(cluster, action=ACTION_NAME, config=config_updated) run_cluster_action_and_assert_result(cluster, action=ACTION_MULTIJOB_NAME, config=config_updated) diff --git a/tests/functional/test_custom_log_plugin.py b/tests/functional/test_custom_log_plugin.py index f43a8bdf74..b29ed66899 100644 --- a/tests/functional/test_custom_log_plugin.py +++ b/tests/functional/test_custom_log_plugin.py @@ -49,9 +49,9 @@ def test_different_storage_types_with_format(sdk_client_fs: ADCMClient, bundle): with allure.step('Check if logs are equal 3, job state and logs'): job = task.job() logs = job.log_list() - log = job.log(job_id=job.id, log_id=logs[2].id) assert len(logs) == 3, f"Logs count {len(logs)}. Expected 3" assert job.status == 'success', f"Current job status {job.status}. Expected: success" + log = logs[2] err_msg = f"Expected log format {log_format}. Actual log format {log.format}" assert log.format == log_format, err_msg assert log.type == 'custom' @@ -67,7 +67,7 @@ def test_path_and_content(sdk_client_fs: ADCMClient): with allure.step('Check logs content and format'): job = task.job() logs = job.log_list() - log = job.log(job_id=job.id, log_id=logs[2].id) + log = logs[2] assert log.content == '{\n "key": "value"\n}' assert log.format == 'json' @@ -81,8 +81,7 @@ def test_multiple_tasks(sdk_client_fs: ADCMClient, bundle): task = cluster.action(name='custom_log').run() task.wait() with allure.step('Check 4 logs entries'): - job = task.job() - logs = job.log_list() + logs = task.job().log_list() assert len(logs) == 4, "Expected 4 logs entries, because 2 tasks in playbook" @@ -95,8 +94,7 @@ def test_check_text_file_content(sdk_client_fs: ADCMClient): task.wait() with allure.step('Check logs content and format'): job = task.job() - logs = job.log_list() - log = job.log(job_id=job.id, log_id=logs[2].id) + log = job.log_list()[2] assert log.content == 'Hello world!\n' assert log.format == 'txt' @@ -110,8 +108,7 @@ def test_check_text_content(sdk_client_fs: ADCMClient): task.wait() with allure.step('Check logs content'): job = task.job() - logs = job.log_list() - log = job.log(job_id=job.id, log_id=logs[2].id) + log = job.log_list()[2] assert log.content == 'shalala' @@ -124,8 +121,7 @@ def test_check_json_content(sdk_client_fs: ADCMClient): task.wait() with allure.step('Check logs content'): job = task.job() - logs = job.log_list() - log = job.log(job_id=job.id, log_id=logs[2].id) + log = job.log_list()[2] assert log.content == '{\n "hello": "world"\n}' @@ -138,6 +134,5 @@ def test_incorrect_syntax_for_fields(sdk_client_fs: ADCMClient): task.wait() with allure.step('Check logs content'): job = task.job() - logs = job.log_list() - log = job.log(job_id=job.id, log_id=logs[2].id) + log = job.log_list()[2] assert log.content == '{1: "world"}' diff --git a/tests/functional/test_delete_object_with_component.py b/tests/functional/test_delete_object_with_component.py index f58fd4621e..022d3cde34 100644 --- a/tests/functional/test_delete_object_with_component.py +++ b/tests/functional/test_delete_object_with_component.py @@ -18,7 +18,6 @@ import pytest from adcm_client.objects import ADCMClient from adcm_pytest_plugin.utils import get_data_dir - from tests.library import errorcodes as err diff --git a/tests/functional/test_delete_service.py b/tests/functional/test_delete_service.py index d8f6dff492..661cf05ba2 100644 --- a/tests/functional/test_delete_service.py +++ b/tests/functional/test_delete_service.py @@ -11,23 +11,167 @@ # limitations under the License. """Tests for service delete method""" +from time import sleep + import allure -from adcm_client.objects import ADCMClient -from adcm_pytest_plugin.utils import get_data_dir +import pytest +from adcm_client.objects import ADCMClient, Cluster, Provider, Task +from adcm_pytest_plugin.utils import get_data_dir, wait_until_step_succeeds +from tests.functional.conftest import only_clean_adcm +from tests.library.assertions import expect_api_error, expect_no_api_error +from tests.library.errorcodes import SERVICE_CONFLICT, SERVICE_DELETE_ERROR + +# pylint: disable=redefined-outer-name + + +pytestmark = [only_clean_adcm] + + +@pytest.fixture() +def cluster(sdk_client_fs) -> Cluster: + return sdk_client_fs.upload_from_fs(get_data_dir(__file__, "with_action")).cluster_create( + "Cluster with service remove actions" + ) def test_delete_service(sdk_client_fs: ADCMClient): """ If host has NO component, then we can simply remove it from cluster. """ - bundle = sdk_client_fs.upload_from_fs(get_data_dir(__file__)) + bundle = sdk_client_fs.upload_from_fs(get_data_dir(__file__, "default")) cluster = bundle.cluster_create("test") service = cluster.service_add(name="zookeeper") cluster.reread() with allure.step("Ensure there's a concern on cluster from service's config"): - assert len(cluster.concerns()) > 0, 'There should be a concern on cluster from config of the service' - with allure.step('Delete service'): + assert len(cluster.concerns()) > 0, "There should be a concern on cluster from config of the service" + with allure.step("Delete service"): service.delete() - with allure.step('Ensure that concern is gone from cluster after service removal'): + with allure.step("Ensure that concern is gone from cluster after service removal"): cluster.reread() - assert len(cluster.concerns()) == 0, 'Concern on cluster should be removed alongside with the service' + assert len(cluster.concerns()) == 0, "Concern on cluster should be removed alongside with the service" + + +def test_forbid_service_deletion_no_action(sdk_client_fs: ADCMClient, generic_provider: Provider) -> None: + cluster = sdk_client_fs.upload_from_fs(get_data_dir(__file__, "forbidden_to_delete")).cluster_create( + "Cluster with forbidden to delete services" + ) + + with allure.step("Check service required for cluster can't be deleted"): + service = cluster.service_add(name="required_service") + expect_api_error("delete required service", operation=service.delete, err_=SERVICE_CONFLICT) + + with allure.step("Check service mapped to hosts can't be deleted"): + service = cluster.service_add(name="with_component") + cluster.hostcomponent_set((cluster.host_add(generic_provider.host_create("host-fqdn")), service.component())) + expect_api_error("delete mapped service without action", operation=service.delete) + + with allure.step("Check service that is imported can't be deleted"): + importer: Cluster = sdk_client_fs.upload_from_fs(get_data_dir(__file__, "with_import")).cluster_create( + "Importer Cluster" + ) + importer.bind(service) + expect_api_error("delete service with export", operation=service.delete, err_=SERVICE_CONFLICT) + + with allure.step("Check unmapped service can't be deleted when not in 'created' state"): + service = cluster.service_add(name="state_change") + service.action().run().wait() + service.reread() + assert service.state != "created" + expect_api_error("delete service not in 'created' state", operation=service.delete, err_=SERVICE_DELETE_ERROR) + + +def test_service_deletion_with_action(cluster: Cluster, sdk_client_fs: ADCMClient, generic_provider: Provider) -> None: + with allure.step("Check service required for cluster can't be deleted"): + service = cluster.service_add(name="required_service") + expect_api_error("delete required service", operation=service.delete, err_=SERVICE_CONFLICT) + _check_actions_amount(sdk_client_fs, 0) + + with allure.step("Check unmapped service with bonded action can be simply deleted"): + service = cluster.service_add(name="with_component") + service.delete() + _check_actions_amount(sdk_client_fs, 0) + + with allure.step("Check unmapped service not in 'created' state can be deleted with action"): + service = cluster.service_add(name="state_change") + service.action(name="change_state").run().wait() + expect_no_api_error("Delete service with 'remove' action", operation=service.delete) + _wait_all_tasks_succeed_or_aborted(sdk_client_fs, 2) + + with allure.step("Check that service that others depend on can't be deleted"): + service_with_component = cluster.service_add(name="with_component") + dependent_service = cluster.service_add(name="with_dependent_component") + host = cluster.host_add(generic_provider.host_create("some-fqdn")) + cluster.hostcomponent_set((host, service_with_component.component()), (host, dependent_service.component())) + expect_api_error( + "delete service with 'requires' component", operation=service_with_component.delete, err_=SERVICE_CONFLICT + ) + + with allure.step("Check that delete dependant service first is allowed"): + expect_no_api_error("delete dependant service", operation=dependent_service.delete) + _wait_all_tasks_succeed_or_aborted(sdk_client_fs, 3) + expect_no_api_error("delete mapped service", operation=service_with_component.delete) + _wait_all_tasks_succeed_or_aborted(sdk_client_fs, 4) + + with allure.step("Check that imported service can't be deleted even with action"): + importer: Cluster = sdk_client_fs.upload_from_fs(get_data_dir(__file__, "with_import")).cluster_create( + "Importer Cluster" + ) + service = cluster.service_add(name="with_component") + importer.bind(service) + expect_api_error("delete service with export", operation=service.delete, err_=SERVICE_CONFLICT) + + +def test_service_delete_with_concerns(cluster: Cluster, sdk_client_fs: ADCMClient, generic_provider: Provider): + host = cluster.host_add(generic_provider.host_create("some-host")) + cluster.service_add(name="required_service") + + with allure.step("Check service with concern about config can be deleted"): + service_with_concern = cluster.service_add(name="with_concern") + cluster.hostcomponent_set((host, service_with_concern.component())) + expect_no_api_error("delete mapped service with concern", operation=service_with_concern.delete) + _wait_all_tasks_succeed_or_aborted(sdk_client_fs, 1) + + with allure.step("Check service deletion stops any launched action"): + service_to_delete = cluster.service_add(name="with_component") + cluster.hostcomponent_set((host, service_to_delete.component())) + task = cluster.action().run() + # to avoid catching "termination is too early" + err = None + for _ in range(12): + try: + expect_no_api_error("delete service", operation=service_to_delete.delete) + break + except AssertionError as e: + err = e + sleep(0.3) + else: + raise err + _wait_job_status(task, "aborted") + _wait_all_tasks_succeed_or_aborted(sdk_client_fs, 3) + + with allure.step("Check service deletion is forbidden if it's already running"): + service_long_deletion = cluster.service_add(name="with_long_remove") + service_to_delete = cluster.service_add(name="with_component") + cluster.hostcomponent_set((host, service_to_delete.component()), (host, service_long_deletion.component())) + expect_no_api_error("delete service 1st time", operation=service_long_deletion.delete) + expect_api_error("delete service 2nd time", operation=service_long_deletion.delete, err_=SERVICE_DELETE_ERROR) + + +@allure.step("Check amount of jobs is {expected_amount} and all tasks finish successfully") +def _wait_all_tasks_succeed_or_aborted(client: ADCMClient, expected_amount: int): + jobs = client.job_list() + _check_actions_amount(client, expected_amount) + assert all(job.task().wait() in ("success", "aborted") for job in jobs) + + +def _check_actions_amount(client: ADCMClient, expected_amount: int) -> None: + assert ( + actual := len(client.job_list()) + ) == expected_amount, f"Expected jobs amount should be {expected_amount}.\nActual: {actual}" + + +def _wait_job_status(task: Task, status: str) -> None: + def _wait(): + assert task.job().status == status + + wait_until_step_succeeds(_wait, timeout=3, period=0.5) diff --git a/tests/functional/test_delete_service_data/config.yaml b/tests/functional/test_delete_service_data/default/config.yaml similarity index 100% rename from tests/functional/test_delete_service_data/config.yaml rename to tests/functional/test_delete_service_data/default/config.yaml diff --git a/tests/functional/test_delete_service_data/forbidden_to_delete/config.yaml b/tests/functional/test_delete_service_data/forbidden_to_delete/config.yaml new file mode 100644 index 0000000000..f4321e57b3 --- /dev/null +++ b/tests/functional/test_delete_service_data/forbidden_to_delete/config.yaml @@ -0,0 +1,56 @@ +# 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. + +- type: cluster + name: dependencies + version: 1 + +- type: service + version: 2 + name: required_service + description: | + This service is required for cluster, + so it can't be deleted + required: true + +- type: service + version: 1 + name: with_component + description: | + Another service is dependent on this service, + because of "requires" in another service + + config: + - name: param + default: 12 + type: integer + + export: + - param + + components: + component: + +- type: service + version: 2 + name: state_change + + actions: + change_state: + type: job + script_type: ansible + script: ./actions.yaml + display_name: "Long action" + states: + available: any + on_fail: failed + on_success: succeed diff --git a/tests/functional/test_delete_service_data/with_action/actions.yaml b/tests/functional/test_delete_service_data/with_action/actions.yaml new file mode 100644 index 0000000000..f6bf0e4e08 --- /dev/null +++ b/tests/functional/test_delete_service_data/with_action/actions.yaml @@ -0,0 +1,22 @@ +# 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. +--- +- name: Do nothing playbook + hosts: all + connection: local + gather_facts: no + + tasks: + - pause: + seconds: 2 + - debug: + msg: "Unstucked now" diff --git a/tests/functional/test_delete_service_data/with_action/config.yaml b/tests/functional/test_delete_service_data/with_action/config.yaml new file mode 100644 index 0000000000..f85e8c0868 --- /dev/null +++ b/tests/functional/test_delete_service_data/with_action/config.yaml @@ -0,0 +1,116 @@ +# 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. + +- type: cluster + name: dependencies + version: 1 + + actions: + long: + type: job + script_type: ansible + script: ./long.yaml + display_name: "Long action" + states: + available: any + +- type: service + version: 2 + name: required_service + description: | + This service is required for cluster, + so it can't be deleted + required: true + + actions: &actions + adcm_delete_service: &action + type: job + script_type: ansible + script: ./remove.yaml + states: + available: any + +- type: service + version: 1 + name: with_component + description: | + Another service is dependent on this service, + because of "requires" in another service + + config: + - name: param + default: 12 + type: integer + + actions: *actions + + export: + - param + + components: + component: + +- type: service + version: 2 + name: state_change + + actions: + change_state: &job + type: job + script_type: ansible + script: ./actions.yaml + states: + available: any + on_fail: failed + on_success: succeed + + adcm_delete_service: *action + +- type: service + version: 1 + name: with_dependent_component + + actions: *actions + + components: + component: + requires: + - service: with_component + component: component + +- type: service + version: 1 + name: with_concern + + config: + - name: param + type: integer + + actions: *actions + + components: + component: + config: + - name: param + type: integer + +- type: service + version: 3 + name: with_long_remove + + actions: + adcm_delete_service: + <<: *action + script: ./long_remove.yaml + + components: + component: diff --git a/tests/functional/test_delete_service_data/with_action/long.yaml b/tests/functional/test_delete_service_data/with_action/long.yaml new file mode 100644 index 0000000000..34254ee2fa --- /dev/null +++ b/tests/functional/test_delete_service_data/with_action/long.yaml @@ -0,0 +1,20 @@ +# 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. +--- +- name: Sleep for a while + hosts: localhost + connection: local + gather_facts: no + + tasks: + - pause: + seconds: 10 diff --git a/tests/functional/test_delete_service_data/with_action/long_remove.yaml b/tests/functional/test_delete_service_data/with_action/long_remove.yaml new file mode 100644 index 0000000000..1d6f56cf13 --- /dev/null +++ b/tests/functional/test_delete_service_data/with_action/long_remove.yaml @@ -0,0 +1,22 @@ +# 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. +--- +- name: Sleep for a while and remove service + hosts: localhost + connection: local + gather_facts: no + + tasks: + - pause: + seconds: 3 + + - adcm_delete_service: diff --git a/tests/functional/test_delete_service_data/with_action/remove.yaml b/tests/functional/test_delete_service_data/with_action/remove.yaml new file mode 100644 index 0000000000..0c90919b1e --- /dev/null +++ b/tests/functional/test_delete_service_data/with_action/remove.yaml @@ -0,0 +1,19 @@ +# 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. +--- +- name: Remove service + hosts: localhost + connection: local + gather_facts: no + + tasks: + - adcm_delete_service: diff --git a/gues_version.sh b/tests/functional/test_delete_service_data/with_import/config.yaml old mode 100755 new mode 100644 similarity index 65% rename from gues_version.sh rename to tests/functional/test_delete_service_data/with_import/config.yaml index 6884ffa3ba..a8feca34ff --- a/gues_version.sh +++ b/tests/functional/test_delete_service_data/with_import/config.yaml @@ -1,4 +1,3 @@ -#!/bin/sh # 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 @@ -11,10 +10,14 @@ # See the License for the specific language governing permissions and # limitations under the License. -current_date=$(date '+%Y.%m.%d.%H') -current_hash=$(git log --pretty=format:'%h' -n 1) +- type: cluster + version: 1 + name: cluster_with_import -printf '{\n\t"version": "%s",\n\t"commit_id": "%s"\n}\n' "${current_date}" "${current_hash}" > config.json + import: + with_component: + required: true -# Copy information for Angular to work on -cp config.json web/src/assets/config.json +- type: service + version: 2 + name: service_with_import diff --git a/tests/functional/test_delete_service_plugin.py b/tests/functional/test_delete_service_plugin.py index a4803c61b1..3fe0b75d35 100644 --- a/tests/functional/test_delete_service_plugin.py +++ b/tests/functional/test_delete_service_plugin.py @@ -12,9 +12,9 @@ """Tests for delete_service plugin""" +import allure from adcm_client.objects import ADCMClient from adcm_pytest_plugin import utils -import allure def test_delete_service_plugin(sdk_client_fs: ADCMClient): diff --git a/tests/functional/test_delete_service_with_active_export.py b/tests/functional/test_delete_service_with_active_export.py index 5b4a4032fe..ceff1cc196 100644 --- a/tests/functional/test_delete_service_with_active_export.py +++ b/tests/functional/test_delete_service_with_active_export.py @@ -19,7 +19,6 @@ import pytest from adcm_client.objects import ADCMClient from adcm_pytest_plugin.utils import get_data_dir - from tests.library import errorcodes as err diff --git a/tests/functional/test_events.py b/tests/functional/test_events.py index a0d7047dc4..e71708277f 100644 --- a/tests/functional/test_events.py +++ b/tests/functional/test_events.py @@ -24,7 +24,6 @@ # pylint: disable=redefined-outer-name from adcm_client.objects import ADCMClient from adcm_pytest_plugin import utils - from tests.functional.conftest import only_clean_adcm DATADIR = utils.get_data_dir(__file__) diff --git a/tests/functional/test_full_upgrade.py b/tests/functional/test_full_upgrade.py index 2361245b8e..1d38387019 100644 --- a/tests/functional/test_full_upgrade.py +++ b/tests/functional/test_full_upgrade.py @@ -12,9 +12,9 @@ """Tests for full update of objects""" +import allure from adcm_client.objects import ADCMClient from adcm_pytest_plugin.utils import get_data_dir -import allure def test_full_upgrade_hostprovider_first(sdk_client_fs: ADCMClient): diff --git a/tests/functional/test_host_components.py b/tests/functional/test_host_components.py index 31608e9d95..3cb918d9e3 100644 --- a/tests/functional/test_host_components.py +++ b/tests/functional/test_host_components.py @@ -23,10 +23,14 @@ import yaml from _pytest.mark import ParameterSet from adcm_client.base import ObjectNotFound -from adcm_client.objects import ADCMClient, Bundle, Provider, Cluster -from adcm_pytest_plugin.utils import random_string, get_data_dir, get_data_subdirs_as_parameters, catch_failed +from adcm_client.objects import ADCMClient, Bundle, Cluster, Provider +from adcm_pytest_plugin.utils import ( + catch_failed, + get_data_dir, + get_data_subdirs_as_parameters, + random_string, +) from coreapi.exceptions import ErrorMessage - from tests.functional.conftest import only_clean_adcm CASES_PATH = "cases" @@ -132,7 +136,6 @@ def parametrize_by_constraint(case_type: Literal['positive', 'negative']): return pytest.mark.parametrize(test_arg_names, parameters, ids=ids) -# pylint: disable-next=too-many-arguments def _test_constraint( constraint: str, hosts_amounts: List[int], diff --git a/tests/functional/test_host_functions.py b/tests/functional/test_host_functions.py index 11aff37049..e31eeeb779 100644 --- a/tests/functional/test_host_functions.py +++ b/tests/functional/test_host_functions.py @@ -17,12 +17,10 @@ import random import allure - import pytest - -from adcm_client.objects import ADCMClient, Bundle, Provider, Cluster, Host -from adcm_pytest_plugin.utils import get_data_dir +from adcm_client.objects import ADCMClient, Bundle, Cluster, Host, Provider from adcm_pytest_plugin import utils +from adcm_pytest_plugin.utils import get_data_dir from jsonschema import validate # pylint: disable=redefined-outer-name diff --git a/tests/functional/test_hostproviders_functions.py b/tests/functional/test_hostproviders_functions.py index bdad0d8bb7..611e4cc587 100644 --- a/tests/functional/test_hostproviders_functions.py +++ b/tests/functional/test_hostproviders_functions.py @@ -22,7 +22,6 @@ from coreapi import exceptions from jsonschema import validate - # pylint: disable=protected-access from tests.library import errorcodes diff --git a/tests/functional/test_import.py b/tests/functional/test_import.py index e55cf620f2..e2366b244b 100644 --- a/tests/functional/test_import.py +++ b/tests/functional/test_import.py @@ -16,11 +16,10 @@ import coreapi import pytest from adcm_client.objects import ADCMClient -from adcm_pytest_plugin.utils import parametrize_by_data_subdirs, get_data_dir - +from adcm_pytest_plugin.utils import get_data_dir, parametrize_by_data_subdirs +from tests.functional.conftest import only_clean_adcm from tests.library import errorcodes as err from tests.library.errorcodes import INVALID_VERSION_DEFINITION -from tests.functional.conftest import only_clean_adcm pytestmark = [only_clean_adcm] diff --git a/tests/functional/test_import_data/config_generator_negative.py b/tests/functional/test_import_data/config_generator_negative.py index 56c7498097..30708ce0d4 100644 --- a/tests/functional/test_import_data/config_generator_negative.py +++ b/tests/functional/test_import_data/config_generator_negative.py @@ -1,6 +1,16 @@ +# 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 os - VARIABLES = [ ("less-equal", "2.2", "3.0", 'max', 'min', "2.1"), ("less-equal", "2.2", "3.0", 'max', 'min_strict', "2.2"), diff --git a/tests/functional/test_import_data/config_generator_positive.py b/tests/functional/test_import_data/config_generator_positive.py index 54faac912e..7862357ba7 100644 --- a/tests/functional/test_import_data/config_generator_positive.py +++ b/tests/functional/test_import_data/config_generator_positive.py @@ -1,6 +1,16 @@ +# 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 os - VARIABLES = [ ("2.2", "3.0", 'max', 'min', "2.5"), ("2.2", "3.0", 'max', 'min_strict', "3.0"), diff --git a/tests/functional/test_inventories.py b/tests/functional/test_inventories.py index 1410c37224..b5d55e0f0f 100644 --- a/tests/functional/test_inventories.py +++ b/tests/functional/test_inventories.py @@ -19,15 +19,26 @@ import allure import pytest from _pytest.fixtures import SubRequest -from adcm_client.objects import ADCM, ADCMClient, Cluster, Component, Host, Provider, Service +from adcm_client.objects import ( + ADCM, + ADCMClient, + Cluster, + Component, + Host, + Provider, + Service, +) from adcm_pytest_plugin import utils from adcm_pytest_plugin.docker_utils import get_file_from_container from adcm_pytest_plugin.steps.actions import run_cluster_action_and_assert_result from adcm_pytest_plugin.utils import get_data_dir from docker.models.containers import Container - from tests.functional.conftest import only_clean_adcm -from tests.functional.tools import BEFORE_UPGRADE_DEFAULT_STATE, create_config_group_and_add_host, get_inventory_file +from tests.functional.tools import ( + BEFORE_UPGRADE_DEFAULT_STATE, + create_config_group_and_add_host, + get_inventory_file, +) # pylint: disable=redefined-outer-name @@ -46,8 +57,18 @@ 'simple_field': 1, }, 'attr': { - 'group_keys': {'json_field': True, 'map_field': True, 'simple_field': False, '__main_info': False}, - 'custom_group_keys': {'json_field': True, 'map_field': True, 'simple_field': True, '__main_info': False}, + 'group_keys': { + 'json_field': True, + 'map_field': True, + 'simple_field': False, + '__main_info': False, + }, + 'custom_group_keys': { + 'json_field': True, + 'map_field': True, + 'simple_field': True, + '__main_info': False, + }, }, } @@ -147,14 +168,23 @@ def cluster_with_components(self, sdk_client_fs: ADCMClient) -> Tuple[Cluster, S bundle = sdk_client_fs.upload_from_fs(get_data_dir(__file__, "cluster_with_components")) cluster = bundle.cluster_create("Test Cluster") service = cluster.service_add(name="test_service") - return cluster, service, service.component(name="first_component"), service.component(name="second_component") + return ( + cluster, + service, + service.component(name="first_component"), + service.component(name="second_component"), + ) @pytest.fixture() def second_service_with_components(self, cluster_with_components) -> Tuple[Service, Component, Component]: """Add second service to the cluster""" cluster, *_ = cluster_with_components service = cluster.service_add(name="second_service") - return service, service.component(name="first_component"), service.component(name="second_component") + return ( + service, + service.component(name="first_component"), + service.component(name="second_component"), + ) @pytest.fixture() def _map_hosts_to_components(self, hosts, cluster_with_components, second_service_with_components) -> None: diff --git a/tests/functional/test_inventories_data/cluster-inventory.json b/tests/functional/test_inventories_data/cluster-inventory.json index f843f2f310..8ee1b9ea52 100644 --- a/tests/functional/test_inventories_data/cluster-inventory.json +++ b/tests/functional/test_inventories_data/cluster-inventory.json @@ -57,10 +57,14 @@ "svc-w-installed": 222, "svc-read-only": 2.5 }, + "maintenance_mode": false, + "display_name": "zookeeper", "GROUP_BUDDY": { "component_id": 1, "state": "created", "multi_state": [], + "maintenance_mode": false, + "display_name": "GROUP_BUDDY", "config": { "__main_info": "I'm a very important string, don't touch me", "simple_field": 1, @@ -90,7 +94,9 @@ "sc-file-type": null }, "state": "created", - "multi_state": [] + "multi_state": [], + "maintenance_mode": false, + "display_name": "ZOOKEEPER_CLIENT" }, "ZOOKEEPER_SERVER": { "component_id": 3, @@ -108,7 +114,9 @@ "state": "created", "multi_state": [ "component_multi_state" - ] + ], + "maintenance_mode": false, + "display_name": "ZOOKEEPER_SERVER" } } } @@ -150,7 +158,9 @@ } }, "state": "created", - "multi_state": [] + "multi_state": [], + "maintenance_mode": false, + "display_name": "GROUP_BUDDY" }, "ZOOKEEPER_CLIENT": { "component_id": 2, @@ -170,7 +180,9 @@ "sc-file-type": null }, "state": "created", - "multi_state": [] + "multi_state": [], + "maintenance_mode": false, + "display_name": "ZOOKEEPER_CLIENT" }, "ZOOKEEPER_SERVER": { "component_id": 3, @@ -188,8 +200,12 @@ "state": "created", "multi_state": [ "component_multi_state" - ] - } + ], + "maintenance_mode": false, + "display_name": "ZOOKEEPER_SERVER" + }, + "maintenance_mode": false, + "display_name": "zookeeper" } } } diff --git a/tests/functional/test_known_bugs.py b/tests/functional/test_known_bugs.py index 46f1ee2676..d508a40236 100644 --- a/tests/functional/test_known_bugs.py +++ b/tests/functional/test_known_bugs.py @@ -14,10 +14,10 @@ import allure import pytest -from adcm_client.objects import Cluster, Provider, Bundle +from adcm_client.objects import Bundle, Cluster, Provider from adcm_pytest_plugin.steps.actions import run_service_action_and_assert_result -from adcm_pytest_plugin.utils import get_data_dir - +from adcm_pytest_plugin.utils import catch_failed, get_data_dir +from coreapi.exceptions import ErrorMessage from tests.functional.conftest import only_clean_adcm # pylint: disable=redefined-outer-name @@ -87,3 +87,20 @@ def test_missing_service_outside_config_group(cluster_with_services, provider): with allure.step('Run actions on services and check config dicts are available'): run_service_action_and_assert_result(first_service, action_name) run_service_action_and_assert_result(second_service, action_name) + + +def test_launch_action_with_activatable_config_group(sdk_client_fs): + """Known bug caught when running action with at least one activatable group in action's config""" + cluster = sdk_client_fs.upload_from_fs(get_data_dir(__file__, "activatable_group_in_action_config")).cluster_create( + "Test Cluster" + ) + for param_1, param_2 in ((False, False), (True, False), (False, True)): + with allure.step(f"Try to set active status of {param_1=} and {param_2=}"): + with catch_failed(ErrorMessage, "Running cluster action should not raise exception"): + cluster.action(name="enable_something").run( + config={ + "param_1": {"somethingtwo": "jjj"}, + "param_2": {"somethingone": ["ololo"]}, + }, + attr={"param_1": {"active": param_1}, "param_2": {"active": param_2}}, + ).wait() diff --git a/tests/functional/test_known_bugs_data/activatable_group_in_action_config/config.yaml b/tests/functional/test_known_bugs_data/activatable_group_in_action_config/config.yaml new file mode 100644 index 0000000000..6b752364a2 --- /dev/null +++ b/tests/functional/test_known_bugs_data/activatable_group_in_action_config/config.yaml @@ -0,0 +1,47 @@ +- type: cluster + name: cluster + version: 1.1 + + config: + - name: somegroup + type: group + activatable: true + active: false + subs: + - name: parampampam + type: integer + default: 4 + - name: anothergroup + type: group + activatable: true + active: true + subs: + - name: parampampam + type: integer + default: 4 + + actions: + enable_something: + display_name: Enable Something + type: task + states: + available: any + config: + - name: param_1 + type: group + activatable: true + active: false + subs: + - name: somethingtwo + type: string + - name: param_2 + type: group + activatable: true + active: false + subs: + - name: somethingone + type: list + scripts: + - name: step1 + script_type: ansible + script: ./actions.yaml diff --git a/tests/functional/test_locked_objects.py b/tests/functional/test_locked_objects.py index 985befa661..2b9465ec22 100644 --- a/tests/functional/test_locked_objects.py +++ b/tests/functional/test_locked_objects.py @@ -13,24 +13,24 @@ """Tests for ADCM objects locks""" -from typing import Union, Tuple, List +from typing import List, Tuple, Union import allure import pytest from _pytest.outcomes import Failed from adcm_client.base import ObjectNotFound from adcm_client.objects import ( - Provider, + ADCMClient, Cluster, + Component, Host, - ADCMClient, + Provider, Service, Task, - Component, ) from adcm_pytest_plugin import utils from adcm_pytest_plugin.steps.asserts import assert_state -from adcm_pytest_plugin.utils import random_string, catch_failed +from adcm_pytest_plugin.utils import catch_failed, random_string from coreapi.exceptions import ErrorMessage LOCK_ACTION_NAMES = ["lock", "lock_multijob"] @@ -219,7 +219,8 @@ def test_down_lock(self, complete_cluster: Cluster, host: Host, lock_action): - Host """ task = _lock_obj( - complete_cluster.service(name="first_service").component(name="first_service_component_1"), lock_action + complete_cluster.service(name="first_service").component(name="first_service_component_1"), + lock_action, ) is_locked(host) task.wait() @@ -664,7 +665,9 @@ def _cluster_with_components(cluster: Cluster, hosts: List[Host]): def _lock_obj( - obj: Union[Cluster, Service, Component, Provider, Host], lock_action: str = "lock", duration: int = 5 + obj: Union[Cluster, Service, Component, Provider, Host], + lock_action: str = "lock", + duration: int = 5, ) -> Task: """ Run action lock on object diff --git a/tests/functional/test_multiple_adcm.py b/tests/functional/test_multiple_adcm.py index 197cacbafc..960d56ea98 100644 --- a/tests/functional/test_multiple_adcm.py +++ b/tests/functional/test_multiple_adcm.py @@ -16,20 +16,18 @@ # pylint: disable=redefined-outer-name -from typing import Set, Iterable, Tuple +from typing import Iterable, Set, Tuple import allure import pytest from adcm_client.base import ObjectNotFound -from adcm_client.objects import ADCMClient, Provider, Cluster -from adcm_pytest_plugin.utils import get_data_dir, catch_failed -from adcm_pytest_plugin.docker_utils import copy_file_to_container, ADCM +from adcm_client.objects import ADCMClient, Cluster, Provider +from adcm_pytest_plugin.docker_utils import ADCM, copy_file_to_container from adcm_pytest_plugin.steps.actions import run_cluster_action_and_assert_result from adcm_pytest_plugin.steps.commands import dump_cluster, load_cluster - -from tests.library.assertions import sets_are_equal, dicts_are_equal +from adcm_pytest_plugin.utils import catch_failed, get_data_dir from tests.functional.tools import AnyADCMObject, get_object_represent - +from tests.library.assertions import dicts_are_equal, sets_are_equal CLUSTER_NAME = 'test cluster to export' PROVIDER_NAME = 'test_provider_to_export' @@ -61,7 +59,7 @@ def second_adcm_sdk(extra_adcm_fs: ADCM, adcm_api_credentials) -> ADCMClient: @pytest.fixture() -def upload_bundle_to_both_adcm(bundle_archives, sdk_client_fs, second_adcm_sdk) -> None: +def _upload_bundle_to_both_adcm(bundle_archives, sdk_client_fs, second_adcm_sdk) -> None: """ * Upload cluster and provider bundles to two ADCMs * Create cluster and provider on both @@ -85,9 +83,11 @@ def upload_bundle_to_both_adcm(bundle_archives, sdk_client_fs, second_adcm_sdk) @pytest.mark.parametrize( - 'bundle_archives', [(get_data_dir(__file__, 'cluster'), get_data_dir(__file__, 'provider'))], indirect=True + 'bundle_archives', + [(get_data_dir(__file__, 'cluster'), get_data_dir(__file__, 'provider'))], + indirect=True, ) -@pytest.mark.usefixtures('upload_bundle_to_both_adcm') +@pytest.mark.usefixtures('_upload_bundle_to_both_adcm') def test_export_cluster_from_another_adcm(adcm_fs, extra_adcm_fs, sdk_client_fs, second_adcm_sdk): """ Test basic scenario export of a cluster from one ADCM to another @@ -117,7 +117,8 @@ def import_cluster_to_second_adcm( copy_file_to_container(export_from_adcm.container, import_to_adcm.container, path_to_dump, path_to_dump) load_cluster(import_to_adcm, path_to_dump, password) with catch_failed( - ObjectNotFound, f'Either cluster "{CLUSTER_NAME}" or provider "{PROVIDER_NAME}" were not found after the import' + ObjectNotFound, + f'Either cluster "{CLUSTER_NAME}" or provider "{PROVIDER_NAME}" were not found after the import', ): return second_adcm_sdk.cluster(name=CLUSTER_NAME), second_adcm_sdk.provider(name=PROVIDER_NAME) diff --git a/tests/functional/test_nullable_fields.py b/tests/functional/test_nullable_fields.py index d8fb168588..683d9b085b 100644 --- a/tests/functional/test_nullable_fields.py +++ b/tests/functional/test_nullable_fields.py @@ -24,7 +24,6 @@ from adcm_client.objects import ADCMClient from adcm_pytest_plugin import utils from jinja2 import Template - from tests.library import errorcodes as err DATADIR = utils.get_data_dir(__file__) diff --git a/tests/functional/test_plugins_adcm_hc.py b/tests/functional/test_plugins_adcm_hc.py index 8673601c23..03515902b9 100644 --- a/tests/functional/test_plugins_adcm_hc.py +++ b/tests/functional/test_plugins_adcm_hc.py @@ -12,17 +12,25 @@ """Test plugin adcm_hc""" -from typing import Tuple, Callable +from typing import Callable, Tuple import allure import pytest -from adcm_client.objects import Cluster, Provider, ADCMClient, Service, Host, Component, Job -from adcm_pytest_plugin.utils import get_data_dir +from adcm_client.objects import ( + ADCMClient, + Cluster, + Component, + Host, + Job, + Provider, + Service, +) from adcm_pytest_plugin.steps.actions import ( run_cluster_action_and_assert_result, - run_service_action_and_assert_result, run_component_action_and_assert_result, + run_service_action_and_assert_result, ) +from adcm_pytest_plugin.utils import get_data_dir # pylint: disable=redefined-outer-name diff --git a/tests/functional/test_plugins_config.py b/tests/functional/test_plugins_config.py index 5ad0015b01..d90a6c4ee3 100644 --- a/tests/functional/test_plugins_config.py +++ b/tests/functional/test_plugins_config.py @@ -12,31 +12,30 @@ """Tests for adcm_config plugin""" -from typing import Tuple, Callable +from typing import Callable, Tuple import allure import pytest -from adcm_client.objects import ADCMClient, Cluster, Provider, Host, Component, Service +from adcm_client.objects import ADCMClient, Cluster, Component, Host, Provider, Service from adcm_pytest_plugin.steps.actions import ( - run_provider_action_and_assert_result, run_cluster_action_and_assert_result, - run_service_action_and_assert_result, run_host_action_and_assert_result, + run_provider_action_and_assert_result, + run_service_action_and_assert_result, ) - -from tests.functional.tools import AnyADCMObject, get_config from tests.functional.plugin_utils import ( + TestImmediateChange, build_objects_checker, - generate_cluster_success_params, compose_name, - run_successful_task, - get_cluster_related_object, - generate_provider_success_params, - get_provider_related_object, create_two_clusters, create_two_providers, - TestImmediateChange, + generate_cluster_success_params, + generate_provider_success_params, + get_cluster_related_object, + get_provider_related_object, + run_successful_task, ) +from tests.functional.tools import AnyADCMObject, get_config # pylint:disable=redefined-outer-name @@ -148,7 +147,9 @@ def test_host_from_provider(two_providers: Tuple[Provider, Provider], sdk_client def test_multijob( - two_clusters: Tuple[Cluster, Cluster], two_providers: Tuple[Provider, Provider], sdk_client_fs: ADCMClient + two_clusters: Tuple[Cluster, Cluster], + two_providers: Tuple[Provider, Provider], + sdk_client_fs: ADCMClient, ): """Check that multijob actions change config or object itself""" component = (service := (cluster := two_clusters[0]).service()).component() @@ -187,18 +188,25 @@ def test_forbidden_actions(sdk_client_fs: ADCMClient): first_host, second_host, *_ = provider.host_list() with check_config_changed(sdk_client_fs): run_host_action_and_assert_result( - first_host, 'change_host_from_provider', status='failed', config={'host_id': second_host.id} + first_host, + 'change_host_from_provider', + status='failed', + config={'host_id': second_host.id}, ) def test_from_host_actions( - two_clusters: Tuple[Cluster, Cluster], two_providers: Tuple[Provider, Provider], sdk_client_fs: ADCMClient + two_clusters: Tuple[Cluster, Cluster], + two_providers: Tuple[Provider, Provider], + sdk_client_fs: ADCMClient, ): """Test that host actions actually change config""" name = "first" affected_objects = set() check_config_changed_local = build_objects_checker( - extractor=get_config, changed={**INITIAL_CONFIG, 'int': CHANGED_CONFIG['int']}, field_name='Config' + extractor=get_config, + changed={**INITIAL_CONFIG, 'int': CHANGED_CONFIG['int']}, + field_name='Config', ) with allure.step('Bind component to host'): component = (service := (cluster := two_clusters[0]).service(name=name)).component(name=name) diff --git a/tests/functional/test_plugins_multi_state_set_unset.py b/tests/functional/test_plugins_multi_state_set_unset.py index bc125962f3..88e643df12 100644 --- a/tests/functional/test_plugins_multi_state_set_unset.py +++ b/tests/functional/test_plugins_multi_state_set_unset.py @@ -12,33 +12,31 @@ """Test adcm_plugin_multi_sate set/unset""" -from typing import Tuple, Callable +from typing import Callable, Tuple -import pytest import allure - +import pytest +from adcm_client.objects import ADCMClient, Cluster, Component, Host, Provider, Service from adcm_pytest_plugin.steps.actions import ( run_cluster_action_and_assert_result, - run_service_action_and_assert_result, - run_provider_action_and_assert_result, - run_host_action_and_assert_result, run_component_action_and_assert_result, + run_host_action_and_assert_result, + run_provider_action_and_assert_result, + run_service_action_and_assert_result, ) -from adcm_client.objects import ADCMClient, Cluster, Provider, Host, Service, Component - -from tests.functional.tools import AnyADCMObject from tests.functional.plugin_utils import ( + TestImmediateChange, + build_objects_checker, + compose_name, + create_two_clusters, + create_two_providers, generate_cluster_success_params, generate_provider_success_params, - compose_name, - build_objects_checker, get_cluster_related_object, get_provider_related_object, - create_two_clusters, - create_two_providers, run_successful_task, - TestImmediateChange, ) +from tests.functional.tools import AnyADCMObject # pylint: disable=redefined-outer-name,unnecessary-lambda-assignment @@ -64,7 +62,9 @@ def two_clusters(request, sdk_client_fs: ADCMClient) -> Tuple[Cluster, Cluster]: """Get two clusters with both services""" return create_two_clusters( - sdk_client_fs, caller_file=__file__, bundle_dir="cluster" if not hasattr(request, 'param') else request.param + sdk_client_fs, + caller_file=__file__, + bundle_dir="cluster" if not hasattr(request, 'param') else request.param, ) @@ -133,7 +133,9 @@ def test_provider_related_objects( def test_double_call_to_multi_state_set(two_clusters: Tuple[Cluster, Cluster], sdk_client_fs: ADCMClient): """Test that double call to plugin from two files doesn't fail""" check_multi_state_after_set = build_objects_checker( - sorted(['much', 'better', 'actually']), extractor=_multi_state_extractor, field_name=FIELD_NAME + sorted(['much', 'better', 'actually']), + extractor=_multi_state_extractor, + field_name=FIELD_NAME, ) check_multi_state_after_unset = build_objects_checker( ['actually'], extractor=_multi_state_extractor, field_name=FIELD_NAME @@ -175,7 +177,12 @@ def test_forbidden_multi_state_set_actions(sdk_client_fs: ADCMClient): with allure.step(f'Check forbidden from cluster "{name}" context actions'): cluster = sdk_client_fs.cluster(name=name) # missing is used because it should fail for misconfiguration reasons, not because state not set - for forbidden_action in ('set_service', 'set_component', 'unset_service_missing', 'unset_component_missing'): + for forbidden_action in ( + 'set_service', + 'set_component', + 'unset_service_missing', + 'unset_component_missing', + ): with check_objects_multi_state_changed(sdk_client_fs): run_cluster_action_and_assert_result(cluster, forbidden_action, status='failed') with allure.step(f'Check forbidden from service "{name}" context actions'): @@ -191,7 +198,9 @@ def test_forbidden_multi_state_set_actions(sdk_client_fs: ADCMClient): def test_missing_ok_multi_state_unset( - two_providers: Tuple[Provider, Provider], two_clusters: Tuple[Cluster, Cluster], sdk_client_fs: ADCMClient + two_providers: Tuple[Provider, Provider], + two_clusters: Tuple[Cluster, Cluster], + sdk_client_fs: ADCMClient, ): """ Checking behaviour of flag "missing_ok": @@ -232,7 +241,9 @@ def test_missing_ok_multi_state_unset( def test_multi_state_set_from_host_actions( - two_clusters: Tuple[Cluster, Cluster], two_providers: Tuple[Provider, Provider], sdk_client_fs: ADCMClient + two_clusters: Tuple[Cluster, Cluster], + two_providers: Tuple[Provider, Provider], + sdk_client_fs: ADCMClient, ): """Test that host actions actually change multi state""" name = "first" diff --git a/tests/functional/test_plugins_state.py b/tests/functional/test_plugins_state.py index b4804239c8..766e63fef8 100644 --- a/tests/functional/test_plugins_state.py +++ b/tests/functional/test_plugins_state.py @@ -17,29 +17,26 @@ import allure import pytest - -from adcm_client.objects import ADCMClient, Cluster, Provider, Host, Service, Component +from adcm_client.objects import ADCMClient, Cluster, Component, Host, Provider, Service from adcm_pytest_plugin.steps.actions import ( run_cluster_action_and_assert_result, - run_service_action_and_assert_result, - run_provider_action_and_assert_result, run_host_action_and_assert_result, + run_provider_action_and_assert_result, + run_service_action_and_assert_result, ) - from tests.functional.plugin_utils import ( + TestImmediateChange, build_objects_checker, - generate_cluster_success_params, - get_cluster_related_object, compose_name, + create_two_clusters, + create_two_providers, + generate_cluster_success_params, generate_provider_success_params, + get_cluster_related_object, get_provider_related_object, - create_two_providers, - create_two_clusters, run_successful_task, - TestImmediateChange, ) - check_objects_state_changed = build_objects_checker( field_name='State', changed='ifeelgood!', @@ -59,7 +56,9 @@ def two_providers(sdk_client_fs: ADCMClient) -> Tuple[Provider, Provider]: def two_clusters(request, sdk_client_fs: ADCMClient) -> Tuple[Cluster, Cluster]: """Get two clusters with both services""" return create_two_clusters( - sdk_client_fs, caller_file=__file__, bundle_dir="cluster" if not hasattr(request, 'param') else request.param + sdk_client_fs, + caller_file=__file__, + bundle_dir="cluster" if not hasattr(request, 'param') else request.param, ) @@ -158,7 +157,10 @@ def test_forbidden_state_set_actions(sdk_client_fs: ADCMClient): host_second = sdk_client_fs.host(fqdn=first_second_fqdn) with check_objects_state_changed(sdk_client_fs): run_host_action_and_assert_result( - host_first, 'set_host_from_provider', config={'host_id': host_second.id}, status='failed' + host_first, + 'set_host_from_provider', + config={'host_id': host_second.id}, + status='failed', ) @@ -172,7 +174,9 @@ def test_double_call_to_state_set(two_clusters: Tuple[Cluster, Cluster], sdk_cli def test_state_set_from_host_actions( - two_providers: Tuple[Provider, Provider], two_clusters: Tuple[Cluster, Cluster], sdk_client_fs: ADCMClient + two_providers: Tuple[Provider, Provider], + two_clusters: Tuple[Cluster, Cluster], + sdk_client_fs: ADCMClient, ): """Test that host actions actually change state""" name = "first" diff --git a/tests/functional/test_read_only_parameters.py b/tests/functional/test_read_only_parameters.py index 746e1fe52d..164e256cd8 100644 --- a/tests/functional/test_read_only_parameters.py +++ b/tests/functional/test_read_only_parameters.py @@ -15,12 +15,10 @@ import allure import coreapi import pytest - from adcm_pytest_plugin import utils - from tests.library.errorcodes import CONFIG_VALUE_ERROR -# pylint: disable=too-many-arguments,redefined-outer-name +# pylint: disable=redefined-outer-name TEST_DATA = [ ("read-only-when-runned", False, True, "run", False, True), diff --git a/tests/functional/test_rotation.py b/tests/functional/test_rotation.py index 043ff55252..6a4413f981 100644 --- a/tests/functional/test_rotation.py +++ b/tests/functional/test_rotation.py @@ -20,14 +20,27 @@ import allure import pytest -from adcm_client.objects import ADCM, ADCMClient, Cluster, Component, Host, Provider, Service, Task +from adcm_client.objects import ( + ADCM, + ADCMClient, + Cluster, + Component, + Host, + Provider, + Service, + Task, +) from adcm_pytest_plugin.steps.commands import logrotate from adcm_pytest_plugin.utils import get_data_dir, random_string from docker.models.containers import Container - from tests.functional.conftest import only_clean_adcm from tests.library.assertions import does_not_intersect, is_superset_of -from tests.library.db import set_configs_date, set_job_directories_date, set_jobs_date, set_tasks_date +from tests.library.db import ( + set_configs_date, + set_job_directories_date, + set_jobs_date, + set_tasks_date, +) pytestmark = [only_clean_adcm] @@ -93,7 +106,10 @@ def config_group_objects(objects) -> Tuple[Tuple[int, ...], Tuple[int, ...]]: @pytest.fixture() def separated_configs(config_objects, config_group_objects) -> Tuple[Tuple[int, ...], Tuple[int, ...]]: """Return separately bonded and not bonded configs' and group configs' ids""" - return (*config_objects[0], *config_group_objects[0]), (*config_objects[1], *config_group_objects[1]) + return (*config_objects[0], *config_group_objects[0]), ( + *config_objects[1], + *config_group_objects[1], + ) # !===== Tests =====! @@ -250,7 +266,6 @@ def test_remove_only_expired_config_logs(sdk_client_fs, adcm_fs, adcm_db, separa _check_config_groups_exists(objects) -# pylint: disable-next=too-many-arguments def test_logrotate_command_target_job(sdk_client_fs, adcm_fs, adcm_db, simple_tasks, separated_configs, objects): """ Check that "logrotate --target job" deletes only configs, but not the jobs @@ -270,7 +285,6 @@ def test_logrotate_command_target_job(sdk_client_fs, adcm_fs, adcm_db, simple_ta _check_config_groups_exists(objects) -# pylint: disable-next=too-many-arguments def test_logrotate_command_target_config(sdk_client_fs, adcm_fs, adcm_db, simple_tasks, separated_configs, objects): """ Check that "logrotate --target config" deletes only configs, but not the jobs @@ -342,7 +356,10 @@ def test_only_finished_tasks_removed(sdk_client_fs, adcm_fs, adcm_db, objects): def set_rotation_info_in_adcm_config( - adcm: ADCM, jobs_in_db: Optional[int] = None, jobs_on_fs: Optional[int] = None, config_in_db: Optional[int] = None + adcm: ADCM, + jobs_in_db: Optional[int] = None, + jobs_on_fs: Optional[int] = None, + config_in_db: Optional[int] = None, ) -> dict: """Update ADCM config with new jobs/config log rotation info""" # `is not None` because 0 is a legit value @@ -374,7 +391,9 @@ def check_task_logs_are_removed_from_db(client: ADCMClient, task_ids: Collection with allure.step(f'Check that task logs are removed from DB: {", ".join(map(str, task_ids))}'): presented_task_ids = {job.task().id for job in client.job_list()} does_not_intersect( - presented_task_ids, task_ids, 'Some of the task logs that should be removed were found in db' + presented_task_ids, + task_ids, + 'Some of the task logs that should be removed were found in db', ) @@ -382,7 +401,11 @@ def check_job_logs_are_removed_from_db(client: ADCMClient, job_ids: Collection[i """Check job logs are removed from the database""" with allure.step(f'Check that job logs are removed from DB: {", ".join(map(str, job_ids))}'): presented_job_ids = {job.id for job in client.job_list()} - does_not_intersect(presented_job_ids, job_ids, 'Some of the job logs that should be removed were found in db') + does_not_intersect( + presented_job_ids, + job_ids, + 'Some of the job logs that should be removed were found in db', + ) def check_job_logs_are_removed_from_fs(container: Container, job_ids: Collection[int]): @@ -390,7 +413,9 @@ def check_job_logs_are_removed_from_fs(container: Container, job_ids: Collection with allure.step(f'Check that job logs are removed from FS: {", ".join(map(str, job_ids))}'): presented_tasks = _get_ids_of_job_logs_on_fs(container) does_not_intersect( - presented_tasks, set(job_ids), 'Some of the job logs that should be removed were found on filesystem' + presented_tasks, + set(job_ids), + 'Some of the job logs that should be removed were found on filesystem', ) @@ -399,7 +424,9 @@ def check_config_logs_are_removed(client: ADCMClient, config_ids: Collection[int with allure.step(f'Check that config logs are removed from DB: {", ".join(map(str, config_ids))}'): presented_configs = _retrieve_config_ids(client) does_not_intersect( - presented_configs, set(config_ids), 'Some of the config logs that should be removed were found in db' + presented_configs, + set(config_ids), + 'Some of the config logs that should be removed were found in db', ) diff --git a/tests/functional/test_secrets.py b/tests/functional/test_secrets.py index 0468d3c2ed..18410d6a41 100644 --- a/tests/functional/test_secrets.py +++ b/tests/functional/test_secrets.py @@ -22,7 +22,6 @@ from adcm_pytest_plugin.utils import get_data_dir from docker.models.containers import Container - # pylint: disable=redefined-outer-name OLD_PASSWORD = 'simplePassword' @@ -99,7 +98,11 @@ def _check_no_secrets_in_config(cluster: Cluster): found_secrets = [secret for secret in SECRETS if secret in text_config] if not found_secrets: return - allure.attach(text_config, name='Config with revealed secrets', attachment_type=allure.attachment_type.JSON) + allure.attach( + text_config, + name='Config with revealed secrets', + attachment_type=allure.attachment_type.JSON, + ) raise AssertionError('\n'.join(('Some of secrets were found in config:', *found_secrets))) diff --git a/tests/functional/test_stacks.py b/tests/functional/test_stacks.py index 9d8b9f0b9d..e061abfa3e 100644 --- a/tests/functional/test_stacks.py +++ b/tests/functional/test_stacks.py @@ -13,7 +13,7 @@ """Tests for /stack related objects ans stuff""" import json -from typing import Tuple, List +from typing import List, Tuple import allure import coreapi @@ -22,7 +22,6 @@ from adcm_client.objects import ADCMClient from adcm_pytest_plugin import utils from jsonschema import validate - from tests.library import errorcodes from tests.library.errorcodes import ADCMError diff --git a/tests/functional/test_stacks_data/cluster_proto_wo_actions/services/cluster/job.py b/tests/functional/test_stacks_data/cluster_proto_wo_actions/services/cluster/job.py index d89c068a82..981fc867bb 100644 --- a/tests/functional/test_stacks_data/cluster_proto_wo_actions/services/cluster/job.py +++ b/tests/functional/test_stacks_data/cluster_proto_wo_actions/services/cluster/job.py @@ -10,10 +10,9 @@ # See the License for the specific language governing permissions and # limitations under the License. -from cm.logger import logger from cm.errors import AdcmEx - -from cm.models import Prototype, Action +from cm.logger import logger +from cm.models import Action, Prototype def task_generator(action, selector): diff --git a/tests/functional/test_states.py b/tests/functional/test_states.py index 8485db6fb0..ea10129a97 100644 --- a/tests/functional/test_states.py +++ b/tests/functional/test_states.py @@ -15,21 +15,20 @@ # todo add new DSL variant for job and multijob # pylint:disable=redefined-outer-name -from typing import Tuple, Callable +from typing import Callable, Tuple import allure from adcm_client.objects import ADCMClient, Cluster, Provider from adcm_pytest_plugin.steps.actions import ( run_cluster_action_and_assert_result, - run_service_action_and_assert_result, run_component_action_and_assert_result, - run_provider_action_and_assert_result, run_host_action_and_assert_result, + run_provider_action_and_assert_result, + run_service_action_and_assert_result, ) from adcm_pytest_plugin.utils import fixture_parametrized_by_data_subdirs from tests.functional.plugin_utils import build_objects_checker - ACTION_NAME = 'state_changing_action' ACTION_SET_MULTISTATE_NAME = "set_multistate" @@ -184,6 +183,7 @@ def test_cluster_multi_state_plus_states_after_action( Test cluster and multi states and states after action Before action add multi state that should be unset via action """ + object_to_be_changed, check_objects_multi_state_changed = cluster_and_multi_states_plus_states_checker run_cluster_action_and_assert_result(object_to_be_changed, ACTION_SET_MULTISTATE_NAME) with check_objects_multi_state_changed(sdk_client_fs, {object_to_be_changed}), allure.step( diff --git a/tests/functional/test_task_log_bulk_download.py b/tests/functional/test_task_log_bulk_download.py new file mode 100644 index 0000000000..98c7c5e973 --- /dev/null +++ b/tests/functional/test_task_log_bulk_download.py @@ -0,0 +1,398 @@ +# 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. + +"""Tests on downloading all job logs in task as one archive""" + +import re +import tarfile +from operator import methodcaller +from os import PathLike +from pathlib import Path +from typing import Callable, Collection, Dict, List, NamedTuple, Set, Union + +import allure +import pytest +from adcm_client.objects import ( + ADCM, + ADCMClient, + Cluster, + Component, + Host, + Provider, + Service, + Task, +) +from adcm_pytest_plugin.docker_utils import ADCM as ADCMTest +from adcm_pytest_plugin.utils import get_data_dir +from docker.models.containers import Container +from tests.functional.conftest import only_clean_adcm +from tests.functional.ldap_auth.utils import TEST_CONNECTION_ACTION +from tests.functional.tools import ( + AnyADCMObject, + ClusterRelatedObject, + ProviderRelatedObject, +) +from tests.library.assertions import sets_are_equal +from tests.library.utils import build_full_archive_name + +# pylint: disable=redefined-outer-name + +CLUSTER_NAME = "Cluster Name" +PROVIDER_NAME = "Provider Name" +CONTENT_DISPOSITION = "Content-Disposition" + +filename_regex = re.compile('filename="(.*)"') + + +class TaskLogInfo(NamedTuple): + """Helper for storing info about archive naming""" + + action_name: str + # action's "part" of archive name + action_in_archive_name: str + # jobs' parts of directory in archive name + # {job_id}-{job_archive_name} + jobs_in_archive: Set[str] + + +ACTION_NAME_MAP: Dict[str, TaskLogInfo] = { + tli.action_name: tli + for tli in ( + TaskLogInfo("without_display_name_simple", "withoutdisplaynamesimple", {"withoutdisplaynamesimple"}), + TaskLogInfo("without_display_name_s.mpl-x", "withoutdisplaynamesmplx", {"withoutdisplaynamesmplx"}), + TaskLogInfo("with_display_name_simple", "simple-action-display-name", {"simple-action-display-name"}), + TaskLogInfo( + "with_display_name_complex", + "very-cool-n4mefor-b3t-actn", + {"very-cool-n4mefor-b3t-actn"}, + ), + TaskLogInfo( + "complex", + "compl3x-task", + {"withoutdisplaynamesimple", "wth-diisplaay-n4m3", "ill-just-fail"}, + ), + ) +} +FS_RUN_DIR_FILES = { + "inventory.json", + "config.json", + "ansible-stderr.txt", + "ansible-stdout.txt", + "ansible.cfg", +} +FS_RUN_DIR_FILES_PY = { + "inventory.json", + "config.json", + "python-stderr.txt", + "python-stdout.txt", + "ansible.cfg", +} +DB_RUN_DIR_FILES = {"ansible-stderr.txt", "ansible-stdout.txt"} + +# !===== Utilities =====! + + +def build_host_archive_name(host: Host, task: Task, action_name_in_archive_name: str) -> str: + """Prepare expected name for host action's task""" + cleaned_fqdn = host.fqdn.replace("-", "").replace(".", "") + return f"{cleaned_fqdn}_{action_name_in_archive_name}_{task.id}" + + +def get_filenames_from_archive(archive: Path) -> List[str]: + """Extract names from an archive""" + with tarfile.open(archive) as tar: + return tar.getnames() + + +def get_unique_directory_names(names_in_archive: Collection[str]) -> Set[str]: + """Get unique names of directories extracted from archive names""" + return {n.split("/", maxsplit=1)[0] for n in names_in_archive} + + +def get_unique_directory_names_wo_job_id(names_in_archive: Collection[str]) -> Set[str]: + """Get unique names of directories extracted from archive names (task id is removed)""" + return {n.split("-", maxsplit=1)[1] for n in get_unique_directory_names(names_in_archive)} + + +def get_files_from_dir(dirname: str, names_in_archive: Collection[str]) -> Set[str]: + """Extract filenames that belong to a given directory""" + return {dir_and_file[-1] for n in names_in_archive if dirname in (dir_and_file := n.rsplit("/", maxsplit=1))[0]} + + +def _get_task_of(adcm_object: Union[ClusterRelatedObject, ProviderRelatedObject], client: ADCMClient) -> Task: + object_type = adcm_object.__class__.__name__.lower() + object_task = next( + filter( + lambda task: task.object_type == object_type and task.object_id == adcm_object.id, + map(methodcaller("task"), client.job_list()), + ), + None, + ) + if object_task is None: + raise RuntimeError(f"Suitable task not found for {adcm_object}") + return object_task + + +# !===== Steps and Checks =====! + + +def run_all_actions(adcm_object: AnyADCMObject) -> List[Task]: + """Run all actions on object""" + tasks = [] + for action in adcm_object.action_list(): + tasks.append(action.run()) + tasks[-1].wait() + return tasks + + +def check_archive_name(archive: Path, expected_name: str) -> None: + """Check archive file name""" + with allure.step(f"Check archive name is {expected_name}"): + assert ( + actual := archive.with_suffix("").stem + ) == expected_name, f"Incorrect archive name.\nExpected: {expected_name}\nActual: {actual}" + + +def check_job_directories( + filenames: List[str], + jobs_in_archive: Set[str], + dir_name_extractor: Callable[[List[str]], Set[str]] = get_unique_directory_names_wo_job_id, +) -> None: + """Check that archive contains directories of all jobs""" + with allure.step("Check that archive contains directories of all jobs"): + sets_are_equal( + dir_name_extractor(filenames), + jobs_in_archive, + "Incorrect job directory names in archive", + ) + + +def check_all_files_presented_in_all_directories( + filenames: List[str], jobs_in_archive: Set[str], expected_files: Set[str] +) -> None: + """ + Check that in each directory of an archive (job's directories) there are all required files (logs, configs, etc.) + """ + for dirname in jobs_in_archive: + with allure.step(f"Check content of directory {dirname} in an archive"): + sets_are_equal( + get_files_from_dir(dirname, filenames), + expected_files, + f"Incorrect files in '{dirname}' job's directory in archive", + ) + + +def check_archive_naming(adcm_object, task: Task, expected_files: Set[str], name_builder: Callable, tmpdir: PathLike): + """Check archive name, names of jobs' directories in it and filenames in all directories""" + with allure.step(f"Check task logs archive download from {adcm_object.__class__}'s action"): + archive: Path = task.download_logs(tmpdir) + archive_task_info = ACTION_NAME_MAP[task.action().name] + check_archive_name(archive, name_builder(adcm_object, task, archive_task_info.action_in_archive_name)) + filenames = get_filenames_from_archive(archive) + check_job_directories(filenames, archive_task_info.jobs_in_archive) + check_all_files_presented_in_all_directories(filenames, archive_task_info.jobs_in_archive, expected_files) + + +@allure.step("Remove downloaded archives") +def remove_archives(tmpdir: PathLike) -> None: + """Remove downloaded archives""" + directory = Path(tmpdir) + for archive in filter(lambda file: set(file.suffixes) == {"tar", "gz"}, directory.iterdir()): + archive.unlink() + + +@allure.step("Remove all task log directories from FS") +def remove_task_logs_from_fs(adcm_container: Container) -> None: + """Remove all task log directories from FS""" + exit_code, output = adcm_container.exec_run(["sh", "-c", "rm -r /adcm/data/run/*"]) + if exit_code != 0: + raise RuntimeError(f"Failed to remove task log directories from FS: {output.decode('utf-8')}") + + +# !===== Fixtures ======! + + +@pytest.fixture(params=["naming"]) +def cluster(request, sdk_client_fs) -> Cluster: + """Create cluster""" + return sdk_client_fs.upload_from_fs(get_data_dir(__file__, request.param, "cluster")).cluster_create(CLUSTER_NAME) + + +@pytest.fixture(params=["naming"]) +def provider(request, sdk_client_fs) -> Provider: + """Create provider""" + return sdk_client_fs.upload_from_fs(get_data_dir(__file__, request.param, "provider")).provider_create( + PROVIDER_NAME + ) + + +@pytest.fixture() +def _prepare_cluster_and_provider(cluster, provider) -> None: + cluster.service_add(name="service_proto_name") + provider.host_create("just-fqdn.domain") + + +# !===== Tests ======! + + +class TestArchiveNaming: + """Test task logs archive naming""" + + @only_clean_adcm + @pytest.mark.usefixtures("_prepare_cluster_and_provider") + def test_naming(self, cluster, provider, adcm_fs, sdk_client_fs, tmpdir): + """Test naming of task's archive and its contents""" + self._test_archiving_adcm_task(sdk_client_fs.adcm(), tmpdir) + service = cluster.service() + for adcm_object in (cluster, service, service.component(), provider): + self._test_archiving_general_object_task(adcm_object, tmpdir) + self._test_archiving_host_task(provider.host(), tmpdir) + remove_archives(tmpdir) + self._test_archiving_from_db(sdk_client_fs, adcm_fs.container, tmpdir) + remove_archives(tmpdir) + self._test_no_prototype(sdk_client_fs, tmpdir) + + @allure.step("Test ADCM's task archive naming") + def _test_archiving_adcm_task(self, adcm: ADCM, tmpdir: PathLike) -> None: + clean_action_name = "test-ldap-connection" + adcm.config_set_diff( + { + "attr": {"ldap_integration": {"active": True}}, + "config": { + "ldap_integration": {k: k for k in ("ldap_uri", "ldap_user", "ldap_password", "user_search_base")} + }, + } + ) + task = adcm.action(name=TEST_CONNECTION_ACTION).run() + task.wait() + with allure.step("Download task logs and check naming"): + archive = task.download_logs(tmpdir) + check_archive_name(archive, f"adcm_{clean_action_name}_{task.id}") + filenames = get_filenames_from_archive(archive) + check_job_directories(filenames, {clean_action_name}) + check_all_files_presented_in_all_directories( + filenames, {clean_action_name}, expected_files=FS_RUN_DIR_FILES_PY + ) + + def _test_archiving_general_object_task( + self, adcm_object: Union[Cluster, Service, Component, Provider], tmpdir: PathLike + ) -> None: + with allure.step(f"Test {adcm_object.__class__.__name__}'s task archive naming"): + for task in run_all_actions(adcm_object): + check_archive_naming(adcm_object, task, FS_RUN_DIR_FILES, build_full_archive_name, tmpdir) + + @allure.step("Test Host's task archive naming") + def _test_archiving_host_task(self, host: Host, tmpdir: PathLike) -> None: + for task in run_all_actions(host): + check_archive_naming(host, task, FS_RUN_DIR_FILES, build_host_archive_name, tmpdir) + + def _test_archiving_from_db(self, client: ADCMClient, adcm_container: Container, tmpdir: PathLike) -> None: + remove_task_logs_from_fs(adcm_container) + for adcm_object in ( + client.cluster(), + client.service(), + client.component(), + client.provider(), + ): + check_archive_naming( + adcm_object, + _get_task_of(adcm_object, client), + DB_RUN_DIR_FILES, + build_full_archive_name, + tmpdir, + ) + host = client.host() + check_archive_naming(host, _get_task_of(host, client), DB_RUN_DIR_FILES, build_host_archive_name, tmpdir) + + @allure.step("Test tasks without action's prototype") + def _test_no_prototype(self, client: ADCMClient, tmpdir: PathLike) -> None: + objects = ( + client.cluster(), + client.service(), + client.component(), + client.provider(), + client.host(), + ) + each_object_tasks: List[Task] = [_get_task_of(adcm_object, client) for adcm_object in objects] + with allure.step("Delete all bundles"): + client.host().delete() + client.provider().delete() + client.cluster().delete() + for bundle in client.bundle_list(): + bundle.delete() + with allure.step("Check logs download of task with no action prototype"): + for task in each_object_tasks: + job_ids = {str(job.id) for job in task.job_list()} + archive: Path = task.download_logs(tmpdir) + check_archive_name(archive, str(task.id)) + filenames = get_filenames_from_archive(archive) + check_job_directories(filenames, job_ids, get_unique_directory_names) + check_all_files_presented_in_all_directories(filenames, job_ids, DB_RUN_DIR_FILES) + + +class TestArchiveContent: + """Test content of an archive""" + + tmpdir: PathLike + adcm_fs: ADCMTest + + @pytest.fixture() + def _init(self, tmpdir, adcm_fs): + self.tmpdir = tmpdir + self.adcm_fs = adcm_fs + + @pytest.mark.parametrize("cluster", ["content"], indirect=True) + @pytest.mark.usefixtures("_prepare_cluster_and_provider", "_init") + def test_content(self, cluster, adcm_fs, tmpdir): + """Test content of archived files: from FS and DB""" + task = self.run_component_action(cluster) + self.check_logs_from_fs(task) + remove_archives(tmpdir) + remove_task_logs_from_fs(adcm_fs.container) + self.check_logs_from_db(task) + + def run_component_action(self, cluster: Cluster) -> Task: + """Run component action, wait until it's finished and return task""" + task = cluster.service().component().action().run() + task.wait() + return task + + def check_logs_from_fs(self, task: Task) -> None: + """Check that logs in archive is the same as log files on FS""" + archive: Path = task.download_logs(self.tmpdir) + with tarfile.open(archive, "r") as tar: + for name_in_archive in tar.getnames(): + with allure.step(f"Check that file {name_in_archive} in archive has the content from FS"): + file_from_archive = tar.extractfile(name_in_archive).read().decode("utf-8") + name = name_in_archive.split("/")[-1] + exit_code, output = self.adcm_fs.container.exec_run(["cat", f"/adcm/data/run/{task.id}/{name}"]) + file_from_fs = output.decode("utf-8") + if exit_code != 0: + raise ValueError(f"docker exec failed: {file_from_fs}") + if file_from_archive != file_from_fs: + allure.attach(file_from_archive, "File from archive") + allure.attach(file_from_fs, "File from FS") + raise AssertionError("Incorrect file content") + + def check_logs_from_db(self, task: Task) -> None: + """Check that logs in archive is the same as log records in DB""" + archive: Path = task.download_logs(self.tmpdir) + with tarfile.open(archive, "r") as tar: + for log_type in ("stdout", "stderr"): + with allure.step(f"Check archived log from DB of type {log_type}"): + filename = next(name for name in tar.getnames() if log_type in name) + file_from_archive = tar.extractfile(filename).read().decode("utf-8") + log_from_db = task.job().log(type=log_type).content + if file_from_archive != log_from_db: + allure.attach(file_from_archive, "File from archive") + allure.attach(log_from_db, "Log from DB") + raise AssertionError("Incorrect file content") diff --git a/tests/functional/test_task_log_bulk_download_data/content/cluster/actions.yaml b/tests/functional/test_task_log_bulk_download_data/content/cluster/actions.yaml new file mode 100644 index 0000000000..4338f4b123 --- /dev/null +++ b/tests/functional/test_task_log_bulk_download_data/content/cluster/actions.yaml @@ -0,0 +1,32 @@ +# 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. + +--- +- name: Write and check + hosts: localhost + connection: local + gather_facts: no + + tasks: + - name: Just write something + debug: + msg: "Comparison" + - name: Check + adcm_check: + title: "Check" + result: yes + group_title: "Name of group check." + group_success_msg: "Group success" + group_fail_msg: "Group fail" + msg: "Description of check or results of check. Required, if no ‘success_msg’ and ‘fail_msg’ fields" + success_msg: "Task success" + fail_msg: "Task fail" diff --git a/tests/functional/test_task_log_bulk_download_data/content/cluster/config.yaml b/tests/functional/test_task_log_bulk_download_data/content/cluster/config.yaml new file mode 100644 index 0000000000..6ae2ca2d65 --- /dev/null +++ b/tests/functional/test_task_log_bulk_download_data/content/cluster/config.yaml @@ -0,0 +1,30 @@ +# 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. + +- type: cluster + name: cluster_proto_name + version: 3 + +- type: service + name: service_proto_name + version: 3.4 + + + components: + component_proto_name: + actions: &actions + action_with_check: + type: job + script_type: ansible + script: ./actions.yaml + states: + available: any diff --git a/tests/functional/test_task_log_bulk_download_data/naming/cluster/actions.yaml b/tests/functional/test_task_log_bulk_download_data/naming/cluster/actions.yaml new file mode 100644 index 0000000000..2cbd6ebd4b --- /dev/null +++ b/tests/functional/test_task_log_bulk_download_data/naming/cluster/actions.yaml @@ -0,0 +1,22 @@ +# 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. + +--- +- name: Just debug + hosts: localhost + connection: local + gather_facts: no + + tasks: + - name: Just write something + debug: + msg: "Comparison" diff --git a/tests/functional/test_task_log_bulk_download_data/naming/cluster/config.yaml b/tests/functional/test_task_log_bulk_download_data/naming/cluster/config.yaml new file mode 100644 index 0000000000..7873548e18 --- /dev/null +++ b/tests/functional/test_task_log_bulk_download_data/naming/cluster/config.yaml @@ -0,0 +1,58 @@ +# 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. + +- type: cluster + name: cluster_proto_name + version: 3 + + actions: &actions + without_display_name_simple: &job + type: job + script_type: ansible + script: ./actions.yaml + states: + available: any + without_display_name_s.mpl-x: *job + with_display_name_simple: + <<: *job + display_name: Simple Action Display Name + with_display_name_complex: + <<: *job + display_name: Very coo_l N4mE-For / b3$T act!.n + complex: + type: task + display_name: Compl3X Task! + scripts: + - &multijob + name: without_display_name_simple + script_type: ansible + script: ./actions.yaml + - <<: *multijob + name: with_display_name_complex + display_name: W!th Diisplaay N4m3 + - <<: *multijob + name: illfail + display_name: I'll just fail + script: ./not-exist.yaml + states: + available: any + +- type: service + name: service_proto_name + version: 3.4 + + actions: *actions + + components: + component_proto_name: + actions: *actions + diff --git a/tests/functional/test_task_log_bulk_download_data/naming/provider/actions.yaml b/tests/functional/test_task_log_bulk_download_data/naming/provider/actions.yaml new file mode 100644 index 0000000000..2cbd6ebd4b --- /dev/null +++ b/tests/functional/test_task_log_bulk_download_data/naming/provider/actions.yaml @@ -0,0 +1,22 @@ +# 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. + +--- +- name: Just debug + hosts: localhost + connection: local + gather_facts: no + + tasks: + - name: Just write something + debug: + msg: "Comparison" diff --git a/tests/functional/test_task_log_bulk_download_data/naming/provider/config.yaml b/tests/functional/test_task_log_bulk_download_data/naming/provider/config.yaml new file mode 100644 index 0000000000..af76623be0 --- /dev/null +++ b/tests/functional/test_task_log_bulk_download_data/naming/provider/config.yaml @@ -0,0 +1,53 @@ +# 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. + +- type: provider + name: provider_proto_name + version: 3 + + actions: &actions + without_display_name_simple: &job + type: job + script_type: ansible + script: ./actions.yaml + states: + available: any + without_display_name_s.mpl-x: *job + with_display_name_simple: + <<: *job + display_name: Simple Action Display Name + with_display_name_complex: + <<: *job + display_name: Very coo_l N4mE-For / b3$T act!.n + complex: + type: task + display_name: Compl3X Task! + scripts: + - &multijob + name: without_display_name_simple + script_type: ansible + script: ./actions.yaml + - <<: *multijob + name: with_display_name_complex + display_name: W!th Diisplaay N4m3 + - <<: *multijob + name: illfail + display_name: I'll just fail + script: ./not-exist.yaml + states: + available: any + +- type: host + version: 3.2 + name: host_proto_name + + actions: *actions diff --git a/tests/functional/test_upgrade_actions.py b/tests/functional/test_upgrade_actions.py index 6157a9d164..f6f5a8d5d2 100644 --- a/tests/functional/test_upgrade_actions.py +++ b/tests/functional/test_upgrade_actions.py @@ -17,25 +17,36 @@ import json import os from pathlib import Path -from typing import Set, Tuple, Optional, Collection +from typing import Collection, Optional, Set, Tuple import allure import pytest import yaml -from adcm_client.objects import Cluster, ADCMClient, Bundle, Host, Component, Service -from adcm_pytest_plugin.docker_utils import get_file_from_container, ADCM -from adcm_pytest_plugin.steps.actions import run_cluster_action_and_assert_result, wait_for_task_and_assert_result -from adcm_pytest_plugin.utils import get_data_dir, catch_failed, parametrize_by_data_subdirs, random_string +from adcm_client.objects import ADCMClient, Bundle, Cluster, Component, Host, Service +from adcm_pytest_plugin.docker_utils import ADCM, get_file_from_container +from adcm_pytest_plugin.steps.actions import ( + run_cluster_action_and_assert_result, + wait_for_task_and_assert_result, +) +from adcm_pytest_plugin.utils import ( + catch_failed, + get_data_dir, + parametrize_by_data_subdirs, + random_string, +) from coreapi.exceptions import ErrorMessage - from tests.functional.conftest import only_clean_adcm from tests.functional.tools import build_hc_for_hc_acl_action, get_inventory_file -from tests.library.assertions import sets_are_equal, expect_api_error, expect_no_api_error +from tests.library.assertions import ( + expect_api_error, + expect_no_api_error, + sets_are_equal, +) from tests.library.errorcodes import ( - INVALID_UPGRADE_DEFINITION, - INVALID_OBJECT_DEFINITION, - INVALID_ACTION_DEFINITION, COMPONENT_CONSTRAINT_ERROR, + INVALID_ACTION_DEFINITION, + INVALID_OBJECT_DEFINITION, + INVALID_UPGRADE_DEFINITION, ) # pylint: disable=redefined-outer-name @@ -265,7 +276,8 @@ def test_successful_upgrade_with_content_change(self, sdk_client_fs, old_cluster for job_name in ('before_switch', 'after_switch'): job = next( filter( - lambda x: x.display_name == job_name, sdk_client_fs.job_list() # pylint: disable=cell-var-from-loop + lambda x: x.display_name == job_name, # pylint: disable=cell-var-from-loop + sdk_client_fs.job_list(), ) ) assert expected_message in job.log().content, f'"{expected_message}" should be in log' @@ -359,9 +371,13 @@ def _check_inventories_of_hc_acl_upgrade(self, adcm: ADCM, host_1, host_2): self._check_host_is_in_group(host_1, second_component_group, groups) self._check_host_is_in_group(host_1, some_component_group, groups) - # pylint: disable=too-many-arguments def _upgrade_to_newly_uploaded_version( - self, client, old_cluster, upgrade_name, upgrade_config, new_bundle_dirs=('successful', 'new') + self, + client, + old_cluster, + upgrade_name, + upgrade_config, + new_bundle_dirs=('successful', 'new'), ): with allure.step('Upload new version of cluster bundle'): new_bundle = client.upload_from_fs(get_data_dir(__file__, *new_bundle_dirs)) @@ -391,7 +407,6 @@ def _check_host_is_in_group(host, group_name, groups): ), f'Host {host.fqdn} should be in group {group_name}, but not found in: {hosts}' -# pylint: disable-next=too-few-public-methods class FailedUploadMixin: """Useful methods for upload failures tests""" @@ -499,7 +514,9 @@ class TestUpgradeWithHCFailures(FailedUploadMixin): """Test upgrades failures with `hc_acl` in upgrade""" @pytest.mark.parametrize( - 'upgrade_name', ['fail after switch', 'fail on first action after switch'], ids=lambda x: x.replace(' ', '_') + 'upgrade_name', + ['fail after switch', 'fail on first action after switch'], + ids=lambda x: x.replace(' ', '_'), ) def test_hc_acl_fail_after_switch(self, upgrade_name: str, sdk_client_fs, old_cluster, two_hosts): """ @@ -540,7 +557,6 @@ def test_hc_acl_fail_before_switch(self, sdk_client_fs, old_cluster, two_hosts): sets_are_equal(actual_hc, expected_hc, 'The hostcomponent from before the upgrade was expected') -# pylint: disable-next=too-few-public-methods class TestUpgradeActionRelations: """Test cases when upgrade action""" @@ -661,7 +677,7 @@ def hc_acl_block(self, request) -> dict: ] } - @pytest.fixture() # pylint: disable-next=too-many-arguments + @pytest.fixture() def upload_bundles( self, request, sdk_client_fs, tmpdir, with_hc_in_upgrade, hc_acl_block, dummy_action_content ) -> Tuple[Bundle, Bundle]: @@ -722,7 +738,7 @@ def cluster_with_component(self, upload_bundles) -> Tuple[Cluster, Component]: @allure.title('Create hosts and set hostcomponent') @pytest.fixture() - def set_hc(self, request, cluster_with_component, generic_provider) -> None: + def _set_hc(self, request, cluster_with_component, generic_provider) -> None: """Set hostcomponent based on given component on host / hosts in cluster amount""" # total amount of hosts shouldn't be 0, it'll conflict with dummy component if isinstance(request.param, int): @@ -743,7 +759,7 @@ def set_hc(self, request, cluster_with_component, generic_provider) -> None: # wrap it in something readable @pytest.mark.parametrize('with_hc_in_upgrade', [True, False], indirect=True, ids=lambda i: f'with_hc_acl_{i}') @pytest.mark.parametrize( - ('upload_bundles', 'set_hc'), + ('upload_bundles', '_set_hc'), [ # from many hosts to 1 ((['+'], [1]), 2), @@ -768,7 +784,7 @@ def set_hc(self, request, cluster_with_component, generic_provider) -> None: indirect=True, ids=_set_ids_for_upload_bundles_set_hc, ) - @pytest.mark.usefixtures('set_hc', 'upload_bundles') + @pytest.mark.usefixtures('_set_hc', 'upload_bundles') def test_incorrect_hc_in_upgrade_with_actions(self, sdk_client_fs, cluster_with_component, with_hc_in_upgrade): """ Test that when incorrect for new constraints HC is set, @@ -805,12 +821,12 @@ def test_incorrect_hc_in_upgrade_with_actions(self, sdk_client_fs, cluster_with_ ], ) @pytest.mark.parametrize( - ('upload_bundles', 'set_hc'), + ('upload_bundles', '_set_hc'), [(([1], []), 1)], indirect=True, ids=_set_ids_for_upload_bundles_set_hc, ) - @pytest.mark.usefixtures('set_hc', 'upload_bundles', 'with_hc_in_upgrade') + @pytest.mark.usefixtures('_set_hc', 'upload_bundles', 'with_hc_in_upgrade') def test_constraint_removed(self, cluster_with_component): """Test constraint is removed in new bundle version""" cluster, component = cluster_with_component diff --git a/tests/functional/test_upgrade_actions_data/successful/inventory_1.json b/tests/functional/test_upgrade_actions_data/successful/inventory_1.json index 60310f6ac1..2542e1d865 100644 --- a/tests/functional/test_upgrade_actions_data/successful/inventory_1.json +++ b/tests/functional/test_upgrade_actions_data/successful/inventory_1.json @@ -25,6 +25,8 @@ "version": "2.3", "state": "created", "multi_state": [], + "maintenance_mode": false, + "display_name": "test_service", "config": { "somestring": null, "someint": 12 @@ -36,7 +38,9 @@ "someint": 12 }, "state": "created", - "multi_state": [] + "multi_state": [], + "maintenance_mode": false, + "display_name": "test_component" } } } diff --git a/tests/functional/test_upgrade_actions_data/successful/inventory_3.json b/tests/functional/test_upgrade_actions_data/successful/inventory_3.json index 211447d575..96b4ab2ab1 100644 --- a/tests/functional/test_upgrade_actions_data/successful/inventory_3.json +++ b/tests/functional/test_upgrade_actions_data/successful/inventory_3.json @@ -30,6 +30,8 @@ "version": "2.3", "state": "created", "multi_state": [], + "maintenance_mode": false, + "display_name": "test_service", "config": { "somestring": null, "someint": 12, @@ -41,6 +43,8 @@ }, "test_component": { "component_id": 1, + "maintenance_mode": false, + "display_name": "test_component", "config": { "somestring": null, "someint": 12, diff --git a/tests/functional/test_upgrade_cluster.py b/tests/functional/test_upgrade_cluster.py index e2de7a71bd..32919a3bc2 100644 --- a/tests/functional/test_upgrade_cluster.py +++ b/tests/functional/test_upgrade_cluster.py @@ -15,14 +15,11 @@ """Tests for cluster upgrade""" import allure -import coreapi import pytest -from coreapi.exceptions import ErrorMessage from adcm_client.objects import ADCMClient, Bundle, Cluster, Service -from adcm_pytest_plugin.utils import get_data_dir, catch_failed from adcm_pytest_plugin.docker_utils import ADCM - -from tests.library.errorcodes import UPGRADE_ERROR +from adcm_pytest_plugin.utils import catch_failed, get_data_dir +from coreapi.exceptions import ErrorMessage from tests.functional.tools import BEFORE_UPGRADE_DEFAULT_STATE, get_object_represent @@ -149,27 +146,40 @@ def test_upgrade_cluster_with_config_groups(sdk_client_fs): with allure.step('Assert that configs save success after upgrade'): cluster.config_set( { - "attr": {"activatable_group_with_ro": {"active": True}, "activatable_group": {"active": True}}, + "attr": { + "activatable_group_with_ro": {"active": True}, + "activatable_group": {"active": True}, + }, "config": { **cluster.config(), "activatable_group_with_ro": {"readonly-key": "value"}, - "activatable_group": {"required": 10, "writable-key": "value", "readonly-key": "value"}, + "activatable_group": { + "required": 10, + "writable-key": "value", + "readonly-key": "value", + }, }, } ) service.config_set( { - "attr": {"activatable_group_with_ro": {"active": True}, "activatable_group": {"active": True}}, + "attr": { + "activatable_group_with_ro": {"active": True}, + "activatable_group": {"active": True}, + }, "config": { **service.config(), "activatable_group_with_ro": {"readonly-key": "value"}, - "activatable_group": {"required": 10, "writable-key": "value", "readonly-key": "value"}, + "activatable_group": { + "required": 10, + "writable-key": "value", + "readonly-key": "value", + }, }, } ) -@pytest.mark.xfail(reason="https://tracker.yandex.ru/ADCM-3033") def test_cannot_upgrade_with_state(sdk_client_fs: ADCMClient, old_bundle): """Test upgrade should not be available ant stater""" with allure.step('Create upgradable cluster with unsupported state'): @@ -179,11 +189,7 @@ def test_cannot_upgrade_with_state(sdk_client_fs: ADCMClient, old_bundle): upgr = cluster.upgrade(name='upgrade to 1.6') upgr.do() cluster.reread() - upgr = cluster.upgrade(name='upgrade 2') - with pytest.raises(coreapi.exceptions.ErrorMessage) as e: - upgr.do() - with allure.step('Check error: cluster state is not in available states list'): - UPGRADE_ERROR.equal(e, 'cluster state', 'is not in available states list') + assert len(cluster.upgrade_list()) == 0, "No upgrade should be available" @pytest.mark.usefixtures("upgradable_bundle") @@ -227,7 +233,10 @@ def new_bundle(self, sdk_client_fs) -> Bundle: """Upload new cluster bundle""" return sdk_client_fs.upload_from_fs(get_data_dir(__file__, self._DIR, 'new')) - @allure.issue(name='Component miss config after upgrade', url='https://arenadata.atlassian.net/browse/ADCM-2376') + @allure.issue( + name='Component miss config after upgrade', + url='https://arenadata.atlassian.net/browse/ADCM-2376', + ) @pytest.mark.usefixtures('new_bundle') def test_upgrade_with_components(self, adcm_fs, sdk_client_fs, old_cluster): """ diff --git a/tests/functional/test_upgrade_cluster_imports.py b/tests/functional/test_upgrade_cluster_imports.py index 2af38b08e4..19c5a0c3fa 100644 --- a/tests/functional/test_upgrade_cluster_imports.py +++ b/tests/functional/test_upgrade_cluster_imports.py @@ -17,7 +17,6 @@ import pytest from adcm_client.objects import ADCMClient from adcm_pytest_plugin.utils import get_data_dir, parametrize_by_data_subdirs - from tests.library import errorcodes as err diff --git a/tests/functional/test_upgrade_cluster_imports_data/config_generator_import.py b/tests/functional/test_upgrade_cluster_imports_data/config_generator_import.py index c10e605512..46c45d6f51 100644 --- a/tests/functional/test_upgrade_cluster_imports_data/config_generator_import.py +++ b/tests/functional/test_upgrade_cluster_imports_data/config_generator_import.py @@ -1,3 +1,14 @@ +# 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 os SERVICE_VERSIONS = (('service-less', '2.3', '2.4'), ("service-greater", '1', '2')) diff --git a/tests/functional/test_upgrade_cluster_imports_data/config_generator_strict_import.py b/tests/functional/test_upgrade_cluster_imports_data/config_generator_strict_import.py index e0dcb200ac..95007c2293 100644 --- a/tests/functional/test_upgrade_cluster_imports_data/config_generator_strict_import.py +++ b/tests/functional/test_upgrade_cluster_imports_data/config_generator_strict_import.py @@ -1,3 +1,14 @@ +# 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 os SERVICE_VERSIONS = ( diff --git a/tests/functional/test_upgrade_hostprovider.py b/tests/functional/test_upgrade_hostprovider.py index 3651f9b809..37188720a5 100644 --- a/tests/functional/test_upgrade_hostprovider.py +++ b/tests/functional/test_upgrade_hostprovider.py @@ -13,13 +13,9 @@ """Tests for hostprovider update""" import allure -import coreapi -import pytest from adcm_client.objects import ADCMClient from adcm_pytest_plugin.utils import get_data_dir -from tests.library.errorcodes import UPGRADE_ERROR - @allure.step('Create host') def create_host(hostprovider): @@ -161,7 +157,6 @@ def test_change_config(sdk_client_fs: ADCMClient): assert host_config_before[key] == host_config_after[key] -@pytest.mark.xfail(reason="https://tracker.yandex.ru/ADCM-3033") def test_cannot_upgrade_with_state(sdk_client_fs: ADCMClient): """Upgrade hostprovider from unsupported state""" with allure.step('Create hostprovider with unsupported state'): @@ -172,8 +167,4 @@ def test_cannot_upgrade_with_state(sdk_client_fs: ADCMClient): upgr = hostprovider.upgrade(name='upgrade to 2.0') upgr.do() hostprovider.reread() - upgr = hostprovider.upgrade(name='upgrade 2') - with pytest.raises(coreapi.exceptions.ErrorMessage) as e: - upgr.do() - with allure.step('Check error: provider state is not in available states list'): - UPGRADE_ERROR.equal(e, 'provider state', 'is not in available states list') + assert len(hostprovider.upgrade_list()) == 0, "No upgrade should be available at new state" diff --git a/tests/functional/test_users.py b/tests/functional/test_users.py index 3f6c28e668..3c7ef230c6 100644 --- a/tests/functional/test_users.py +++ b/tests/functional/test_users.py @@ -22,11 +22,18 @@ from adcm_pytest_plugin.docker_utils import ADCM from adcm_pytest_plugin.steps.actions import wait_for_task_and_assert_result from docker.models.containers import Container - from tests.functional.audit.conftest import make_auth_header from tests.functional.conftest import only_clean_adcm -from tests.functional.ldap_auth.utils import get_ldap_user_from_adcm, login_should_fail, login_should_succeed -from tests.functional.tools import check_user_is_active, check_user_is_deactivated, run_ldap_sync +from tests.functional.ldap_auth.utils import ( + get_ldap_user_from_adcm, + login_should_fail, + login_should_succeed, +) +from tests.functional.tools import ( + check_user_is_active, + check_user_is_deactivated, + run_ldap_sync, +) from tests.library.ldap_interactions import LDAPEntityManager # pylint: disable=redefined-outer-name @@ -69,7 +76,7 @@ def ldap_user(sdk_client_fs, created_ldap_user, configure_adcm_ldap_ad) -> User: @only_clean_adcm @pytest.mark.ldap() -@pytest.mark.usefixtures("configure_adcm_ldap_ad") # pylint: disable-next=too-many-arguments +@pytest.mark.usefixtures("configure_adcm_ldap_ad") def test_users_deactivation( adcm_user: User, ldap_user: User, diff --git a/tests/functional/test_yet_another_tests.py b/tests/functional/test_yet_another_tests.py index 41af5a3dbe..d782e7ac1a 100644 --- a/tests/functional/test_yet_another_tests.py +++ b/tests/functional/test_yet_another_tests.py @@ -15,10 +15,8 @@ import allure import coreapi import pytest - -from adcm_pytest_plugin import utils from adcm_client.packer.bundle_build import build - +from adcm_pytest_plugin import utils from tests.library.errorcodes import BUNDLE_ERROR, INVALID_OBJECT_DEFINITION testcases = ["cluster", "host"] diff --git a/tests/functional/tools.py b/tests/functional/tools.py index eb5a63ecfa..0fa51b9c0f 100644 --- a/tests/functional/tools.py +++ b/tests/functional/tools.py @@ -117,7 +117,9 @@ def get_object_represent(obj: AnyADCMObject) -> str: def create_config_group_and_add_host( - group_name: str, object_with_group: Union[ClusterRelatedObject, Provider], *hosts: Iterable[Host] + group_name: str, + object_with_group: Union[ClusterRelatedObject, Provider], + *hosts: Iterable[Host], ) -> GroupConfig: """Create config group with given name and add all passed hosts""" with allure.step(f"Create config group '{group_name}' and add hosts: {' '.join((h.fqdn for h in hosts))}"): diff --git a/tests/library/adcm_websockets.py b/tests/library/adcm_websockets.py index afbab51614..8c27a4e654 100644 --- a/tests/library/adcm_websockets.py +++ b/tests/library/adcm_websockets.py @@ -16,13 +16,12 @@ import json from datetime import datetime from pprint import pformat -from typing import Any, Collection, NamedTuple, Dict, Optional, Tuple, List +from typing import Any, Collection, Dict, List, NamedTuple, Optional, Tuple import allure from adcm_pytest_plugin.utils import catch_failed -from websockets.legacy.client import WebSocketClientProtocol - from tests.library.types import WaitTimeout +from websockets.legacy.client import WebSocketClientProtocol WSMessageData = Dict[str, Any] MismatchReason = Optional[str] @@ -95,7 +94,10 @@ async def get_message(self, timeout: Optional[WaitTimeout] = None) -> WSMessageD @allure.step('Get up to {max_messages} messages') async def get_messages( - self, max_messages: int, single_msg_timeout: WaitTimeout = 1, break_on_first_fail: bool = True + self, + max_messages: int, + single_msg_timeout: WaitTimeout = 1, + break_on_first_fail: bool = True, ) -> List[WSMessageData]: """ Get messages until `max_messages` is reached @@ -227,10 +229,14 @@ def check_message_is( return allure.attach( - pformat(expected), name='Expected message fields to be', attachment_type=allure.attachment_type.TEXT + pformat(expected), + name='Expected message fields to be', + attachment_type=allure.attachment_type.TEXT, ) allure.attach( - pformat(message_object), name='Actual message fields', attachment_type=allure.attachment_type.TEXT + pformat(message_object), + name='Actual message fields', + attachment_type=allure.attachment_type.TEXT, ) raise AssertionError(f'WS message is incorrect: {explanation}') @@ -266,7 +272,9 @@ def check_message_is_not( attachment_type=allure.attachment_type.TEXT, ) allure.attach( - pformat(message_object), name='Actual message fields', attachment_type=allure.attachment_type.TEXT + pformat(message_object), + name='Actual message fields', + attachment_type=allure.attachment_type.TEXT, ) raise AssertionError('WS message should not match.\nCheck attachments for more details.') @@ -291,7 +299,9 @@ def _check_messages_unordered_presence(self, expected: Tuple[EventMessage], mess if len(missing_messages) == 0: return allure.attach( - pformat(missing_messages), name='Missing WS messages', attachment_type=allure.attachment_type.TEXT + pformat(missing_messages), + name='Missing WS messages', + attachment_type=allure.attachment_type.TEXT, ) allure.attach(pformat(messages), name='Searched messages', attachment_type=allure.attachment_type.TEXT) raise AssertionError('Some of the expected WS messages were missing, check attachments for more details') diff --git a/tests/library/api/__init__.py b/tests/library/api/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/library/api/client.py b/tests/library/api/client.py new file mode 100644 index 0000000000..4054a969cc --- /dev/null +++ b/tests/library/api/client.py @@ -0,0 +1,31 @@ +# 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 contextlib import contextmanager + +from tests.library.api.core import Requester +from tests.library.api.nodes import ComponentNode, HostNode, ServiceNode + + +class APIClient(Requester): + def __init__(self, url: str, credentials: dict[str, str]): + super().__init__(f"{url}/api/v1") + self.auth_header = self.get_auth_header(credentials) + self.host = HostNode(self) + self.service = ServiceNode(self) + self.component = ComponentNode(self) + + @contextmanager + def logged_as_another_user(self, *, token: str): + original_header = {**self.auth_header} + self.auth_header = self._build_header(token) + yield + self.auth_header = original_header diff --git a/tests/library/api/core.py b/tests/library/api/core.py new file mode 100644 index 0000000000..3d4da30786 --- /dev/null +++ b/tests/library/api/core.py @@ -0,0 +1,66 @@ +# 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 typing import Literal + +from requests import Response, post + + +class RequestResult: + def __init__(self, response: Response): + self.response = response + self._data = None + + @property + def data(self): + if self._data is None: + self._data = self.response.json() + return self._data + + def __getitem__(self, item): + return self.data[item] + + def check_code(self, expected: int) -> "RequestResult": + assert ( + self.response.status_code == expected + ), f"Incorrect request status code.\nActual: {self.response.status_code}\nExpected: {expected}" + return self + + +class Requester: + def __init__(self, url: str): + self.base_url = url + self.auth_header: dict[Literal["Authorization"], str] = {} + + @staticmethod + def _build_header(token: str) -> dict[str, str]: + return {"Authorization": f"Token {token}"} + + def get_auth_header(self, credentials: dict[str, str]) -> dict[str, str]: + token = self.post("token", json=credentials, authorized=False)["token"] + return self._build_header(token) + + def post(self, *path: str, authorized: bool = True, **request_kwargs) -> RequestResult: + if authorized: + if not self.auth_header: + raise RuntimeError("Authorization header is not initialized") + if "headers" not in request_kwargs: + request_kwargs["headers"] = self.auth_header + elif "Authorization" not in request_kwargs["headers"]: + request_kwargs["headers"]["Authorization"] = self.auth_header["Authorization"] + + return RequestResult(post(f"{'/'.join((self.base_url, *path))}/", **request_kwargs)) + + +class Node: + def __init__(self, requester: Requester): + self._requester = requester diff --git a/tests/library/api/nodes.py b/tests/library/api/nodes.py new file mode 100644 index 0000000000..eb64fb09d4 --- /dev/null +++ b/tests/library/api/nodes.py @@ -0,0 +1,33 @@ +# 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 typing import Literal + +from tests.library.api.core import Node, RequestResult + + +class HostNode(Node): + def change_maintenance_mode(self, host_id: int, value: Literal["ON", "OFF"]) -> RequestResult: + return self._requester.post("host", str(host_id), "maintenance-mode", json={"maintenance_mode": value}) + + +class ServiceNode(Node): + def change_maintenance_mode(self, service_id: int, value: Literal["ON", "OFF"]) -> RequestResult: + return self._requester.post("service", str(service_id), "maintenance-mode", json={"maintenance_mode": value}) + + +class ComponentNode(Node): + def change_maintenance_mode(self, component_id: int, value: Literal["ON", "OFF"]) -> RequestResult: + return self._requester.post( + "component", str(component_id), "maintenance-mode", json={"maintenance_mode": value} + ) diff --git a/tests/library/assertions.py b/tests/library/assertions.py index fc5e0213c0..5b24820187 100644 --- a/tests/library/assertions.py +++ b/tests/library/assertions.py @@ -13,13 +13,12 @@ """Various "rich" checks for common assertions""" import json import pprint -from typing import Callable, Union, Collection, TypeVar, Optional +from typing import Callable, Collection, Optional, TypeVar, Union import allure from adcm_client.wrappers.api import ADCMApiError from adcm_pytest_plugin.utils import catch_failed from coreapi.exceptions import ErrorMessage - from tests.library.errorcodes import ADCMError T = TypeVar('T') @@ -122,9 +121,15 @@ def dicts_are_equal(actual: dict, expected: dict, message: Union[str, Callable] if actual == expected: return - allure.attach(json.dumps(actual, indent=2), name='Actual dictionary', attachment_type=allure.attachment_type.JSON) allure.attach( - json.dumps(expected, indent=2), name='Expected dictionary', attachment_type=allure.attachment_type.JSON + json.dumps(actual, indent=2), + name='Actual dictionary', + attachment_type=allure.attachment_type.JSON, + ) + allure.attach( + json.dumps(expected, indent=2), + name='Expected dictionary', + attachment_type=allure.attachment_type.JSON, ) message = message if not callable(message) else message(**kwargs) if not message: diff --git a/tests/library/audit/__init__.py b/tests/library/audit/__init__.py index e69de29bb2..824dd6c8fe 100644 --- a/tests/library/audit/__init__.py +++ b/tests/library/audit/__init__.py @@ -0,0 +1,11 @@ +# 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. diff --git a/tests/library/audit/checkers.py b/tests/library/audit/checkers.py index ea13511049..be090c18e0 100644 --- a/tests/library/audit/checkers.py +++ b/tests/library/audit/checkers.py @@ -19,7 +19,6 @@ import allure from adcm_client.audit import AuditOperation from adcm_client.objects import ADCMClient - from tests.library.audit.operations import Operation, convert_to_operations from tests.library.audit.readers import ParsedAuditLog @@ -76,7 +75,10 @@ def check(self, audit_records: List[AuditOperation]): sorted_audit_records = sorted(audit_records, key=lambda rec: rec.operation_time) total_amount = len(sorted_audit_records) operations = convert_to_operations( - self._raw_operations, self._operation_defaults.username, self._operation_defaults.result, self._user_map + self._raw_operations, + self._operation_defaults.username, + self._operation_defaults.result, + self._user_map, ) first_expected_operation = operations[0] try: @@ -105,7 +107,10 @@ def check(self, audit_records: List[AuditOperation]): last_found_ind = total_amount - len(suitable_records) - 1 def set_user_map( - self, client_: Optional[ADCMClient] = None, user_id_map_: Optional[Dict[str, int]] = None, **user_ids: int + self, + client_: Optional[ADCMClient] = None, + user_id_map_: Optional[Dict[str, int]] = None, + **user_ids: int, ) -> None: """ When there are custom users in the scenario, you should use this method to provide full user list. diff --git a/tests/library/audit/operations.py b/tests/library/audit/operations.py index 19309ee899..0671eff587 100644 --- a/tests/library/audit/operations.py +++ b/tests/library/audit/operations.py @@ -13,7 +13,17 @@ """Defines basic entities like Operation and NamedOperation to work with audit log scenarios""" from dataclasses import dataclass, field, fields -from typing import ClassVar, Collection, Dict, List, Literal, NamedTuple, Optional, Tuple, Union +from typing import ( + ClassVar, + Collection, + Dict, + List, + Literal, + NamedTuple, + Optional, + Tuple, + Union, +) from adcm_client.audit import AuditOperation, ObjectType, OperationResult, OperationType @@ -104,6 +114,8 @@ def resolve(self, object_type: ObjectType, **format_args) -> str: ObjectType.ROLE, ObjectType.POLICY, ObjectType.CLUSTER, + ObjectType.SERVICE, + ObjectType.COMPONENT, ObjectType.HOST, ), ), @@ -147,7 +159,11 @@ def resolve(self, object_type: ObjectType, **format_args) -> str: # Upgrades NamedOperation('do-upgrade', 'Upgraded to {name}', (ObjectType.CLUSTER, ObjectType.PROVIDER)), NamedOperation('launch-upgrade', '{name} upgrade launched', (ObjectType.CLUSTER, ObjectType.PROVIDER)), - NamedOperation('complete-upgrade', '{name} upgrade completed', (ObjectType.CLUSTER, ObjectType.PROVIDER)), + NamedOperation( + 'complete-upgrade', + '{name} upgrade completed', + (ObjectType.CLUSTER, ObjectType.PROVIDER), + ), ) } @@ -293,10 +309,16 @@ def _nullify_user(self) -> None: def convert_to_operations( - raw_operations: List[dict], default_username: str, default_result: str, username_id_map: Dict[str, int] + raw_operations: List[dict], + default_username: str, + default_result: str, + username_id_map: Dict[str, int], ) -> List[Operation]: """Convert parsed from file audit operations to Operation objects""" - required_users = {default_username, *[op['username'] for op in raw_operations if 'username' in op]} + required_users = { + default_username, + *[op['username'] for op in raw_operations if 'username' in op], + } _check_all_users_are_presented(required_users, username_id_map) operations = [] for data in raw_operations: diff --git a/tests/library/audit/readers.py b/tests/library/audit/readers.py index aac5d98bb2..71235fb81b 100644 --- a/tests/library/audit/readers.py +++ b/tests/library/audit/readers.py @@ -91,6 +91,8 @@ def _read(self, filename: str, context: Dict[str, Union[str, int]]) -> dict: data = yaml.safe_load(rendered_file_content) jsonschema.validate(data, _get_schema()) allure.attach( - json.dumps(data, indent=2), name='Audit Log scenario', attachment_type=allure.attachment_type.JSON + json.dumps(data, indent=2), + name='Audit Log scenario', + attachment_type=allure.attachment_type.JSON, ) return data diff --git a/tests/library/conditional_retriever.py b/tests/library/conditional_retriever.py index 7fea7b94b9..46d13a9dd3 100644 --- a/tests/library/conditional_retriever.py +++ b/tests/library/conditional_retriever.py @@ -16,7 +16,18 @@ """ import sys import traceback -from typing import NamedTuple, Callable, Collection, Any, Dict, Optional, Type, List, Tuple, TypeVar +from typing import ( + Any, + Callable, + Collection, + Dict, + List, + NamedTuple, + Optional, + Tuple, + Type, + TypeVar, +) import allure diff --git a/tests/library/errorcodes.py b/tests/library/errorcodes.py index 0010faf99a..aa7a897fe2 100644 --- a/tests/library/errorcodes.py +++ b/tests/library/errorcodes.py @@ -12,12 +12,11 @@ """Tools for ADCM errors handling in tests""" -from typing import List, Iterable +from typing import Iterable, List import pytest_check as check from adcm_client.wrappers.api import ADCMApiError from coreapi.exceptions import ErrorMessage -from pytest_check.check_methods import get_failures class ADCMError: @@ -40,25 +39,30 @@ def equal(self, e, *args): self._compare_error_message(e, *args) def _compare_error_message(self, e: ErrorMessage, *args): - error = e.value.error if hasattr(e, 'value') else e.error + error = e.value.error if hasattr(e, "value") else e.error title = error.title code = error.get("code", "") desc = error.get("desc", "") error_args = error.get("args", "") check.equal(title, self.title, f'Expected title is "{self.title}", actual is "{title}"') check.equal(code, self.code, f'Expected error code is "{self.code}", actual is "{code}"') + # workaround for pytest-check 1.1.2 to stop execution right here + raise_assertion = False for i in args: - err_msg = 'Unknown' - check.is_true( + err_msg = "Unknown" + check_result = check.is_true( i in desc or i in error_args or i in (err_msg := self._get_data_err_messages(error)), ( f"Text '{i}' should be present in error message. Either in:\n" - f'Description: {desc}\n' - f'Error arguments: {error_args}\n' - f'Or message: {err_msg}' + f"Description: {desc}\n" + f"Error arguments: {error_args}\n" + f"Or message: {err_msg}" ), ) - assert not get_failures(), "All assertions should passed" + if check_result is False: + raise_assertion = True + if raise_assertion: + raise AssertionError("All assertions should passed") def _compare_adcm_api_error(self, e: ADCMApiError, *_): code, *_ = e.args @@ -66,7 +70,7 @@ def _compare_adcm_api_error(self, e: ADCMApiError, *_): def _get_data_err_messages(self, error) -> List[str]: """Extract all messages from _data attribute or an error if it is presented""" - data = getattr(error, '_data', None) + data = getattr(error, "_data", None) if data is None: return [] if isinstance(data, dict): @@ -79,211 +83,213 @@ def _get_data_err_messages(self, error) -> List[str]: else: messages.append(val) return messages - raise ValueError('error._dict expected to be dict instance') + raise ValueError("error._dict expected to be dict instance") def __str__(self): - return f'{self.code} {self.title}' + return f"{self.code} {self.title}" INVALID_OBJECT_DEFINITION = ADCMError( - '409 Conflict', - 'INVALID_OBJECT_DEFINITION', + "409 Conflict", + "INVALID_OBJECT_DEFINITION", ) INVALID_CONFIG_DEFINITION = ADCMError( - '409 Conflict', - 'INVALID_CONFIG_DEFINITION', + "409 Conflict", + "INVALID_CONFIG_DEFINITION", ) UPGRADE_ERROR = ADCMError( - '409 Conflict', - 'UPGRADE_ERROR', + "409 Conflict", + "UPGRADE_ERROR", ) BUNDLE_ERROR = ADCMError( - '409 Conflict', - 'BUNDLE_ERROR', + "409 Conflict", + "BUNDLE_ERROR", ) BUNDLE_CONFLICT = ADCMError( - '409 Conflict', - 'BUNDLE_CONFLICT', + "409 Conflict", + "BUNDLE_CONFLICT", ) UPGRADE_NOT_FOUND = ADCMError( - '404 Not Found', - 'UPGRADE_NOT_FOUND', + "404 Not Found", + "UPGRADE_NOT_FOUND", ) CONFIG_VALUE_ERROR = ADCMError( - '400 Bad Request', - 'CONFIG_VALUE_ERROR', + "400 Bad Request", + "CONFIG_VALUE_ERROR", ) CONFIG_KEY_ERROR = ADCMError( - '400 Bad Request', - 'CONFIG_KEY_ERROR', + "400 Bad Request", + "CONFIG_KEY_ERROR", ) GROUP_CONFIG_HOST_ERROR = ADCMError( - '400 Bad Request', - 'GROUP_CONFIG_HOST_ERROR', + "400 Bad Request", + "GROUP_CONFIG_HOST_ERROR", ) GROUP_CONFIG_HOST_EXISTS = ADCMError( - '400 Bad Request', - 'GROUP_CONFIG_HOST_EXISTS', + "400 Bad Request", + "GROUP_CONFIG_HOST_EXISTS", ) ATTRIBUTE_ERROR = ADCMError( - '400 Bad Request', - 'ATTRIBUTE_ERROR', + "400 Bad Request", + "ATTRIBUTE_ERROR", ) TASK_ERROR = ADCMError( - '409 Conflict', - 'TASK_ERROR', + "409 Conflict", + "TASK_ERROR", ) GROUP_CONFIG_CHANGE_UNSELECTED_FIELD = ADCMError( - '400 Bad Request', - 'GROUP_CONFIG_CHANGE_UNSELECTED_FIELD', + "400 Bad Request", + "GROUP_CONFIG_CHANGE_UNSELECTED_FIELD", ) STACK_LOAD_ERROR = ADCMError( - '409 Conflict', - 'STACK_LOAD_ERROR', + "409 Conflict", + "STACK_LOAD_ERROR", ) DEFINITION_KEY_ERROR = ADCMError( - '409 Conflict', - 'DEFINITION_KEY_ERROR', + "409 Conflict", + "DEFINITION_KEY_ERROR", ) INVALID_UPGRADE_DEFINITION = ADCMError( - '409 Conflict', - 'INVALID_UPGRADE_DEFINITION', + "409 Conflict", + "INVALID_UPGRADE_DEFINITION", ) INVALID_VERSION_DEFINITION = ADCMError( - '409 Conflict', - 'INVALID_VERSION_DEFINITION', + "409 Conflict", + "INVALID_VERSION_DEFINITION", ) INVALID_ACTION_DEFINITION = ADCMError( - '409 Conflict', - 'INVALID_ACTION_DEFINITION', + "409 Conflict", + "INVALID_ACTION_DEFINITION", ) JSON_ERROR = ADCMError( - '400 Bad Request', - 'JSON_ERROR', + "400 Bad Request", + "JSON_ERROR", ) FOREIGN_HOST = ADCMError( - '409 Conflict', - 'FOREIGN_HOST', + "409 Conflict", + "FOREIGN_HOST", ) PROTOTYPE_NOT_FOUND = ADCMError( - '404 Not Found', - 'PROTOTYPE_NOT_FOUND', + "404 Not Found", + "PROTOTYPE_NOT_FOUND", ) CONFIG_NOT_FOUND = ADCMError( - '404 Not Found', - 'CONFIG_NOT_FOUND', + "404 Not Found", + "CONFIG_NOT_FOUND", ) PROVIDER_NOT_FOUND = ADCMError( - '404 Not Found', - 'PROVIDER_NOT_FOUND', + "404 Not Found", + "PROVIDER_NOT_FOUND", ) HOST_NOT_FOUND = ADCMError( - '404 Not Found', - 'HOST_NOT_FOUND', + "404 Not Found", + "HOST_NOT_FOUND", ) CLUSTER_NOT_FOUND = ADCMError( - '404 Not Found', - 'CLUSTER_NOT_FOUND', + "404 Not Found", + "CLUSTER_NOT_FOUND", ) CLUSTER_SERVICE_NOT_FOUND = ADCMError( - '404 Not Found', - 'CLUSTER_SERVICE_NOT_FOUND', + "404 Not Found", + "CLUSTER_SERVICE_NOT_FOUND", ) SERVICE_NOT_FOUND = ADCMError( - '404 Not Found', - 'SERVICE_NOT_FOUND', + "404 Not Found", + "SERVICE_NOT_FOUND", ) HOSTSERVICE_NOT_FOUND = ADCMError( - '404 Not Found', - 'HOSTSERVICE_NOT_FOUND', + "404 Not Found", + "HOSTSERVICE_NOT_FOUND", ) TASK_GENERATOR_ERROR = ADCMError( - '409 Conflict', - 'TASK_GENERATOR_ERROR', + "409 Conflict", + "TASK_GENERATOR_ERROR", ) SERVICE_CONFLICT = ADCMError( - '409 Conflict', - 'SERVICE_CONFLICT', + "409 Conflict", + "SERVICE_CONFLICT", ) +SERVICE_DELETE_ERROR = ADCMError("409 Conflict", "SERVICE_DELETE_ERROR") + HOST_CONFLICT = ADCMError( - '409 Conflict', - 'HOST_CONFLICT', + "409 Conflict", + "HOST_CONFLICT", ) CLUSTER_CONFLICT = ADCMError( - '409 Conflict', - 'CLUSTER_CONFLICT', + "409 Conflict", + "CLUSTER_CONFLICT", ) PROVIDER_CONFLICT = ADCMError( - '409 Conflict', - 'PROVIDER_CONFLICT', + "409 Conflict", + "PROVIDER_CONFLICT", ) WRONG_NAME = ADCMError( - '400 Bad Request', - 'WRONG_NAME', + "400 Bad Request", + "WRONG_NAME", ) BIND_ERROR = ADCMError( - '409 Conflict', - 'BIND_ERROR', + "409 Conflict", + "BIND_ERROR", ) MAINTENANCE_MODE_NOT_AVAILABLE = ADCMError( - '409 Conflict', - 'MAINTENANCE_MODE_NOT_AVAILABLE', + "409 Conflict", + "MAINTENANCE_MODE_NOT_AVAILABLE", ) ACTION_ERROR = ADCMError( - '409 Conflict', - 'ACTION_ERROR', + "409 Conflict", + "ACTION_ERROR", ) INVALID_HC_HOST_IN_MM = ADCMError( - '409 Conflict', - 'INVALID_HC_HOST_IN_MM', + "409 Conflict", + "INVALID_HC_HOST_IN_MM", ) -USER_UPDATE_ERROR = ADCMError('400 Bad Request', 'USER_UPDATE_ERROR') +USER_UPDATE_ERROR = ADCMError('409 Conflict', 'USER_CONFLICT') -GROUP_UPDATE_ERROR = ADCMError('400 Bad Request', 'GROUP_UPDATE_ERROR') +GROUP_UPDATE_ERROR = ADCMError('409 Conflict', 'GROUP_CONFLICT') # ADCMApiError -AUTH_ERROR = ADCMError('400 Bad Request', 'AUTH_ERROR') +AUTH_ERROR = ADCMError("400 Bad Request", "AUTH_ERROR") COMPONENT_CONSTRAINT_ERROR = ADCMError( - '409 Conflict', - 'COMPONENT_CONSTRAINT_ERROR', + "409 Conflict", + "COMPONENT_CONSTRAINT_ERROR", ) diff --git a/tests/library/ldap_interactions.py b/tests/library/ldap_interactions.py index 7eed9dd078..233545e558 100644 --- a/tests/library/ldap_interactions.py +++ b/tests/library/ldap_interactions.py @@ -22,8 +22,8 @@ import ldap from adcm_client.objects import ADCMClient from adcm_pytest_plugin.custom_types import SecureString +from adcm_pytest_plugin.steps.actions import wait_for_task_and_assert_result from ldap.ldapobject import SimpleLDAPObject - from tests.library.utils import ConfigError LDAP_PREFIX = "ldap://" @@ -67,8 +67,8 @@ class LDAPEntityManager: } def __init__(self, config: LDAPTestConfig, test_name: str): - ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, ldap.OPT_X_TLS_NEVER) # pylint: disable=no-member - self.conn = ldap.initialize(config.uri) # pylint: disable=no-member + ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, ldap.OPT_X_TLS_NEVER) + self.conn = ldap.initialize(config.uri) self.conn.simple_bind_s(config.admin_dn, config.admin_pass) self._created_records = [] @@ -124,7 +124,10 @@ def create_user( extra_modlist = extra_modlist or [] base_dn = custom_base_dn or self.test_dn new_dn = f"CN={name},{base_dn}" - self.conn.add_s(new_dn, self._BASE_USER_MODLIST + [("sAMAccountName", name.encode("utf-8"))] + extra_modlist) + self.conn.add_s( + new_dn, + self._BASE_USER_MODLIST + [("sAMAccountName", name.encode("utf-8"))] + extra_modlist, + ) self._created_records.append(new_dn) self.set_user_password(new_dn, password) self.activate_user(new_dn) @@ -138,25 +141,26 @@ def delete(self, dn: str) -> None: @allure.step("Activate user {user_dn}") def activate_user(self, user_dn: str, uac: bytes = _ACTIVE_USER_UAC) -> None: """Activate user""" - self.conn.modify_s(user_dn, [(ldap.MOD_REPLACE, "userAccountControl", uac)]) # pylint: disable=no-member + self.conn.modify_s(user_dn, [(ldap.MOD_REPLACE, "userAccountControl", uac)]) @allure.step("Deactivate user {user_dn}") def deactivate_user(self, user_dn: str, uac: bytes = _INACTIVE_USER_UAC) -> None: """Deactivate user""" - self.conn.modify_s(user_dn, [(ldap.MOD_REPLACE, "userAccountControl", uac)]) # pylint: disable=no-member + self.conn.modify_s(user_dn, [(ldap.MOD_REPLACE, "userAccountControl", uac)]) @allure.step('Set password "{password}" for user {user_dn}') def set_user_password(self, user_dn: str, password: str) -> None: """Set password for an existing user""" pass_utf16 = f'"{password}"'.encode("utf-16-le") - self.conn.modify_s(user_dn, [(ldap.MOD_REPLACE, "unicodePwd", [pass_utf16])]) # pylint: disable=no-member + self.conn.modify_s(user_dn, [(ldap.MOD_REPLACE, "unicodePwd", [pass_utf16])]) @allure.step("Update user in LDAP") def update_user(self, user_dn: str, **fields: str): """Update user record""" try: self.conn.modify_s( - user_dn, [(ldap.MOD_REPLACE, self._ATTR_MAP[k], [v.encode("utf-8")]) for k, v in fields.items()] + user_dn, + [(ldap.MOD_REPLACE, self._ATTR_MAP[k], [v.encode("utf-8")]) for k, v in fields.items()], ) except KeyError as e: unknown_fields = {k for k in fields if k in self._ATTR_MAP} @@ -168,13 +172,13 @@ def update_user(self, user_dn: str, **fields: str): @allure.step("Add user {user_dn} to {group_dn}") def add_user_to_group(self, user_dn: str, group_dn: str) -> None: """Add user to a group""" - mod_group = [(ldap.MOD_ADD, "member", [user_dn.encode("utf-8")])] # pylint: disable=no-member + mod_group = [(ldap.MOD_ADD, "member", [user_dn.encode("utf-8")])] self.conn.modify_s(group_dn, mod_group) @allure.step("Remove user {user_dn} from {group_dn}") def remove_user_from_group(self, user_dn: str, group_dn: str) -> None: """Remove user from group""" - mod_group = [(ldap.MOD_DELETE, "member", [user_dn.encode("utf-8")])] # pylint: disable=no-member + mod_group = [(ldap.MOD_DELETE, "member", [user_dn.encode("utf-8")])] self.conn.modify_s(group_dn, mod_group) @allure.step("Cleat test OU") @@ -182,13 +186,17 @@ def clean_test_ou(self): """Remove every object in test OU""" if self.test_dn is None: return + with allure.step("Clean users"): + for user_dn in self._get_users(): + self.conn.delete_s(user_dn) # not everything is deleted on first round - for _ in range(10): + for i in range(10): entities = self._get_entities_from_test_ou() if len(entities) == 0: break - for dn in entities: # pylint: disable=invalid-name - self.conn.delete(dn) + with allure.step(f"Clean other entities, round #{i}"): + for dn in entities: + self.conn.delete_s(dn) else: # error was leading to tests re-run that can make more "dead" objects warnings.warn(f"Not all entities in test OU were deleted: {entities}") @@ -196,18 +204,27 @@ def clean_test_ou(self): self._created_records = [] self.test_dn = None + def _get_users(self): + try: + return [ + dn + for dn, entry in self.conn.search_s(self.test_dn, ldap.SCOPE_SUBTREE) + if b"person" in entry["objectClass"] + ] + except ldap.NO_SUCH_OBJECT: + return [] + def _get_entities_from_test_ou(self): try: return sorted( - (dn for dn, _ in self.conn.search_s(self.test_dn, ldap.SCOPE_SUBTREE)), # pylint: disable=no-member + (dn for dn, _ in self.conn.search_s(self.test_dn, ldap.SCOPE_SUBTREE)), key=len, reverse=True, ) - except ldap.NO_SUCH_OBJECT: # pylint: disable=no-member + except ldap.NO_SUCH_OBJECT: return [] -# pylint: disable-next=too-many-arguments def configure_adcm_for_ldap( client: ADCMClient, config: LDAPTestConfig, @@ -249,3 +266,15 @@ def configure_adcm_for_ldap( }, attach_to_allure=False, ) + + +@allure.step("Run ldap sync") +def sync_adcm_with_ldap(client: ADCMClient) -> None: + """method to run ldap sync""" + action = client.adcm().action(name="run_ldap_sync") + wait_for_task_and_assert_result(action.run(), "success") + + +def change_adcm_ldap_config(client: ADCMClient, attach_to_allure: bool = False, **params) -> None: + """method to change adcm ldap config""" + client.adcm().config_set_diff({"ldap_integration": params}, attach_to_allure=attach_to_allure) diff --git a/tests/library/retry.py b/tests/library/retry.py new file mode 100644 index 0000000000..7aaa0250d3 --- /dev/null +++ b/tests/library/retry.py @@ -0,0 +1,61 @@ +# 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. + +"""Utilities for retrying operations that require some context""" + +from dataclasses import dataclass, field +from typing import Any, Callable, Collection + +import allure + + +@dataclass() +class Step: + """ + Wrapper to a test step or state restoration step + """ + + func: Callable + args: Collection[Any] = () + kwargs: dict[Any, Any] = field(default_factory=dict) + + def exec(self) -> Any: + """Execute wrapped function""" + return self.func(*self.args, **self.kwargs) + + +class RetryFromCheckpoint: + """ + In some cases simple "assertion retry" isn't good enough (especially for UI tests) + AND we can "retry" some operation after restoring "initial" state. + That's what this class is made for. + + It's a work in progress, so there changes may and will be done. + """ + + def __init__(self, execution_steps: Collection[Step], restoration_steps: Collection[Step]): + self._execution_steps = tuple(execution_steps) + self._restoration_steps = tuple(restoration_steps) + + def __call__(self, restore_from: Exception | tuple[Exception], max_retries: int = 3, *, counter: int = 0) -> None: + try: + for step in self._execution_steps: + step.exec() + + except restore_from as e: + if counter >= max_retries: + raise + + with allure.step(f"Caught exception {e}, restoring state"): + for step in self._restoration_steps: + step.exec() + self(restore_from, max_retries, counter=counter + 1) diff --git a/tests/library/status.py b/tests/library/status.py index d2beb67032..4af4f114f6 100644 --- a/tests/library/status.py +++ b/tests/library/status.py @@ -18,10 +18,8 @@ from typing import Collection, Tuple, Union import requests - -from adcm_client.objects import Host, Component, ADCMClient, Cluster +from adcm_client.objects import ADCMClient, Cluster, Component, Host from adcm_pytest_plugin.docker_utils import ADCM - from tests.library.utils import RequestFailedException, get_json_or_text POSITIVE_STATUS = 0 diff --git a/tests/library/utils.py b/tests/library/utils.py index b3ff93c4c9..8c0a3c2942 100644 --- a/tests/library/utils.py +++ b/tests/library/utils.py @@ -12,13 +12,13 @@ """Common utils for ADCM tests""" -import time import json import random -from typing import Tuple, Iterable +import time +from typing import Iterable, Tuple import requests -from adcm_client.objects import Host +from adcm_client.objects import Cluster, Component, Host, Provider, Service, Task from adcm_pytest_plugin.plugin import parametrized_by_adcm_version @@ -198,3 +198,26 @@ def lower_class_name(obj: object) -> str: def get_hosts_fqdn_representation(hosts: Iterable[Host]): """Return string with host FQDNs separated by ','""" return ", ".join(host.fqdn for host in hosts) + + +# !===== Bulk Log Download =====! + + +def build_full_archive_name( + adcm_object: Cluster | Service | Component | Provider, + task: Task, + action_name_in_archive_name: str, +) -> str: + """Build expected archive name for general object action's task (without extension)""" + top_level_object = adcm_object if not isinstance(adcm_object, (Service, Component)) else adcm_object.cluster() + return "_".join( + map( + lambda p: p.replace(" ", "-").replace("_", "").lower(), + ( + top_level_object.name, + adcm_object.prototype().display_name, + action_name_in_archive_name, + str(task.id), + ), + ) + ) diff --git a/tests/pyproject.toml b/tests/pyproject.toml deleted file mode 100644 index 584493c23c..0000000000 --- a/tests/pyproject.toml +++ /dev/null @@ -1,43 +0,0 @@ -[tool.black] -line-length = 120 -skip-string-normalization = true -target-version = ['py39'] -include = '\.py$' -exclude = ''' -( - \.git - | \.venv - | /__pycache__/ - | /data/ - | /docs/ - | /os/ - | /spec/ - | /build/ - | /dist/ -) -''' - -[tool.pylint.master] -ignore-paths = [ - "^build/.*$", - "^dist/.*$", -] - -[tool.pylint.format] -max-line-length = 120 -max-module-lines = 1000 - -[tool.pylint.similarities] -ignore-imports = "yes" -min-similarity-lines=5 - -[tool.pylint.message_control] -disable=[ - "fixme", "invalid-name", "duplicate-code", "too-few-public-methods", "no-member" -] - -[tool.pylint.basic] -good-names = ["i", "j", "k", "v", "e", "ui", "by", "pytest_plugins"] - -[tool.isort] -line_length = 120 diff --git a/tests/stack/cluster_bundle/cluster/job.py b/tests/stack/cluster_bundle/cluster/job.py index e536ab8227..2102aa8d50 100644 --- a/tests/stack/cluster_bundle/cluster/job.py +++ b/tests/stack/cluster_bundle/cluster/job.py @@ -10,10 +10,9 @@ # See the License for the specific language governing permissions and # limitations under the License. -from cm.logger import logger from cm.errors import AdcmEx - -from cm.models import Prototype, Action +from cm.logger import logger +from cm.models import Action, Prototype def task_generator(action, selector): diff --git a/tests/stack_fake/common/command.py b/tests/stack_fake/common/command.py index f64aa495e7..2bfc897edb 100755 --- a/tests/stack_fake/common/command.py +++ b/tests/stack_fake/common/command.py @@ -14,9 +14,9 @@ # Since this module is beyond QA responsibility we will not fix docstrings here # pylint: disable=missing-function-docstring, missing-class-docstring, missing-module-docstring +import logging import os import sys -import logging from contextlib import contextmanager from subprocess import call @@ -58,7 +58,6 @@ def add_path(path): return env -# pylint: disable-next=too-many-arguments def run_python_script(base_dir, py_script, command, json_config, out_file, err_file): try: res = call( diff --git a/tests/stack_fake/common/template.py b/tests/stack_fake/common/template.py index 7df661d60f..8780757a27 100755 --- a/tests/stack_fake/common/template.py +++ b/tests/stack_fake/common/template.py @@ -14,9 +14,10 @@ # Since this module is beyond QA responsibility we will not fix docstrings here # pylint: disable=missing-function-docstring, missing-class-docstring, missing-module-docstring +import json import os import sys -import json + from jinja2 import Template diff --git a/tests/stack_fake/services/cluster/job.py b/tests/stack_fake/services/cluster/job.py index ae9cbf126b..f98388d579 100644 --- a/tests/stack_fake/services/cluster/job.py +++ b/tests/stack_fake/services/cluster/job.py @@ -14,10 +14,9 @@ # pylint: disable=missing-function-docstring, missing-class-docstring, missing-module-docstring # pylint: disable=import-error -from cm.logger import logger from cm.errors import AdcmEx - -from cm.models import Prototype, Action +from cm.logger import logger +from cm.models import Action, Prototype def task_generator(action, selector): diff --git a/tests/ui_tests/app/__init__.py b/tests/ui_tests/app/__init__.py index a74d23a16e..e69b3398a2 100644 --- a/tests/ui_tests/app/__init__.py +++ b/tests/ui_tests/app/__init__.py @@ -1 +1,12 @@ +# 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. """PageObjects for UI tests""" diff --git a/tests/ui_tests/app/app.py b/tests/ui_tests/app/app.py index d1c251254a..cd019b40d8 100644 --- a/tests/ui_tests/app/app.py +++ b/tests/ui_tests/app/app.py @@ -15,7 +15,7 @@ # Created by a1wen at 27.02.19 import os -from typing import Union, Optional +from typing import Optional, Union import allure from adcm_client.wrappers.docker import ADCM diff --git a/tests/ui_tests/app/helpers/__init__.py b/tests/ui_tests/app/helpers/__init__.py index ccb7436595..b651a664d7 100644 --- a/tests/ui_tests/app/helpers/__init__.py +++ b/tests/ui_tests/app/helpers/__init__.py @@ -1 +1,13 @@ +# 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. + """UI tests helpers""" diff --git a/tests/ui_tests/app/helpers/config_generator_invisible_fields.py b/tests/ui_tests/app/helpers/config_generator_invisible_fields.py index 5f8e852840..4f8b7e29d6 100644 --- a/tests/ui_tests/app/helpers/config_generator_invisible_fields.py +++ b/tests/ui_tests/app/helpers/config_generator_invisible_fields.py @@ -1,3 +1,14 @@ +# 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. """Config generator for UI tests""" import os diff --git a/tests/ui_tests/app/helpers/config_generator_mixed_options.py b/tests/ui_tests/app/helpers/config_generator_mixed_options.py index b3640837ab..b5dcda879b 100644 --- a/tests/ui_tests/app/helpers/config_generator_mixed_options.py +++ b/tests/ui_tests/app/helpers/config_generator_mixed_options.py @@ -1,3 +1,14 @@ +# 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. """Config generator for UI tests""" import os diff --git a/tests/ui_tests/app/helpers/config_generator_numbers.py b/tests/ui_tests/app/helpers/config_generator_numbers.py index e89411489a..f22689bc98 100644 --- a/tests/ui_tests/app/helpers/config_generator_numbers.py +++ b/tests/ui_tests/app/helpers/config_generator_numbers.py @@ -1,8 +1,18 @@ +# 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. """Config generator for UI tests""" import os - integers = [ (0, 0, 0, 'nulls'), (-10, 5, 0, 'positive_and_negative'), diff --git a/tests/ui_tests/app/helpers/configs_generator.py b/tests/ui_tests/app/helpers/configs_generator.py index 4d8176b0a3..0f903c76f1 100644 --- a/tests/ui_tests/app/helpers/configs_generator.py +++ b/tests/ui_tests/app/helpers/configs_generator.py @@ -9,7 +9,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. -# pylint: disable=too-many-branches,too-many-statements,too-many-arguments,too-many-locals +# pylint: disable=too-many-branches,too-many-statements,too-many-locals """UI tests for config page""" @@ -25,7 +25,19 @@ pytestmark = [pytest.mark.full()] -TYPES = ['string', 'password', 'integer', 'text', 'boolean', 'float', 'list', 'map', 'json', 'file', 'secrettext'] +TYPES = [ + 'string', + 'password', + 'integer', + 'text', + 'boolean', + 'float', + 'list', + 'map', + 'json', + 'file', + 'secrettext', +] CONFIG_FILE = 'config.yaml' DEFAULT_VALUE = { @@ -137,7 +149,7 @@ def generate_configs( return (config, expected_result) -def prepare_config(config): +def prepare_config(config, *, enforce_file: bool = False): """Create config file and return config, expected result and path to config file""" config_info = config[0][0]['config'][0] @@ -154,12 +166,16 @@ def prepare_config(config): os.makedirs(d_name) config[0][0]["name"] = random_string() - if config_info['name'] == 'file': + if enforce_file or config_info['name'] == 'file': with open(f"{d_name}/file.txt", 'w', encoding='utf_8') as file: file.write("test") with open(f"{d_name}/{CONFIG_FILE}", 'w', encoding='utf_8') as yaml_file: yaml.dump(config[0], yaml_file) - allure.attach.file("/".join([d_name, CONFIG_FILE]), attachment_type=allure.attachment_type.YAML, name=CONFIG_FILE) + allure.attach.file( + "/".join([d_name, CONFIG_FILE]), + attachment_type=allure.attachment_type.YAML, + name=CONFIG_FILE, + ) return config[0][0], config[1], d_name @@ -268,7 +284,7 @@ def generate_group_configs( return (config, expected_result) -def prepare_group_config(config): +def prepare_group_config(config, *, enforce_file: bool = False): """Create config file with group and return config, expected result and path to config file""" config_info = config[0][0]['config'][0] @@ -299,10 +315,14 @@ def prepare_group_config(config): d_name = f"{temdir}/configs/groups/{config_folder_name}" os.makedirs(d_name) config[0][0]["name"] = random_string() - if config_subs['name'] == 'file': + if enforce_file or config_subs['name'] == 'file': with open(f"{d_name}/file.txt", 'w', encoding='utf_8') as file: file.write("test") with open(f"{d_name}/{CONFIG_FILE}", 'w', encoding='utf_8') as yaml_file: yaml.dump(list(config[0]), yaml_file) - allure.attach.file("/".join([d_name, CONFIG_FILE]), attachment_type=allure.attachment_type.YAML, name=CONFIG_FILE) + allure.attach.file( + "/".join([d_name, CONFIG_FILE]), + attachment_type=allure.attachment_type.YAML, + name=CONFIG_FILE, + ) return config[0][0], config[1], d_name diff --git a/tests/ui_tests/app/page/admin/locators.py b/tests/ui_tests/app/page/admin/locators.py index 8f8d6d42db..9b464f7523 100644 --- a/tests/ui_tests/app/page/admin/locators.py +++ b/tests/ui_tests/app/page/admin/locators.py @@ -13,11 +13,7 @@ """Admin page locators""" from selenium.webdriver.common.by import By - -from tests.ui_tests.app.helpers.locator import ( - Locator, - TemplateLocator, -) +from tests.ui_tests.app.helpers.locator import Locator, TemplateLocator from tests.ui_tests.app.page.common.configuration.locators import CommonConfigMenu @@ -28,9 +24,12 @@ class CommonAdminPagesLocators: delete_btn = Locator(By.CSS_SELECTOR, ".controls>button", "Delete Group button") field_error = TemplateLocator(By.XPATH, "//mat-error[contains(text(), '{}')]", 'Error "{}"') item = Locator(By.CSS_SELECTOR, "adwp-selection-list mat-list-option", "select items") - save_btn = Locator( - By.XPATH, "//button[./span[contains(text(), 'Add') or contains(text(), 'Update')]]", "Save button" + save_update_btn = Locator( + By.XPATH, + "//button[./span[contains(text(), 'Add') or contains(text(), 'Update')]]", + "Save/Update button", ) + cancel_btn = Locator(By.XPATH, "//button[./span[contains(text(), 'Cancel')]]", "Cancel button") class AdminIntroLocators: @@ -49,10 +48,12 @@ class AdminUsersLocators: create_user_button = Locator(By.XPATH, "//button[@adcm_test='create-btn']", "Add user button") user_row = Locator(By.CSS_SELECTOR, "mat-row", "Table row") - filter_btn = Locator(By.CSS_SELECTOR, "app-filter .filter-toggle-button", "Fulter button") - filter_dropdown_select = Locator(By.CSS_SELECTOR, "app-filter mat-select", "Filter dropdown select") + filter_btn = Locator(By.CSS_SELECTOR, "app-server-filter .filter-toggle-button", "Filter button") + filter_dropdown_select = Locator(By.CSS_SELECTOR, "app-server-filter mat-select", "Filter dropdown select") filter_dropdown_option = Locator(By.CSS_SELECTOR, "div[role='listbox'] mat-option", "Filter dropdown option") - filter_dropdown_remove = Locator(By.CSS_SELECTOR, "app-filter button[aria-label='Remove']", "Filter remove button") + filter_dropdown_remove = Locator( + By.CSS_SELECTOR, "app-server-filter button[aria-label='Remove']", "Filter remove button" + ) class Row: """Existing user row""" @@ -60,7 +61,9 @@ class Row: username = Locator(By.XPATH, ".//mat-cell[2]", "Username in row") password = Locator(By.CSS_SELECTOR, "input[data-placeholder='Password']", "Password in row") password_confirm = Locator( - By.CSS_SELECTOR, "input[data-placeholder='Confirm Password']", "Password confirmation in row" + By.CSS_SELECTOR, + "input[data-placeholder='Confirm Password']", + "Password confirmation in row", ) confirm_update_btn = Locator( By.XPATH, @@ -77,10 +80,14 @@ class AddUserPopup(CommonAdminPagesLocators): username = Locator(By.NAME, "username", "New user username") password = Locator(By.CSS_SELECTOR, "input[data-placeholder='Password']", "New user password") password_confirm = Locator( - By.CSS_SELECTOR, "input[data-placeholder='Confirm password']", "New user password confirmation" + By.CSS_SELECTOR, + "input[data-placeholder='Confirm password']", + "New user password confirmation", ) adcm_admin_chbx = Locator( - By.CSS_SELECTOR, "mat-checkbox[formcontrolname='is_superuser']", "Checkbox ADCM Administrator" + By.CSS_SELECTOR, + "mat-checkbox[formcontrolname='is_superuser']", + "Checkbox ADCM Administrator", ) first_name = Locator(By.NAME, "first_name", "New user first name") last_name = Locator(By.NAME, "last_name", "New user last name") @@ -140,18 +147,26 @@ class AddRolePopup: By.CSS_SELECTOR, "adwp-input[controlname='display_name'] input", "Input for role name" ) description_name_input = Locator( - By.CSS_SELECTOR, "adwp-input[controlname='description'] input", "Input for role description" + By.CSS_SELECTOR, + "adwp-input[controlname='description'] input", + "Input for role description", ) class PermissionItemsBlock: filter_input = Locator( - By.CSS_SELECTOR, ".adcm-input-rbac-permissions__selected-filter input", "Filter input" + By.CSS_SELECTOR, + ".adcm-input-rbac-permissions__selected-filter input", + "Filter input", ) item = Locator( - By.CSS_SELECTOR, ".adcm-input-rbac-permissions__selected-field mat-chip", "Selected permission item" + By.CSS_SELECTOR, + ".adcm-input-rbac-permissions__selected-field mat-chip", + "Selected permission item", ) clear_all_btn = Locator( - By.CSS_SELECTOR, ".adcm-input-rbac-permissions__selected-filter-clear", "Clear all button" + By.CSS_SELECTOR, + ".adcm-input-rbac-permissions__selected-filter-clear", + "Clear all button", ) class PermissionItem: @@ -160,7 +175,9 @@ class PermissionItem: class SelectPermissionsBlock: permissions_filters = Locator( - By.CSS_SELECTOR, ".adcm-rbac-permission__filter mat-chip", "filter item for permissions list" + By.CSS_SELECTOR, + ".adcm-rbac-permission__filter mat-chip", + "filter item for permissions list", ) permissions_search_row = Locator(By.CSS_SELECTOR, "adwp-selection-list-actions", "Permission search row") permissions_item_row = Locator( @@ -199,7 +216,9 @@ class FirstStep: description_input = Locator(By.CSS_SELECTOR, "input[name='description']", "Input description") role_select = Locator(By.CSS_SELECTOR, "mat-select[placeholder='Role']", "select role") role_item = Locator( - By.XPATH, "//div[./mat-option//*[@placeholderlabel='Select role']]/mat-option", "select items for role" + By.XPATH, + "//div[./mat-option//*[@placeholderlabel='Select role']]/mat-option", + "select items for role", ) users_select = Locator(By.CSS_SELECTOR, "adwp-input-select[label='User'] adwp-select", "select users") @@ -227,7 +246,9 @@ class SecondStep: parent_select = Locator(By.XPATH, "//div[./span//span[text()='Parent']]//adwp-select", "select parent") hosts_select = Locator(By.CSS_SELECTOR, "app-parametrized-by-host mat-form-field", "select hosts") next_btn_second = Locator( - By.CSS_SELECTOR, "app-rbac-policy-form-step-two~div .mat-stepper-next", "Next button from second step" + By.CSS_SELECTOR, + "app-rbac-policy-form-step-two~div .mat-stepper-next", + "Next button from second step", ) class ThirdStep: diff --git a/tests/ui_tests/app/page/admin/page.py b/tests/ui_tests/app/page/admin/page.py index 96849f450e..de354136cf 100644 --- a/tests/ui_tests/app/page/admin/page.py +++ b/tests/ui_tests/app/page/admin/page.py @@ -13,36 +13,29 @@ """Admin pages PageObjects classes""" from dataclasses import dataclass -from typing import ( - List, - Optional, -) +from typing import List, Optional import allure from adcm_pytest_plugin.utils import wait_until_step_succeeds -from selenium.common.exceptions import ( - StaleElementReferenceException, -) -from selenium.common.exceptions import TimeoutException +from selenium.common.exceptions import StaleElementReferenceException, TimeoutException from selenium.webdriver.remote.webelement import WebElement - from tests.ui_tests.app.helpers.locator import Locator from tests.ui_tests.app.page.admin.locators import ( - AdminUsersLocators, - AdminIntroLocators, - AdminSettingsLocators, AdminGroupsLocators, - AdminRolesLocators, + AdminIntroLocators, AdminPoliciesLocators, + AdminRolesLocators, + AdminSettingsLocators, + AdminUsersLocators, ) from tests.ui_tests.app.page.common.base_page import ( BasePageObject, - PageHeader, PageFooter, + PageHeader, ) from tests.ui_tests.app.page.common.common_locators import ObjectPageMenuLocators from tests.ui_tests.app.page.common.configuration.page import CommonConfigMenuObj -from tests.ui_tests.app.page.common.dialogs_locators import DeleteDialog +from tests.ui_tests.app.page.common.dialogs.locators import DeleteDialog from tests.ui_tests.app.page.common.popups.locator import CommonPopupLocators from tests.ui_tests.app.page.common.table.locator import CommonTable from tests.ui_tests.app.page.common.table.page import CommonTableObj @@ -236,10 +229,13 @@ def is_user_presented(self, username: str) -> bool: return True return False + def is_user_deactivated(self, username: str) -> bool: + """Get user's deactivation status on UI""" + row = self.get_user_row_by_username(username) + return "inactive" in row.get_attribute("class") + @allure.step('Create new user "{username}" with password "{password}"') - def create_user( - self, username: str, password: str, first_name: str, last_name: str, email: str - ): # pylint: disable-next=too-many-arguments + def create_user(self, username: str, password: str, first_name: str, last_name: str, email: str): """Create new user via add user popup""" self.find_and_click(AdminUsersLocators.create_user_button) self.wait_element_visible(AdminUsersLocators.AddUserPopup.block) @@ -249,7 +245,7 @@ def create_user( self.send_text_to_element(AdminUsersLocators.AddUserPopup.first_name, first_name) self.send_text_to_element(AdminUsersLocators.AddUserPopup.last_name, last_name) self.send_text_to_element(AdminUsersLocators.AddUserPopup.email, email) - self.find_and_click(AdminUsersLocators.AddUserPopup.save_btn) + self.find_and_click(AdminUsersLocators.AddUserPopup.save_update_btn) self.wait_element_hide(AdminUsersLocators.AddUserPopup.block) @allure.step('Update user {username} info') @@ -261,7 +257,7 @@ def update_user_info( last_name: Optional[str] = None, email: Optional[str] = None, group: Optional[str] = None, - ): # pylint: disable-next=too-many-arguments + ): """Update some of fields for user""" if not (password or first_name or last_name or email or group): raise ValueError("You should provide at least one field's value to make an update") @@ -292,9 +288,23 @@ def update_user_info( else: raise AssertionError(f"There are no group {group} in select group popup") - self.scroll_to(AdminUsersLocators.AddUserPopup.save_btn).click() + self.scroll_to(AdminUsersLocators.AddUserPopup.save_update_btn).click() self.wait_element_hide(AdminUsersLocators.AddUserPopup.block) + @allure.step("Check user update is not allowed") + def check_user_update_is_not_allowed(self, username: str): + """Check that user can't be edited via UI""" + self.get_user_row_by_username(username).click() + self.wait_element_visible(AdminUsersLocators.AddUserPopup.block) + locators = AdminUsersLocators.AddUserPopup + assert not self.is_element_displayed(locators.save_update_btn, timeout=1), "Update button should not be visible" + # we don't check is_superuser and groups here because of their complex structure + # and minor effect of such check + for field in (locators.username, locators.first_name, locators.last_name, locators.email): + assert not self.find_element(field).is_enabled(), f"Field '{field.name}' should not be editable" + self.find_and_click(locators.cancel_btn) + self.wait_element_hide(locators.block) + @allure.step('Change password of user {username} to {password}') def change_user_password(self, username: str, password: str): """Change user password""" @@ -303,7 +313,7 @@ def change_user_password(self, username: str, password: str): self.wait_element_visible(AdminUsersLocators.AddUserPopup.block) self.send_text_to_element(AdminUsersLocators.AddUserPopup.password, password) self.send_text_to_element(AdminUsersLocators.AddUserPopup.password_confirm, password) - self.find_and_click(AdminUsersLocators.AddUserPopup.save_btn) + self.find_and_click(AdminUsersLocators.AddUserPopup.save_update_btn) self.wait_element_hide(AdminUsersLocators.AddUserPopup.block) @allure.step('Check that changing user group is prohibited') @@ -343,7 +353,7 @@ def is_disabled(locators: [Locator]): AdminUsersLocators.AddUserPopup.first_name, AdminUsersLocators.AddUserPopup.last_name, AdminUsersLocators.AddUserPopup.email, - AdminUsersLocators.AddUserPopup.save_btn, + AdminUsersLocators.AddUserPopup.save_update_btn, ] ) assert ( @@ -360,11 +370,10 @@ def delete_user(self, username: str): self.find_and_click(DeleteDialog.yes) self.wait_element_hide(DeleteDialog.body) - @allure.step('Check delete button is not presented for user {username}') - def check_delete_button_not_presented(self, username: str): + def is_delete_button_presented(self, username: str): """Check that delete button is not presented in user row""" - user_row = self.get_user_row_by_username(username) - assert not self.is_child_displayed(user_row, AdminUsersLocators.Row.delete_btn, timeout=3) + row = self.get_user_row_by_username(username) + return self.is_child_displayed(row, AdminUsersLocators.Row.delete_btn, timeout=1) @allure.step('Filter users by {filter_name}') def filter_users_by(self, filter_name: str, filter_option_name: str): @@ -432,12 +441,16 @@ def create_custom_group(self, name: str, description: Optional[str], users: Opti self.hover_element(user_chbx) user_chbx.click() self.find_and_click(AdminGroupsLocators.AddGroupPopup.users_select) - self.find_and_click(AdminGroupsLocators.save_btn) + self.find_and_click(AdminGroupsLocators.save_update_btn) self.wait_element_hide(AdminGroupsLocators.AddGroupPopup.block) @allure.step('Update group {name}') def update_group( - self, name: str, new_name: Optional[str] = None, description: Optional[str] = None, users: Optional[str] = None + self, + name: str, + new_name: Optional[str] = None, + description: Optional[str] = None, + users: Optional[str] = None, ): self.get_group_by_name(name).click() if new_name: @@ -456,7 +469,7 @@ def update_group( self.hover_element(user_chbx) user_chbx.click() self.find_and_click(AdminGroupsLocators.AddGroupPopup.users_select) - self.find_and_click(AdminGroupsLocators.save_btn) + self.find_and_click(AdminGroupsLocators.save_update_btn) self.wait_element_hide(AdminGroupsLocators.AddGroupPopup.block) def get_all_groups(self) -> [AdminGroupInfo]: @@ -507,7 +520,7 @@ def is_disabled(locators: [Locator]): [ AdminGroupsLocators.AddGroupPopup.name_input, AdminGroupsLocators.AddGroupPopup.description_input, - AdminGroupsLocators.save_btn, + AdminGroupsLocators.save_update_btn, ] ) assert "disabled" in self.find_element(AdminGroupsLocators.AddGroupPopup.users_select).get_attribute( @@ -564,7 +577,7 @@ def check_default_roles(self): description='', permissions='Create host, Upload bundle, Edit cluster configurations, Edit host configurations, ' 'Add service, Remove service, Remove hosts, Map hosts, Unmap hosts, Edit host-components, ' - 'Upgrade cluster bundle, Remove bundle, Service Administrator, Manage Maintenance mode', + 'Upgrade cluster bundle, Remove bundle, Service Administrator', ), AdminRoleInfo( name='Provider Administrator', @@ -643,17 +656,18 @@ def remove_permissions_in_add_role_popup( for permission in selected_permission: if permission_to_remove in permission.text: self.find_child( - permission, AdminRolesLocators.AddRolePopup.PermissionItemsBlock.PermissionItem.delete_btn + permission, + AdminRolesLocators.AddRolePopup.PermissionItemsBlock.PermissionItem.delete_btn, ).click() break def click_save_btn_in_role_popup(self): - self.find_and_click(AdminRolesLocators.save_btn) + self.find_and_click(AdminRolesLocators.save_update_btn) @allure.step('Check that save button is disabled') def check_save_button_disabled(self): assert ( - self.find_element(AdminRolesLocators.save_btn).get_attribute("disabled") == 'true' + self.find_element(AdminRolesLocators.save_update_btn).get_attribute("disabled") == 'true' ), "Save role button should be disabled" @allure.step("Check {error_message} error is presented") @@ -690,7 +704,12 @@ def open_create_policy_popup(self): @allure.step('Fill first step in new policy') def fill_first_step_in_policy_popup( - self, policy_name: str, description: Optional[str], role: str, users: Optional[str], groups: Optional[str] + self, + policy_name: str, + description: Optional[str], + role: str, + users: Optional[str], + groups: Optional[str], ): if not (users or groups): raise ValueError("There are should be users or groups in the policy") @@ -758,7 +777,9 @@ def fill_select(locator_select: Locator, locator_items: Locator, values: str): if clusters: fill_select( - AdminPoliciesLocators.AddPolicyPopup.SecondStep.cluster_select, AdminPoliciesLocators.item, clusters + AdminPoliciesLocators.AddPolicyPopup.SecondStep.cluster_select, + AdminPoliciesLocators.item, + clusters, ) self.find_and_click(AdminPoliciesLocators.AddPolicyPopup.SecondStep.cluster_select) if services: @@ -770,24 +791,32 @@ def fill_select(locator_select: Locator, locator_items: Locator, values: str): services, ) fill_select( - AdminPoliciesLocators.AddPolicyPopup.SecondStep.parent_select, AdminPoliciesLocators.item, parent + AdminPoliciesLocators.AddPolicyPopup.SecondStep.parent_select, + AdminPoliciesLocators.item, + parent, ) self.find_and_click(AdminPoliciesLocators.AddPolicyPopup.SecondStep.parent_select) if hosts: - fill_select(AdminPoliciesLocators.AddPolicyPopup.SecondStep.hosts_select, AdminPoliciesLocators.item, hosts) + fill_select( + AdminPoliciesLocators.AddPolicyPopup.SecondStep.hosts_select, + AdminPoliciesLocators.item, + hosts, + ) self.find_and_click(AdminPoliciesLocators.AddPolicyPopup.SecondStep.hosts_select) if providers: fill_select( - AdminPoliciesLocators.AddPolicyPopup.SecondStep.provider_select, AdminPoliciesLocators.item, providers + AdminPoliciesLocators.AddPolicyPopup.SecondStep.provider_select, + AdminPoliciesLocators.item, + providers, ) self.find_and_click(AdminPoliciesLocators.AddPolicyPopup.SecondStep.provider_select) self.find_and_click(AdminPoliciesLocators.AddPolicyPopup.SecondStep.next_btn_second) @allure.step('Fill third step in new policy') def fill_third_step_in_policy_popup(self): - self.wait_element_visible(AdminPoliciesLocators.save_btn) - self.find_and_click(AdminPoliciesLocators.save_btn) + self.wait_element_visible(AdminPoliciesLocators.save_update_btn) + self.find_and_click(AdminPoliciesLocators.save_update_btn) self.wait_element_hide(AdminPoliciesLocators.AddPolicyPopup.block) @allure.step('Create new policy') diff --git a/tests/ui_tests/app/page/bundle/locators.py b/tests/ui_tests/app/page/bundle/locators.py index 1edf4f17e7..d508463dfb 100644 --- a/tests/ui_tests/app/page/bundle/locators.py +++ b/tests/ui_tests/app/page/bundle/locators.py @@ -13,7 +13,6 @@ """Bundle page locators""" from selenium.webdriver.common.by import By - from tests.ui_tests.app.helpers.locator import Locator from tests.ui_tests.app.page.common.common_locators import ObjectPageLocators diff --git a/tests/ui_tests/app/page/bundle/page.py b/tests/ui_tests/app/page/bundle/page.py index 2a0c4d59a1..eb216901f3 100644 --- a/tests/ui_tests/app/page/bundle/page.py +++ b/tests/ui_tests/app/page/bundle/page.py @@ -13,9 +13,15 @@ """Bundle page PageObjects classes""" import allure - -from tests.ui_tests.app.page.bundle.locators import BundleLocators, BundleMainMenuLocators -from tests.ui_tests.app.page.common.base_page import BasePageObject, PageHeader, PageFooter +from tests.ui_tests.app.page.bundle.locators import ( + BundleLocators, + BundleMainMenuLocators, +) +from tests.ui_tests.app.page.common.base_page import ( + BasePageObject, + PageFooter, + PageHeader, +) from tests.ui_tests.app.page.common.configuration.page import CommonConfigMenuObj from tests.ui_tests.app.page.common.tooltip_links.page import CommonToolbar diff --git a/tests/ui_tests/app/page/bundle_list/locators.py b/tests/ui_tests/app/page/bundle_list/locators.py index 01eefab060..16406e3f1b 100644 --- a/tests/ui_tests/app/page/bundle_list/locators.py +++ b/tests/ui_tests/app/page/bundle_list/locators.py @@ -13,7 +13,6 @@ """Bundle List page locators""" from selenium.webdriver.common.by import By - from tests.ui_tests.app.helpers.locator import Locator from tests.ui_tests.app.page.common.table.locator import CommonTable @@ -39,11 +38,17 @@ class Row: description = Locator(By.CSS_SELECTOR, "mat-cell:nth-child(4)", "Bundle description in row") delete_btn = Locator(By.CSS_SELECTOR, "mat-cell:nth-child(5) button", "Bundle delete button in row") license_btn = Locator( - By.CSS_SELECTOR, "button[mattooltip='Accept license agreement']", "Licence warning button in row" + By.CSS_SELECTOR, + "button[mattooltip='Accept license agreement']", + "Licence warning button in row", ) class LicensePopup: """Bundle List page licence popup elements locators""" - block = Locator(By.XPATH, "//app-dialog[./h3[contains(text(), 'license')]]", "block with license agreement") + block = Locator( + By.XPATH, + "//app-dialog[./h3[contains(text(), 'license')]]", + "block with license agreement", + ) agree_btn = Locator(By.XPATH, "//button[./span[contains(text(), 'Yes')]]", "Agree button") diff --git a/tests/ui_tests/app/page/bundle_list/page.py b/tests/ui_tests/app/page/bundle_list/page.py index 36059af5e4..116584de17 100644 --- a/tests/ui_tests/app/page/bundle_list/page.py +++ b/tests/ui_tests/app/page/bundle_list/page.py @@ -16,14 +16,13 @@ import allure from selenium.webdriver.remote.webelement import WebElement - from tests.ui_tests.app.page.bundle_list.locators import BundleListLocators from tests.ui_tests.app.page.common.base_page import ( BasePageObject, - PageHeader, PageFooter, + PageHeader, ) -from tests.ui_tests.app.page.common.dialogs_locators import DeleteDialog +from tests.ui_tests.app.page.common.dialogs.locators import DeleteDialog from tests.ui_tests.app.page.common.table.page import CommonTableObj diff --git a/tests/ui_tests/app/page/cluster/locators.py b/tests/ui_tests/app/page/cluster/locators.py index c4e814183c..74850a518d 100644 --- a/tests/ui_tests/app/page/cluster/locators.py +++ b/tests/ui_tests/app/page/cluster/locators.py @@ -13,11 +13,10 @@ """Cluster page locators""" from selenium.webdriver.common.by import By - -from tests.ui_tests.app.helpers.locator import ( - Locator, +from tests.ui_tests.app.helpers.locator import Locator +from tests.ui_tests.app.page.common.host_components.locators import ( + HostComponentsLocators, ) -from tests.ui_tests.app.page.common.host_components.locators import HostComponentsLocators from tests.ui_tests.app.page.host_list.locators import HostListLocators @@ -56,7 +55,8 @@ class ServiceTableRow: actions = Locator(By.CSS_SELECTOR, "app-actions-button button", "Service actions") service_import = Locator(By.CSS_SELECTOR, "mat-cell:nth-child(6) button", "Service import") config = Locator(By.CSS_SELECTOR, "mat-cell:nth-child(7) button", "Service config") - delete_btn = Locator(By.CSS_SELECTOR, "mat-cell:nth-child(8) button", "Row delete button") + maintenance_mode = Locator(By.CSS_SELECTOR, "mat-cell:nth-child(8) button", "Maintenance mode button") + delete_btn = Locator(By.CSS_SELECTOR, "mat-cell:nth-child(9) button", "Row delete button") class ClusterHostLocators: @@ -67,14 +67,10 @@ class ClusterHostLocators: class HostTable(HostListLocators.HostTable): """Cluster host page host table elements locators""" - ... - class ClusterComponentsLocators(HostComponentsLocators): """Cluster components page elements locators""" - pass - class ClusterActionLocators: """Cluster action page elements locators""" diff --git a/tests/ui_tests/app/page/cluster/page.py b/tests/ui_tests/app/page/cluster/page.py index c046f1557c..986c87adde 100644 --- a/tests/ui_tests/app/page/cluster/page.py +++ b/tests/ui_tests/app/page/cluster/page.py @@ -14,34 +14,43 @@ from contextlib import contextmanager from dataclasses import dataclass -from typing import ( - List, - Optional, -) +from typing import List, Optional import allure from adcm_pytest_plugin.utils import wait_until_step_succeeds from selenium.webdriver.remote.webdriver import WebElement - from tests.ui_tests.app.page.cluster.locators import ( - ClusterServicesLocators, - ClusterHostLocators, ClusterComponentsLocators, + ClusterHostLocators, + ClusterServicesLocators, +) +from tests.ui_tests.app.page.common.base_page import ( + BaseDetailedPage, + BasePageObject, + PageFooter, + PageHeader, +) +from tests.ui_tests.app.page.common.common_locators import ( + ObjectPageLocators, + ObjectPageMenuLocators, ) -from tests.ui_tests.app.page.common.base_page import BasePageObject, PageHeader, PageFooter, BaseDetailedPage -from tests.ui_tests.app.page.common.common_locators import ObjectPageLocators, ObjectPageMenuLocators from tests.ui_tests.app.page.common.configuration.locators import CommonConfigMenu from tests.ui_tests.app.page.common.configuration.page import CommonConfigMenuObj -from tests.ui_tests.app.page.common.dialogs_locators import ActionDialog, DeleteDialog +from tests.ui_tests.app.page.common.dialogs.locators import ActionDialog, DeleteDialog from tests.ui_tests.app.page.common.group_config.page import CommonGroupConfigMenu -from tests.ui_tests.app.page.common.group_config_list.locators import GroupConfigListLocators +from tests.ui_tests.app.page.common.group_config_list.locators import ( + GroupConfigListLocators, +) from tests.ui_tests.app.page.common.group_config_list.page import GroupConfigList from tests.ui_tests.app.page.common.host_components.page import HostComponentsPage from tests.ui_tests.app.page.common.import_page.locators import ImportLocators from tests.ui_tests.app.page.common.import_page.page import ImportPage -from tests.ui_tests.app.page.common.popups.locator import HostAddPopupLocators -from tests.ui_tests.app.page.common.popups.locator import HostCreationLocators -from tests.ui_tests.app.page.common.popups.locator import PageConcernPopupLocators, ListConcernPopupLocators +from tests.ui_tests.app.page.common.popups.locator import ( + HostAddPopupLocators, + HostCreationLocators, + ListConcernPopupLocators, + PageConcernPopupLocators, +) from tests.ui_tests.app.page.common.popups.page import HostCreatePopupObj from tests.ui_tests.app.page.common.status.page import StatusPage from tests.ui_tests.app.page.common.table.locator import CommonTable @@ -58,7 +67,7 @@ class StatusGroupInfo: hosts: list -class ClusterPageMixin(BasePageObject): +class ClusterPageMixin(BasePageObject): # pylint: disable=too-many-instance-attributes """Helpers for working with cluster page""" # /action /main etc. @@ -513,7 +522,7 @@ class ClusterStatusPage(ClusterPageMixin, StatusPage): ] -class ClusterGroupConfigPageMixin(BasePageObject): +class ClusterGroupConfigPageMixin(BasePageObject): # pylint: disable-next=too-many-instance-attributes """Helpers for working with cluster group config page""" MENU_SUFFIX: str diff --git a/tests/ui_tests/app/page/cluster_list/locators.py b/tests/ui_tests/app/page/cluster_list/locators.py index 88ad8bbe4e..d91fbe5089 100644 --- a/tests/ui_tests/app/page/cluster_list/locators.py +++ b/tests/ui_tests/app/page/cluster_list/locators.py @@ -13,7 +13,6 @@ """Cluster List page locators""" from selenium.webdriver.common.by import By - from tests.ui_tests.app.helpers.locator import Locator from tests.ui_tests.app.page.common.table.locator import CommonTable @@ -59,9 +58,14 @@ class ClusterRow: upgrade = Locator(By.CSS_SELECTOR, "app-upgrade button", "Cluster upgrade in row") config = Locator(By.CSS_SELECTOR, "mat-cell:nth-child(9) button", "Cluster config in row") delete_btn = Locator(By.CSS_SELECTOR, "mat-cell:last-of-type button", "Cluster delete button in row") + rename_btn = Locator(By.CLASS_NAME, "rename-button", "Cluster rename button in row") class LicensePopup: """Cluster List page licence popup elements locators""" - block = Locator(By.XPATH, "//app-dialog[./h3[contains(text(), 'license')]]", "block with license agreement") + block = Locator( + By.XPATH, + "//app-dialog[./h3[contains(text(), 'license')]]", + "block with license agreement", + ) agree_btn = Locator(By.XPATH, "//button[./span[contains(text(), 'Yes')]]", "Agree button") diff --git a/tests/ui_tests/app/page/cluster_list/page.py b/tests/ui_tests/app/page/cluster_list/page.py index 678d500686..894c5c1be3 100644 --- a/tests/ui_tests/app/page/cluster_list/page.py +++ b/tests/ui_tests/app/page/cluster_list/page.py @@ -18,32 +18,27 @@ from adcm_pytest_plugin.utils import wait_until_step_succeeds from selenium.common.exceptions import TimeoutException from selenium.webdriver.remote.webdriver import WebElement - -from tests.ui_tests.app.page.cluster.locators import ( - ClusterComponentsLocators, -) +from tests.ui_tests.app.page.cluster.locators import ClusterComponentsLocators from tests.ui_tests.app.page.cluster_list.locators import ClusterListLocators from tests.ui_tests.app.page.common.base_page import ( BasePageObject, - PageHeader, PageFooter, + PageHeader, ) from tests.ui_tests.app.page.common.configuration.locators import CommonConfigMenu from tests.ui_tests.app.page.common.configuration.page import CommonConfigMenuObj -from tests.ui_tests.app.page.common.dialogs_locators import ( - ActionDialog, - DeleteDialog, -) +from tests.ui_tests.app.page.common.dialogs.locators import ActionDialog, DeleteDialog +from tests.ui_tests.app.page.common.dialogs.rename import RenameDialog from tests.ui_tests.app.page.common.host_components.page import HostComponentsPage from tests.ui_tests.app.page.common.popups.locator import ( HostCreationLocators, + ListConcernPopupLocators, ) -from tests.ui_tests.app.page.common.popups.locator import ListConcernPopupLocators from tests.ui_tests.app.page.common.popups.page import HostCreatePopupObj from tests.ui_tests.app.page.common.table.page import CommonTableObj -class ClusterListPage(BasePageObject): +class ClusterListPage(BasePageObject): # pylint: disable=too-many-public-methods """Cluster List Page class""" def __init__(self, driver, base_url): @@ -78,10 +73,10 @@ def upload_bundle_from_cluster_create_popup(self, bundle: str): self.find_and_click(popup.cancel_btn) self.wait_element_hide(popup.block) - def get_cluster_info_from_row(self, row: int) -> dict: + def get_cluster_info_from_row(self, row_pos: int) -> dict: """Get Cluster info from Cluster List row""" row_elements = ClusterListLocators.ClusterTable.ClusterRow - cluster_row = self.table.get_all_rows()[row] + cluster_row = self.table.get_all_rows()[row_pos] return { "name": self.find_child(cluster_row, row_elements.name).text, "bundle": self.find_child(cluster_row, row_elements.bundle).text, @@ -171,6 +166,14 @@ def delete_cluster_by_row(self, row: WebElement): self.find_and_click(DeleteDialog.yes) self.wait_element_hide(DeleteDialog.body) + @allure.step("Open cluster rename dialog by clicking on cluster rename button") + def open_rename_cluster_dialog(self, row: WebElement) -> RenameDialog: + self.hover_element(row) + self.find_child(row, self.table.locators.ClusterRow.rename_btn).click() + dialog = RenameDialog(driver=self.driver, base_url=self.base_url) + dialog.wait_opened() + return dialog + @allure.step("Run upgrade {upgrade_name} for cluster from row") def run_upgrade_in_cluster_row( self, @@ -220,7 +223,8 @@ def run_upgrade_in_cluster_row( self.wait_element_visible(comp_row_name) comp_row_name.click() self.find_child( - self.find_elements(ClusterComponentsLocators.host_row)[0], ClusterComponentsLocators.Row.name + self.find_elements(ClusterComponentsLocators.host_row)[0], + ClusterComponentsLocators.Row.name, ).click() self.find_and_click(ActionDialog.run) self.wait_element_hide(ActionDialog.body) diff --git a/tests/ui_tests/app/page/common/base_page.py b/tests/ui_tests/app/page/common/base_page.py index c8355c1039..ff0402f235 100644 --- a/tests/ui_tests/app/page/common/base_page.py +++ b/tests/ui_tests/app/page/common/base_page.py @@ -13,34 +13,30 @@ """The most basic PageObject classes""" from contextlib import contextmanager -from typing import ( - Optional, - List, - Union, - Callable, -) +from typing import Callable, List, Optional, Union import allure from adcm_pytest_plugin.utils import wait_until_step_succeeds -from selenium.common.exceptions import ElementClickInterceptedException from selenium.common.exceptions import ( + ElementClickInterceptedException, NoSuchElementException, StaleElementReferenceException, TimeoutException, ) from selenium.webdriver.common.action_chains import ActionChains from selenium.webdriver.common.keys import Keys -from selenium.webdriver.remote.webdriver import WebDriver -from selenium.webdriver.remote.webdriver import WebElement +from selenium.webdriver.remote.webdriver import WebDriver, WebElement from selenium.webdriver.support import expected_conditions as EC from selenium.webdriver.support.ui import WebDriverWait as WDW - from tests.ui_tests.app.helpers.locator import Locator -from tests.ui_tests.app.page.common.common_locators import CommonLocators, ObjectPageLocators +from tests.ui_tests.app.page.common.common_locators import ( + CommonLocators, + ObjectPageLocators, +) from tests.ui_tests.app.page.common.footer_locators import CommonFooterLocators from tests.ui_tests.app.page.common.header_locators import ( - CommonHeaderLocators, AuthorizedHeaderLocators, + CommonHeaderLocators, ) from tests.ui_tests.app.page.common.popups.locator import CommonPopupLocators from tests.ui_tests.app.page.common.tooltip_links.locator import CommonToolbarLocators @@ -183,7 +179,7 @@ def find_children(self, element: WebElement, child: Locator, timeout: int = None except TimeoutException: return [] - def find_elements(self, locator: Locator, timeout: int = None) -> [WebElement]: + def find_elements(self, locator: Locator, timeout: int = None) -> list[WebElement]: """Find elements on current page.""" loc_timeout = timeout or self.default_loc_timeout @@ -231,7 +227,7 @@ def check_element_should_be_hidden( try: self.wait_element_hide(element, timeout) except TimeoutException as e: - raise AssertionError(e.msg) + raise AssertionError(e.msg) from e def check_element_should_be_visible( self, element: Union[Locator, WebElement], timeout: Optional[int] = None @@ -240,7 +236,7 @@ def check_element_should_be_visible( try: self.wait_element_visible(element, timeout) except TimeoutException as e: - raise AssertionError(e.msg) + raise AssertionError(e.msg) from e def find_and_click(self, locator: Locator, is_js: bool = False, timeout: int = None) -> None: """Find element on current page and click on it.""" @@ -332,7 +328,11 @@ def _assert_page_is_opened(): @allure.step('Write text to input element: "{text}"') def send_text_to_element( - self, element: Union[Locator, WebElement], text: str, clean_input: bool = True, timeout: Optional[int] = None + self, + element: Union[Locator, WebElement], + text: str, + clean_input: bool = True, + timeout: Optional[int] = None, ): """ Writes text to input element found by locator @@ -402,6 +402,7 @@ def _is_displayed(find_element_func: Callable[[], WebElement]) -> bool: NoSuchElementException, StaleElementReferenceException, TimeoutError, + AssertionError, ): return False diff --git a/tests/ui_tests/app/page/common/common_locators.py b/tests/ui_tests/app/page/common/common_locators.py index c5e8ccaf89..32115b1b33 100644 --- a/tests/ui_tests/app/page/common/common_locators.py +++ b/tests/ui_tests/app/page/common/common_locators.py @@ -13,7 +13,6 @@ """Common locators""" from selenium.webdriver.common.by import By - from tests.ui_tests.app.helpers.locator import Locator diff --git a/tests/ui_tests/app/page/common/configuration/fields.py b/tests/ui_tests/app/page/common/configuration/fields.py index 1826ac3c49..45f8b4bc1b 100644 --- a/tests/ui_tests/app/page/common/configuration/fields.py +++ b/tests/ui_tests/app/page/common/configuration/fields.py @@ -16,9 +16,8 @@ from contextlib import contextmanager from typing import Dict -from selenium.webdriver.remote.webdriver import WebElement from adcm_pytest_plugin.utils import wait_until_step_succeeds - +from selenium.webdriver.remote.webdriver import WebElement from tests.ui_tests.app.page.common.base_page import BasePageObject from tests.ui_tests.app.page.common.configuration.locators import CommonConfigMenu diff --git a/tests/ui_tests/app/page/common/configuration/locators.py b/tests/ui_tests/app/page/common/configuration/locators.py index 3fe5c5f095..75399952e3 100644 --- a/tests/ui_tests/app/page/common/configuration/locators.py +++ b/tests/ui_tests/app/page/common/configuration/locators.py @@ -13,7 +13,6 @@ """Configuration page locators""" from selenium.webdriver.common.by import By - from tests.ui_tests.app.helpers.locator import Locator, TemplateLocator @@ -29,7 +28,9 @@ class CommonConfigMenu: By.CSS_SELECTOR, "app-search button[aria-label='Clear']", "Clear search input button" ) description_input = Locator( - By.CSS_SELECTOR, "input[data-placeholder='Description configuration']", "Config description input" + By.CSS_SELECTOR, + "input[data-placeholder='Description configuration']", + "Config description input", ) save_btn = Locator(By.XPATH, "//button[.//span[text()='Save']]", "Save configuration button") history_btn = Locator(By.XPATH, "//button[.//mat-icon[text()='history']]", "History button") @@ -49,7 +50,9 @@ class CommonConfigMenu: text_row = Locator(By.TAG_NAME, "app-fields-textbox", "Configuration textbox row") field_error = TemplateLocator(By.XPATH, "//mat-error[contains(text(), '{}')]", 'Error "{}"') info_tooltip_icon = TemplateLocator( - By.XPATH, "//div[contains(@adcm_test, '{}')]//mat-icon[@mattooltipclass='info-tooltip']", 'info tooltip "{}"' + By.XPATH, + "//div[contains(@adcm_test, '{}')]//mat-icon[@mattooltipclass='info-tooltip']", + 'info tooltip "{}"', ) tooltip_text = Locator(By.CSS_SELECTOR, "mat-tooltip-component div", "Tooltip text") loading_text = Locator(By.XPATH, "//span[text()='Loading...']", "Loading text") @@ -60,7 +63,11 @@ class ConfigRow: name = Locator(By.CSS_SELECTOR, "label:not(.mat-checkbox-layout)", "Row name") value = Locator(By.CSS_SELECTOR, "input:not([type='checkbox']),textarea", "Row value") - input = Locator(By.CSS_SELECTOR, '*:not([style="display: none;"])>mat-form-field input,textarea', "Row input") + input = Locator( + By.CSS_SELECTOR, + '*:not([style="display: none;"])>mat-form-field input,textarea', + "Row input", + ) password = Locator( By.XPATH, "(.//app-fields-password/div[not(contains(@style, 'none'))]//input)[1]", @@ -78,7 +85,9 @@ class ConfigRow: # complex parameters add_item_btn = Locator( - By.XPATH, ".//button//mat-icon[text()='add_circle_outline']", "Add item to parameter button" + By.XPATH, + ".//button//mat-icon[text()='add_circle_outline']", + "Add item to parameter button", ) map_item = Locator(By.CSS_SELECTOR, "div.item", "Map parameter item") map_input_key = Locator(By.XPATH, ".//input[@formcontrolname='key']", "Map input key input") diff --git a/tests/ui_tests/app/page/common/configuration/page.py b/tests/ui_tests/app/page/common/configuration/page.py index b10ac164f0..4b1a55404f 100644 --- a/tests/ui_tests/app/page/common/configuration/page.py +++ b/tests/ui_tests/app/page/common/configuration/page.py @@ -13,24 +13,18 @@ """Config page PageObjects classes""" from contextlib import contextmanager from dataclasses import dataclass -from typing import ( - List, - Collection, - Optional, - Union, -) +from typing import Collection, List, Optional, Union import allure from adcm_pytest_plugin.utils import wait_until_step_succeeds from selenium.common.exceptions import TimeoutException from selenium.webdriver.common.keys import Keys from selenium.webdriver.remote.webdriver import WebElement - from tests.ui_tests.app.page.common.base_page import BasePageObject from tests.ui_tests.app.page.common.common_locators import ( - ObjectPageMenuLocators, CommonLocators, ObjectPageLocators, + ObjectPageMenuLocators, ) from tests.ui_tests.app.page.common.configuration.fields import ConfigFieldsManipulator from tests.ui_tests.app.page.common.configuration.locators import CommonConfigMenu @@ -485,10 +479,10 @@ def check_subs_visibility(self, group_name: str, is_visible: bool = True): for item in item_rows: self.check_element_should_be_hidden(item) - def get_group_names(self): + def get_group_names(self, timeout: int = 2): """Wait for group elements to be displayed and get them""" try: - return self.find_elements(self.locators.ConfigGroup.name, timeout=2) + return self.find_elements(self.locators.ConfigGroup.name, timeout=timeout) except TimeoutException: return [] @@ -510,9 +504,9 @@ def _wait_changing_rows_amount(): wait_until_step_succeeds(_wait_changing_rows_amount, period=1, timeout=10) @allure.step('Check that there are no rows or groups on config page') - def check_no_rows_or_groups_on_page(self): - assert len(self.get_group_names()) == 0, "Config group should not be visible" - assert len(self.get_all_config_rows(timeout=1)) == 0, "There should not be any rows" + def check_no_rows_or_groups_on_page(self, timeout=1): + assert len(self.get_group_names(timeout)) == 0, "Config group should not be visible" + assert len(self.get_all_config_rows(timeout=timeout)) == 0, "There should not be any rows" @allure.step('Check that there are no rows or groups on config page with advanced settings') def check_no_rows_or_groups_on_page_with_advanced(self): @@ -588,13 +582,15 @@ def check_import_warn_icon_on_left_menu(self): @allure.step("Check warn icon on the left menu Host-Components element") def check_hostcomponents_warn_icon_on_left_menu(self): assert self.is_child_displayed( - self.find_element(ObjectPageMenuLocators.components_tab), ObjectPageMenuLocators.warn_icon + self.find_element(ObjectPageMenuLocators.components_tab), + ObjectPageMenuLocators.warn_icon, ), "No warn icon near Host-Components left menu element" @allure.step("Check warn icon on the left menu Host-Components element") def check_service_components_warn_icon_on_left_menu(self): assert self.is_child_displayed( - self.find_element(ObjectPageMenuLocators.service_components_tab), ObjectPageMenuLocators.warn_icon + self.find_element(ObjectPageMenuLocators.service_components_tab), + ObjectPageMenuLocators.warn_icon, ), "No warn icon near Host-Components left menu element" @allure.step("Fill config page with test values") @@ -678,7 +674,8 @@ def check_config_fields_history_with_test_values(self): self.check_group_is_active("group", is_active=False) with allure.step("Check history value in structure type"): self.wait_history_row_with_value( - self.get_config_row("structure"), '[{"code":1,"country":"Test1"},{"code":2,"country":"Test2"}]' + self.get_config_row("structure"), + '[{"code":1,"country":"Test1"},{"code":2,"country":"Test2"}]', ) with allure.step("Check history value in map type"): self.wait_history_row_with_value(self.get_config_row("map"), '{"age":"24","name":"Joe","sex":"m"}') diff --git a/tests/ui_tests/app/page/common/dialogs_locators.py b/tests/ui_tests/app/page/common/dialogs/locators.py similarity index 70% rename from tests/ui_tests/app/page/common/dialogs_locators.py rename to tests/ui_tests/app/page/common/dialogs/locators.py index aa4cf2827b..23d04e844b 100644 --- a/tests/ui_tests/app/page/common/dialogs_locators.py +++ b/tests/ui_tests/app/page/common/dialogs/locators.py @@ -13,21 +13,27 @@ """Dialog locators""" from selenium.webdriver.common.by import By - from tests.ui_tests.app.helpers.locator import Locator -class DeleteDialog: - """Delete Dialog class""" +class Dialog: + """Generic dialog""" body = Locator(By.CSS_SELECTOR, "mat-dialog-container", "Dialog with choices") - yes = Locator(By.XPATH, "//button//span[contains(text(), 'Yes')]", "Yes button in delete dialog") -class ActionDialog: - """Action Dialog class""" +class RenameDialogLocators(Dialog): + object_name = Locator(By.TAG_NAME, "input", "Object name to set") + error = Locator(By.TAG_NAME, "mat-error", "Error message") + save = Locator(By.XPATH, "//button//span[contains(text(), 'Save')]", "Save button in rename dialog") + cancel = Locator(By.XPATH, "//button//span[contains(text(), 'Cancel')]", "Cancel button in rename dialog") - body = Locator(By.CSS_SELECTOR, "mat-dialog-container", "Dialog with choices") + +class DeleteDialog(Dialog): + yes = Locator(By.XPATH, "//button//span[contains(text(), 'Yes')]", "Yes button in delete dialog") + + +class ActionDialog(Dialog): text = Locator(By.CSS_SELECTOR, "app-dialog mat-dialog-content", "Dialog content") next_btn = Locator(By.CSS_SELECTOR, ".mat-stepper-next", "Next button in action dialog") run = Locator(By.CSS_SELECTOR, "app-dialog button[color='accent']", "Run button in action dialog") diff --git a/tests/ui_tests/app/page/common/dialogs/rename.py b/tests/ui_tests/app/page/common/dialogs/rename.py new file mode 100644 index 0000000000..a97e0e6b2f --- /dev/null +++ b/tests/ui_tests/app/page/common/dialogs/rename.py @@ -0,0 +1,44 @@ +import allure +from selenium.common import TimeoutException +from tests.ui_tests.app.page.common.base_page import BasePageObject +from tests.ui_tests.app.page.common.dialogs.locators import RenameDialogLocators + +WAIT_TIMEOUT = 0.5 +MESSAGE_WAIT_TIMEOUT = 1 + + +class RenameDialog(BasePageObject): + def wait_opened(self): + self.wait_element_visible(RenameDialogLocators.body) + + @allure.step("Set new name/fqdn in rename dialog") + def set_new_name_in_rename_dialog(self, new_name: str) -> None: + dialog = self.find_element(RenameDialogLocators.body, timeout=WAIT_TIMEOUT) + name_input = self.find_child(dialog, RenameDialogLocators.object_name) + name_input.clear() + name_input.send_keys(new_name) + + @allure.step("Click 'Save' button on rename dialog") + def click_save_on_rename_dialog(self): + dialog = self.find_element(RenameDialogLocators.body, timeout=WAIT_TIMEOUT) + self.find_child(dialog, RenameDialogLocators.save).click() + self.wait_element_hide(RenameDialogLocators.body) + + @allure.step("Click 'Cancel' button on rename dialog") + def click_cancel_on_rename_dialog(self): + dialog = self.find_element(RenameDialogLocators.body, timeout=WAIT_TIMEOUT) + self.find_child(dialog, RenameDialogLocators.cancel).click() + self.wait_element_hide(RenameDialogLocators.body) + + def is_dialog_error_message_visible(self): + self.wait_element_visible(RenameDialogLocators.body, timeout=WAIT_TIMEOUT) + try: + self.wait_element_visible(RenameDialogLocators.error, timeout=MESSAGE_WAIT_TIMEOUT) + except TimeoutException: + return False + return True + + def get_dialog_error_message(self): + dialog = self.wait_element_visible(RenameDialogLocators.body, timeout=WAIT_TIMEOUT) + error = self.find_child(dialog, RenameDialogLocators.error, timeout=WAIT_TIMEOUT) + return error.text diff --git a/tests/ui_tests/app/page/common/footer_locators.py b/tests/ui_tests/app/page/common/footer_locators.py index c8b1b210ae..44e17fc915 100644 --- a/tests/ui_tests/app/page/common/footer_locators.py +++ b/tests/ui_tests/app/page/common/footer_locators.py @@ -13,7 +13,6 @@ """Footer locators""" from selenium.webdriver.common.by import By - from tests.ui_tests.app.helpers.locator import Locator diff --git a/tests/ui_tests/app/page/common/group_config/locators.py b/tests/ui_tests/app/page/common/group_config/locators.py index 6d21fdadb0..2f85bd2047 100644 --- a/tests/ui_tests/app/page/common/group_config/locators.py +++ b/tests/ui_tests/app/page/common/group_config/locators.py @@ -13,7 +13,6 @@ """Group Configuration list page locators""" from selenium.webdriver.common.by import By - from tests.ui_tests.app.helpers.locator import Locator diff --git a/tests/ui_tests/app/page/common/group_config/page.py b/tests/ui_tests/app/page/common/group_config/page.py index a41f5caf5a..15ed9633a5 100644 --- a/tests/ui_tests/app/page/common/group_config/page.py +++ b/tests/ui_tests/app/page/common/group_config/page.py @@ -12,15 +12,12 @@ """Config page PageObjects classes""" -from typing import ( - List, -) +from typing import List import allure from adcm_pytest_plugin.utils import wait_until_step_succeeds from selenium.common.exceptions import TimeoutException from selenium.webdriver.remote.webdriver import WebElement - from tests.ui_tests.app.page.common.base_page import BasePageObject from tests.ui_tests.app.page.common.group_config.locators import GroupConfigLocators @@ -34,7 +31,6 @@ def __init__(self, driver, base_url, config_class_locators=GroupConfigLocators): def is_customization_chbx_disabled(self, row: WebElement) -> bool: """Check if customization checkbox is disabled""" - return 'mat-checkbox-disabled' in str( self.find_child(row, self.locators.customization_chbx).get_attribute("class") ) diff --git a/tests/ui_tests/app/page/common/group_config_list/locators.py b/tests/ui_tests/app/page/common/group_config_list/locators.py index 079a16b416..02ba0e2bb7 100644 --- a/tests/ui_tests/app/page/common/group_config_list/locators.py +++ b/tests/ui_tests/app/page/common/group_config_list/locators.py @@ -13,7 +13,6 @@ """Group Configuration list page locators""" from selenium.webdriver.common.by import By - from tests.ui_tests.app.helpers.locator import Locator diff --git a/tests/ui_tests/app/page/common/group_config_list/page.py b/tests/ui_tests/app/page/common/group_config_list/page.py index 816d997083..1795df644b 100644 --- a/tests/ui_tests/app/page/common/group_config_list/page.py +++ b/tests/ui_tests/app/page/common/group_config_list/page.py @@ -14,19 +14,17 @@ from contextlib import contextmanager from dataclasses import dataclass -from typing import ( - List, - Optional, -) +from typing import List, Optional import allure from adcm_pytest_plugin.utils import wait_until_step_succeeds from selenium.common.exceptions import TimeoutException from selenium.webdriver.remote.webdriver import WebElement - from tests.ui_tests.app.page.common.base_page import BasePageObject -from tests.ui_tests.app.page.common.dialogs_locators import DeleteDialog -from tests.ui_tests.app.page.common.group_config_list.locators import GroupConfigListLocators +from tests.ui_tests.app.page.common.dialogs.locators import DeleteDialog +from tests.ui_tests.app.page.common.group_config_list.locators import ( + GroupConfigListLocators, +) @dataclass @@ -69,7 +67,9 @@ def create_group(self, name: str, description: str): self.wait_element_visible(GroupConfigListLocators.CreateGroupPopup.block) self.send_text_to_element(GroupConfigListLocators.CreateGroupPopup.name_input, name, clean_input=True) self.send_text_to_element( - GroupConfigListLocators.CreateGroupPopup.description_input, description, clean_input=True + GroupConfigListLocators.CreateGroupPopup.description_input, + description, + clean_input=True, ) self.find_and_click(GroupConfigListLocators.CreateGroupPopup.create_btn) self.wait_element_hide(GroupConfigListLocators.CreateGroupPopup.block) @@ -97,9 +97,3 @@ def delete_row(self, row: WebElement): self.wait_element_visible(DeleteDialog.body) self.find_and_click(DeleteDialog.yes) self.wait_element_hide(DeleteDialog.body) - - @allure.step("Create {group_amount} groups") - def create_few_groups(self, group_amount: int): - for i in range(group_amount): - with self.wait_rows_change(): - self.create_group(name=f"Test name_{i}", description='Test description') diff --git a/tests/ui_tests/app/page/common/header_locators.py b/tests/ui_tests/app/page/common/header_locators.py index ae2b2e3a9c..366ff9b0bc 100644 --- a/tests/ui_tests/app/page/common/header_locators.py +++ b/tests/ui_tests/app/page/common/header_locators.py @@ -13,7 +13,6 @@ """Header locators""" from selenium.webdriver.common.by import By - from tests.ui_tests.app.helpers.locator import Locator diff --git a/tests/ui_tests/app/page/common/host_components/locators.py b/tests/ui_tests/app/page/common/host_components/locators.py index 3dd339b6d8..836b122617 100644 --- a/tests/ui_tests/app/page/common/host_components/locators.py +++ b/tests/ui_tests/app/page/common/host_components/locators.py @@ -13,7 +13,6 @@ """Cluster List page locators""" from selenium.webdriver.common.by import By - from tests.ui_tests.app.helpers.locator import Locator diff --git a/tests/ui_tests/app/page/common/host_components/page.py b/tests/ui_tests/app/page/common/host_components/page.py index 2f78c5c146..71a89dc473 100644 --- a/tests/ui_tests/app/page/common/host_components/page.py +++ b/tests/ui_tests/app/page/common/host_components/page.py @@ -16,11 +16,10 @@ import allure from selenium.webdriver.remote.webdriver import WebElement - -from tests.ui_tests.app.page.common.base_page import ( - BasePageObject, +from tests.ui_tests.app.page.common.base_page import BasePageObject +from tests.ui_tests.app.page.common.host_components.locators import ( + HostComponentsLocators, ) -from tests.ui_tests.app.page.common.host_components.locators import HostComponentsLocators from tests.ui_tests.app.page.common.popups.locator import HostCreationLocators diff --git a/tests/ui_tests/app/page/common/import_page/locators.py b/tests/ui_tests/app/page/common/import_page/locators.py index e3466fcd71..d0c24b8bbe 100644 --- a/tests/ui_tests/app/page/common/import_page/locators.py +++ b/tests/ui_tests/app/page/common/import_page/locators.py @@ -13,7 +13,6 @@ """Common import page locators""" from selenium.webdriver.common.by import By - from tests.ui_tests.app.helpers.locator import Locator diff --git a/tests/ui_tests/app/page/common/import_page/page.py b/tests/ui_tests/app/page/common/import_page/page.py index e9920d90af..7a1eecee46 100644 --- a/tests/ui_tests/app/page/common/import_page/page.py +++ b/tests/ui_tests/app/page/common/import_page/page.py @@ -14,7 +14,6 @@ from dataclasses import dataclass from selenium.webdriver.remote.webdriver import WebElement - from tests.ui_tests.app.page.common.base_page import BasePageObject from tests.ui_tests.app.page.common.import_page.locators import ImportLocators diff --git a/tests/ui_tests/app/page/common/popups/locator.py b/tests/ui_tests/app/page/common/popups/locator.py index 7d0e1052ae..43b1cd41b5 100644 --- a/tests/ui_tests/app/page/common/popups/locator.py +++ b/tests/ui_tests/app/page/common/popups/locator.py @@ -13,11 +13,7 @@ """Popup page locators""" from selenium.webdriver.common.by import By - -from tests.ui_tests.app.helpers.locator import ( - Locator, - TemplateLocator, -) +from tests.ui_tests.app.helpers.locator import Locator, TemplateLocator class CommonPopupLocators: @@ -63,13 +59,16 @@ class Cluster: cluster_select = Locator(By.CSS_SELECTOR, "mat-select[formcontrolname='cluster_id']", "Cluster choice select") cluster_option = TemplateLocator(By.XPATH, "//mat-option//span[text()='{}']", "Cluster select option") + chosen_cluster = TemplateLocator(By.XPATH, "//span[text()='{}']", "Chosen parent cluster") class HostAddPopupLocators: """Host add popup locators""" add_new_host_btn = Locator( - By.CSS_SELECTOR, "div[class*='actions'] button[cdk-describedby-host]", "Button to open popup for host creating" + By.CSS_SELECTOR, + "div[class*='actions'] button[cdk-describedby-host]", + "Button to open popup for host creating", ) diff --git a/tests/ui_tests/app/page/common/popups/page.py b/tests/ui_tests/app/page/common/popups/page.py index 3396638141..7a9c28ec94 100644 --- a/tests/ui_tests/app/page/common/popups/page.py +++ b/tests/ui_tests/app/page/common/popups/page.py @@ -17,11 +17,8 @@ import allure from selenium.webdriver.support import expected_conditions as EC from selenium.webdriver.support.ui import WebDriverWait as WDW - from tests.ui_tests.app.helpers.locator import Locator -from tests.ui_tests.app.page.common.base_page import ( - BasePageObject, -) +from tests.ui_tests.app.page.common.base_page import BasePageObject from tests.ui_tests.app.page.common.popups.locator import HostCreationLocators @@ -70,6 +67,7 @@ def choose_cluster_in_popup(self, cluster_name: str): option = HostCreationLocators.Cluster.cluster_option self.wait_and_click_on_cluster_option(cluster_name, option) self.wait_element_hide(option) + self.check_element_should_be_visible(HostCreationLocators.Cluster.chosen_cluster(cluster_name)) def wait_and_click_on_cluster_option(self, cluster_name: str, option_locator: Locator): """Wait for cluster and click on it""" diff --git a/tests/ui_tests/app/page/common/status/locators.py b/tests/ui_tests/app/page/common/status/locators.py index e5c2795bb1..1d7969ff84 100644 --- a/tests/ui_tests/app/page/common/status/locators.py +++ b/tests/ui_tests/app/page/common/status/locators.py @@ -13,10 +13,7 @@ """Cluster page locators""" from selenium.webdriver.common.by import By - -from tests.ui_tests.app.helpers.locator import ( - Locator, -) +from tests.ui_tests.app.helpers.locator import Locator class StatusLocators: diff --git a/tests/ui_tests/app/page/common/status/page.py b/tests/ui_tests/app/page/common/status/page.py index 45125467a7..2e7519c014 100644 --- a/tests/ui_tests/app/page/common/status/page.py +++ b/tests/ui_tests/app/page/common/status/page.py @@ -17,7 +17,6 @@ import allure from adcm_pytest_plugin.utils import wait_until_step_succeeds from selenium.webdriver.remote.webdriver import WebElement - from tests.ui_tests.app.helpers.locator import Locator from tests.ui_tests.app.page.common.base_page import BasePageObject from tests.ui_tests.app.page.common.status.locators import StatusLocators diff --git a/tests/ui_tests/app/page/common/table/locator.py b/tests/ui_tests/app/page/common/table/locator.py index 6d9b7b9c4a..26c0f38ab0 100644 --- a/tests/ui_tests/app/page/common/table/locator.py +++ b/tests/ui_tests/app/page/common/table/locator.py @@ -13,11 +13,7 @@ """Table page locators""" from selenium.webdriver.common.by import By - -from tests.ui_tests.app.helpers.locator import ( - Locator, - TemplateLocator, -) +from tests.ui_tests.app.helpers.locator import Locator, TemplateLocator class CommonTable: @@ -34,7 +30,9 @@ class ActionPopup: block = Locator(By.CSS_SELECTOR, "div[role='menu']", "Action popup block") button = TemplateLocator( - By.XPATH, "//button[@adcm_test='action_btn' and ./span[text()='{}']]", "Button with action {}" + By.XPATH, + "//button[@adcm_test='action_btn' and ./span[text()='{}']]", + "Button with action {}", ) action_buttons = Locator(By.CSS_SELECTOR, "button[adcm_test='action_btn']", "Button with action") diff --git a/tests/ui_tests/app/page/common/table/page.py b/tests/ui_tests/app/page/common/table/page.py index 484ee9017e..7406d8294c 100644 --- a/tests/ui_tests/app/page/common/table/page.py +++ b/tests/ui_tests/app/page/common/table/page.py @@ -15,13 +15,11 @@ from contextlib import contextmanager import allure - from adcm_pytest_plugin.utils import wait_until_step_succeeds from selenium.common.exceptions import TimeoutException +from selenium.webdriver.remote.webelement import WebElement from selenium.webdriver.support import expected_conditions as EC from selenium.webdriver.support.ui import WebDriverWait as WDW -from selenium.webdriver.remote.webelement import WebElement - from tests.ui_tests.app.page.common.base_page import BasePageObject from tests.ui_tests.app.page.common.table.locator import CommonTable from tests.ui_tests.app.page.common.tooltip_links.locator import CommonToolbarLocators diff --git a/tests/ui_tests/app/page/common/tooltip_links/locator.py b/tests/ui_tests/app/page/common/tooltip_links/locator.py index a1f797a75c..40017af5c1 100644 --- a/tests/ui_tests/app/page/common/tooltip_links/locator.py +++ b/tests/ui_tests/app/page/common/tooltip_links/locator.py @@ -13,11 +13,7 @@ """Tooltip page locators""" from selenium.webdriver.common.by import By - -from tests.ui_tests.app.helpers.locator import ( - Locator, - TemplateLocator, -) +from tests.ui_tests.app.helpers.locator import Locator, TemplateLocator class CommonToolbarLocators: @@ -29,7 +25,9 @@ class CommonToolbarLocators: text_link = TemplateLocator(By.XPATH, "//a[text()='{}']", "Link to {}") action_btn = TemplateLocator(By.XPATH, "//span[.//a[text()='{}']]//app-action-list/button", "Action button to {}") adcm_action_btn = Locator( - By.XPATH, "//mat-nav-list[./a[@routerlink='/admin']]//app-action-list/button", "Action button to adcm" + By.XPATH, + "//mat-nav-list[./a[@routerlink='/admin']]//app-action-list/button", + "Action button to adcm", ) upgrade_btn = TemplateLocator(By.XPATH, "//*[.//a[text()='{}']]//app-upgrade/button", "Upgrade button to {}") warn_btn = TemplateLocator(By.XPATH, "//span[.//a[text()='{}']]//app-concern-list-ref/button", "Warn button to {}") diff --git a/tests/ui_tests/app/page/common/tooltip_links/page.py b/tests/ui_tests/app/page/common/tooltip_links/page.py index da2fc6de94..4d3a9fd4dc 100644 --- a/tests/ui_tests/app/page/common/tooltip_links/page.py +++ b/tests/ui_tests/app/page/common/tooltip_links/page.py @@ -13,11 +13,8 @@ """Tooltip page PageObjects classes""" import allure - from tests.ui_tests.app.page.common.base_page import BasePageObject -from tests.ui_tests.app.page.common.dialogs_locators import ( - ActionDialog, -) +from tests.ui_tests.app.page.common.dialogs.locators import ActionDialog from tests.ui_tests.app.page.common.tooltip_links.locator import CommonToolbarLocators diff --git a/tests/ui_tests/app/page/component/locators.py b/tests/ui_tests/app/page/component/locators.py index 9ce0e0ee65..f478a650d5 100644 --- a/tests/ui_tests/app/page/component/locators.py +++ b/tests/ui_tests/app/page/component/locators.py @@ -13,11 +13,14 @@ """Component page locators""" from selenium.webdriver.common.by import By - from tests.ui_tests.app.helpers.locator import Locator class ComponentMainLocators: """Component main page elements locators""" - text = Locator(By.CSS_SELECTOR, "app-service-component-details .mat-card-content", "Component main page text") + text = Locator( + By.CSS_SELECTOR, + "app-service-component-details .mat-card-content", + "Component main page text", + ) diff --git a/tests/ui_tests/app/page/component/page.py b/tests/ui_tests/app/page/component/page.py index 9ad1eeb536..aa8495b425 100644 --- a/tests/ui_tests/app/page/component/page.py +++ b/tests/ui_tests/app/page/component/page.py @@ -13,12 +13,11 @@ """Component page PageObjects classes""" import allure - from tests.ui_tests.app.page.common.base_page import ( + BaseDetailedPage, BasePageObject, - PageHeader, PageFooter, - BaseDetailedPage, + PageHeader, ) from tests.ui_tests.app.page.common.common_locators import ( ObjectPageLocators, @@ -26,7 +25,9 @@ ) from tests.ui_tests.app.page.common.configuration.locators import CommonConfigMenu from tests.ui_tests.app.page.common.configuration.page import CommonConfigMenuObj -from tests.ui_tests.app.page.common.group_config_list.locators import GroupConfigListLocators +from tests.ui_tests.app.page.common.group_config_list.locators import ( + GroupConfigListLocators, +) from tests.ui_tests.app.page.common.group_config_list.page import GroupConfigList from tests.ui_tests.app.page.common.status.locators import StatusLocators from tests.ui_tests.app.page.common.status.page import StatusPage diff --git a/tests/ui_tests/app/page/host/locators.py b/tests/ui_tests/app/page/host/locators.py index 41fd1cdbd4..429af14d6e 100644 --- a/tests/ui_tests/app/page/host/locators.py +++ b/tests/ui_tests/app/page/host/locators.py @@ -13,11 +13,8 @@ """Host page locators""" from selenium.webdriver.common.by import By - from tests.ui_tests.app.helpers.locator import Locator, TemplateLocator -from tests.ui_tests.app.page.common.common_locators import ( - ObjectPageMenuLocators, -) +from tests.ui_tests.app.page.common.common_locators import ObjectPageMenuLocators class HostLocators: diff --git a/tests/ui_tests/app/page/host/page.py b/tests/ui_tests/app/page/host/page.py index 5d0fcf3797..16e39e11ab 100644 --- a/tests/ui_tests/app/page/host/page.py +++ b/tests/ui_tests/app/page/host/page.py @@ -16,9 +16,13 @@ import allure from adcm_pytest_plugin.utils import wait_until_step_succeeds - from tests.ui_tests.app.helpers.locator import Locator -from tests.ui_tests.app.page.common.base_page import BasePageObject, PageHeader, PageFooter, BaseDetailedPage +from tests.ui_tests.app.page.common.base_page import ( + BaseDetailedPage, + BasePageObject, + PageFooter, + PageHeader, +) from tests.ui_tests.app.page.common.common_locators import ObjectPageLocators from tests.ui_tests.app.page.common.configuration.locators import CommonConfigMenu from tests.ui_tests.app.page.common.configuration.page import CommonConfigMenuObj diff --git a/tests/ui_tests/app/page/host_list/locators.py b/tests/ui_tests/app/page/host_list/locators.py index c87a0a4adc..732d0f8ee1 100644 --- a/tests/ui_tests/app/page/host_list/locators.py +++ b/tests/ui_tests/app/page/host_list/locators.py @@ -13,11 +13,7 @@ """Host List page locators""" from selenium.webdriver.common.by import By - -from tests.ui_tests.app.helpers.locator import ( - Locator, - TemplateLocator, -) +from tests.ui_tests.app.helpers.locator import Locator, TemplateLocator from tests.ui_tests.app.page.common.table.locator import CommonTable @@ -36,6 +32,7 @@ class HostTable(CommonTable): cluster_option = TemplateLocator( By.XPATH, "//mat-option//span[contains(text(), '{}')]", "Table dropdown option" ) + header = Locator(By.TAG_NAME, "mat-header-row", "Header of the table") class HostRow: """Host List page host row elements locators""" @@ -55,3 +52,4 @@ class HostRow: dropdown_menu = Locator(By.CSS_SELECTOR, "div[role='menu']", "Dropdown menu") action_option = TemplateLocator(By.XPATH, "//button/span[text()='{}']", "Action dropdown option") action_option_all = Locator(By.CSS_SELECTOR, "button[adcm_test='action_btn']", "Action dropdown options") + rename_btn = Locator(By.CLASS_NAME, "rename-button", "Cluster rename button in row") diff --git a/tests/ui_tests/app/page/host_list/page.py b/tests/ui_tests/app/page/host_list/page.py index 36c17e1f43..f101fcfea7 100644 --- a/tests/ui_tests/app/page/host_list/page.py +++ b/tests/ui_tests/app/page/host_list/page.py @@ -13,24 +13,22 @@ """Host List page PageObjects classes""" from dataclasses import dataclass -from typing import Optional, ClassVar +from typing import ClassVar, Optional import allure from adcm_pytest_plugin.utils import wait_until_step_succeeds -from selenium.common.exceptions import ( - TimeoutException, -) +from selenium.common.exceptions import TimeoutException from selenium.webdriver.remote.webelement import WebElement from selenium.webdriver.support import expected_conditions as EC from selenium.webdriver.support.ui import WebDriverWait as WDW - from tests.ui_tests.app.helpers.locator import Locator from tests.ui_tests.app.page.common.base_page import ( BasePageObject, - PageHeader, PageFooter, + PageHeader, ) -from tests.ui_tests.app.page.common.dialogs_locators import DeleteDialog, ActionDialog +from tests.ui_tests.app.page.common.dialogs.locators import ActionDialog, DeleteDialog +from tests.ui_tests.app.page.common.dialogs.rename import RenameDialog from tests.ui_tests.app.page.common.popups.locator import HostCreationLocators from tests.ui_tests.app.page.common.popups.page import HostCreatePopupObj from tests.ui_tests.app.page.common.table.page import CommonTableObj @@ -50,7 +48,7 @@ class HostRowInfo: state: str -class HostListPage(BasePageObject): +class HostListPage(BasePageObject): # pylint: disable=too-many-public-methods """Host List Page class""" def __init__(self, driver, base_url): @@ -134,18 +132,34 @@ def create_provider_and_host( # because we don't pass provider name return provider_name - def get_all_available_actions(self, host_row_num: int): - "Return list with actions" + def open_run_action_menu(self, row: WebElement) -> None: + self.find_child(row, HostListLocators.HostTable.HostRow.actions).click() + self.wait_element_visible(HostListLocators.HostTable.HostRow.dropdown_menu, timeout=2) - host_row = HostListLocators.HostTable.HostRow - self.click_on_row_child(host_row_num, host_row.actions) - self.wait_element_visible(host_row.dropdown_menu) + def get_actions_from_opened_menu(self) -> list[WebElement]: try: - actions = [action.text for action in self.find_elements(host_row.action_option_all, timeout=2)] - return actions + return self.find_elements(HostListLocators.HostTable.HostRow.action_option_all, timeout=1) except TimeoutException: return [] + def close_run_action_menu(self) -> None: + # Typing Escape and clicking somewhere doesn't work + self.driver.refresh() + + def get_enabled_action_names(self, row_num: int) -> list[str]: + row = self.get_host_row(row_num) + self.open_run_action_menu(row) + action_names = [action.text for action in self.get_actions_from_opened_menu() if action.is_enabled()] + self.close_run_action_menu() + return action_names + + def get_disabled_action_names(self, row_num: int) -> list[str]: + row = self.get_host_row(row_num) + self.open_run_action_menu(row) + action_names = [action.text for action in self.get_actions_from_opened_menu() if not action.is_enabled()] + self.close_run_action_menu() + return action_names + @allure.step('Run action "{action_display_name}" on host in row {host_row_num}') def run_action(self, host_row_num: int, action_display_name: str): """Run action from Host row""" @@ -253,6 +267,14 @@ def click_create_host_in_popup(self): """Click create host button in popup""" self.find_and_click(HostCreationLocators.create_btn) + @allure.step("Open host rename dialog by clicking on host rename button") + def open_rename_dialog(self, row: WebElement) -> RenameDialog: + self.hover_element(row) + self.find_child(row, self.table.locators.HostRow.rename_btn).click() + dialog = RenameDialog(driver=self.driver, base_url=self.base_url) + dialog.wait_opened() + return dialog + def _insert_new_host_info(self, fqdn: str, cluster: Optional[str] = None): """Insert new host info in fields of opened popup""" self.wait_element_visible(HostCreationLocators.fqdn_input) diff --git a/tests/ui_tests/app/page/hostprovider_list/page.py b/tests/ui_tests/app/page/hostprovider_list/page.py index 2fd7fd1cee..c259b76afa 100644 --- a/tests/ui_tests/app/page/hostprovider_list/page.py +++ b/tests/ui_tests/app/page/hostprovider_list/page.py @@ -14,8 +14,8 @@ from tests.ui_tests.app.page.common.base_page import ( BasePageObject, - PageHeader, PageFooter, + PageHeader, ) diff --git a/tests/ui_tests/app/page/job/locators.py b/tests/ui_tests/app/page/job/locators.py index d525885f80..e9250a888f 100644 --- a/tests/ui_tests/app/page/job/locators.py +++ b/tests/ui_tests/app/page/job/locators.py @@ -13,11 +13,8 @@ """Job page locators""" from selenium.webdriver.common.by import By - from tests.ui_tests.app.helpers.locator import Locator -from tests.ui_tests.app.page.common.common_locators import ( - ObjectPageLocators, -) +from tests.ui_tests.app.page.common.common_locators import ObjectPageLocators class JobPageLocators(ObjectPageLocators): diff --git a/tests/ui_tests/app/page/job/page.py b/tests/ui_tests/app/page/job/page.py index 926050433c..c9a3092b67 100644 --- a/tests/ui_tests/app/page/job/page.py +++ b/tests/ui_tests/app/page/job/page.py @@ -16,9 +16,12 @@ from typing import Literal import allure - from tests.ui_tests.app.helpers.locator import Locator -from tests.ui_tests.app.page.common.base_page import BasePageObject, PageHeader, PageFooter +from tests.ui_tests.app.page.common.base_page import ( + BasePageObject, + PageFooter, + PageHeader, +) from tests.ui_tests.app.page.common.common_locators import ObjectPageLocators from tests.ui_tests.app.page.common.tooltip_links.locator import CommonToolbarLocators from tests.ui_tests.app.page.common.tooltip_links.page import CommonToolbar diff --git a/tests/ui_tests/app/page/job_list/locators.py b/tests/ui_tests/app/page/job_list/locators.py index c5d7ec3e5c..ee91a3531d 100644 --- a/tests/ui_tests/app/page/job_list/locators.py +++ b/tests/ui_tests/app/page/job_list/locators.py @@ -13,7 +13,6 @@ """Job List page locators""" from selenium.webdriver.common.by import By - from tests.ui_tests.app.helpers.locator import Locator from tests.ui_tests.app.page.common.table.locator import CommonTable @@ -42,6 +41,7 @@ class Row: invoker_objects = Locator(By.CSS_SELECTOR, "app-task-objects a", "Object that invoked action in row") start_date = Locator(By.CSS_SELECTOR, "mat-cell.action_date:nth-child(4)", "Start date in row") finish_date = Locator(By.CSS_SELECTOR, "mat-cell.action_date:nth-child(5)", "Finish date in row") + download_log = Locator(By.TAG_NAME, "app-download-button-column", "Log download button") # span for done_all and mat-icon for running # but in both cases we can identify status by class status = Locator(By.CSS_SELECTOR, "app-task-status-column *", "Status span in row") diff --git a/tests/ui_tests/app/page/job_list/page.py b/tests/ui_tests/app/page/job_list/page.py index cb4c8db51d..e405951064 100644 --- a/tests/ui_tests/app/page/job_list/page.py +++ b/tests/ui_tests/app/page/job_list/page.py @@ -14,19 +14,17 @@ from dataclasses import dataclass from enum import Enum -from typing import TypeVar, Union, List +from typing import List, TypeVar, Union import allure from selenium.common.exceptions import TimeoutException - from selenium.webdriver.remote.webelement import WebElement - -from tests.library.conditional_retriever import FromOneOf, DataSource +from tests.library.conditional_retriever import DataSource, FromOneOf from tests.ui_tests.app.helpers.locator import Locator from tests.ui_tests.app.page.common.base_page import ( BasePageObject, - PageHeader, PageFooter, + PageHeader, ) from tests.ui_tests.app.page.common.header_locators import AuthorizedHeaderLocators from tests.ui_tests.app.page.common.table.page import CommonTableObj @@ -109,10 +107,10 @@ def extract_status(locator): ) get_name_element = FromOneOf( [ - DataSource(self.find_child, [row, row_locators.action_name]), - DataSource(self.find_child, [row, row_locators.task_action_name]), + DataSource(self.find_child, [row, row_locators.action_name, 1]), + DataSource(self.find_child, [row, row_locators.task_action_name, 1]), ], - (TimeoutError, TimeoutException), + (TimeoutError, TimeoutException, AssertionError), ) return TableTaskInfo( action_name=get_name_element().text, @@ -168,6 +166,10 @@ def click_on_action_name_in_row(self, row: WebElement): locator = TaskListLocators.Table.Row.action_name row.find_element(locator.by, locator.value).click() + @allure.step("Click on log download button") + def click_on_log_download(self, row: WebElement): + self.find_child(row, TaskListLocators.Table.Row.download_log).click() + @allure.step('Select the "All" filter tab') def select_filter_all_tab(self): """Show all tasks""" diff --git a/tests/ui_tests/app/page/login/locators.py b/tests/ui_tests/app/page/login/locators.py index 12091ecb04..ecd74f257d 100644 --- a/tests/ui_tests/app/page/login/locators.py +++ b/tests/ui_tests/app/page/login/locators.py @@ -13,7 +13,6 @@ """Login page locators""" from selenium.webdriver.common.by import By - from tests.ui_tests.app.helpers.locator import Locator diff --git a/tests/ui_tests/app/page/login/page.py b/tests/ui_tests/app/page/login/page.py index 6052febc4b..8c1c16a7e9 100644 --- a/tests/ui_tests/app/page/login/page.py +++ b/tests/ui_tests/app/page/login/page.py @@ -14,11 +14,10 @@ import allure from adcm_pytest_plugin.utils import wait_until_step_succeeds - from tests.ui_tests.app.page.common.base_page import ( BasePageObject, - PageHeader, PageFooter, + PageHeader, ) from tests.ui_tests.app.page.login.locators import LoginPageLocators diff --git a/tests/ui_tests/app/page/profile/locators.py b/tests/ui_tests/app/page/profile/locators.py index 0979c6d7d4..c9d578087a 100644 --- a/tests/ui_tests/app/page/profile/locators.py +++ b/tests/ui_tests/app/page/profile/locators.py @@ -13,7 +13,6 @@ """Profile page locators""" from selenium.webdriver.common.by import By - from tests.ui_tests.app.helpers.locator import Locator diff --git a/tests/ui_tests/app/page/profile/page.py b/tests/ui_tests/app/page/profile/page.py index 20c94cba74..e62b74cdfa 100644 --- a/tests/ui_tests/app/page/profile/page.py +++ b/tests/ui_tests/app/page/profile/page.py @@ -13,13 +13,11 @@ """Profile page PageObjects classes""" import allure - from adcm_pytest_plugin.utils import wait_until_step_succeeds - from tests.ui_tests.app.page.common.base_page import ( BasePageObject, - PageHeader, PageFooter, + PageHeader, ) from tests.ui_tests.app.page.profile.locators import ProfileLocators diff --git a/tests/ui_tests/app/page/provider/page.py b/tests/ui_tests/app/page/provider/page.py index 21cbe1297b..a64a336303 100644 --- a/tests/ui_tests/app/page/provider/page.py +++ b/tests/ui_tests/app/page/provider/page.py @@ -13,12 +13,21 @@ """Provider page PageObjects classes""" import allure - -from tests.ui_tests.app.page.common.base_page import BasePageObject, PageHeader, PageFooter, BaseDetailedPage -from tests.ui_tests.app.page.common.common_locators import ObjectPageLocators, ObjectPageMenuLocators +from tests.ui_tests.app.page.common.base_page import ( + BaseDetailedPage, + BasePageObject, + PageFooter, + PageHeader, +) +from tests.ui_tests.app.page.common.common_locators import ( + ObjectPageLocators, + ObjectPageMenuLocators, +) from tests.ui_tests.app.page.common.configuration.locators import CommonConfigMenu from tests.ui_tests.app.page.common.configuration.page import CommonConfigMenuObj -from tests.ui_tests.app.page.common.group_config_list.locators import GroupConfigListLocators +from tests.ui_tests.app.page.common.group_config_list.locators import ( + GroupConfigListLocators, +) from tests.ui_tests.app.page.common.group_config_list.page import GroupConfigList from tests.ui_tests.app.page.common.table.locator import CommonTable from tests.ui_tests.app.page.common.table.page import CommonTableObj diff --git a/tests/ui_tests/app/page/provider_list/locators.py b/tests/ui_tests/app/page/provider_list/locators.py index 116b2102e0..814b2af4b1 100644 --- a/tests/ui_tests/app/page/provider_list/locators.py +++ b/tests/ui_tests/app/page/provider_list/locators.py @@ -13,10 +13,7 @@ """Profile List page locators""" from selenium.webdriver.common.by import By - -from tests.ui_tests.app.helpers.locator import ( - Locator, -) +from tests.ui_tests.app.helpers.locator import Locator from tests.ui_tests.app.page.common.table.locator import CommonTable diff --git a/tests/ui_tests/app/page/provider_list/page.py b/tests/ui_tests/app/page/provider_list/page.py index 4106ccb433..909f6f5bfc 100644 --- a/tests/ui_tests/app/page/provider_list/page.py +++ b/tests/ui_tests/app/page/provider_list/page.py @@ -19,16 +19,12 @@ import allure from adcm_pytest_plugin.utils import wait_until_step_succeeds from selenium.webdriver.remote.webelement import WebElement - from tests.ui_tests.app.page.common.base_page import ( BasePageObject, - PageHeader, PageFooter, + PageHeader, ) -from tests.ui_tests.app.page.common.dialogs_locators import ( - ActionDialog, - DeleteDialog, -) +from tests.ui_tests.app.page.common.dialogs.locators import ActionDialog, DeleteDialog from tests.ui_tests.app.page.common.popups.page import HostCreatePopupObj from tests.ui_tests.app.page.common.table.page import CommonTableObj from tests.ui_tests.app.page.common.tooltip_links.page import CommonToolbar diff --git a/tests/ui_tests/app/page/service/locators.py b/tests/ui_tests/app/page/service/locators.py index fc792b5f6a..391e0d4090 100644 --- a/tests/ui_tests/app/page/service/locators.py +++ b/tests/ui_tests/app/page/service/locators.py @@ -13,7 +13,6 @@ """Service page locators""" from selenium.webdriver.common.by import By - from tests.ui_tests.app.helpers.locator import Locator diff --git a/tests/ui_tests/app/page/service/page.py b/tests/ui_tests/app/page/service/page.py index 3e6d85e697..cf8c52d924 100644 --- a/tests/ui_tests/app/page/service/page.py +++ b/tests/ui_tests/app/page/service/page.py @@ -18,19 +18,22 @@ import allure from adcm_pytest_plugin.utils import wait_until_step_succeeds from selenium.webdriver.remote.webdriver import WebElement - from tests.ui_tests.app.page.common.base_page import ( + BaseDetailedPage, BasePageObject, - PageHeader, PageFooter, - BaseDetailedPage, + PageHeader, +) +from tests.ui_tests.app.page.common.common_locators import ( + ObjectPageLocators, + ObjectPageMenuLocators, ) -from tests.ui_tests.app.page.common.common_locators import ObjectPageLocators -from tests.ui_tests.app.page.common.common_locators import ObjectPageMenuLocators from tests.ui_tests.app.page.common.configuration.locators import CommonConfigMenu from tests.ui_tests.app.page.common.configuration.page import CommonConfigMenuObj -from tests.ui_tests.app.page.common.dialogs_locators import ActionDialog -from tests.ui_tests.app.page.common.group_config_list.locators import GroupConfigListLocators +from tests.ui_tests.app.page.common.dialogs.locators import ActionDialog +from tests.ui_tests.app.page.common.group_config_list.locators import ( + GroupConfigListLocators, +) from tests.ui_tests.app.page.common.group_config_list.page import GroupConfigList from tests.ui_tests.app.page.common.import_page.locators import ImportLocators from tests.ui_tests.app.page.common.import_page.page import ImportPage @@ -41,7 +44,7 @@ from tests.ui_tests.app.page.service.locators import ServiceComponentLocators -class ServicePageMixin(BasePageObject): +class ServicePageMixin(BasePageObject): # pylint: disable=too-many-instance-attributes """Helpers for working with service page""" # /action /main etc. diff --git a/tests/ui_tests/conftest.py b/tests/ui_tests/conftest.py index 12bfdc56fc..abec40277d 100644 --- a/tests/ui_tests/conftest.py +++ b/tests/ui_tests/conftest.py @@ -26,7 +26,6 @@ from adcm_client.objects import ADCMClient from adcm_client.wrappers.docker import ADCM from selenium.common.exceptions import WebDriverException - from tests.conftest import CLEAN_ADCM_PARAM from tests.ui_tests.app.app import ADCMTest from tests.ui_tests.app.page.admin.page import AdminIntroPage @@ -57,7 +56,7 @@ def downloads_directory(tmpdir_factory: pytest.TempdirFactory): @pytest.fixture() -def clean_downloads_fs(request: SubRequest, downloads_directory): +def _clean_downloads_fs(request: SubRequest, downloads_directory): """Clean downloads directory before use""" if downloads_directory == SELENOID_DOWNLOADS_PATH: yield @@ -93,18 +92,85 @@ def web_driver(browser, downloads_directory): @pytest.fixture() -def skip_firefox(browser: str): +def _skip_firefox(browser: str): """Skip one test on firefox""" if browser == 'Firefox': pytest.skip("This test shouldn't be launched on Firefox") +@allure.title("Data for failure investigation") @pytest.fixture() -def app_fs(adcm_fs: ADCM, web_driver: ADCMTest, request): +def _attach_debug_info_on_ui_test_fail(request, web_driver): + """Attach screenshot, etc. to allure + cleanup for firefox""" + yield + try: + if not (request.node.rep_setup.failed or request.node.rep_call.failed): + return + allure.attach( + web_driver.driver.page_source, + name="page_source", + attachment_type=allure.attachment_type.HTML, + ) + web_driver.driver.execute_script("document.body.bgColor = 'white';") + allure.attach( + web_driver.driver.get_screenshot_as_png(), + name="screenshot", + attachment_type=allure.attachment_type.PNG, + ) + allure.attach( + json.dumps(web_driver.driver.execute_script("return localStorage"), indent=2), + name="localStorage", + attachment_type=allure.attachment_type.JSON, + ) + # this way of getting logs does not work for Firefox, see ADCM-1497 + if web_driver.capabilities['browserName'] != 'firefox': + console_logs = web_driver.driver.get_log('browser') + perf_log = web_driver.driver.get_log("performance") + events = [_process_browser_log_entry(entry) for entry in perf_log] + network_logs = [event for event in events if 'Network.response' in event['method']] + events_json = _write_json_file("all_logs", events) + network_console_logs = _write_json_file("network_log", network_logs) + console_logs = _write_json_file("console_logs", console_logs) + allure.attach( + web_driver.driver.current_url, + name='Current URL', + attachment_type=allure.attachment_type.TEXT, + ) + allure.attach.file(console_logs, name="console_log", attachment_type=allure.attachment_type.TEXT) + allure.attach.file( + network_console_logs, + name="network_log", + attachment_type=allure.attachment_type.TEXT, + ) + allure.attach.file(events_json, name="all_events_log", attachment_type=allure.attachment_type.TEXT) + except AttributeError: + # rep_setup and rep_call attributes are generated in runtime and can be absent + pass + + +@pytest.fixture() +def _cleanup_browser_logs(request, web_driver): + """Cleanup browser logs""" + try: + if ( + not (request.node.rep_setup.failed or request.node.rep_call.failed) + and web_driver.capabilities['browserName'] != 'firefox' + ): + with allure.step("Flush browser logs so as not to affect next tests"): + web_driver.driver.get_log('browser') + web_driver.driver.get_log("performance") + except AttributeError: + # rep_setup and rep_call attributes are generated in runtime and can be absent + pass + + +@pytest.fixture() +def app_fs(adcm_fs: ADCM, web_driver: ADCMTest, _attach_debug_info_on_ui_test_fail, _cleanup_browser_logs): """ Attach ADCM API to ADCMTest object and open new tab in browser for test Collect logs on failure and close browser tab after test is done """ + _ = _attach_debug_info_on_ui_test_fail web_driver.attache_adcm(adcm_fs) try: web_driver.new_tab() @@ -112,53 +178,7 @@ def app_fs(adcm_fs: ADCM, web_driver: ADCMTest, request): # this exception could be raised in case # when driver was crashed for some reason web_driver.create_driver() - yield web_driver - try: - if request.node.rep_setup.failed or request.node.rep_call.failed: - allure.attach( - web_driver.driver.page_source, - name="page_source", - attachment_type=allure.attachment_type.HTML, - ) - web_driver.driver.execute_script("document.body.bgColor = 'white';") - allure.attach( - web_driver.driver.get_screenshot_as_png(), - name="screenshot", - attachment_type=allure.attachment_type.PNG, - ) - allure.attach( - json.dumps(web_driver.driver.execute_script("return localStorage"), indent=2), - name="localStorage", - attachment_type=allure.attachment_type.JSON, - ) - # this way of getting logs does not work for Firefox, see ADCM-1497 - if web_driver.capabilities['browserName'] != 'firefox': - console_logs = web_driver.driver.get_log('browser') - perf_log = web_driver.driver.get_log("performance") - events = [_process_browser_log_entry(entry) for entry in perf_log] - network_logs = [event for event in events if 'Network.response' in event['method']] - events_json = _write_json_file("all_logs", events) - network_console_logs = _write_json_file("network_log", network_logs) - console_logs = _write_json_file("console_logs", console_logs) - allure.attach( - web_driver.driver.current_url, - name='Current URL', - attachment_type=allure.attachment_type.TEXT, - ) - allure.attach.file(console_logs, name="console_log", attachment_type=allure.attachment_type.TEXT) - allure.attach.file( - network_console_logs, - name="network_log", - attachment_type=allure.attachment_type.TEXT, - ) - allure.attach.file(events_json, name="all_events_log", attachment_type=allure.attachment_type.TEXT) - elif web_driver.capabilities['browserName'] != 'firefox': - with allure.step("Flush browser logs so as not to affect next tests"): - web_driver.driver.get_log('browser') - web_driver.driver.get_log("performance") - except AttributeError: - # rep_setup and rep_call attributes are generated in runtime and can be absent - pass + return web_driver @pytest.fixture(scope='session') @@ -199,14 +219,14 @@ def login_over_api(app_fs, credentials): @allure.title("Login in ADCM over API") @pytest.fixture() -def login_to_adcm_over_api(app_fs, adcm_credentials): +def _login_to_adcm_over_api(app_fs, adcm_credentials): """Perform login via API call""" login_over_api(app_fs, adcm_credentials).wait_config_loaded() @allure.title("Login in ADCM over UI") @pytest.fixture() -def login_to_adcm_over_ui(app_fs, adcm_credentials): +def _login_to_adcm_over_ui(app_fs, adcm_credentials): """Perform login on Login page ADCM""" login = LoginPage(app_fs.driver, app_fs.adcm.url).open() diff --git a/tests/ui_tests/test_activatable_groups_ui_options.py b/tests/ui_tests/test_activatable_groups_ui_options.py index 6b63e13657..bd86d5e254 100644 --- a/tests/ui_tests/test_activatable_groups_ui_options.py +++ b/tests/ui_tests/test_activatable_groups_ui_options.py @@ -9,22 +9,17 @@ # 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. -# pylint: disable=redefined-outer-name,unused-argument,too-many-public-methods +# pylint: disable=redefined-outer-name,too-many-public-methods """Tests for activatable groups""" import allure import pytest -from adcm_client.objects import ( - ADCMClient, - Cluster, -) -from adcm_pytest_plugin.utils import parametrize_by_data_subdirs -from adcm_pytest_plugin.utils import random_string - +from adcm_client.objects import ADCMClient, Cluster +from adcm_pytest_plugin.utils import parametrize_by_data_subdirs, random_string from tests.ui_tests.app.page.cluster.page import ClusterConfigPage -pytestmark = [pytest.mark.usefixtures("login_to_adcm_over_api")] +pytestmark = [pytest.mark.usefixtures("_login_to_adcm_over_api")] # !===== Fixtures =====! @@ -75,11 +70,17 @@ def test_group_advanced_false_invisible_false_field_advanced_true_invisible_true group_name = path.split("/")[-1] config_page.config.check_group_is_active(group_name=group_name, is_active=False) config_page.check_groups( - group_names=[group_name], is_group_visible=True, is_group_active=False, is_subs_visible=False + group_names=[group_name], + is_group_visible=True, + is_group_active=False, + is_subs_visible=False, ) config_page.config.click_on_advanced() config_page.check_groups( - group_names=[group_name], is_group_visible=True, is_group_active=True, is_subs_visible=False + group_names=[group_name], + is_group_visible=True, + is_group_active=True, + is_subs_visible=False, ) @parametrize_by_data_subdirs( @@ -99,11 +100,17 @@ def test_group_advanced_false_invisible_false_field_advanced_false_invisible_tru group_name = path.split("/")[-1] config_page.config.check_group_is_active(group_name=group_name, is_active=False) config_page.check_groups( - group_names=[group_name], is_group_visible=True, is_group_active=False, is_subs_visible=False + group_names=[group_name], + is_group_visible=True, + is_group_active=False, + is_subs_visible=False, ) config_page.config.click_on_advanced() config_page.check_groups( - group_names=[group_name], is_group_visible=True, is_group_active=True, is_subs_visible=False + group_names=[group_name], + is_group_visible=True, + is_group_active=True, + is_subs_visible=False, ) @parametrize_by_data_subdirs( @@ -123,11 +130,17 @@ def test_group_advanced_false_invisible_false_field_advanced_false_invisible_tru group_name = path.split("/")[-1] config_page.config.check_group_is_active(group_name=group_name) config_page.check_groups( - group_names=[group_name], is_group_visible=True, is_group_active=False, is_subs_visible=False + group_names=[group_name], + is_group_visible=True, + is_group_active=False, + is_subs_visible=False, ) config_page.config.click_on_advanced() config_page.check_groups( - group_names=[group_name], is_group_visible=True, is_group_active=True, is_subs_visible=False + group_names=[group_name], + is_group_visible=True, + is_group_active=True, + is_subs_visible=False, ) @parametrize_by_data_subdirs( @@ -147,11 +160,17 @@ def test_group_advanced_false_invisible_false_field_advanced_true_invisible_true group_name = path.split("/")[-1] config_page.config.check_group_is_active(group_name=group_name) config_page.check_groups( - group_names=[group_name], is_group_visible=True, is_group_active=False, is_subs_visible=False + group_names=[group_name], + is_group_visible=True, + is_group_active=False, + is_subs_visible=False, ) config_page.config.click_on_advanced() config_page.check_groups( - group_names=[group_name], is_group_visible=True, is_group_active=True, is_subs_visible=False + group_names=[group_name], + is_group_visible=True, + is_group_active=True, + is_subs_visible=False, ) @parametrize_by_data_subdirs( @@ -174,10 +193,16 @@ def test_group_advanced_true_invisible_false_field_advanced_false_invisible_fals config_page.config.click_on_advanced() config_page.config.check_group_is_active(group_name=group_name, is_active=False) config_page.check_groups( - group_names=[group_name], is_group_visible=True, is_group_active=False, is_subs_visible=False + group_names=[group_name], + is_group_visible=True, + is_group_active=False, + is_subs_visible=False, ) config_page.check_groups( - group_names=[group_name], is_group_visible=True, is_group_active=True, is_subs_visible=True + group_names=[group_name], + is_group_visible=True, + is_group_active=True, + is_subs_visible=True, ) @parametrize_by_data_subdirs( @@ -200,10 +225,16 @@ def test_group_advanced_true_invisible_false_field_advanced_false_invisible_fals config_page.config.click_on_advanced() config_page.config.check_group_is_active(group_name=group_name) config_page.check_groups( - group_names=[group_name], is_group_visible=True, is_group_active=False, is_subs_visible=False + group_names=[group_name], + is_group_visible=True, + is_group_active=False, + is_subs_visible=False, ) config_page.check_groups( - group_names=[group_name], is_group_visible=True, is_group_active=True, is_subs_visible=True + group_names=[group_name], + is_group_visible=True, + is_group_active=True, + is_subs_visible=True, ) @parametrize_by_data_subdirs( @@ -450,10 +481,16 @@ def test_group_advanced_true_invisible_false_field_advanced_false_invisible_true config_page.config.click_on_advanced() config_page.config.check_group_is_active(group_name=group_name) config_page.check_groups( - group_names=[group_name], is_group_visible=True, is_group_active=False, is_subs_visible=False + group_names=[group_name], + is_group_visible=True, + is_group_active=False, + is_subs_visible=False, ) config_page.check_groups( - group_names=[group_name], is_group_visible=True, is_group_active=True, is_subs_visible=False + group_names=[group_name], + is_group_visible=True, + is_group_active=True, + is_subs_visible=False, ) @parametrize_by_data_subdirs( @@ -476,10 +513,16 @@ def test_group_advanced_true_invisible_false_field_advanced_true_invisible_true_ config_page.config.click_on_advanced() config_page.config.check_group_is_active(group_name=group_name) config_page.check_groups( - group_names=[group_name], is_group_visible=True, is_group_active=False, is_subs_visible=False + group_names=[group_name], + is_group_visible=True, + is_group_active=False, + is_subs_visible=False, ) config_page.check_groups( - group_names=[group_name], is_group_visible=True, is_group_active=True, is_subs_visible=False + group_names=[group_name], + is_group_visible=True, + is_group_active=True, + is_subs_visible=False, ) @parametrize_by_data_subdirs( @@ -499,11 +542,17 @@ def test_group_advanced_false_invisible_false_field_advanced_false_invisible_fal group_name = path.split("/")[-1] config_page.config.check_group_is_active(group_name=group_name, is_active=False) config_page.check_groups( - group_names=[group_name], is_group_visible=True, is_group_active=False, is_subs_visible=False + group_names=[group_name], + is_group_visible=True, + is_group_active=False, + is_subs_visible=False, ) config_page.config.click_on_advanced() config_page.check_groups( - group_names=[group_name], is_group_visible=True, is_group_active=True, is_subs_visible=True + group_names=[group_name], + is_group_visible=True, + is_group_active=True, + is_subs_visible=True, ) @parametrize_by_data_subdirs( @@ -523,11 +572,17 @@ def test_group_advanced_false_invisible_false_field_advanced_false_invisible_fal group_name = path.split("/")[-1] config_page.config.check_group_is_active(group_name=group_name) config_page.check_groups( - group_names=[group_name], is_group_visible=True, is_group_active=False, is_subs_visible=False + group_names=[group_name], + is_group_visible=True, + is_group_active=False, + is_subs_visible=False, ) config_page.config.click_on_advanced() config_page.check_groups( - group_names=[group_name], is_group_visible=True, is_group_active=True, is_subs_visible=True + group_names=[group_name], + is_group_visible=True, + is_group_active=True, + is_subs_visible=True, ) @parametrize_by_data_subdirs( @@ -547,14 +602,23 @@ def test_group_advanced_false_invisible_false_field_advanced_true_invisible_fals group_name = path.split("/")[-1] config_page.config.check_group_is_active(group_name=group_name, is_active=False) config_page.check_groups( - group_names=[group_name], is_group_visible=True, is_group_active=False, is_subs_visible=False + group_names=[group_name], + is_group_visible=True, + is_group_active=False, + is_subs_visible=False, ) config_page.check_groups( - group_names=[group_name], is_group_visible=True, is_group_active=True, is_subs_visible=False + group_names=[group_name], + is_group_visible=True, + is_group_active=True, + is_subs_visible=False, ) config_page.config.click_on_advanced() config_page.check_groups( - group_names=[group_name], is_group_visible=True, is_group_active=True, is_subs_visible=True + group_names=[group_name], + is_group_visible=True, + is_group_active=True, + is_subs_visible=True, ) @parametrize_by_data_subdirs( @@ -574,14 +638,23 @@ def test_group_advanced_false_invisible_false_field_advanced_true_invisible_fals group_name = path.split("/")[-1] config_page.config.check_group_is_active(group_name=group_name) config_page.check_groups( - group_names=[group_name], is_group_visible=True, is_group_active=False, is_subs_visible=False + group_names=[group_name], + is_group_visible=True, + is_group_active=False, + is_subs_visible=False, ) config_page.check_groups( - group_names=[group_name], is_group_visible=True, is_group_active=True, is_subs_visible=False + group_names=[group_name], + is_group_visible=True, + is_group_active=True, + is_subs_visible=False, ) config_page.config.click_on_advanced() config_page.check_groups( - group_names=[group_name], is_group_visible=True, is_group_active=True, is_subs_visible=True + group_names=[group_name], + is_group_visible=True, + is_group_active=True, + is_subs_visible=True, ) @parametrize_by_data_subdirs( @@ -604,10 +677,16 @@ def test_group_advanced_true_invisible_false_field_advanced_false_invisible_true config_page.config.click_on_advanced() config_page.config.check_group_is_active(group_name=group_name, is_active=False) config_page.check_groups( - group_names=[group_name], is_group_visible=True, is_group_active=False, is_subs_visible=False + group_names=[group_name], + is_group_visible=True, + is_group_active=False, + is_subs_visible=False, ) config_page.check_groups( - group_names=[group_name], is_group_visible=True, is_group_active=True, is_subs_visible=False + group_names=[group_name], + is_group_visible=True, + is_group_active=True, + is_subs_visible=False, ) @parametrize_by_data_subdirs( @@ -630,10 +709,16 @@ def test_group_advanced_true_invisible_false_field_advanced_true_invisible_false config_page.config.click_on_advanced() config_page.config.check_group_is_active(group_name=group_name, is_active=False) config_page.check_groups( - group_names=[group_name], is_group_visible=True, is_group_active=False, is_subs_visible=False + group_names=[group_name], + is_group_visible=True, + is_group_active=False, + is_subs_visible=False, ) config_page.check_groups( - group_names=[group_name], is_group_visible=True, is_group_active=True, is_subs_visible=True + group_names=[group_name], + is_group_visible=True, + is_group_active=True, + is_subs_visible=True, ) @parametrize_by_data_subdirs( @@ -656,10 +741,16 @@ def test_group_advanced_true_invisible_false_field_advanced_true_invisible_false config_page.config.click_on_advanced() config_page.config.check_group_is_active(group_name=group_name) config_page.check_groups( - group_names=[group_name], is_group_visible=True, is_group_active=False, is_subs_visible=False + group_names=[group_name], + is_group_visible=True, + is_group_active=False, + is_subs_visible=False, ) config_page.check_groups( - group_names=[group_name], is_group_visible=True, is_group_active=True, is_subs_visible=True + group_names=[group_name], + is_group_visible=True, + is_group_active=True, + is_subs_visible=True, ) @parametrize_by_data_subdirs( @@ -682,8 +773,14 @@ def test_group_advanced_true_invisible_false_field_advanced_true_invisible_true_ config_page.config.click_on_advanced() config_page.config.check_group_is_active(group_name=group_name, is_active=False) config_page.check_groups( - group_names=[group_name], is_group_visible=True, is_group_active=False, is_subs_visible=False + group_names=[group_name], + is_group_visible=True, + is_group_active=False, + is_subs_visible=False, ) config_page.check_groups( - group_names=[group_name], is_group_visible=True, is_group_active=True, is_subs_visible=False + group_names=[group_name], + is_group_visible=True, + is_group_active=True, + is_subs_visible=False, ) diff --git a/tests/ui_tests/test_activatable_groups_ui_options_data/config_generator.py b/tests/ui_tests/test_activatable_groups_ui_options_data/config_generator.py index 8ee0e00cb3..613565dfd2 100644 --- a/tests/ui_tests/test_activatable_groups_ui_options_data/config_generator.py +++ b/tests/ui_tests/test_activatable_groups_ui_options_data/config_generator.py @@ -1,3 +1,17 @@ +# 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. + +"""Dummy config generator""" + import os DATA = [ @@ -294,7 +308,10 @@ for t in TYPES: for config in DATA: - d_name = f"group_advanced_{config[0]}_invisible_{config[1]}_field_advanced_{config[2]}_invisible_{config[3]}_activated_{config[4]}/{t}" + d_name = ( + f"group_advanced_{config[0]}_invisible_{config[1]}_" + f"field_advanced_{config[2]}_invisible_{config[3]}_activated_{config[4]}/{t}" + ) os.makedirs(d_name) tmpl = '' with open(f"{d_name}/config.yaml", "w+", encoding='utf_8') as f: diff --git a/tests/ui_tests/test_adcm_upgrade.py b/tests/ui_tests/test_adcm_upgrade.py index c1331cdb70..479828831e 100644 --- a/tests/ui_tests/test_adcm_upgrade.py +++ b/tests/ui_tests/test_adcm_upgrade.py @@ -21,7 +21,6 @@ from adcm_pytest_plugin.plugin import parametrized_by_adcm_version from adcm_pytest_plugin.utils import wait_until_step_succeeds from selenium.common.exceptions import StaleElementReferenceException, TimeoutException - from tests.ui_tests.app.app import ADCMTest from tests.ui_tests.app.page.admin.page import AdminIntroPage from tests.ui_tests.app.page.bundle_list.page import BundleListPage @@ -84,7 +83,8 @@ def test_upgrade_adcm( intro_page.wait_config_loaded() with allure.step('Start ADCM upgrade with client'): upgrade_thread = threading.Thread( - target=upgrade_adcm_version, args=(app_fs.adcm, sdk_client_fs, adcm_api_credentials, adcm_image_tags) + target=upgrade_adcm_version, + args=(app_fs.adcm, sdk_client_fs, adcm_api_credentials, adcm_image_tags), ) upgrade_thread.start() with allure.step('Check update popup messages are present'): diff --git a/tests/ui_tests/test_admin_page.py b/tests/ui_tests/test_admin_page.py index 62f6d3201b..bab3a0a3b4 100644 --- a/tests/ui_tests/test_admin_page.py +++ b/tests/ui_tests/test_admin_page.py @@ -10,45 +10,35 @@ # See the License for the specific language governing permissions and # limitations under the License. -# pylint: disable=redefined-outer-name, unused-argument, too-many-lines +# pylint: disable=redefined-outer-name,too-many-lines """UI tests for /admin page""" import os from copy import deepcopy -from typing import ( - Tuple, -) +from typing import Tuple import allure import pytest -from adcm_client.objects import ( - Bundle, - Cluster, - ADCMClient, - Host, - Service, - Provider, -) +from adcm_client.objects import ADCMClient, Bundle, Cluster, Host, Provider, Service from adcm_pytest_plugin import utils from adcm_pytest_plugin.steps.actions import wait_for_task_and_assert_result from adcm_pytest_plugin.utils import random_string - from tests.ui_tests.app.app import ADCMTest from tests.ui_tests.app.page.admin.page import ( - AdminIntroPage, - AdminUsersPage, - AdminSettingsPage, - AdminRolesPage, - AdminGroupsPage, - AdminRoleInfo, AdminGroupInfo, + AdminGroupsPage, + AdminIntroPage, AdminPoliciesPage, AdminPolicyInfo, + AdminRoleInfo, + AdminRolesPage, + AdminSettingsPage, + AdminUsersPage, ) from tests.ui_tests.app.page.cluster.page import ( - ClusterConfigPage, ClusterComponentsPage, + ClusterConfigPage, ) from tests.ui_tests.app.page.cluster_list.page import ClusterListPage from tests.ui_tests.app.page.component.page import ComponentConfigPage @@ -66,8 +56,8 @@ CLUSTER_NAME = "test cluster" SERVICE_NAME = "test_service_1" FIRST_COMPONENT_NAME = "first" -PROVIDER_NAME = 'test provider' -HOST_NAME = 'test-host' +PROVIDER_NAME = "test provider" +HOST_NAME = "test-host" # !===== Fixtures =====! @@ -108,9 +98,9 @@ def create_cluster_with_component( """Create cluster with component""" cluster, service = create_cluster_with_service - provider_bundle = sdk_client_fs.upload_from_fs(os.path.join(utils.get_data_dir(__file__), 'provider')) - provider = provider_bundle.provider_create('test provider') - host = provider.host_create('test-host') + provider_bundle = sdk_client_fs.upload_from_fs(os.path.join(utils.get_data_dir(__file__), "provider")) + provider = provider_bundle.provider_create("test provider") + host = provider.host_create("test-host") cluster.host_add(host) cluster.hostcomponent_set((host, service.component(name=FIRST_COMPONENT_NAME))) return cluster, service, host, provider @@ -119,7 +109,7 @@ def create_cluster_with_component( # !===== Tests =====! -CUSTOM_ROLE_NAME = 'Test_Role' +CUSTOM_ROLE_NAME = "Test_Role" CUSTOM_POLICY = AdminPolicyInfo( name="Test policy name", description="Test policy description", @@ -130,7 +120,7 @@ def create_cluster_with_component( ) -@pytest.mark.usefixtures("login_to_adcm_over_api") +@pytest.mark.usefixtures("_login_to_adcm_over_api") class TestAdminIntroPage: """Tests for the /admin/intro""" @@ -143,7 +133,7 @@ def test_open_by_tab_admin_intro_page(self, app_fs): intro_page.check_admin_toolbar() -@pytest.mark.usefixtures("login_to_adcm_over_api") +@pytest.mark.usefixtures("_login_to_adcm_over_api") class TestAdminSettingsPage: """Tests for the /admin/roles""" @@ -161,61 +151,73 @@ def test_open_by_tab_admin_settings_page(self, app_fs): def test_settings_filter(self, settings_page: AdminSettingsPage): """Apply different filters on Admin Settings page""" params = { - 'search_text': 'ADCM', - 'field_display_name': "ADCM's URL", - 'group': 'Global Options', + "search_text": "ADCM", + "field_display_name": "ADCM's URL", + "group": "Global Options", } get_rows_func = settings_page.config.get_all_config_rows with allure.step( f'Search {params["search_text"]} and check {params["field_display_name"]} is presented after search' ): with expect_rows_amount_change(get_rows_func): - settings_page.config.search(params['search_text']) + settings_page.config.search(params["search_text"]) settings_page.config.get_config_row(params["field_display_name"]) - with allure.step('Clear search'), expect_rows_amount_change(get_rows_func): + with allure.step("Clear search"), expect_rows_amount_change(get_rows_func): settings_page.config.clear_search() with allure.step( f'Click on {params["group"]} group and check {params["field_display_name"]} ' - 'is not presented after group roll up' + "is not presented after group roll up" ): with expect_rows_amount_change(get_rows_func): - settings_page.config.click_on_group(params['group']) + settings_page.config.click_on_group(params["group"]) with pytest.raises(AssertionError): settings_page.config.get_config_row(params["field_display_name"]) with allure.step( f'Click on {params["group"]} group and check {params["field_display_name"]} ' - 'is presented after group expand' + "is presented after group expand" ): with expect_rows_amount_change(get_rows_func): - settings_page.config.click_on_group(params['group']) + settings_page.config.click_on_group(params["group"]) settings_page.config.get_config_row(params["field_display_name"]) @pytest.mark.full() def test_save_settings_with_different_name(self, settings_page: AdminSettingsPage): """Save settings with different name""" - params = {'new_name': 'test_settings', 'field_display_name': 'client_id', 'field_value': '123'} - settings_page.config.set_description(params['new_name']) + params = { + "new_name": "test_settings", + "field_display_name": "client_id", + "field_value": "123", + } + settings_page.config.set_description(params["new_name"]) with allure.step(f'Change value of field {params["field_display_name"]} to {params["field_value"]}'): - config_field_row = settings_page.config.get_config_row(params['field_display_name']) - settings_page.config.type_in_field_with_few_inputs(row=config_field_row, values=[params['field_value']]) + config_field_row = settings_page.config.get_config_row(params["field_display_name"]) + settings_page.config.type_in_field_with_few_inputs(row=config_field_row, values=[params["field_value"]]) settings_page.config.save_config() - settings_page.config.compare_versions(params['new_name'], 'init') - with allure.step('Check history'): - config_field_row = settings_page.config.get_config_row(params['field_display_name']) + settings_page.config.compare_versions(params["new_name"], "init") + with allure.step("Check history"): + config_field_row = settings_page.config.get_config_row(params["field_display_name"]) history = settings_page.config.get_history_in_row(config_field_row) assert len(history) == 1, f'History should has exactly one entry for field {params["field_display_name"]}' - assert (actual_value := history[0]) == (expected_value := params['field_value']), ( + assert (actual_value := history[0]) == (expected_value := params["field_value"]), ( f'History entry for field {params["field_display_name"]} ' - f'should be {expected_value}, not {actual_value}' + f"should be {expected_value}, not {actual_value}" ) @pytest.mark.full() def test_negative_values_in_adcm_config(self, settings_page: AdminSettingsPage): """Put negative numbers in the fields of ADCM settings""" params = ( - ('Log rotation from file system', -1, 'Field [Log rotation from file system] value cannot be less than 0!'), - ('Log rotation from database', -1, 'Field [Log rotation from database] value cannot be less than 0!'), - ('Forks', 0, 'Field [Forks] value cannot be less than 1!'), + ( + "Log rotation from file system", + -1, + "Field [Log rotation from file system] value cannot be less than 0!", + ), + ( + "Log rotation from database", + -1, + "Field [Log rotation from database] value cannot be less than 0!", + ), + ("Forks", 0, "Field [Forks] value cannot be less than 1!"), ) for field, inappropriate_value, error_message in params: @@ -231,57 +233,61 @@ def test_negative_values_in_adcm_config(self, settings_page: AdminSettingsPage): def test_reset_config(self, settings_page: AdminSettingsPage): """Change config field, save, reset""" - params = {'field_display_name': 'client_id', 'init_value': '', 'changed_value': '123'} + params = {"field_display_name": "client_id", "init_value": "", "changed_value": "123"} with allure.step(f'Set value of {params["field_display_name"]} to {params["changed_value"]}'): - config_field_row = settings_page.config.get_config_row(params['field_display_name']) - settings_page.config.type_in_field_with_few_inputs(row=config_field_row, values=[params['changed_value']]) - with allure.step('Save config'): + config_field_row = settings_page.config.get_config_row(params["field_display_name"]) + settings_page.config.type_in_field_with_few_inputs(row=config_field_row, values=[params["changed_value"]]) + with allure.step("Save config"): settings_page.config.save_config() - settings_page.config.assert_input_value_is(params['changed_value'], params['field_display_name']) + settings_page.config.assert_input_value_is(params["changed_value"], params["field_display_name"]) with allure.step(f'Reset value of {params["field_display_name"]}'): - config_field_row = settings_page.config.get_config_row(params['field_display_name']) + config_field_row = settings_page.config.get_config_row(params["field_display_name"]) settings_page.config.reset_to_default(config_field_row) - settings_page.config.assert_input_value_is(params['init_value'], params['field_display_name']) + settings_page.config.assert_input_value_is(params["init_value"], params["field_display_name"]) @pytest.mark.ldap() def test_ldap_config(self, settings_page: AdminSettingsPage): """Test ldap""" - params = {'test_action': "Test LDAP connection", 'connect_action': "Run LDAP sync", "test_value": "test"} + params = { + "test_action": "Test LDAP connection", + "connect_action": "Run LDAP sync", + "test_value": "test", + } with allure.step("Check ldap actions are disabled"): assert settings_page.toolbar.is_adcm_action_inactive( - action_name=params['connect_action'] + action_name=params["connect_action"] ), f"Action {params['connect_action']} should be disabled" assert settings_page.toolbar.is_adcm_action_inactive( - action_name=params['test_action'] + action_name=params["test_action"] ), f"Action {params['test_action']} should be disabled" with allure.step("Fill ldap config"): settings_page.config.expand_or_close_group(group_name="LDAP integration") - settings_page.config.type_in_field_with_few_inputs(row="LDAP URI", values=[params['test_value']]) - settings_page.config.type_in_field_with_few_inputs(row="Bind DN", values=[params['test_value']]) + settings_page.config.type_in_field_with_few_inputs(row="LDAP URI", values=[params["test_value"]]) + settings_page.config.type_in_field_with_few_inputs(row="Bind DN", values=[params["test_value"]]) settings_page.config.type_in_field_with_few_inputs( - row="Bind Password", values=[params['test_value'], params['test_value']] + row="Bind Password", values=[params["test_value"], params["test_value"]] ) - settings_page.config.type_in_field_with_few_inputs(row="User search base", values=[params['test_value']]) - settings_page.config.type_in_field_with_few_inputs(row="Group search base", values=[params['test_value']]) + settings_page.config.type_in_field_with_few_inputs(row="User search base", values=[params["test_value"]]) + settings_page.config.type_in_field_with_few_inputs(row="Group search base", values=[params["test_value"]]) settings_page.config.save_config() settings_page.config.wait_config_loaded() with allure.step("Check ldap actions are enabled"): assert not settings_page.toolbar.is_adcm_action_inactive( - action_name=params['connect_action'] + action_name=params["connect_action"] ), f"Action {params['connect_action']} should be enabled" assert not settings_page.toolbar.is_adcm_action_inactive( - action_name=params['test_action'] + action_name=params["test_action"] ), f"Action {params['test_action']} should be enabled" with allure.step("Check Test LDAP connection action"): - settings_page.toolbar.run_adcm_action(action_name=params['test_action']) + settings_page.toolbar.run_adcm_action(action_name=params["test_action"]) settings_page.header.wait_in_progress_job_amount_from_header(expected_job_amount=1) settings_page.header.wait_in_progress_job_amount_from_header(expected_job_amount=0) with allure.step("Check Run LDAP sync action"): - settings_page.toolbar.run_adcm_action(action_name=params['connect_action']) + settings_page.toolbar.run_adcm_action(action_name=params["connect_action"]) settings_page.header.wait_in_progress_job_amount_from_header(expected_job_amount=1) -@pytest.mark.usefixtures("login_to_adcm_over_api") +@pytest.mark.usefixtures("_login_to_adcm_over_api") class TestAdminUsersPage: """Tests for the /admin/users""" @@ -299,65 +305,63 @@ def test_new_user_creation(self, users_page: AdminUsersPage): """Create new user, change password and login with new password""" params = { - 'username': 'testuser', - 'password': 'test_pass', - 'new_password': 'testtest', - 'first_name': 'First', - 'last_name': 'Last', - 'email': 'priv@et.ru', + "username": "testuser", + "password": "test_pass", + "new_password": "testtest", + "first_name": "First", + "last_name": "Last", + "email": "priv@et.ru", } users_page.create_user( - params['username'], params['password'], params['first_name'], params['last_name'], params['email'] + params["username"], + params["password"], + params["first_name"], + params["last_name"], + params["email"], ) with allure.step(f'Check user {params["username"]} is listed in users list'): - assert users_page.is_user_presented(params['username']), f'User {params["username"]} was not created' - users_page.change_user_password(params['username'], params['new_password']) + assert users_page.is_user_presented(params["username"]), f'User {params["username"]} was not created' + users_page.change_user_password(params["username"], params["new_password"]) users_page.header.logout() with allure.step(f'Login as user {params["username"]} with password {params["new_password"]}'): login_page = LoginPage(users_page.driver, users_page.base_url) login_page.wait_page_is_opened() - login_page.login_user(params['username'], params['new_password']) - with allure.step('Check login was successful'): + login_page.login_user(params["username"], params["new_password"]) + with allure.step("Check login was successful"): AdminIntroPage(users_page.driver, users_page.base_url).wait_page_is_opened(timeout=5) def test_delete_user(self, users_page: AdminUsersPage): """Create new user, delete it and check current user can't be deleted""" + username = "testuser" + current_user = "admin" - params = { - 'username': 'testuser', - 'password': 'test_pass', - 'current_user': 'admin', - 'first_name': 'First', - 'last_name': 'Last', - 'email': 'priv@et.ru', - } - users_page.check_delete_button_not_presented(params['current_user']) - with allure.step(f'Create user {params["username"]}'): - users_page.create_user( - params['username'], params['password'], params['first_name'], params['last_name'], params['email'] - ) - assert users_page.is_user_presented(params['username']), f'User {params["username"]} was not created' - with allure.step(f'Deactivate user {params["username"]}'): - users_page.delete_user(params['username']) - assert users_page.is_user_presented( - params['username'] - ), f'User {params["username"]} should be in users list' - # TODO after ADCM-2582 - # * check user row looks deactivated - # * check user detail table can't be edited + with allure.step("Check user can't delete itself"): + assert not users_page.is_delete_button_presented( + current_user + ), f"Delete button for user {current_user} should be disabled" + + with allure.step(f"Create user {username} and check it has appeared in users list"): + users_page.create_user(username, "test_pass", "First", "Last", "priv@et.ru") + assert users_page.is_user_presented(username), f"User {username} was not created" + + with allure.step(f"Deactivate user {username} and check UI reacted on it"): + users_page.delete_user(username) + assert users_page.is_user_presented(username), f"User {username} should be in users list" + assert users_page.is_user_deactivated(username), "User should be deactivated" + users_page.check_user_update_is_not_allowed(username) def test_change_admin_password(self, users_page: AdminUsersPage): """Change admin password, login with new credentials""" - params = {'username': 'admin', 'password': 'new_pass'} - users_page.update_user_info(params['username'], first_name='Best', last_name='Admin') + params = {"username": "admin", "password": "new_pass"} + users_page.update_user_info(params["username"], first_name="Best", last_name="Admin") users_page.change_user_password(**params) users_page.driver.refresh() - with allure.step('Check Login page is opened'): + with allure.step("Check Login page is opened"): login_page = LoginPage(users_page.driver, users_page.base_url) login_page.wait_page_is_opened() login_page.login_user(**params) - with allure.step('Check login was successful'): + with allure.step("Check login was successful"): AdminIntroPage(users_page.driver, users_page.base_url).wait_page_is_opened(timeout=5) @pytest.mark.ldap() @@ -368,32 +372,32 @@ def test_ldap_user_change_is_forbidden(self, users_page: AdminUsersPage, ldap_us users_page.header.wait_success_job_amount_from_header(1) with allure.step(f'Check user {ldap_user_in_group["name"]} is listed in users list'): assert users_page.is_user_presented( - ldap_user_in_group['name'] + ldap_user_in_group["name"] ), f'User {ldap_user_in_group["name"]} was not created' users_page.check_ldap_user(ldap_user_in_group["name"]) def test_add_user_to_group(self, user, users_page, sdk_client_fs): """Add group for user""" - params = {'name': 'test', 'email': 'test@test.ru'} - test_group = sdk_client_fs.group_create('test_group') + params = {"name": "test", "email": "test@test.ru"} + test_group = sdk_client_fs.group_create("test_group") users_page.update_user_info( user.username, - first_name=params['name'], - last_name=params['name'], - email=params['email'], + first_name=params["name"], + last_name=params["name"], + email=params["email"], group=test_group.name, ) - with allure.step(f'Check user {user.username} is listed in users list with changed params'): + with allure.step(f"Check user {user.username} is listed in users list with changed params"): user_row = users_page.get_user_row_by_username(user.username) assert test_group.name in user_row.text, "User group didn't changed" - assert params['email'] in user_row.text, "User email didn't changed" + assert params["email"] in user_row.text, "User email didn't changed" @pytest.mark.ldap() @pytest.mark.usefixtures("configure_adcm_ldap_ad") def test_add_ldap_group_to_users(self, user, users_page, sdk_client_fs, ldap_user_in_group): """Check that user can't add ldap group to usual user""" with allure.step("Wait ldap integration ends"): - wait_for_task_and_assert_result(sdk_client_fs.adcm().action(name="run_ldap_sync").run(), 'success') + wait_for_task_and_assert_result(sdk_client_fs.adcm().action(name="run_ldap_sync").run(), "success") users_page.check_user_group_change_is_disabled(user.username, "adcm_users") @pytest.mark.ldap() @@ -402,11 +406,11 @@ def test_add_group_to_ldap_users(self, user, users_page, sdk_client_fs, ldap_use """Check that user can add group to ldap user""" with allure.step("Wait ldap integration ends"): - wait_for_task_and_assert_result(sdk_client_fs.adcm().action(name="run_ldap_sync").run(), 'success') - test_group = sdk_client_fs.group_create('test_group') - users_page.update_user_info(ldap_user_in_group['name'], group=test_group.name) - with allure.step(f'Check user {user.username} is listed in users list with changed params'): - user_row = users_page.get_user_row_by_username(ldap_user_in_group['name']) + wait_for_task_and_assert_result(sdk_client_fs.adcm().action(name="run_ldap_sync").run(), "success") + test_group = sdk_client_fs.group_create("test_group") + users_page.update_user_info(ldap_user_in_group["name"], group=test_group.name) + with allure.step(f"Check user {user.username} is listed in users list with changed params"): + user_row = users_page.get_user_row_by_username(ldap_user_in_group["name"]) assert test_group.name in user_row.text, "User group didn't changed" @pytest.mark.ldap() @@ -415,7 +419,7 @@ def test_filter_users(self, user, users_page, sdk_client_fs, ldap_user_in_group) """Check that users can be filtered""" with allure.step("Wait ldap integration ends"): - wait_for_task_and_assert_result(sdk_client_fs.adcm().action(name="run_ldap_sync").run(), 'success') + wait_for_task_and_assert_result(sdk_client_fs.adcm().action(name="run_ldap_sync").run(), "success") users_page.driver.refresh() users_page.filter_users_by("status", "active") with allure.step("Check users are filtered by active status"): @@ -432,29 +436,29 @@ def test_filter_users(self, user, users_page, sdk_client_fs, ldap_user_in_group) users_page.filter_users_by("type", "local") with allure.step("Check users are filtered by local type"): assert users_page.get_all_user_names() == [ - user.username for user in sdk_client_fs.user_list(type='local') + user.username for user in sdk_client_fs.user_list(type="local") ], "Not all local users are visible" users_page.remove_user_filter() users_page.filter_users_by("type", "ldap") with allure.step("Check users are filtered by ldap status"): assert users_page.get_all_user_names() == [ - user.username for user in sdk_client_fs.user_list(type='ldap') + user.username for user in sdk_client_fs.user_list(type="ldap") ], "Not all ldap users are visible" users_page.filter_users_by("status", "active") with allure.step("Check users are filtered both by active status and ldap"): assert users_page.get_all_user_names() == [ - user.username for user in sdk_client_fs.user_list(is_active=True, type='ldap') + user.username for user in sdk_client_fs.user_list(is_active=True, type="ldap") ], "Not all active ldap users are visible" -@pytest.mark.usefixtures("login_to_adcm_over_api") +@pytest.mark.usefixtures("_login_to_adcm_over_api") class TestAdminRolesPage: """Tests for the /admin/roles""" custom_role = AdminRoleInfo( - name='Test_role_name', - description='Test role description', - permissions='Create provider, Create cluster, Create user, Remove policy', + name="Test_role_name", + description="Test role description", + permissions="Create provider, Create cluster, Create user, Remove policy", ) @pytest.mark.smoke() @@ -466,7 +470,7 @@ def test_open_by_tab_admin_roles_page(self, app_fs): roles_page = intro_page.open_roles_menu() roles_page.check_all_elements() roles_page.check_default_roles() - with allure.step('Check that there are 4 default roles'): + with allure.step("Check that there are 4 default roles"): assert len(roles_page.table.get_all_rows()) == 4, "There should be 4 default roles" roles_page.check_admin_toolbar() @@ -496,9 +500,9 @@ def test_check_role_popup_on_roles_page(self, app_fs): """Test changing a role on /admin/roles page""" custom_role_changed = AdminRoleInfo( - name='Test_another_name', - description='Test role description 2', - permissions='Upload bundle', + name="Test_another_name", + description="Test role description 2", + permissions="Upload bundle", ) page = AdminRolesPage(app_fs.driver, app_fs.adcm.url).open() @@ -507,8 +511,8 @@ def test_check_role_popup_on_roles_page(self, app_fs): with allure.step("Check that update unavailable without the role name"): page.fill_role_name_in_role_popup(" ") page.check_save_button_disabled() - page.check_field_error_in_role_popup('Role name is required.') - page.check_field_error_in_role_popup('Role name too short.') + page.check_field_error_in_role_popup("Role name is required.") + page.check_field_error_in_role_popup("Role name too short.") page.fill_role_name_in_role_popup("") page.check_save_button_disabled() page.check_field_error_in_role_popup("Role name is required.") @@ -536,15 +540,15 @@ def test_delete_role_from_roles_page(self, app_fs): page.select_all_roles() page.click_delete_button() page.check_default_roles() - with allure.step('Check that role has been deleted'): + with allure.step("Check that role has been deleted"): assert len(page.table.get_all_rows()) == 4, "There should be 4 default roles" -@pytest.mark.usefixtures("login_to_adcm_over_api") +@pytest.mark.usefixtures("_login_to_adcm_over_api") class TestAdminGroupsPage: """Tests for the /admin/groups""" - custom_group = AdminGroupInfo(name='Test_group', description='Test description', users='admin') + custom_group = AdminGroupInfo(name="Test_group", description="Test description", users="admin") @pytest.mark.smoke() @pytest.mark.include_firefox() @@ -562,7 +566,7 @@ def test_create_group_on_admin_groups_page(self, app_fs): groups_page = AdminGroupsPage(app_fs.driver, app_fs.adcm.url).open() groups_page.create_custom_group(self.custom_group.name, self.custom_group.description, self.custom_group.users) current_groups = groups_page.get_all_groups() - with allure.step('Check that there are 1 custom group'): + with allure.step("Check that there are 1 custom group"): assert len(current_groups) == 1, "There should be 1 group on the page" assert self.custom_group in current_groups, "Created group should be on the page" @@ -571,16 +575,20 @@ def test_create_group_on_admin_groups_page(self, app_fs): def test_create_group_with_ldap_user_on_admin_groups_page(self, sdk_client_fs, app_fs, ldap_user_in_group): """Test create a group on /admin/groups""" - wait_for_task_and_assert_result(sdk_client_fs.adcm().action(name="run_ldap_sync").run(), 'success') + wait_for_task_and_assert_result(sdk_client_fs.adcm().action(name="run_ldap_sync").run(), "success") groups_page = AdminGroupsPage(app_fs.driver, app_fs.adcm.url).open() groups_page.create_custom_group( self.custom_group.name, self.custom_group.description, ldap_user_in_group["name"] ) current_groups = groups_page.get_all_groups() - with allure.step('Check that there are 1 custom group and 1 ldap'): + with allure.step("Check that there are 1 custom group and 1 ldap"): assert len(current_groups) == 2, "There should be 2 group on the page" assert ( - AdminGroupInfo(name='Test_group', description='Test description', users=ldap_user_in_group["name"]) + AdminGroupInfo( + name="Test_group", + description="Test description", + users=ldap_user_in_group["name"], + ) in current_groups ), "Created group should be on the page" @@ -605,7 +613,7 @@ def test_delete_group_from_groups_page(self, app_fs): page.create_custom_group(self.custom_group.name, self.custom_group.description, self.custom_group.users) page.select_all_groups() page.click_delete_button() - with allure.step('Check that group has been deleted'): + with allure.step("Check that group has been deleted"): assert len(page.table.get_all_rows()) == 0, "There should be 0 groups" @pytest.mark.ldap() @@ -613,35 +621,35 @@ def test_delete_group_from_groups_page(self, app_fs): def test_ldap_group_change_is_forbidden(self, app_fs, ldap_user_in_group): """Change ldap group""" - params = {'group_name': 'adcm_users'} + params = {"group_name": "adcm_users"} groups_page = AdminGroupsPage(app_fs.driver, app_fs.adcm.url).open() groups_page.header.wait_success_job_amount_from_header(1) with allure.step(f"Check group {params['group_name']} is listed in groups list"): assert ( - groups_page.get_all_groups()[0].name == params['group_name'] + groups_page.get_all_groups()[0].name == params["group_name"] ), f"Group {params['group_name']} should be in groups list" - groups_page.check_ldap_group(params['group_name']) + groups_page.check_ldap_group(params["group_name"]) @pytest.mark.ldap() @pytest.mark.usefixtures("configure_adcm_ldap_ad") def test_add_ldap_user_to_group(self, app_fs, ldap_user_in_group): """Add ldap user to group""" - params = {'group_name': 'Test_group'} + params = {"group_name": "Test_group"} groups_page = AdminGroupsPage(app_fs.driver, app_fs.adcm.url).open() - groups_page.create_custom_group(name=params['group_name'], description=None, users=None) + groups_page.create_custom_group(name=params["group_name"], description=None, users=None) groups_page.header.wait_success_job_amount_from_header(1) - groups_page.update_group(name=params['group_name'], users=ldap_user_in_group["name"]) + groups_page.update_group(name=params["group_name"], users=ldap_user_in_group["name"]) with allure.step(f"Check group {params['group_name']} has user {ldap_user_in_group['name']}"): assert ( - ldap_user_in_group["name"] in groups_page.get_group_by_name(params['group_name']).text + ldap_user_in_group["name"] in groups_page.get_group_by_name(params["group_name"]).text ), f"Group {params['group_name']} should have user {ldap_user_in_group['name']}" class TestAdminPolicyPage: """Tests for the /admin/policies""" - @allure.step('Check custome policy') + @allure.step("Check custome policy") def check_custom_policy(self, policies_page, policy=None): """Check that there is only one created policy with expected params""" @@ -650,7 +658,7 @@ def check_custom_policy(self, policies_page, policy=None): assert len(current_policies) == 1, "There should be 1 policy on the page" assert current_policies == [policy], "Created policy should be on the page" - @pytest.mark.usefixtures("login_to_adcm_over_api") + @pytest.mark.usefixtures("_login_to_adcm_over_api") @pytest.mark.smoke() @pytest.mark.include_firefox() def test_open_by_tab_admin_policies_page(self, app_fs): @@ -661,7 +669,7 @@ def test_open_by_tab_admin_policies_page(self, app_fs): policies_page.check_all_elements() policies_page.check_admin_toolbar() - @pytest.mark.usefixtures("login_to_adcm_over_api") + @pytest.mark.usefixtures("_login_to_adcm_over_api") def test_create_policy_on_admin_groups_page(self, app_fs): """Test create a group on /admin/policies""" @@ -674,7 +682,7 @@ def test_create_policy_on_admin_groups_page(self, app_fs): ) self.check_custom_policy(policies_page) - @pytest.mark.usefixtures("login_to_adcm_over_api") + @pytest.mark.usefixtures("_login_to_adcm_over_api") @pytest.mark.full() def test_check_pagination_policy_list_page(self, app_fs): """Test pagination on /admin/policies page""" @@ -690,7 +698,7 @@ def test_check_pagination_policy_list_page(self, app_fs): ) policies_page.table.check_pagination(second_page_item_amount=1) - @pytest.mark.usefixtures("login_to_adcm_over_api") + @pytest.mark.usefixtures("_login_to_adcm_over_api") def test_delete_policy_from_policies_page(self, app_fs): """Test delete custom group on /admin/policies page""" @@ -703,31 +711,51 @@ def test_delete_policy_from_policies_page(self, app_fs): ) policies_page.delete_all_policies() - # pylint: disable=too-many-arguments - @pytest.mark.usefixtures("login_to_adcm_over_api") + @pytest.mark.usefixtures("_login_to_adcm_over_api") @pytest.mark.parametrize( ("clusters", "services", "providers", "hosts", "parents", "role_name"), [ - (CLUSTER_NAME, None, None, None, None, 'View cluster configurations'), - (None, SERVICE_NAME, None, None, CLUSTER_NAME, 'View service configurations'), - (None, None, PROVIDER_NAME, None, None, 'View provider configurations'), - (None, None, None, HOST_NAME, None, 'View host configurations'), - (None, SERVICE_NAME, None, None, CLUSTER_NAME, 'View component configurations'), - (CLUSTER_NAME, None, None, None, None, 'View cluster configurations, View service configurations'), + (CLUSTER_NAME, None, None, None, None, "View cluster configurations"), + (None, SERVICE_NAME, None, None, CLUSTER_NAME, "View service configurations"), + (None, None, PROVIDER_NAME, None, None, "View provider configurations"), + (None, None, None, HOST_NAME, None, "View host configurations"), + (None, SERVICE_NAME, None, None, CLUSTER_NAME, "View component configurations"), + ( + CLUSTER_NAME, + None, + None, + None, + None, + "View cluster configurations, View service configurations", + ), ( None, SERVICE_NAME, None, None, CLUSTER_NAME, - 'View cluster configurations, View service configurations, View component configurations, ' - 'View host configurations', + "View cluster configurations, View service configurations, View component configurations, " + "View host configurations", + ), + ( + None, + None, + PROVIDER_NAME, + None, + None, + "View provider configurations, View host configurations", + ), + ( + None, + None, + None, + HOST_NAME, + None, + "View provider configurations, View host configurations", ), - (None, None, PROVIDER_NAME, None, None, 'View provider configurations, View host configurations'), - (None, None, None, HOST_NAME, None, 'View provider configurations, View host configurations'), ], ) - @pytest.mark.usefixtures('parents', 'create_cluster_with_component') + @pytest.mark.usefixtures("parents", "create_cluster_with_component") def test_check_policy_popup_for_entities( self, sdk_client_fs, @@ -781,7 +809,7 @@ def test_policy_permission_to_view_access_cluster( sdk_client_fs.policy_create( name="Test policy", role=test_role, - user=[sdk_client_fs.user(username=another_user['username'])], + user=[sdk_client_fs.user(username=another_user["username"])], objects=[cluster], ) with allure.step("Create second cluster"): @@ -812,7 +840,7 @@ def test_policy_permission_to_view_access_service( sdk_client_fs.policy_create( name="Test policy", role=test_role, - user=[sdk_client_fs.user(username=another_user['username'])], + user=[sdk_client_fs.user(username=another_user["username"])], objects=[service], ) with allure.step("Create second service"): @@ -847,13 +875,14 @@ def test_policy_permission_to_view_access_component( sdk_client_fs.policy_create( name="Test policy", role=test_role, - user=[sdk_client_fs.user(username=another_user['username'])], + user=[sdk_client_fs.user(username=another_user["username"])], objects=[service], ) with allure.step("Create second component"): second_service = cluster.service_add(name="test_service_2") cluster.hostcomponent_set( - (host, service.component(name=FIRST_COMPONENT_NAME)), (host, second_service.component(name="second")) + (host, service.component(name=FIRST_COMPONENT_NAME)), + (host, second_service.component(name="second")), ) login_page = LoginPage(app_fs.driver, app_fs.adcm.url).open() login_page.login_user(**another_user) @@ -872,8 +901,8 @@ def test_policy_permission_to_view_access_component( def test_policy_permission_to_view_access_provider(self, sdk_client_fs, app_fs, another_user): """Test for the permissions to provider.""" - provider_bundle = sdk_client_fs.upload_from_fs(os.path.join(utils.get_data_dir(__file__), 'provider')) - provider = provider_bundle.provider_create('test_provider') + provider_bundle = sdk_client_fs.upload_from_fs(os.path.join(utils.get_data_dir(__file__), "provider")) + provider = provider_bundle.provider_create("test_provider") with allure.step("Create test role"): test_role = sdk_client_fs.role_create( name=CUSTOM_ROLE_NAME, @@ -884,14 +913,14 @@ def test_policy_permission_to_view_access_provider(self, sdk_client_fs, app_fs, sdk_client_fs.policy_create( name="Test policy", role=test_role, - user=[sdk_client_fs.user(username=another_user['username'])], + user=[sdk_client_fs.user(username=another_user["username"])], objects=[provider], ) with allure.step("Create second provider"): provider_bundle = sdk_client_fs.upload_from_fs( - os.path.join(utils.get_data_dir(__file__), 'second_provider') + os.path.join(utils.get_data_dir(__file__), "second_provider") ) - second_provider = provider_bundle.provider_create('second_test_provider') + second_provider = provider_bundle.provider_create("second_test_provider") login_page = LoginPage(app_fs.driver, app_fs.adcm.url).open() login_page.login_user(**another_user) AdminIntroPage(app_fs.driver, app_fs.adcm.url).wait_page_is_opened() @@ -905,9 +934,9 @@ def test_policy_permission_to_view_access_provider(self, sdk_client_fs, app_fs, def test_policy_permission_to_view_access_host(self, sdk_client_fs, app_fs, another_user): """Test for the permissions to host.""" - provider_bundle = sdk_client_fs.upload_from_fs(os.path.join(utils.get_data_dir(__file__), 'provider')) - provider = provider_bundle.provider_create('test_provider') - host = provider.host_create('test-host') + provider_bundle = sdk_client_fs.upload_from_fs(os.path.join(utils.get_data_dir(__file__), "provider")) + provider = provider_bundle.provider_create("test_provider") + host = provider.host_create("test-host") with allure.step("Create test role"): test_role = sdk_client_fs.role_create( name=CUSTOM_ROLE_NAME, @@ -918,11 +947,11 @@ def test_policy_permission_to_view_access_host(self, sdk_client_fs, app_fs, anot sdk_client_fs.policy_create( name="Test policy", role=test_role, - user=[sdk_client_fs.user(username=another_user['username'])], + user=[sdk_client_fs.user(username=another_user["username"])], objects=[host], ) with allure.step("Create second host"): - second_host = provider.host_create('test-host-2') + second_host = provider.host_create("test-host-2") login_page = LoginPage(app_fs.driver, app_fs.adcm.url).open() login_page.login_user(**another_user) AdminIntroPage(app_fs.driver, app_fs.adcm.url).wait_page_is_opened() @@ -950,7 +979,7 @@ def test_policy_permission_to_run_cluster_action_and_view_task( sdk_client_fs.policy_create( name="Test policy", role=test_role, - user=[sdk_client_fs.user(username=another_user['username'])], + user=[sdk_client_fs.user(username=another_user["username"])], objects=[cluster], ) with allure.step("Create second cluster"): @@ -971,7 +1000,7 @@ def test_policy_permission_to_run_cluster_action_and_view_task( sdk_client_fs.policy_create( name="Test policy 2", role=test_role_2, - user=[sdk_client_fs.user(username=another_user['username'])], + user=[sdk_client_fs.user(username=another_user["username"])], objects=[second_cluster], ) with allure.step("Check that user can view second cluster"): @@ -980,13 +1009,13 @@ def test_policy_permission_to_run_cluster_action_and_view_task( assert len(cluster_rows) == 2, "There should be 2 row with cluster" with allure.step("Check actions in clusters"): assert cluster_list_page.get_all_actions_name_in_cluster(cluster_rows[0]) == [ - 'some_action' + "some_action" ], "First cluster action should be visible" assert ( cluster_list_page.get_all_actions_name_in_cluster(cluster_rows[1]) == [] ), "Second cluster action should not be visible" with allure.step("Run action from first cluster"): - cluster_list_page.run_action_in_cluster_row(cluster_rows[0], 'some_action') + cluster_list_page.run_action_in_cluster_row(cluster_rows[0], "some_action") with allure.step("Check task"): cluster_list_page.header.click_job_block_in_header() assert len(cluster_list_page.header.get_job_rows_from_popup()) == 1, "Job amount should be 1" @@ -994,7 +1023,7 @@ def test_policy_permission_to_run_cluster_action_and_view_task( job_rows = job_list_page.table.get_all_rows() assert len(job_rows) == 1, "Should be only 1 task" task_info = job_list_page.get_task_info_from_table(0) - assert task_info.action_name == 'some_action', "Wrong task name" + assert task_info.action_name == "some_action", "Wrong task name" assert task_info.invoker_objects == cluster.name, "Wrong cluster name" job_list_page.click_on_action_name_in_row(job_rows[0]) JobPageStdout(app_fs.driver, app_fs.adcm.url, 1).wait_page_is_opened() @@ -1006,7 +1035,7 @@ def test_policy_permission_to_run_cluster_action_and_view_task( ), "There are no permission hint" # pylint: enable=too-many-locals - @pytest.mark.usefixtures("login_to_adcm_over_api") + @pytest.mark.usefixtures("_login_to_adcm_over_api") def test_policy_with_maintenance_mode(self, sdk_client_fs, app_fs, another_user, create_cluster_with_component): """Test create a group on /admin/policies""" @@ -1021,7 +1050,7 @@ def test_policy_with_maintenance_mode(self, sdk_client_fs, app_fs, another_user, sdk_client_fs.policy_create( name="Test policy", role=test_role, - user=[sdk_client_fs.user(username=another_user['username'])], + user=[sdk_client_fs.user(username=another_user["username"])], objects=[host], ) login_over_api(app_fs, another_user) diff --git a/tests/ui_tests/test_bundle_list_page.py b/tests/ui_tests/test_bundle_list_page.py index e6ee2e5a2e..51861ddf02 100644 --- a/tests/ui_tests/test_bundle_list_page.py +++ b/tests/ui_tests/test_bundle_list_page.py @@ -21,12 +21,11 @@ from adcm_pytest_plugin import utils from adcm_pytest_plugin.utils import catch_failed from selenium.common.exceptions import ElementClickInterceptedException - from tests.conftest import DUMMY_CLUSTER_BUNDLE from tests.ui_tests.app.app import ADCMTest from tests.ui_tests.app.page.admin.page import AdminIntroPage from tests.ui_tests.app.page.bundle.page import BundlePage -from tests.ui_tests.app.page.bundle_list.page import BundleListPage, BundleInfo +from tests.ui_tests.app.page.bundle_list.page import BundleInfo, BundleListPage from tests.ui_tests.app.page.cluster_list.page import ClusterListPage from tests.ui_tests.app.page.host_list.page import HostListPage @@ -91,8 +90,7 @@ def check_bundle_info_is_equal(actual_info: BundleInfo, expected_info: BundleInf @pytest.fixture() -# pylint: disable-next=unused-argument -def page(app_fs: ADCMTest, login_to_adcm_over_api) -> BundleListPage: +def page(app_fs: ADCMTest, _login_to_adcm_over_api) -> BundleListPage: """Get BundleListPage after authorization""" return BundleListPage(app_fs.driver, app_fs.adcm.url).open() diff --git a/tests/ui_tests/test_cluster_list_page.py b/tests/ui_tests/test_cluster_list_page.py index a68c88c753..3ed0ac7c6e 100644 --- a/tests/ui_tests/test_cluster_list_page.py +++ b/tests/ui_tests/test_cluster_list_page.py @@ -17,41 +17,27 @@ import allure import pytest from _pytest.fixtures import SubRequest -from adcm_client.objects import ( - ADCMClient, - Bundle, - Provider, - Cluster, - Host, -) -from adcm_pytest_plugin import params -from adcm_pytest_plugin import utils -from adcm_pytest_plugin.utils import get_data_dir -from adcm_pytest_plugin.utils import parametrize_by_data_subdirs -from adcm_pytest_plugin.utils import random_string +from adcm_client.objects import ADCMClient, Bundle, Cluster, Host, Provider +from adcm_pytest_plugin import params, utils +from adcm_pytest_plugin.utils import get_data_dir, parametrize_by_data_subdirs from selenium.common.exceptions import TimeoutException -from selenium.webdriver.remote.webdriver import WebElement - from tests.library.status import ADCMObjectStatusChanger from tests.ui_tests.app.helpers.configs_generator import ( generate_configs, prepare_config, - TYPES, - generate_group_configs, - prepare_group_config, ) from tests.ui_tests.app.page.admin.page import AdminIntroPage from tests.ui_tests.app.page.cluster.page import ( - ClusterImportPage, + ClusterComponentsPage, ClusterConfigPage, + ClusterGroupConfigConfig, + ClusterGroupConfigHosts, ClusterGroupConfigPage, - ClusterMainPage, ClusterHostPage, + ClusterImportPage, + ClusterMainPage, ClusterServicesPage, - ClusterComponentsPage, ClusterStatusPage, - ClusterGroupConfigConfig, - ClusterGroupConfigHosts, ) from tests.ui_tests.app.page.cluster_list.page import ClusterListPage from tests.ui_tests.app.page.common.configuration.page import CONFIG_ITEMS @@ -59,22 +45,21 @@ from tests.ui_tests.app.page.common.host_components.page import ComponentsHostRowInfo from tests.ui_tests.app.page.common.import_page.page import ImportItemInfo from tests.ui_tests.app.page.common.status.page import ( - SUCCESS_COLOR, NEGATIVE_COLOR, + SUCCESS_COLOR, + StatusRowInfo, ) -from tests.ui_tests.app.page.common.status.page import StatusRowInfo -from tests.ui_tests.app.page.host.page import ( - HostMainPage, - HostConfigPage, -) +from tests.ui_tests.app.page.host.page import HostConfigPage, HostMainPage from tests.ui_tests.app.page.service.page import ( - ServiceMainPage, ServiceConfigPage, ServiceImportPage, + ServiceMainPage, ) from tests.ui_tests.utils import ( - wait_and_assert_ui_info, check_host_value, + create_few_groups, + prepare_cluster_and_open_config_page, + wait_and_assert_ui_info, wrap_in_dict, ) @@ -108,10 +93,11 @@ DISCLAIMER_TEXT = "Are you really want to click me?" -# pylint: disable=redefined-outer-name,unused-argument,too-many-lines,too-many-public-methods -# pylint: disable=too-many-arguments,too-many-boolean-expressions,too-many-branches,too-many-nested-blocks,too-many-locals +# pylint: disable=redefined-outer-name,too-many-lines,too-many-public-methods +# pylint: disable=too-many-boolean-expressions +# pylint: disable=too-many-branches,too-many-nested-blocks,too-many-locals -pytestmark = pytest.mark.usefixtures("login_to_adcm_over_api") +pytestmark = pytest.mark.usefixtures("_login_to_adcm_over_api") # !===== Fixtures =====! @@ -217,41 +203,6 @@ def check_components_host_info(host_info: ComponentsHostRowInfo, name: str, comp # !===== Funcs =====! -@allure.step("Prepare cluster and open config page") -def prepare_cluster_and_open_config_page(sdk_client: ADCMClient, path, app): - """Upload bundle, create cluster and open config page""" - bundle = sdk_client.upload_from_fs(path) - cluster = bundle.cluster_create(name=f"Test cluster {random_string()}") - config = ClusterConfigPage(app.driver, app.adcm.url, cluster.cluster_id).open() - config.wait_page_is_opened() - return cluster, config - - -def check_default_field_values_in_configs( - cluster_config_page: ClusterConfigPage, config_item: WebElement, field_type: str, config -): - """Check that input value in config is equal to the default one""" - main_config = config['config'][0]['subs'][0] if "subs" in config['config'][0] else config['config'][0] - if field_type == 'boolean': - cluster_config_page.config.assert_checkbox_state(config_item, expected_value=main_config['default']) - elif field_type in ("password", "secrettext"): - is_password_value = bool(field_type == "password") - cluster_config_page.config.assert_input_value_is( - expected_value='********', display_name=field_type, is_password=is_password_value - ) - elif field_type == "list": - cluster_config_page.config.assert_list_value_is(expected_value=main_config['default'], display_name=field_type) - elif field_type == "map": - cluster_config_page.config.assert_map_value_is(expected_value=main_config['default'], display_name=field_type) - elif field_type == "file": - cluster_config_page.config.assert_input_value_is(expected_value="test", display_name=field_type) - else: - expected_value = ( - str(main_config['default']) if field_type in ("integer", "float", "json") else main_config['default'] - ) - cluster_config_page.config.assert_input_value_is(expected_value=expected_value, display_name=field_type) - - @allure.step("Check that cluster has been upgraded") def check_cluster_upgraded(app_fs, upgrade_cluster_name: str, state: str): """Open cluster list page and check that cluster has been upgraded""" @@ -261,34 +212,6 @@ def check_cluster_upgraded(app_fs, upgrade_cluster_name: str, state: str): assert cluster_page.get_cluster_state_from_row(row) == state, f"Cluster state should be {state}" -@allure.step("Check save button and save config") -def _check_save_in_configs(cluster_config_page, field_type, expected_state, is_default): - """ - Check save button and save config. - It is a workaround for each type of field because it won't work other way on ui with selenium. - """ - - config_row = cluster_config_page.config.get_config_row(field_type) - if field_type == 'list': - cluster_config_page.config.click_add_item_btn_in_row(config_row) - if field_type in ['string', 'integer', 'text', 'float', 'file', 'json']: - config_row.click() - if field_type == 'secrettext': - cluster_config_page.config.reset_to_default(config_row) - if field_type == 'boolean' and is_default: - for _ in range(3): - cluster_config_page.config.click_boolean_checkbox(config_row) - if field_type == 'password': - if is_default: - cluster_config_page.config.reset_to_default(config_row) - else: - config_row.click() - if field_type == 'map': - cluster_config_page.config.click_add_item_btn_in_row(config_row) - cluster_config_page.config.reset_to_default(config_row) - cluster_config_page.config.check_save_btn_state_and_save_conf(expected_state) - - # !===== Tests =====! @@ -433,8 +356,20 @@ def test_run_upgrade_on_cluster_list_page(self, sdk_client_fs, app_fs): ("upgrade_with_config", {"somestring2": "test"}, False, False, False), ("upgrade_with_config_and_hc_acl", {"somestring2": "test"}, True, False, False), ("upgrade_with_hc_acl_and_disclaimer", None, True, False, DISCLAIMER_TEXT), - ("upgrade_with_config_and_disclaimer", {"somestring2": "test"}, False, False, DISCLAIMER_TEXT), - ("upgrade_with_config_and_hc_acl_and_disclaimer", {"somestring2": "test"}, True, False, DISCLAIMER_TEXT), + ( + "upgrade_with_config_and_disclaimer", + {"somestring2": "test"}, + False, + False, + DISCLAIMER_TEXT, + ), + ( + "upgrade_with_config_and_hc_acl_and_disclaimer", + {"somestring2": "test"}, + True, + False, + DISCLAIMER_TEXT, + ), # next steps are skipped until https://tracker.yandex.ru/ADCM-3001 # ("upgrade_with_hc_acl", None, True, True, False), # ("upgrade_with_config_and_hc_acl", {"somestring2": "test"}, True, True, False), @@ -454,9 +389,7 @@ def test_run_upgrade_v2_on_cluster_list_page( disclaimer_text, ): """Test run upgrade new version from the /cluster page""" - params = { - "state": "upgraded", - } + params = {"state": "upgraded"} with allure.step("Upload main cluster bundle"): bundle = cluster_bundle(sdk_client_fs, BUNDLE_COMMUNITY) cluster = bundle.cluster_create(name=CLUSTER_NAME) @@ -906,7 +839,9 @@ def test_check_cluster_components_page_create_components(self, app_fs, sdk_clien check_components_host_info(cluster_components_page.get_row_info(host_row), HOST_NAME, "1") component_row = cluster_components_page.get_components_rows()[0] check_components_host_info( - cluster_components_page.get_row_info(component_row), COMPONENT_NAME, "1" if is_not_required else "1 / 1" + cluster_components_page.get_row_info(component_row), + COMPONENT_NAME, + "1" if is_not_required else "1 / 1", ) def test_check_cluster_components_page_restore_components( @@ -977,7 +912,8 @@ def test_warning_on_cluster_components_page(self, app_fs, sdk_client_fs): cluster_components_page = ClusterComponentsPage(app_fs.driver, app_fs.adcm.url, cluster.id).open() cluster_components_page.config.check_hostcomponents_warn_icon_on_left_menu() cluster_components_page.toolbar.check_warn_button( - tab_name=CLUSTER_NAME, expected_warn_text=['Test cluster has an issue with host-component mapping'] + tab_name=CLUSTER_NAME, + expected_warn_text=['Test cluster has an issue with host-component mapping'], ) @@ -1030,7 +966,12 @@ def test_save_custom_config_on_cluster_config_page(self, app_fs, create_cluster_ def test_reset_config_in_row_on_cluster_config_page(self, app_fs, create_community_cluster): """Test config reset on cluster/{}/config page""" - params = {"row_name": "str_param", "row_value_new": "test", "row_value_old": "123", "config_name": "test_name"} + params = { + "row_name": "str_param", + "row_value_new": "test", + "row_value_old": "123", + "config_name": "test_name", + } cluster_config_page = ClusterConfigPage(app_fs.driver, app_fs.adcm.url, create_community_cluster.id).open() config_row = cluster_config_page.config.get_all_config_rows()[0] cluster_config_page.config.type_in_field_with_few_inputs( @@ -1100,136 +1041,6 @@ def test_field_tooltips_on_cluster_config_page(self, app_fs, sdk_client_fs): for item in CONFIG_ITEMS: cluster_config_page.config.check_text_in_tooltip(item, f"Test description {item}") - @pytest.mark.full() - @pytest.mark.parametrize("field_type", TYPES) - @pytest.mark.parametrize("is_advanced", [True, False], ids=("field_advanced", "field_non-advanced")) - @pytest.mark.parametrize("is_default", [True, False], ids=("default", "not_default")) - @pytest.mark.parametrize("is_required", [True, False], ids=("required", "not_required")) - @pytest.mark.parametrize("is_read_only", [True, False], ids=("read_only", "not_read_only")) - @pytest.mark.parametrize( - "config_group_customization", - [ - pytest.param(True, id="config_group_customization_true", marks=pytest.mark.regression), - pytest.param(False, id="config_group_customization_false", marks=pytest.mark.regression), - ], - ) - @pytest.mark.parametrize( - "group_customization", - [ - pytest.param(True, id="group_customization_true", marks=pytest.mark.regression), - pytest.param(False, id="group_customization_false", marks=pytest.mark.regression), - ], - ) - @pytest.mark.usefixtures("login_to_adcm_over_api") - def test_configs_fields_invisible_true( - self, - sdk_client_fs: ADCMClient, - app_fs, - field_type, - is_advanced, - is_default, - is_required, - is_read_only, - config_group_customization, - group_customization, - ): - """Check RO field with invisible true - Scenario: - 1. Check that field invisible - 2. Check that save button not active - 3. Click advanced - 4. Check that field invisible - """ - - _, _, path = prepare_config( - generate_configs( - field_type=field_type, - advanced=is_advanced, - default=is_default, - required=is_required, - read_only=is_read_only, - config_group_customization=config_group_customization, - group_customization=group_customization, - ) - ) - _, cluster_config_page = prepare_cluster_and_open_config_page(sdk_client_fs, path, app_fs) - cluster_config_page.config.check_no_rows_or_groups_on_page() - cluster_config_page.config.check_no_rows_or_groups_on_page_with_advanced() - with allure.step('Check that save button is disabled'): - assert cluster_config_page.config.is_save_btn_disabled(), 'Save button should be disabled' - - # pylint: disable=too-many-locals - @pytest.mark.skip(reason="https://tracker.yandex.ru/ADCM-3216") - @pytest.mark.full() - @pytest.mark.parametrize("field_type", TYPES) - @pytest.mark.parametrize("is_advanced", [True, False], ids=("field_advanced", "field_non-advanced")) - @pytest.mark.parametrize("is_default", [True, False], ids=("default", "not_default")) - @pytest.mark.parametrize("is_required", [True, False], ids=("required", "not_required")) - @pytest.mark.parametrize("is_read_only", [True, False], ids=("read_only", "not_read_only")) - @pytest.mark.parametrize( - "config_group_customization", - [ - pytest.param(True, id="config_group_customization_true", marks=pytest.mark.regression), - pytest.param(False, id="config_group_customization_false", marks=pytest.mark.regression), - ], - ) - @pytest.mark.parametrize( - "group_customization", - [ - pytest.param(True, id="group_customization_true", marks=pytest.mark.regression), - pytest.param(False, id="group_customization_false", marks=pytest.mark.regression), - ], - ) - @pytest.mark.usefixtures("login_to_adcm_over_api") - def test_configs_fields_invisible_false( - self, - sdk_client_fs: ADCMClient, - app_fs, - field_type, - is_advanced, - is_default, - is_required, - is_read_only, - config_group_customization, - group_customization, - ): - """Test config fields that aren't invisible""" - config, expected, path = prepare_config( - generate_configs( - field_type=field_type, - invisible=False, - advanced=is_advanced, - default=is_default, - required=is_required, - read_only=is_read_only, - config_group_customization=config_group_customization, - group_customization=group_customization, - ) - ) - _, cluster_config_page = prepare_cluster_and_open_config_page(sdk_client_fs, path, app_fs) - - def check_expectations(): - with allure.step('Check that field visible'): - for config_item in cluster_config_page.config.get_all_config_rows(): - assert config_item.is_displayed(), f"Config field {field_type} should be visible" - if is_default: - check_default_field_values_in_configs(cluster_config_page, config_item, field_type, config) - if is_read_only: - assert cluster_config_page.config.is_element_read_only( - config_item - ), f"Config field {field_type} should be read only" - if expected['alerts'] and not is_read_only: - cluster_config_page.config.check_invalid_value_message(field_type) - - with allure.step('Check that save button is disabled'): - assert cluster_config_page.config.is_save_btn_disabled(), 'Save button should be disabled' - if is_advanced: - cluster_config_page.config.check_no_rows_or_groups_on_page() - else: - check_expectations() - cluster_config_page.config.click_on_advanced() - check_expectations() - # pylint: enable=too-many-locals @pytest.mark.full() @parametrize_by_data_subdirs(__file__, 'bundles_for_numbers_tests') @@ -1315,7 +1126,9 @@ def test_save_list_on_cluster_config_page(self, sdk_client_fs: ADCMClient, app_f ) _, cluster_config_page = prepare_cluster_and_open_config_page(sdk_client_fs, path, app_fs) cluster_config_page.config.type_in_field_with_few_inputs( - row=cluster_config_page.config.get_all_config_rows()[0], values=params["new_value"], clear=True + row=cluster_config_page.config.get_all_config_rows()[0], + values=params["new_value"], + clear=True, ) cluster_config_page.config.save_config() cluster_config_page.driver.refresh() @@ -1351,168 +1164,6 @@ def test_save_map_on_cluster_config_page(self, sdk_client_fs: ADCMClient, app_fs expected_value=params["new_value"], display_name=config['config'][0]['name'] ) - @pytest.mark.full() - @pytest.mark.parametrize("field_type", TYPES) - @pytest.mark.parametrize("activatable", [True, False], ids=("activatable", "non-activatable")) - @pytest.mark.parametrize( - "active", [pytest.param(True, id="active"), pytest.param(False, id="inactive", marks=pytest.mark.regression)] - ) - @pytest.mark.parametrize("group_advanced", [True, False], ids=("group_advanced", "group_non-advanced")) - @pytest.mark.parametrize("is_default", [True, False], ids=("default", "not_default")) - @pytest.mark.parametrize("is_required", [True, False], ids=("required", "not_required")) - @pytest.mark.parametrize("is_read_only", [True, False], ids=("read_only", "not_read_only")) - @pytest.mark.parametrize("field_invisible", [True, False], ids=("invisible", "visible")) - @pytest.mark.parametrize( - "field_advanced", - [ - pytest.param(True, id="field_advanced", marks=pytest.mark.regression), - pytest.param(False, id="field_non_advanced"), - ], - ) - @pytest.mark.usefixtures("login_to_adcm_over_api") - def test_group_configs_fields_invisible_true( - self, - sdk_client_fs: ADCMClient, - app_fs, - field_type, - activatable, - active, - group_advanced, - is_default, - is_required, - is_read_only, - field_invisible, - field_advanced, - ): - """Test for configuration fields with groups. Before start test actions - we always create configuration and expected result. All logic for test - expected result in functions before this test function. If we have - advanced fields inside configuration and group visible we check - field and group status after clicking advanced button. For activatable - groups we don't change group status. We have two types of tests for activatable - groups: the first one when group is active and the second when group not active. - Scenario: - 1. Generate configuration and expected result for test - 2. Upload bundle - 3. Create cluster - 4. Open configuration page - 5. Check save button status - 6. Check field configuration (depends on expected result dict and bundle configuration)""" - - _, _, path = prepare_group_config( - generate_group_configs( - field_type=field_type, - activatable=activatable, - active=active, - group_advanced=group_advanced, - default=is_default, - required=is_required, - read_only=is_read_only, - field_invisible=field_invisible, - field_advanced=field_advanced, - ) - ) - _, cluster_config_page = prepare_cluster_and_open_config_page(sdk_client_fs, path, app_fs) - cluster_config_page.config.check_no_rows_or_groups_on_page() - cluster_config_page.config.check_no_rows_or_groups_on_page_with_advanced() - with allure.step('Check that save button is disabled'): - assert cluster_config_page.config.is_save_btn_disabled(), 'Save button should be disabled' - - @pytest.mark.full() - @pytest.mark.parametrize("field_type", TYPES) - @pytest.mark.parametrize("activatable", [True, False], ids=("activatable", "non-activatable")) - @pytest.mark.parametrize( - "active", [pytest.param(True, id="active"), pytest.param(False, id="inactive", marks=pytest.mark.regression)] - ) - @pytest.mark.parametrize("group_advanced", [True, False], ids=("group_advanced", "group_non-advanced")) - @pytest.mark.parametrize("is_default", [True, False], ids=("default", "not_default")) - @pytest.mark.parametrize("is_required", [True, False], ids=("required", "not_required")) - @pytest.mark.parametrize("is_read_only", [True, False], ids=("read_only", "not_read_only")) - @pytest.mark.parametrize("field_invisible", [True, False], ids=("invisible", "visible")) - @pytest.mark.parametrize( - "field_advanced", - [ - pytest.param(True, id="field_advanced", marks=pytest.mark.regression), - pytest.param(False, id="field_non_advanced"), - ], - ) - @pytest.mark.usefixtures("login_to_adcm_over_api") # pylint: disable-next=too-many-locals - def test_group_configs_fields_invisible_false( - self, - sdk_client_fs: ADCMClient, - app_fs, - field_type, - activatable, - active, - group_advanced, - is_default, - is_required, - is_read_only, - field_invisible, - field_advanced, - ): - """Test group configs with not-invisible fields""" - config, expected, path = prepare_group_config( - generate_group_configs( - field_type=field_type, - activatable=activatable, - active=active, - group_invisible=False, - group_advanced=group_advanced, - default=is_default, - required=is_required, - read_only=is_read_only, - field_invisible=field_invisible, - field_advanced=field_advanced, - ) - ) - _, cluster_config_page = prepare_cluster_and_open_config_page(sdk_client_fs, path, app_fs) - - def check_expectations(): - with allure.step('Check that field visible'): - for config_item in cluster_config_page.config.get_all_config_rows(): - group_name = cluster_config_page.config.get_group_names()[0].text - assert group_name == 'group', "Should be group 'group' visible" - if not activatable: - continue - if not cluster_config_page.config.advanced: - cluster_config_page.config.check_group_is_active(group_name, config['config'][0]['active']) - if not field_invisible and ( - (cluster_config_page.config.advanced and field_advanced) or not field_advanced - ): - cluster_config_page.config.expand_or_close_group(group_name, expand=True) - assert len(cluster_config_page.config.get_all_config_rows()) >= 2, "Field should be visible" - if is_default: - check_default_field_values_in_configs(cluster_config_page, config_item, field_type, config) - if is_read_only and config_item.tag_name == 'app-field': - assert cluster_config_page.config.is_element_read_only( - config_item - ), f"Config field {field_type} should be read only" - if expected['alerts'] and not is_read_only: - if field_type == "map": - is_advanced = cluster_config_page.config.advanced - cluster_config_page.driver.refresh() - if is_advanced: - cluster_config_page.config.click_on_advanced() - cluster_config_page.config.expand_or_close_group(group_name, expand=True) - if field_type == "password": - cluster_config_page.config.reset_to_default(config_item) - else: - cluster_config_page.config.click_on_advanced() - cluster_config_page.config.click_on_advanced() - cluster_config_page.config.check_invalid_value_message(field_type) - else: - assert len(cluster_config_page.config.get_all_config_rows()) == 1, "Field should not be visible" - - if group_advanced: - cluster_config_page.config.check_no_rows_or_groups_on_page() - else: - check_expectations() - cluster_config_page.config.click_on_advanced() - check_expectations() - if (not is_read_only) and (not field_invisible) and (not is_required) and is_default: - _check_save_in_configs(cluster_config_page, field_type, expected["save"], is_default) - class TestClusterGroupConfigPage: """Tests for the cluster/{}/group_config page""" @@ -1575,249 +1226,11 @@ def test_check_pagination_on_group_config_component_page(self, app_fs, create_co """Test pagination on cluster/{}/group_config page""" group_conf_page = ClusterGroupConfigPage(app_fs.driver, app_fs.adcm.url, create_community_cluster.id).open() - group_conf_page.group_config.create_few_groups(11) + create_few_groups(group_conf_page.group_config) group_conf_page.table.check_pagination(second_page_item_amount=1) # pylint: disable=too-many-locals, undefined-loop-variable, too-many-statements - @pytest.mark.full() - @pytest.mark.parametrize("field_type", TYPES) - @pytest.mark.parametrize("is_advanced", [True, False], ids=("field_advanced", "field_non-advanced")) - @pytest.mark.parametrize("is_default", [True, False], ids=("default", "not_default")) - @pytest.mark.parametrize("is_required", [True, False], ids=("required", "not_required")) - @pytest.mark.parametrize("is_read_only", [True, False], ids=("read_only", "not_read_only")) - @pytest.mark.parametrize( - "config_group_customization", - [True, False, None], - ids=("config_group_customization_true", "config_group_customization_false", "no_config_group_customization"), - ) - @pytest.mark.parametrize( - "group_customization", - [True, False, None], - ids=("group_customization_true", "group_customization_false", "no_group_customization"), - ) - @pytest.mark.usefixtures("login_to_adcm_over_api") - def test_group_config_fields_invisible_false( - self, - sdk_client_fs: ADCMClient, - app_fs, - field_type, - is_advanced, - is_default, - is_required, - is_read_only, - config_group_customization, - group_customization, - ): - """Test group config fields that aren't invisible""" - config, expected, path = prepare_config( - generate_configs( - field_type=field_type, - invisible=False, - advanced=is_advanced, - default=is_default, - required=is_required, - read_only=is_read_only, - config_group_customization=config_group_customization, - group_customization=group_customization, - ) - ) - cluster, *_ = prepare_cluster_and_open_config_page(sdk_client_fs, path, app_fs) - cluster_group_config = cluster.group_config_create(name="Test group") - cluster_config_page = ClusterGroupConfigConfig( - app_fs.driver, app_fs.adcm.url, cluster.id, cluster_group_config.id - ).open() - - def check_expectations(): - with allure.step('Check that field visible'): - - for config_item in cluster_config_page.group_config.get_all_group_config_rows(): - assert config_item.is_displayed(), f"Config field {field_type} should be visible" - if is_default: - check_default_field_values_in_configs(cluster_config_page, config_item, field_type, config) - if not config_group_customization: - cluster_config_page.config.check_inputs_disabled( - config_item, is_password=bool(field_type == "password") - ) - if is_read_only: - assert cluster_config_page.config.is_element_read_only( - config_item - ), f"Config field {field_type} should be read only" - if not config_group_customization: - assert cluster_config_page.group_config.is_customization_chbx_disabled( - config_item - ), f"Checkbox for field {field_type} should be disabled" - if expected['alerts'] and (not is_read_only) and config_group_customization: - if not cluster_config_page.group_config.is_customization_chbx_checked(config_item): - cluster_config_page.config.activate_group_chbx(config_item) - cluster_config_page.config.check_invalid_value_message(field_type) - - if is_advanced: - cluster_config_page.config.check_no_rows_or_groups_on_page() - else: - check_expectations() - cluster_config_page.config.click_on_advanced() - check_expectations() - if config_group_customization and not is_read_only: - config_row = cluster_config_page.config.get_config_row(field_type) - if not cluster_config_page.group_config.is_customization_chbx_checked(config_row): - cluster_config_page.config.activate_group_chbx(config_row) - if not is_required: - _check_save_in_configs(cluster_config_page, field_type, expected["save"], is_default) - assert cluster_config_page.group_config.is_customization_chbx_checked( - cluster_config_page.config.get_config_row(field_type) - ), f"Config field {field_type} should be checked" - - @pytest.mark.full() - @pytest.mark.parametrize("field_type", TYPES) - @pytest.mark.parametrize("activatable", [True, False], ids=("activatable", "non-activatable")) - @pytest.mark.parametrize("is_default", [True, False], ids=("default", "not_default")) - @pytest.mark.parametrize("group_advanced", [True, False], ids=("group_advanced", "group_non-advanced")) - @pytest.mark.parametrize("is_read_only", [True, False], ids=("read_only", "not_read_only")) - @pytest.mark.parametrize( - "field_advanced", - [ - pytest.param(True, id="field_advanced", marks=pytest.mark.regression), - pytest.param(False, id="field_non_advanced"), - ], - ) - @pytest.mark.parametrize( - "config_group_customization", - [True, False, None], - ids=("config_group_customization_true", "config_group_customization_false", "no_config_group_customization"), - ) - @pytest.mark.parametrize( - "group_customization", - [True, False, None], - ids=("group_customization_true", "group_customization_false", "no_group_customization"), - ) - @pytest.mark.parametrize( - "field_customization", - [True, False, None], - ids=("field_customization_true", "field_customization_false", "no_field_customization"), - ) - @pytest.mark.usefixtures("login_to_adcm_over_api") # pylint: disable-next=too-many-locals - def test_group_configs_fields_in_group_invisible_false( - self, - sdk_client_fs: ADCMClient, - app_fs, - field_type, - activatable, - is_default, - group_advanced, - is_read_only, - field_advanced, - config_group_customization, - group_customization, - field_customization, - ): - """Test group configs with not-invisible fields""" - config, expected, path = prepare_group_config( - generate_group_configs( - field_type=field_type, - activatable=activatable, - active=True, - group_invisible=False, - group_advanced=group_advanced, - default=is_default, - required=False, - read_only=is_read_only, - field_invisible=False, - field_advanced=field_advanced, - config_group_customization=config_group_customization, - group_customization=group_customization, - field_customization=field_customization, - ) - ) - cluster, *_ = prepare_cluster_and_open_config_page(sdk_client_fs, path, app_fs) - cluster_group_config = cluster.group_config_create(name="Test group") - cluster_config_page = ClusterGroupConfigConfig( - app_fs.driver, app_fs.adcm.url, cluster.id, cluster_group_config.id - ).open() - - def check_expectations(): - with allure.step('Check that field visible'): - for config_item in cluster_config_page.group_config.get_all_group_config_rows(): - group_name = cluster_config_page.config.get_group_names()[0].text - assert group_name == 'group', "Should be group 'group' visible" - if not activatable: - continue - if not cluster_config_page.config.advanced: - cluster_config_page.config.check_group_is_active(group_name, config['config'][0]['active']) - if (cluster_config_page.config.advanced and field_advanced) or not field_advanced: - cluster_config_page.config.expand_or_close_group(group_name, expand=True) - assert len(cluster_config_page.config.get_all_config_rows()) >= 2, "Field should be visible" - if is_default: - check_default_field_values_in_configs(cluster_config_page, config_item, field_type, config) - if is_read_only: - if config_item.tag_name == 'app-field': - assert cluster_config_page.config.is_element_read_only( - config_item - ), f"Config field {field_type} should be read only" - else: - if ( - (config_group_customization is False or config_group_customization is None) - and (field_customization is False or field_customization is None) - ) or ((config_group_customization is not False) and (field_customization is False)): - cluster_config_page.config.check_inputs_disabled( - config_item, is_password=bool(field_type == "password") - ) - assert cluster_config_page.group_config.is_customization_chbx_disabled( - config_item - ), f"Checkbox for field {field_type} should be disabled" - else: - cluster_config_page.config.activate_group_chbx(config_item) - cluster_config_page.config.check_inputs_enabled( - config_item, is_password=bool(field_type == "password") - ) - assert not cluster_config_page.group_config.is_customization_chbx_disabled( - config_item - ), f"Checkbox for field {field_type} should not be disabled" - if not cluster_config_page.group_config.is_customization_chbx_checked(config_item): - cluster_config_page.group_config.click_on_customization_chbx(config_item) - assert cluster_config_page.group_config.is_customization_chbx_checked( - config_item - ), f"Config field {field_type} should be checked" - if expected['alerts']: - if field_type == "map": - is_advanced = cluster_config_page.config.advanced - cluster_config_page.driver.refresh() - if is_advanced: - cluster_config_page.config.click_on_advanced() - cluster_config_page.config.expand_or_close_group(group_name, expand=True) - else: - cluster_config_page.config.click_on_advanced() - cluster_config_page.config.click_on_advanced() - cluster_config_page.config.check_invalid_value_message(field_type) - else: - assert len(cluster_config_page.config.get_all_config_rows()) == 1, "Field should not be visible" - - if group_advanced: - cluster_config_page.config.check_no_rows_or_groups_on_page() - cluster_config_page.group_config.check_no_rows() - else: - check_expectations() - cluster_config_page.config.click_on_advanced() - check_expectations() - if config_group_customization is not False and not is_read_only: - group_row = cluster_config_page.group_config.get_all_group_rows()[0] - config_row = cluster_config_page.group_config.get_all_group_config_rows()[0] - if ( - activatable - and group_customization - and not cluster_config_page.group_config.is_customization_chbx_checked(group_row) - and not cluster_config_page.group_config.is_customization_chbx_disabled(group_row) - ): - cluster_config_page.group_config.click_on_customization_chbx(group_row) - if field_customization: - if not cluster_config_page.group_config.is_customization_chbx_checked(config_row): - cluster_config_page.config.activate_group_chbx(config_row) - if not is_read_only: - _check_save_in_configs(cluster_config_page, field_type, expected["save"], is_default) - assert cluster_config_page.group_config.is_customization_chbx_checked( - cluster_config_page.config.get_config_row(field_type) - ), f"Config field {field_type} should be checked" - def test_two_fields_on_cluster_config_page(self, sdk_client_fs: ADCMClient, app_fs): """Test two different fields on group config page""" @@ -1971,5 +1384,73 @@ def test_warning_on_cluster_import_page(self, app_fs, sdk_client_fs): import_page = ClusterImportPage(app_fs.driver, app_fs.adcm.url, cluster.id).open() import_page.config.check_import_warn_icon_on_left_menu() import_page.toolbar.check_warn_button( - tab_name=CLUSTER_NAME, expected_warn_text=['Test cluster has an issue with required import'] + tab_name=CLUSTER_NAME, + expected_warn_text=['Test cluster has an issue with required import'], + ) + + +class TestClusterRenaming: + + SPECIAL_CHARS = (".", "-", "_") + DISALLOWED_AT_START_END = (*SPECIAL_CHARS, " ") + EXPECTED_ERROR = "Please enter a valid name" + + def test_rename_cluster(self, sdk_client_fs, app_fs, create_community_cluster): + cluster = create_community_cluster + cluster_page = ClusterListPage(app_fs.driver, app_fs.adcm.url).open() + self._test_correct_name_can_be_set(cluster, cluster_page) + self._test_an_error_is_shown_on_incorrect_char_in_name(cluster, cluster_page) + self._test_an_error_is_not_shown_on_correct_char_in_name(cluster, cluster_page) + + @allure.step("Check settings new correct cluster name") + def _test_correct_name_can_be_set(self, cluster: Cluster, page: ClusterListPage) -> None: + new_name = "Hahahah" + + dialog = page.open_rename_cluster_dialog(page.get_row_by_cluster_name(cluster.name)) + dialog.set_new_name_in_rename_dialog(new_name) + dialog.click_save_on_rename_dialog() + with allure.step("Check name of cluster in table"): + name_in_row = page.get_cluster_info_from_row(0)["name"] + assert name_in_row == new_name, f"Incorrect cluster name, expected: {new_name}" + cluster.reread() + assert cluster.name == new_name, f"Cluster name on backend is incorrect, expected: {new_name}" + + def _test_an_error_is_shown_on_incorrect_char_in_name(self, cluster: Cluster, page: ClusterListPage) -> None: + dummy_name = "clUster" + incorrect_names = ( + *[f"{char}{dummy_name}" for char in self.DISALLOWED_AT_START_END], + *[f"{dummy_name}{char}" for char in self.DISALLOWED_AT_START_END], + *[f"{dummy_name[0]}{char}{dummy_name[1:]}" for char in ("и", "!")], + ) + + dialog = page.open_rename_cluster_dialog(page.get_row_by_cluster_name(cluster.name)) + + for cluster_name in incorrect_names: + with allure.step(f"Check if printing cluster name '{cluster_name}' triggers a warning message"): + dialog.set_new_name_in_rename_dialog(dummy_name) + dialog.set_new_name_in_rename_dialog(cluster_name) + assert dialog.is_dialog_error_message_visible(), "Error about incorrect name should be visible" + assert ( + dialog.get_dialog_error_message() == self.EXPECTED_ERROR + ), f"Incorrect error message, expected: {self.EXPECTED_ERROR}" + + dialog.click_cancel_on_rename_dialog() + + def _test_an_error_is_not_shown_on_correct_char_in_name(self, cluster: Cluster, page: ClusterListPage) -> None: + dummy_name = "clUster" + correct_names = ( + *[f"{dummy_name[0]}{char}{dummy_name[1:]}" for char in (*self.DISALLOWED_AT_START_END, "9")], + f"9{dummy_name}", ) + + dialog = page.open_rename_cluster_dialog(page.get_row_by_cluster_name(cluster.name)) + + for cluster_name in correct_names: + with allure.step(f"Check if printing cluster name '{cluster_name}' shows no error"): + dialog.set_new_name_in_rename_dialog(dummy_name) + dialog.set_new_name_in_rename_dialog(cluster_name) + assert not dialog.is_dialog_error_message_visible(), "Error about correct name should not be shown" + dialog.click_save_on_rename_dialog() + name_in_row = page.get_cluster_info_from_row(0)["name"] + assert name_in_row == cluster_name, f"Incorrect cluster name, expected: {cluster_name}" + dialog = page.open_rename_cluster_dialog(page.get_row_by_cluster_name(cluster_name)) diff --git a/tests/ui_tests/test_component_page.py b/tests/ui_tests/test_component_page.py index a663d20884..9c25971039 100644 --- a/tests/ui_tests/test_component_page.py +++ b/tests/ui_tests/test_component_page.py @@ -10,44 +10,35 @@ # See the License for the specific language governing permissions and # limitations under the License. -# pylint: disable=redefined-outer-name,unused-argument +# pylint: disable=redefined-outer-name """UI tests for /cluster page""" import os -from typing import ( - Tuple, -) +from typing import Tuple import allure import pytest -from adcm_client.objects import ADCMClient -from adcm_client.objects import ( - Cluster, - Service, - Host, -) +from adcm_client.objects import ADCMClient, Cluster, Host, Service from adcm_pytest_plugin import utils - from tests.library.status import ADCMObjectStatusChanger from tests.ui_tests.app.page.admin.page import AdminIntroPage from tests.ui_tests.app.page.common.configuration.page import CONFIG_ITEMS from tests.ui_tests.app.page.common.group_config_list.page import GroupConfigRowInfo from tests.ui_tests.app.page.common.status.page import ( - SUCCESS_COLOR, NEGATIVE_COLOR, + SUCCESS_COLOR, + StatusRowInfo, ) -from tests.ui_tests.app.page.common.status.page import StatusRowInfo from tests.ui_tests.app.page.component.page import ( - ComponentMainPage, ComponentConfigPage, ComponentGroupConfigPage, + ComponentMainPage, ComponentStatusPage, ) -from tests.ui_tests.app.page.host.page import ( - HostStatusPage, -) +from tests.ui_tests.app.page.host.page import HostStatusPage from tests.ui_tests.app.page.service.page import ServiceComponentPage +from tests.ui_tests.utils import create_few_groups BUNDLE_COMMUNITY = "cluster_community" COMPONENT_WITH_REQUIRED_FIELDS = "component_with_required_string" @@ -61,7 +52,7 @@ FIRST_COMPONENT_NAME = "first" SECOND_COMPONENT_NAME = "second" -pytestmark = pytest.mark.usefixtures("login_to_adcm_over_api") +pytestmark = pytest.mark.usefixtures("_login_to_adcm_over_api") # !===== Fixtures =====! @@ -195,7 +186,9 @@ def test_filter_config_on_component_config_page(self, app_fs, create_cluster_wit component_config_page.config.click_on_group(params["group_name"]) @pytest.mark.parametrize( - "bundle_archive", [utils.get_data_dir(__file__, COMPONENT_WITH_DESCRIPTION_FIELDS)], indirect=True + "bundle_archive", + [utils.get_data_dir(__file__, COMPONENT_WITH_DESCRIPTION_FIELDS)], + indirect=True, ) def test_save_custom_config_on_component_config_page( self, app_fs, create_cluster_with_service, create_bundle_archives @@ -222,7 +215,12 @@ def test_save_custom_config_on_component_config_page( def test_reset_config_in_row_on_component_config_page(self, app_fs, create_cluster_with_service): """Test config reset on /cluster/{}/service/{}/component/{}/config page""" - params = {"row_name": "str_param", "row_value_new": "test", "row_value_old": "123", "config_name": "test_name"} + params = { + "row_name": "str_param", + "row_value_new": "test", + "row_value_old": "123", + "config_name": "test_name", + } cluster, service = create_cluster_with_service component = service.component(name=FIRST_COMPONENT_NAME) @@ -243,7 +241,9 @@ def test_reset_config_in_row_on_component_config_page(self, app_fs, create_clust ) @pytest.mark.parametrize( - "bundle_archive", [utils.get_data_dir(__file__, COMPONENT_WITH_REQUIRED_FIELDS)], indirect=True + "bundle_archive", + [utils.get_data_dir(__file__, COMPONENT_WITH_REQUIRED_FIELDS)], + indirect=True, ) def test_field_validation_on_component_config_page( self, app_fs, create_cluster_with_service, create_bundle_archives @@ -267,11 +267,14 @@ def test_field_validation_on_component_config_page( component_config_page.config.check_field_is_invalid(params['not_req_name']) component_config_page.config.check_config_warn_icon_on_left_menu() component_config_page.toolbar.check_warn_button( - tab_name=FIRST_COMPONENT_NAME, expected_warn_text=[f'{FIRST_COMPONENT_NAME} has an issue with its config'] + tab_name=FIRST_COMPONENT_NAME, + expected_warn_text=[f'{FIRST_COMPONENT_NAME} has an issue with its config'], ) @pytest.mark.parametrize( - "bundle_archive", [utils.get_data_dir(__file__, COMPONENT_WITH_DEFAULT_FIELDS)], indirect=True + "bundle_archive", + [utils.get_data_dir(__file__, COMPONENT_WITH_DEFAULT_FIELDS)], + indirect=True, ) def test_field_validation_on_component_config_page_with_default_value( self, app_fs, create_cluster_with_service, create_bundle_archives @@ -296,7 +299,9 @@ def test_field_validation_on_component_config_page_with_default_value( ) @pytest.mark.parametrize( - "bundle_archive", [utils.get_data_dir(__file__, COMPONENT_WITH_DESCRIPTION_FIELDS)], indirect=True + "bundle_archive", + [utils.get_data_dir(__file__, COMPONENT_WITH_DESCRIPTION_FIELDS)], + indirect=True, ) def test_field_tooltips_on_component_config_page(self, app_fs, create_cluster_with_service, create_bundle_archives): """Test config fields tooltips on /cluster/{}/service/{}/component/{}/config page""" @@ -358,7 +363,7 @@ def test_check_pagination_on_group_config_component_page(self, app_fs, create_cl group_conf_page = ComponentGroupConfigPage( app_fs.driver, app_fs.adcm.url, cluster.id, service.id, component.id ).open() - group_conf_page.group_config.create_few_groups(11) + create_few_groups(group_conf_page.group_config) group_conf_page.table.check_pagination(second_page_item_amount=1) @@ -383,13 +388,21 @@ def test_status_on_component_status_page(self, app_fs, adcm_fs, sdk_client_fs, c success_status = [ StatusRowInfo( - icon_status=True, group_name='first', state='successful 1/1', state_color=SUCCESS_COLOR, link=None + icon_status=True, + group_name='first', + state='successful 1/1', + state_color=SUCCESS_COLOR, + link=None, ), StatusRowInfo(icon_status=True, group_name=None, state=None, state_color=None, link='test-host'), ] negative_status = [ StatusRowInfo( - icon_status=False, group_name='first', state='successful 0/1', state_color=NEGATIVE_COLOR, link=None + icon_status=False, + group_name='first', + state='successful 0/1', + state_color=NEGATIVE_COLOR, + link=None, ), StatusRowInfo(icon_status=False, group_name=None, state=None, state_color=None, link='test-host'), ] diff --git a/tests/ui_tests/test_config_page/__init__.py b/tests/ui_tests/test_config_page/__init__.py new file mode 100644 index 0000000000..824dd6c8fe --- /dev/null +++ b/tests/ui_tests/test_config_page/__init__.py @@ -0,0 +1,11 @@ +# 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. diff --git a/tests/ui_tests/test_config_page/conftest.py b/tests/ui_tests/test_config_page/conftest.py new file mode 100644 index 0000000000..1df676a3c1 --- /dev/null +++ b/tests/ui_tests/test_config_page/conftest.py @@ -0,0 +1,48 @@ +# 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. + +"""conftest for config page UI tests""" + +from typing import Generator + +import allure +import pytest +from selenium.common.exceptions import WebDriverException +from tests.ui_tests.app.app import ADCMTest +from tests.ui_tests.conftest import login_over_api + + +@pytest.fixture(scope="module") +def app_ms(adcm_ms, web_driver) -> ADCMTest: + """Analog of app_fs""" + web_driver.attache_adcm(adcm_ms) + # see app_fs for reasoning + try: + web_driver.new_tab() + except WebDriverException: + web_driver.create_driver() + return web_driver + + +@allure.title("Login in ADCM over API") +@pytest.fixture(scope="module") +def _login_over_api_ms(app_ms, adcm_credentials): # pylint: disable=redefined-outer-name + login_over_api(app_ms, adcm_credentials).wait_config_loaded() + + +@pytest.fixture() +def objects_to_delete() -> Generator[list, None, None]: + """Container for objects that'll be deleted after the test""" + objects = [] + yield objects + for obj in objects: + obj.delete() diff --git a/tests/ui_tests/test_config_page/test_invisible_params.py b/tests/ui_tests/test_config_page/test_invisible_params.py new file mode 100644 index 0000000000..2c285bbb1e --- /dev/null +++ b/tests/ui_tests/test_config_page/test_invisible_params.py @@ -0,0 +1,107 @@ +# 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. + +"""Test config page with invisible params in bundles""" + +import allure +import pytest +from adcm_client.objects import ADCMClient +from tests.ui_tests.app.helpers.configs_generator import ( + TYPES, + generate_configs, + generate_group_configs, + prepare_config, + prepare_group_config, +) +from tests.ui_tests.app.page.cluster.page import ClusterGroupConfigConfig +from tests.ui_tests.utils import prepare_cluster_and_open_config_page + + +@pytest.mark.full() +@pytest.mark.usefixtures("_login_to_adcm_over_api") +def test_group_configs_fields_invisible_true(sdk_client_fs: ADCMClient, app_fs): + """Test complex configuration group with all fields invisible""" + with allure.step("Prepare big config"): + res = [ + generate_group_configs( + field_type=field_type, + activatable=activatable, + active=active, + group_advanced=group_advanced, + default=is_default, + required=is_required, + read_only=is_read_only, + field_invisible=field_invisible, + field_advanced=field_advanced, + )[0] + for field_type in TYPES + for field_advanced in (True, False) + for field_invisible in (True, False) + for is_default in (True, False) + for is_required in (True, False) + for is_read_only in (True, False) + for activatable in (True, False) + for active in (True, False) + for group_advanced in (True, False) + ] + full_config = [ + {**combination[0]["config"][0], "name": f"{combination[0]['config'][0]['name']}_{i}"} + for i, combination in enumerate(res) + ] + _, _, path = prepare_group_config(([{**res[0][0], "config": full_config}], None), enforce_file=True) + + cluster, _ = prepare_cluster_and_open_config_page(sdk_client_fs, path, app_fs) + cluster_group_config = cluster.group_config_create(name="Test group") + cluster_config_page = ClusterGroupConfigConfig( + app_fs.driver, app_fs.adcm.url, cluster.id, cluster_group_config.id + ).open() + cluster_config_page.wait_page_is_opened() + cluster_config_page.config.check_no_rows_or_groups_on_page() + cluster_config_page.config.check_no_rows_or_groups_on_page_with_advanced() + with allure.step('Check that save button is disabled'): + assert cluster_config_page.config.is_save_btn_disabled(), 'Save button should be disabled' + + +@pytest.mark.full() +@pytest.mark.usefixtures("_login_to_adcm_over_api") +def test_configs_fields_invisible_true(sdk_client_fs: ADCMClient, app_fs): + """Check RO different variations of invisible config params""" + with allure.step("Prepare big config"): + res = [ + generate_configs( + field_type=field_type, + advanced=is_advanced, + default=is_default, + required=is_required, + read_only=is_read_only, + config_group_customization=config_group_customization, + group_customization=group_customization, + )[0] + for field_type in TYPES + for is_advanced in (True, False) + for is_default in (True, False) + for is_required in (True, False) + for is_read_only in (True, False) + for config_group_customization in (True, False) + for group_customization in (True, False) + ] + full_config = [ + {**combination[0]["config"][0], "name": f"{combination[0]['config'][0]['name']}_{i}"} + for i, combination in enumerate(res) + ] + _, _, path = prepare_config(([{**res[0][0], "config": full_config}], None), enforce_file=True) + + _, cluster_config_page = prepare_cluster_and_open_config_page(sdk_client_fs, path, app_fs) + cluster_config_page.config.check_no_rows_or_groups_on_page() + cluster_config_page.config.check_no_rows_or_groups_on_page_with_advanced() + with allure.step('Check that save button is disabled'): + assert cluster_config_page.config.is_save_btn_disabled(), 'Save button should be disabled' diff --git a/tests/ui_tests/test_config_page/test_visible_params.py b/tests/ui_tests/test_config_page/test_visible_params.py new file mode 100644 index 0000000000..fc1524ef1f --- /dev/null +++ b/tests/ui_tests/test_config_page/test_visible_params.py @@ -0,0 +1,589 @@ +# 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. + +"""Test config page with visible params in bundles""" + +from dataclasses import asdict, dataclass + +import allure +import pytest +from _pytest.fixtures import FixtureRequest +from adcm_client.objects import ADCMClient +from adcm_pytest_plugin.utils import wait_until_step_succeeds +from selenium.webdriver.remote.webelement import WebElement +from tests.ui_tests.app.helpers.configs_generator import ( + TYPES, + generate_configs, + generate_group_configs, + prepare_config, + prepare_group_config, +) +from tests.ui_tests.app.page.cluster.page import ( + ClusterConfigPage, + ClusterGroupConfigConfig, +) +from tests.ui_tests.utils import prepare_cluster_and_open_config_page + +# pylint: disable=too-many-locals,too-many-boolean-expressions, too-many-statements + +pytestmark = [ + pytest.mark.usefixtures("_cleanup_browser_logs", "_attach_debug_info_on_ui_test_fail", "_login_over_api_ms") +] + + +def skip_test_if_one_already_failed(request: FixtureRequest): + if request.session.testsfailed == 0: + return + + test_base_name = request.node.originalname + for item in filter(lambda i: i.originalname == test_base_name, request.session.items): + if hasattr(item, "rep_call") and not item.rep_call.passed: + pytest.skip(f"There's already one {test_base_name} failed") + return + + +def check_default_field_values_in_configs( + cluster_config_page: ClusterConfigPage, config_item: WebElement, field_type: str, config +): + """Check that input value in config is equal to the default one""" + main_config = config['config'][0]['subs'][0] if "subs" in config['config'][0] else config['config'][0] + if field_type == 'boolean': + cluster_config_page.config.assert_checkbox_state(config_item, expected_value=main_config['default']) + elif field_type in ("password", "secrettext"): + is_password_value = bool(field_type == "password") + cluster_config_page.config.assert_input_value_is( + expected_value='********', display_name=field_type, is_password=is_password_value + ) + elif field_type == "list": + cluster_config_page.config.assert_list_value_is(expected_value=main_config['default'], display_name=field_type) + elif field_type == "map": + cluster_config_page.config.assert_map_value_is(expected_value=main_config['default'], display_name=field_type) + elif field_type == "file": + cluster_config_page.config.assert_input_value_is(expected_value="test", display_name=field_type) + else: + expected_value = ( + str(main_config['default']) if field_type in ("integer", "float", "json") else main_config['default'] + ) + cluster_config_page.config.assert_input_value_is(expected_value=expected_value, display_name=field_type) + + +@allure.step("Check save button and save config") +def check_save_in_configs(cluster_config_page, field_type, expected_state, is_default): + """ + Check save button and save config. + It is a workaround for each type of field because it won't work other way on ui with selenium. + """ + + config_row = cluster_config_page.config.get_config_row(field_type) + if field_type == 'list': + cluster_config_page.config.click_add_item_btn_in_row(config_row) + if field_type in ['string', 'integer', 'text', 'float', 'file', 'json']: + config_row.click() + if field_type == 'secrettext': + cluster_config_page.config.reset_to_default(config_row) + if field_type == 'boolean' and is_default: + for _ in range(3): + cluster_config_page.config.click_boolean_checkbox(config_row) + if field_type == 'password': + if is_default: + cluster_config_page.config.reset_to_default(config_row) + else: + config_row.click() + if field_type == 'map': + cluster_config_page.config.click_add_item_btn_in_row(config_row) + cluster_config_page.config.reset_to_default(config_row) + cluster_config_page.config.check_save_btn_state_and_save_conf(expected_state) + + +@dataclass() +class ParamCombination: # pylint: disable=too-many-instance-attributes + """Container for params used in test to generate config""" + + field_type: str + activatable: bool + active: bool + group_advanced: bool + default: bool + required: bool + read_only: bool + field_invisible: bool + field_advanced: bool + + +def _prepare_combinations(): + return [ + ParamCombination( + field_type, + group_advanced, + is_default, + is_required, + is_read_only, + activatable, + active, + invisible, + advanced, + ) + for field_type in TYPES + for group_advanced in (True, False) + for is_default in (True, False) + for is_required in (True, False) + for is_read_only in (True, False) + for activatable in (True, False) + for active in (True, False) + for invisible in (True, False) + for advanced in (True, False) + ] + + +def _check_expectations_for_group_configs_fields(page, combo: ParamCombination, alerts_expected, config): + with allure.step('Check that group field is visible'): + group_name = page.config.get_group_names()[0].text + assert group_name == 'group', "Group with name 'group' should be visible" + + # why ? + if not combo.activatable: # group activatable + return + + if not page.config.advanced: + page.config.check_group_is_active(group_name, config['config'][0]['active']) + + # rewrite this condition, it's unreadable + if not ( + not combo.field_invisible and ((page.config.advanced and combo.field_advanced) or not combo.field_advanced) + ): + with allure.step("Check field is invisible"): + assert len(page.config.get_all_config_rows()) == 1, "Field should not be visible" + return + + page.config.expand_or_close_group(group_name, expand=True) + + def _check_field_is_visible_after_group_is_epanded(): + assert len(page.config.get_all_config_rows()) >= 2, "Field should be visible" + + wait_until_step_succeeds(_check_field_is_visible_after_group_is_epanded, timeout=5, period=0.2) + + config_item = page.config.get_all_config_rows()[1] + + if combo.default: + check_default_field_values_in_configs(page, config_item, combo.field_type, config) + + if combo.read_only and config_item.tag_name == 'app-field': + assert page.config.is_element_read_only(config_item), f"Config field {combo.field_type} should be read only" + + if alerts_expected and not combo.read_only: + if combo.field_type == "map": + is_advanced = page.config.advanced + page.driver.refresh() + if is_advanced: + page.config.click_on_advanced() + page.config.expand_or_close_group(group_name, expand=True) + if combo.field_type == "password": + page.config.reset_to_default(config_item) + else: + page.config.click_on_advanced() + page.config.click_on_advanced() + page.config.check_invalid_value_message(combo.field_type) + + +@pytest.mark.full() +@pytest.mark.parametrize( + "combo", + _prepare_combinations(), + ids=lambda c: "-".join(f"{k}_{v}" for k, v in asdict(c).items()), +) +def test_group_configs_fields(request, combo: ParamCombination, sdk_client_ms: ADCMClient, app_ms, objects_to_delete): + """Test group configs with not-invisible fields""" + skip_test_if_one_already_failed(request) + + config, expected, path = prepare_group_config(generate_group_configs(group_invisible=False, **asdict(combo))) + cluster, page = prepare_cluster_and_open_config_page(sdk_client_ms, path, app_ms) + objects_to_delete.append(cluster) + + if combo.group_advanced: + page.config.check_no_rows_or_groups_on_page() + else: + _check_expectations_for_group_configs_fields( + page=page, combo=combo, alerts_expected=expected['alerts'], config=config + ) + page.config.click_on_advanced() + _check_expectations_for_group_configs_fields( + page=page, combo=combo, alerts_expected=expected['alerts'], config=config + ) + if (not combo.read_only) and (not combo.field_invisible) and (not combo.required) and combo.default: + check_save_in_configs(page, combo.field_type, expected["save"], combo.default) + + +@pytest.mark.full() +@pytest.mark.parametrize("field_type", TYPES) +@pytest.mark.parametrize("is_advanced", [True, False], ids=("field_advanced", "field_non-advanced")) +@pytest.mark.parametrize("is_default", [True, False], ids=("default", "not_default")) +@pytest.mark.parametrize("is_required", [True, False], ids=("required", "not_required")) +@pytest.mark.parametrize("is_read_only", [True, False], ids=("read_only", "not_read_only")) +@pytest.mark.parametrize( + "config_group_customization", + [True, False, None], + ids=( + "config_group_customization_true", + "config_group_customization_false", + "no_config_group_customization", + ), +) +@pytest.mark.parametrize( + "group_customization", + [True, False, None], + ids=("group_customization_true", "group_customization_false", "no_group_customization"), +) +def test_visible_group_config_fields( + request, + sdk_client_ms, + app_ms, + field_type, + is_advanced, + is_default, + is_required, + is_read_only, + config_group_customization, + group_customization, + objects_to_delete, +): + """Test group config fields that aren't invisible""" + skip_test_if_one_already_failed(request) + + config, expected, path = prepare_config( + generate_configs( + field_type=field_type, + invisible=False, + advanced=is_advanced, + default=is_default, + required=is_required, + read_only=is_read_only, + config_group_customization=config_group_customization, + group_customization=group_customization, + ) + ) + cluster, *_ = prepare_cluster_and_open_config_page(sdk_client_ms, path, app_ms) + objects_to_delete.append(cluster) + cluster_group_config = cluster.group_config_create(name="Test group") + cluster_config_page = ClusterGroupConfigConfig( + app_ms.driver, app_ms.adcm.url, cluster.id, cluster_group_config.id + ).open() + + def check_expectations(): + with allure.step('Check that field is visible'): + assert len( + cluster_config_page.group_config.get_all_group_config_rows(timeout=1) + ), f"Config field {field_type} should be visible" + config_row = cluster_config_page.group_config.get_all_group_config_rows(timeout=0.5)[0] + + if is_default: + with allure.step("Check defaults"): + check_default_field_values_in_configs(cluster_config_page, config_row, field_type, config) + if not config_group_customization: + cluster_config_page.config.check_inputs_disabled(config_row, is_password=field_type == "password") + + if is_read_only: + with allure.step("Check read only"): + assert cluster_config_page.config.is_element_read_only( + config_row + ), f"Config field {field_type} should be read only" + + if not config_group_customization: + with allure.step("Check group customization checkbox is disabled"): + assert cluster_config_page.group_config.is_customization_chbx_disabled( + config_row + ), f"Checkbox for field {field_type} should be disabled" + elif expected['alerts'] and (not is_read_only): + if not cluster_config_page.group_config.is_customization_chbx_checked(config_row): + cluster_config_page.config.activate_group_chbx(config_row) + cluster_config_page.config.check_invalid_value_message(field_type) + + if is_advanced: + cluster_config_page.config.check_no_rows_or_groups_on_page() + else: + check_expectations() + cluster_config_page.config.click_on_advanced() + check_expectations() + if not (config_group_customization and not is_read_only): + return + config_row = cluster_config_page.config.get_config_row(field_type) + if not cluster_config_page.group_config.is_customization_chbx_checked(config_row): + cluster_config_page.config.activate_group_chbx(config_row) + if not is_required: + check_save_in_configs(cluster_config_page, field_type, expected["save"], is_default) + assert cluster_config_page.group_config.is_customization_chbx_checked( + cluster_config_page.config.get_config_row(field_type) + ), f"Config field {field_type} should be checked" + + +@pytest.mark.full() +@pytest.mark.parametrize("field_type", TYPES) +@pytest.mark.parametrize("activatable", [True, False], ids=("activatable", "non-activatable")) +@pytest.mark.parametrize("is_default", [True, False], ids=("default", "not_default")) +@pytest.mark.parametrize("group_advanced", [True, False], ids=("group_advanced", "group_non-advanced")) +@pytest.mark.parametrize("is_read_only", [True, False], ids=("read_only", "not_read_only")) +@pytest.mark.parametrize( + "field_advanced", + [ + pytest.param(True, id="field_advanced", marks=pytest.mark.regression), + pytest.param(False, id="field_non_advanced"), + ], +) +@pytest.mark.parametrize( + "config_group_customization", + [True, False, None], + ids=( + "config_group_customization_true", + "config_group_customization_false", + "no_config_group_customization", + ), +) +@pytest.mark.parametrize( + "group_customization", + [True, False, None], + ids=("group_customization_true", "group_customization_false", "no_group_customization"), +) +@pytest.mark.parametrize( + "field_customization", + [True, False, None], + ids=("field_customization_true", "field_customization_false", "no_field_customization"), +) +def test_group_configs_fields_in_group_invisible_false( + request, + sdk_client_ms: ADCMClient, + app_ms, + field_type, + activatable, + is_default, + group_advanced, + is_read_only, + field_advanced, + config_group_customization, + group_customization, + field_customization, + objects_to_delete, +): + """Test group configs with not-invisible fields""" + skip_test_if_one_already_failed(request) + + config, expected, path = prepare_group_config( + generate_group_configs( + field_type=field_type, + activatable=activatable, + active=True, + group_invisible=False, + group_advanced=group_advanced, + default=is_default, + required=False, + read_only=is_read_only, + field_invisible=False, + field_advanced=field_advanced, + config_group_customization=config_group_customization, + group_customization=group_customization, + field_customization=field_customization, + ) + ) + cluster, *_ = prepare_cluster_and_open_config_page(sdk_client_ms, path, app_ms) + objects_to_delete.append(cluster) + cluster_group_config = cluster.group_config_create(name="Test group") + cluster_config_page = ClusterGroupConfigConfig( + app_ms.driver, app_ms.adcm.url, cluster.id, cluster_group_config.id + ).open() + + def check_expectations(): + with allure.step('Check that field visible'): + group_name = cluster_config_page.config.get_group_names()[0].text + assert group_name == 'group', "Should be group 'group' visible" + + if not activatable: + return + + if not cluster_config_page.config.advanced: + cluster_config_page.config.check_group_is_active(group_name, config['config'][0]['active']) + + if not ((cluster_config_page.config.advanced and field_advanced) or not field_advanced): + with allure.step("Check that config field is invisible"): + assert len(cluster_config_page.config.get_all_config_rows()) == 1, "Field should not be visible" + return + + with allure.step("Expand group and check field is visible"): + cluster_config_page.config.expand_or_close_group(group_name, expand=True) + assert len(cluster_config_page.config.get_all_config_rows()) >= 2, "Field should be visible" + + config_row = cluster_config_page.group_config.get_all_group_config_rows()[0] + + if is_default: + check_default_field_values_in_configs(cluster_config_page, config_row, field_type, config) + + if is_read_only: + if config_row.tag_name == 'app-field': + assert cluster_config_page.config.is_element_read_only( + config_row + ), f"Config field {field_type} should be read only" + return + + if ( + (config_group_customization is False or config_group_customization is None) + and (field_customization is False or field_customization is None) + ) or ((config_group_customization is not False) and (field_customization is False)): + cluster_config_page.config.check_inputs_disabled(config_row, is_password=bool(field_type == "password")) + assert cluster_config_page.group_config.is_customization_chbx_disabled( + config_row + ), f"Checkbox for field {field_type} should be disabled" + return + + with allure.step("Activate group and check checkbox is enabled"): + cluster_config_page.config.activate_group_chbx(config_row) + cluster_config_page.config.check_inputs_enabled(config_row, is_password=field_type == "password") + assert not cluster_config_page.group_config.is_customization_chbx_disabled( + config_row + ), f"Checkbox for field {field_type} should not be disabled" + + if not cluster_config_page.group_config.is_customization_chbx_checked(config_row): + cluster_config_page.group_config.click_on_customization_chbx(config_row) + assert cluster_config_page.group_config.is_customization_chbx_checked( + config_row + ), f"Config field {field_type} should be checked" + + if not expected['alerts']: + return + + if field_type == "map": + is_advanced = cluster_config_page.config.advanced + cluster_config_page.driver.refresh() + if is_advanced: + cluster_config_page.config.click_on_advanced() + cluster_config_page.config.expand_or_close_group(group_name, expand=True) + else: + cluster_config_page.config.click_on_advanced() + cluster_config_page.config.click_on_advanced() + cluster_config_page.config.check_invalid_value_message(field_type) + + if group_advanced: + cluster_config_page.config.check_no_rows_or_groups_on_page() + cluster_config_page.group_config.check_no_rows() + else: + check_expectations() + cluster_config_page.config.click_on_advanced() + check_expectations() + + if not (config_group_customization is not False and not is_read_only): + return + + group_row = cluster_config_page.group_config.get_all_group_rows()[0] + config_row = cluster_config_page.group_config.get_all_group_config_rows()[0] + + if ( + activatable + and group_customization + and not cluster_config_page.group_config.is_customization_chbx_checked(group_row) + and not cluster_config_page.group_config.is_customization_chbx_disabled(group_row) + ): + cluster_config_page.group_config.click_on_customization_chbx(group_row) + + if not field_customization: + return + + if not cluster_config_page.group_config.is_customization_chbx_checked(config_row): + cluster_config_page.config.activate_group_chbx(config_row) + + if not is_read_only: + check_save_in_configs(cluster_config_page, field_type, expected["save"], is_default) + + with allure.step("Check that group configuration checkbox is checked"): + assert cluster_config_page.group_config.is_customization_chbx_checked( + cluster_config_page.config.get_config_row(field_type) + ), f"Config field {field_type} should be checked" + + +# pylint: disable=too-many-locals +@pytest.mark.full() +@pytest.mark.parametrize("field_type", TYPES) +@pytest.mark.parametrize("is_advanced", [True, False], ids=("field_advanced", "field_non-advanced")) +@pytest.mark.parametrize("is_default", [True, False], ids=("default", "not_default")) +@pytest.mark.parametrize("is_required", [True, False], ids=("required", "not_required")) +@pytest.mark.parametrize("is_read_only", [True, False], ids=("read_only", "not_read_only")) +@pytest.mark.parametrize( + "config_group_customization", + [ + pytest.param(True, id="config_group_customization_true", marks=pytest.mark.regression), + pytest.param(False, id="config_group_customization_false", marks=pytest.mark.regression), + ], +) +@pytest.mark.parametrize( + "group_customization", + [ + pytest.param(True, id="group_customization_true", marks=pytest.mark.regression), + pytest.param(False, id="group_customization_false", marks=pytest.mark.regression), + ], +) +def test_configs_fields_invisible_false( + request, + sdk_client_ms: ADCMClient, + app_ms, + field_type, + is_advanced, + is_default, + is_required, + is_read_only, + config_group_customization, + group_customization, + objects_to_delete, +): + """Test config fields that aren't invisible""" + skip_test_if_one_already_failed(request) + + config, expected, path = prepare_config( + generate_configs( + field_type=field_type, + invisible=False, + advanced=is_advanced, + default=is_default, + required=is_required, + read_only=is_read_only, + config_group_customization=config_group_customization, + group_customization=group_customization, + ) + ) + cluster, cluster_config_page = prepare_cluster_and_open_config_page(sdk_client_ms, path, app_ms) + objects_to_delete.append(cluster) + + def check_expectations(): + with allure.step('Check that field visible'): + rows = cluster_config_page.config.get_all_config_rows() + assert len(rows), "Config row should be presented" + config_row = rows[0] + + if is_default: + with allure.step("Check default value"): + check_default_field_values_in_configs(cluster_config_page, config_row, field_type, config) + + if is_read_only: + with allure.step("Check is read only"): + assert cluster_config_page.config.is_element_read_only( + config_row + ), f"Config field {field_type} should be read only" + + if expected['alerts'] and not is_read_only: + cluster_config_page.config.check_invalid_value_message(field_type) + + if is_read_only or (is_required and not is_default): + with allure.step('Check that save button is disabled'): + assert cluster_config_page.config.is_save_btn_disabled(), 'Save button should be disabled' + else: + with allure.step('Check that save button is enabled'): + assert not cluster_config_page.config.is_save_btn_disabled(), 'Save button should be enabled' + if is_advanced: + cluster_config_page.config.check_no_rows_or_groups_on_page() + else: + check_expectations() + cluster_config_page.config.click_on_advanced() + check_expectations() diff --git a/tests/ui_tests/test_header_and_footer.py b/tests/ui_tests/test_header_and_footer.py index 01cf650694..cf6eb1ed48 100644 --- a/tests/ui_tests/test_header_and_footer.py +++ b/tests/ui_tests/test_header_and_footer.py @@ -15,27 +15,22 @@ import allure import pytest from adcm_pytest_plugin.common import add_dummy_objects_to_adcm - from tests.ui_tests.app.page.admin.page import AdminIntroPage, AdminSettingsPage from tests.ui_tests.app.page.bundle_list.page import BundleListPage from tests.ui_tests.app.page.cluster_list.page import ClusterListPage from tests.ui_tests.app.page.common.base_page import ( BasePageObject, - PageHeader, PageFooter, + PageHeader, ) from tests.ui_tests.app.page.host_list.page import HostListPage from tests.ui_tests.app.page.hostprovider_list.page import ProviderListPage from tests.ui_tests.app.page.job_list.page import JobListPage from tests.ui_tests.app.page.login.page import LoginPage from tests.ui_tests.app.page.profile.page import ProfilePage -from tests.ui_tests.utils import ( - wait_for_new_window, - close_current_tab, -) - +from tests.ui_tests.utils import close_current_tab, wait_for_new_window -pytestmark = [pytest.mark.usefixtures("login_to_adcm_over_api")] +pytestmark = [pytest.mark.usefixtures("_login_to_adcm_over_api")] class TestHeader: diff --git a/tests/ui_tests/test_host_page.py b/tests/ui_tests/test_host_page.py index 66a8377686..110beed244 100644 --- a/tests/ui_tests/test_host_page.py +++ b/tests/ui_tests/test_host_page.py @@ -10,7 +10,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -# pylint: disable=redefined-outer-name,unused-argument,too-many-arguments +# pylint: disable=redefined-outer-name """UI tests for /host page""" @@ -20,33 +20,29 @@ import allure import pytest from _pytest.fixtures import SubRequest -from adcm_client.objects import ( - ADCMClient, - Bundle, - Provider, - Cluster, -) +from adcm_client.objects import ADCMClient, Bundle, Cluster, Host, Provider from adcm_pytest_plugin import utils - +from selenium.common import StaleElementReferenceException +from tests.library.retry import RetryFromCheckpoint, Step from tests.library.status import ADCMObjectStatusChanger from tests.ui_tests.app.app import ADCMTest from tests.ui_tests.app.page.admin.page import AdminIntroPage from tests.ui_tests.app.page.common.configuration.locators import CommonConfigMenu from tests.ui_tests.app.page.common.configuration.page import CONFIG_ITEMS from tests.ui_tests.app.page.common.status.page import ( - SUCCESS_COLOR, NEGATIVE_COLOR, + SUCCESS_COLOR, + StatusRowInfo, ) -from tests.ui_tests.app.page.common.status.page import StatusRowInfo from tests.ui_tests.app.page.host.locators import HostLocators from tests.ui_tests.app.page.host.page import ( - HostMainPage, HostConfigPage, + HostMainPage, HostStatusPage, ) from tests.ui_tests.app.page.host_list.locators import HostListLocators from tests.ui_tests.app.page.host_list.page import HostListPage -from tests.ui_tests.utils import wait_and_assert_ui_info, expect_rows_amount_change +from tests.ui_tests.utils import expect_rows_amount_change, wait_and_assert_ui_info # defaults HOST_FQDN = 'best-host' @@ -54,6 +50,7 @@ PROVIDER_NAME = 'Black Mark' INIT_ACTION = 'Init' +REINIT_ACTION = "Reinit" # config fields REGULAR_FIELD_NAME = 'Just item' @@ -128,8 +125,7 @@ def upload_and_create_cluster(cluster_bundle: Bundle) -> Tuple[Bundle, Cluster]: @pytest.fixture() @allure.title("Open /host page") -# pylint: disable-next=unused-argument -def page(app_fs: ADCMTest, login_to_adcm_over_api) -> HostListPage: +def page(app_fs: ADCMTest, _login_to_adcm_over_api) -> HostListPage: """Open host page""" return HostListPage(app_fs.driver, app_fs.adcm.url).open() @@ -151,7 +147,7 @@ def _check_job_name(sdk: ADCMClient, action_display_name: str): def _check_menu( menu_name: str, - provider_bundle: Bundle, + provider: Provider, list_page: HostListPage, ): list_page.click_on_row_child(0, HostListLocators.HostTable.HostRow.fqdn) @@ -160,8 +156,7 @@ def _check_menu( host_page.check_fqdn_equal_to(HOST_FQDN) bundle_label = host_page.get_bundle_label() # Test Host is name of host in config.yaml - assert 'Test Host' in bundle_label - assert provider_bundle.version in bundle_label + assert provider.name == bundle_label # !===== TESTS =====! @@ -195,7 +190,6 @@ def test_create_host_with_bundle_upload(self, page: HostListPage, bundle_archive page.get_host_info_from_row, ) - @pytest.mark.skip(reason="https://tracker.yandex.ru/ADCM-3212") @pytest.mark.smoke() @pytest.mark.include_firefox() @pytest.mark.usefixtures("upload_and_create_provider", "upload_and_create_cluster") @@ -209,14 +203,28 @@ def test_create_bonded_to_cluster_host(self, page: HostListPage): 'cluster': CLUSTER_NAME, 'state': 'created', } - page.open_host_creation_popup() - page.host_popup.create_host(host_fqdn, cluster=CLUSTER_NAME) + self._create_host_bonded_to_cluster(page, host_fqdn) wait_and_assert_ui_info( expected_values, page.get_host_info_from_row, timeout=10, ) + @staticmethod + def _create_host_bonded_to_cluster(page: HostListPage, fqdn: str) -> None: + host_bonding_retry = RetryFromCheckpoint( + execution_steps=[ + Step(page.open_host_creation_popup), + Step(page.host_popup.create_host, [fqdn], {"cluster": CLUSTER_NAME}), + ], + restoration_steps=[ + Step(page.driver.refresh), + Step(page.open_host_creation_popup), + ], + ) + with allure.step("Try to bound host to cluster during new host creation"): + host_bonding_retry(restore_from=(AssertionError, TimeoutError, StaleElementReferenceException)) + @pytest.mark.parametrize("_create_many_hosts", [12], indirect=True) @pytest.mark.usefixtures("_create_many_hosts") def test_host_list_pagination(self, page: HostListPage): @@ -305,7 +313,8 @@ def test_delete_bonded_host(self, page: HostListPage, create_bonded_host): def test_open_menu(self, upload_and_create_provider: Tuple[Bundle, Provider], page: HostListPage, menu: str): """Open detailed host page and open menu from side navigation""" - _check_menu(menu, upload_and_create_provider[0], page) + _, provider = upload_and_create_provider + _check_menu(menu, provider, page) @pytest.mark.smoke() @pytest.mark.include_firefox() @@ -337,24 +346,34 @@ def test_maintenance_mode_on_host_page(self, page: HostListPage, create_bonded_h page.assert_maintenance_mode_state(0, None) @pytest.mark.smoke() - def test_action_with_maintenance_mode_on_host_page(self, page: HostListPage, create_bonded_host): + def test_action_with_maintenance_mode_on_host_page(self, sdk_client_fs, page: HostListPage, create_bonded_host): """Test maintenance mode on host page""" - with allure.step("Turn OFF maintenance mode"): - page.driver.refresh() - page.click_on_maintenance_mode_btn(0) - with allure.step("Check there are no actions"): - assert not page.get_all_available_actions(0), "Action list with MM OFF should be empty" - page.driver.refresh() with allure.step("Turn ON maintenance mode"): + page.driver.refresh() page.click_on_maintenance_mode_btn(0) - with allure.step("Check there are action"): - assert page.get_all_available_actions(0) == [ + with allure.step("Check actions are displayed"): + assert page.get_enabled_action_names(0) == [ INIT_ACTION ], f"Action list with MM ON should be with action {INIT_ACTION}" + assert page.get_disabled_action_names(0) == [], "There should be 0 disabled actions" + with allure.step("Run action and check available actions changed"): + page.run_action(0, INIT_ACTION) + _ = [job.wait() for job in sdk_client_fs.job_list()] + page.header.wait_success_job_amount_from_header(1) + page.driver.refresh() + assert page.get_disabled_action_names(0) == [ + REINIT_ACTION + ], f"Action {REINIT_ACTION} should be shown and disabled in new state" + assert page.get_enabled_action_names(0) == [], "There should be 0 enabled actions" + with allure.step("Turn ON maintenance mode and check actions"): + page.click_on_maintenance_mode_btn(0) + assert page.get_enabled_action_names(0) == [ + REINIT_ACTION + ], f"Action list with MM ON should be with action {REINIT_ACTION}" -@pytest.mark.usefixtures('login_to_adcm_over_api') +@pytest.mark.usefixtures('_login_to_adcm_over_api') class TestHostMainPage: """Tests for the /host/{}/config page""" @@ -532,7 +551,7 @@ def test_field_tooltips_on_host_config_page(self, page: HostListPage): host_page.config.check_text_in_tooltip(item, f"Test description {item}") -@pytest.mark.usefixtures('login_to_adcm_over_api') +@pytest.mark.usefixtures('_login_to_adcm_over_api') class TestHostStatusPage: """Tests for the /host/{}/status page""" @@ -592,3 +611,71 @@ def test_status_on_host_status_page(self, app_fs, adcm_fs, sdk_client_fs, create with host_status_page.wait_rows_collapsed(): host_status_page.click_collapse_all_btn() assert len(host_status_page.get_all_rows()) == 1, "Status rows should have been collapsed" + + +class TestHostRenaming: + + SPECIAL_CHARS = (".", "-", "_") + DISALLOWED_AT_START = (".", "-") + EXPECTED_ERROR = "Please enter a valid name" + + @pytest.mark.usefixtures("_login_to_adcm_over_api") + def test_rename_host(self, sdk_client_fs, app_fs, create_host): + host = create_host + page = HostListPage(app_fs.driver, app_fs.adcm.url).open() + self._test_correct_name_can_be_set(host, page) + self._test_an_error_is_shown_on_incorrect_char_in_name(page) + self._test_an_error_is_not_shown_on_correct_char_in_name(page) + + @allure.step("Check settings new correct host FQDN") + def _test_correct_name_can_be_set(self, host: Host, page: HostListPage) -> None: + new_name = "best-host.fqdn" + + dialog = page.open_rename_dialog(page.get_host_row()) + dialog.set_new_name_in_rename_dialog(new_name) + dialog.click_save_on_rename_dialog() + with allure.step("Check fqdn of host in table"): + name_in_row = page.get_host_info_from_row(0).fqdn + assert name_in_row == new_name, f"Incorrect cluster name, expected: {new_name}" + host.reread() + assert host.fqdn == new_name, f"Host FQDN on backend is incorrect, expected: {new_name}" + + def _test_an_error_is_shown_on_incorrect_char_in_name(self, page: HostListPage) -> None: + dummy_name = "hOst" + incorrect_names = ( + *[f"{char}{dummy_name}" for char in self.DISALLOWED_AT_START], + *[f"{dummy_name[0]}{char}{dummy_name[1:]}" for char in ("и", "!", " ")], + ) + + dialog = page.open_rename_dialog(page.get_host_row()) + + for fqdn in incorrect_names: + with allure.step(f"Check if printing host FQDN '{fqdn}' triggers a warning message"): + dialog.set_new_name_in_rename_dialog(dummy_name) + dialog.set_new_name_in_rename_dialog(fqdn) + assert dialog.is_dialog_error_message_visible(), "Error about incorrect name should be visible" + assert ( + dialog.get_dialog_error_message() == self.EXPECTED_ERROR + ), f"Incorrect error message, expected: {self.EXPECTED_ERROR}" + + dialog.click_cancel_on_rename_dialog() + + def _test_an_error_is_not_shown_on_correct_char_in_name(self, page: HostListPage) -> None: + dummy_name = "clUster" + correct_names = ( + *[f"{dummy_name[0]}{char}{dummy_name[1:]}" for char in (".", "-", "9")], + f"9{dummy_name}", + f"{dummy_name}-", + ) + + dialog = page.open_rename_dialog(page.get_host_row()) + + for fqdn in correct_names: + with allure.step(f"Check if printing host FQDN '{fqdn}' shows no error"): + dialog.set_new_name_in_rename_dialog(dummy_name) + dialog.set_new_name_in_rename_dialog(fqdn) + assert not dialog.is_dialog_error_message_visible(), "Error about correct name should not be shown" + dialog.click_save_on_rename_dialog() + name_in_row = page.get_host_info_from_row().fqdn + assert name_in_row == fqdn, f"Incorrect host FQDN, expected: {fqdn}" + dialog = page.open_rename_dialog(page.get_host_row()) diff --git a/tests/ui_tests/test_jobs_page.py b/tests/ui_tests/test_jobs_page.py index da1ea80481..83b7ce9e8c 100644 --- a/tests/ui_tests/test_jobs_page.py +++ b/tests/ui_tests/test_jobs_page.py @@ -14,39 +14,34 @@ import os from dataclasses import asdict -from typing import Union, List +from typing import List, Union import allure import pytest from adcm_client.objects import ( + Action, ADCMClient, - Cluster, Bundle, - Provider, - Service, - Host, + Cluster, Component, - Action, + Host, ObjectNotFound, + Provider, + Service, ) from adcm_pytest_plugin import utils -from adcm_pytest_plugin.steps.actions import ( - run_cluster_action_and_assert_result, -) +from adcm_pytest_plugin.steps.actions import run_cluster_action_and_assert_result from adcm_pytest_plugin.utils import catch_failed - +from tests.library.utils import build_full_archive_name from tests.ui_tests.app.app import ADCMTest from tests.ui_tests.app.page.cluster_list.page import ClusterListPage from tests.ui_tests.app.page.job.page import JobPageStdout -from tests.ui_tests.app.page.job_list.page import ( - JobListPage, - JobStatus, -) +from tests.ui_tests.app.page.job_list.page import JobListPage, JobStatus from tests.ui_tests.app.page.login.page import LoginPage from tests.ui_tests.utils import ( - wait_and_assert_ui_info, - is_not_empty, is_empty, + is_not_empty, + wait_and_assert_ui_info, wait_file_is_presented, wait_until_step_succeeds, ) @@ -54,6 +49,7 @@ LONG_ACTION_DISPLAY_NAME = 'Long action' SUCCESS_ACTION_DISPLAY_NAME = 'Success action' SUCCESS_ACTION_NAME = 'success_action' +CHECK_ACTION_NAME = "with_check" FAIL_ACTION_DISPLAY_NAME = 'Fail action' ON_HOST_ACTION_DISPLAY_NAME = 'Component host action' COMPONENT_ACTION_DISPLAY_NAME = 'Component action' @@ -68,8 +64,7 @@ @pytest.fixture() @allure.title("Open /task page") -# pylint: disable-next=unused-argument -def page(app_fs: ADCMTest, login_to_adcm_over_api) -> JobListPage: +def page(app_fs: ADCMTest, _login_to_adcm_over_api) -> JobListPage: """Open /task page""" return JobListPage(app_fs.driver, app_fs.adcm.url).open() @@ -238,7 +233,7 @@ def test_open_task_by_click_on_name(self, cluster: Cluster, page: JobListPage): @pytest.mark.smoke() @pytest.mark.include_firefox() @pytest.mark.parametrize('log_type', ['stdout', 'stderr'], ids=['stdout_menu', 'stderr_menu']) - @pytest.mark.usefixtures('login_to_adcm_over_api') + @pytest.mark.usefixtures('_login_to_adcm_over_api') def test_open_log_menu(self, log_type: str, cluster: Cluster, app_fs: ADCMTest): """Open stdout/stderr log menu and check info""" action = cluster.action(display_name=SUCCESS_ACTION_DISPLAY_NAME) @@ -258,7 +253,7 @@ def test_open_log_menu(self, log_type: str, cluster: Cluster, app_fs: ADCMTest): ) job_page.check_jobs_toolbar(SUCCESS_ACTION_DISPLAY_NAME.upper()) - @pytest.mark.usefixtures("login_to_adcm_over_api", "clean_downloads_fs") + @pytest.mark.usefixtures("_login_to_adcm_over_ui", "_clean_downloads_fs") def test_download_log(self, cluster: Cluster, app_fs: ADCMTest, downloads_directory): """Download log file from detailed page menu""" downloaded_file_template = '{job_id}-ansible-{log_type}.txt' @@ -269,11 +264,27 @@ def test_download_log(self, cluster: Cluster, app_fs: ADCMTest, downloads_direct with allure.step('Download logfiles'): job_page.click_on_log_download('stdout') wait_file_is_presented( - downloaded_file_template.format(job_id=job_id, log_type='stdout'), app_fs, dirname=downloads_directory + downloaded_file_template.format(job_id=job_id, log_type='stdout'), + app_fs, + dirname=downloads_directory, ) job_page.click_on_log_download('stderr') wait_file_is_presented( - downloaded_file_template.format(job_id=job_id, log_type='stderr'), app_fs, dirname=downloads_directory + downloaded_file_template.format(job_id=job_id, log_type='stderr'), + app_fs, + dirname=downloads_directory, + ) + + @pytest.mark.usefixtures("_login_to_adcm_over_ui", "_clean_downloads_fs") + def test_download_bulk_log(self, cluster: Cluster, app_fs: ADCMTest, downloads_directory): + task = run_cluster_action_and_assert_result(cluster, CHECK_ACTION_NAME) + jobs_page = JobListPage(driver=app_fs.driver, base_url=app_fs.adcm.url).open() + with allure.step("Bulk download logfiles"): + jobs_page.click_on_log_download(row=jobs_page.table.get_row(0)) + wait_file_is_presented( + app_fs=app_fs, + filename=f"{build_full_archive_name(cluster, task, CHECK_ACTION_NAME.replace('_', '-'))}.tar.gz", + dirname=downloads_directory, ) def test_invoker_object_url(self, cluster: Cluster, provider: Provider, page: JobListPage): @@ -314,7 +325,7 @@ class TestTaskHeaderPopup: ], ids=["all_jobs", 'in_progress_jobs', 'success_jobs', 'failed_jobs'], ) - @pytest.mark.usefixtures("login_to_adcm_over_api") + @pytest.mark.usefixtures("_login_to_adcm_over_api") def test_link_to_jobs_in_header_popup(self, app_fs, job_link, job_filter): """Link to /task from popup with filter""" @@ -395,7 +406,7 @@ def test_acknowledge_jobs_in_header_popup(self, cluster: Cluster, page: JobListP ], ids=['success_job', 'failed_job', 'in_progress_job', 'three_job'], ) - @pytest.mark.usefixtures('login_to_adcm_over_api') + @pytest.mark.usefixtures('_login_to_adcm_over_api') def test_job_has_correct_info_in_header_popup(self, job_info: dict, cluster: Cluster, app_fs): """Run action that finishes (success/failed) and check it in header popup""" @@ -442,14 +453,16 @@ def test_on_tasks_in_header_popup(self, cluster: Cluster, page: JobListPage, app job_page.check_title(action_name) job_page.check_text(success_task=action_name == SUCCESS_ACTION_DISPLAY_NAME) - @pytest.mark.usefixtures('login_to_adcm_over_api') + @pytest.mark.usefixtures('_login_to_adcm_over_api') def test_six_tasks_in_header_popup(self, cluster: Cluster, app_fs): """Check list of tasks in header popup""" cluster_page = ClusterListPage(app_fs.driver, app_fs.adcm.url).open() with allure.step('Run actions in cluster'): for _ in range(6): run_cluster_action_and_assert_result( - cluster, cluster.action(display_name=SUCCESS_ACTION_DISPLAY_NAME).name, status='success' + cluster, + cluster.action(display_name=SUCCESS_ACTION_DISPLAY_NAME).name, + status='success', ) cluster_page.header.click_job_block_in_header() with allure.step("Check that in popup 5 tasks"): @@ -461,7 +474,7 @@ def test_six_tasks_in_header_popup(self, cluster: Cluster, app_fs): with allure.step("Check that in job list page 6 tasks"): assert job_page.table.row_count == 6, "Job list page should contain 6 tasks" - @pytest.mark.usefixtures('login_to_adcm_over_api', 'cluster') + @pytest.mark.usefixtures('_login_to_adcm_over_api', 'cluster') def test_acknowledge_running_job_in_header_popup(self, app_fs): """Run action and click acknowledge in header popup while it runs""" cluster_page = ClusterListPage(app_fs.driver, app_fs.adcm.url).open() @@ -572,7 +585,9 @@ def _check_link_to_invoker_object(expected_link: str, page: JobListPage, action: with page.table.wait_rows_change(): action.run() wait_and_assert_ui_info( - expected_value, page.get_task_info_from_table, get_info_kwargs={'full_invoker_objects_link': True} + expected_value, + page.get_task_info_from_table, + get_info_kwargs={'full_invoker_objects_link': True}, ) detail_page = JobPageStdout(page.driver, page.base_url, action.task_list()[0].id).open() wait_and_assert_ui_info(expected_value, detail_page.get_job_info) diff --git a/tests/ui_tests/test_jobs_page_data/cluster/actions.yaml b/tests/ui_tests/test_jobs_page_data/cluster/actions.yaml index 7c4aa0b1b3..0aa7b4614e 100644 --- a/tests/ui_tests/test_jobs_page_data/cluster/actions.yaml +++ b/tests/ui_tests/test_jobs_page_data/cluster/actions.yaml @@ -33,3 +33,10 @@ minutes: 1 tags: - pause + - name: "Check" + adcm_check: + title: "Check entry" + msg: "Check text" + result: yes + tags: + - check diff --git a/tests/ui_tests/test_jobs_page_data/cluster/config.yaml b/tests/ui_tests/test_jobs_page_data/cluster/config.yaml index 14e3c2658d..5e844c0b12 100644 --- a/tests/ui_tests/test_jobs_page_data/cluster/config.yaml +++ b/tests/ui_tests/test_jobs_page_data/cluster/config.yaml @@ -46,7 +46,7 @@ - &multijob name: first_step script_type: ansible - script: actions.yaml + script: ./actions.yaml params: ansible_tags: success - <<: *multijob @@ -55,6 +55,17 @@ name: third_step states: available: any + with_check: + type: task + display_name: "With check" + scripts: + - <<: *multijob + - <<: *multijob + name: second_step + params: + ansible_tags: check + states: + available: any - type: service name: test_service diff --git a/tests/ui_tests/test_login_page.py b/tests/ui_tests/test_login_page.py index a6c4c1f936..4c17b132ee 100644 --- a/tests/ui_tests/test_login_page.py +++ b/tests/ui_tests/test_login_page.py @@ -15,7 +15,6 @@ import allure import pytest from adcm_pytest_plugin.params import including_https - from tests.ui_tests.app.page.admin.page import AdminIntroPage from tests.ui_tests.app.page.login.page import LoginPage diff --git a/tests/ui_tests/test_profile_page.py b/tests/ui_tests/test_profile_page.py index 34c214ef82..fa79dd8f77 100644 --- a/tests/ui_tests/test_profile_page.py +++ b/tests/ui_tests/test_profile_page.py @@ -13,14 +13,17 @@ """UI tests for /profile page""" import pytest - from tests.ui_tests.app.app import ADCMTest from tests.ui_tests.app.page.admin.page import AdminIntroPage from tests.ui_tests.app.page.login.page import LoginPage from tests.ui_tests.app.page.profile.page import ProfilePage # pylint: disable=redefined-outer-name -pytestmark = [pytest.mark.smoke(), pytest.mark.include_firefox(), pytest.mark.usefixtures('login_to_adcm_over_api')] +pytestmark = [ + pytest.mark.smoke(), + pytest.mark.include_firefox(), + pytest.mark.usefixtures('_login_to_adcm_over_api'), +] def test_open_profile(app_fs: ADCMTest): diff --git a/tests/ui_tests/test_provider_page.py b/tests/ui_tests/test_provider_page.py index 5c78bf35db..52b8e242b5 100644 --- a/tests/ui_tests/test_provider_page.py +++ b/tests/ui_tests/test_provider_page.py @@ -17,30 +17,24 @@ import allure import pytest from _pytest.fixtures import SubRequest -from adcm_client.objects import ( - ADCMClient, - Bundle, - Provider, -) +from adcm_client.objects import ADCMClient, Bundle, Provider from adcm_pytest_plugin import utils - from tests.ui_tests.app.page.admin.page import AdminIntroPage from tests.ui_tests.app.page.common.configuration.page import CONFIG_ITEMS from tests.ui_tests.app.page.common.group_config_list.page import GroupConfigRowInfo -from tests.ui_tests.app.page.host.page import ( - HostMainPage, -) +from tests.ui_tests.app.page.host.page import HostMainPage from tests.ui_tests.app.page.provider.page import ( - ProviderMainPage, ProviderConfigPage, ProviderGroupConfigPage, + ProviderMainPage, ) from tests.ui_tests.app.page.provider_list.page import ProviderListPage +from tests.ui_tests.utils import create_few_groups -# pylint: disable=redefined-outer-name,unused-argument +# pylint: disable=redefined-outer-name -pytestmark = pytest.mark.usefixtures("login_to_adcm_over_api") +pytestmark = pytest.mark.usefixtures("_login_to_adcm_over_api") PROVIDER_NAME = 'test_provider' @@ -77,7 +71,9 @@ class TestProviderListPage: @pytest.mark.smoke() @pytest.mark.include_firefox() @pytest.mark.parametrize( - "bundle_archive", [pytest.param(utils.get_data_dir(__file__, "provider"), id="provider")], indirect=True + "bundle_archive", + [pytest.param(utils.get_data_dir(__file__, "provider"), id="provider")], + indirect=True, ) def test_create_provider_on_provider_list_page(self, app_fs, bundle_archive): """Tests create provider from provider list page""" @@ -103,7 +99,9 @@ def test_create_provider_on_provider_list_page(self, app_fs, bundle_archive): @pytest.mark.smoke() @pytest.mark.include_firefox() @pytest.mark.parametrize( - "bundle_archive", [pytest.param(utils.get_data_dir(__file__, "provider"), id="provider")], indirect=True + "bundle_archive", + [pytest.param(utils.get_data_dir(__file__, "provider"), id="provider")], + indirect=True, ) def test_create_custom_provider_on_provider_list_page(self, app_fs, bundle_archive): """Tests create provider from provider list page with custom params""" @@ -116,7 +114,9 @@ def test_create_custom_provider_on_provider_list_page(self, app_fs, bundle_archi provider_page = ProviderListPage(app_fs.driver, app_fs.adcm.url).open() with provider_page.table.wait_rows_change(): provider_page.create_provider( - bundle=bundle_archive, name=provider_params['name'], description=provider_params['description'] + bundle=bundle_archive, + name=provider_params['name'], + description=provider_params['description'], ) with allure.step("Check uploaded provider"): rows = provider_page.table.get_all_rows() @@ -354,7 +354,8 @@ def test_field_validation_on_provider_config_page(self, app_fs, bundle, upload_a provider_config_page.config.check_field_is_invalid(params['not_req_name']) provider_config_page.config.check_config_warn_icon_on_left_menu() provider_config_page.toolbar.check_warn_button( - tab_name="test_provider", expected_warn_text=['test_provider has an issue with its config'] + tab_name="test_provider", + expected_warn_text=['test_provider has an issue with its config'], ) @pytest.mark.parametrize("bundle", ["provider_default_fields"], indirect=True) @@ -427,5 +428,5 @@ def test_check_pagination_on_group_config_provider_page(self, app_fs, upload_and group_conf_page = ProviderGroupConfigPage( app_fs.driver, app_fs.adcm.url, upload_and_create_test_provider.id ).open() - group_conf_page.group_config.create_few_groups(11) + create_few_groups(group_conf_page.group_config) group_conf_page.table.check_pagination(second_page_item_amount=1) diff --git a/tests/ui_tests/test_regression.py b/tests/ui_tests/test_regression.py index 6778d80238..edde6590c4 100644 --- a/tests/ui_tests/test_regression.py +++ b/tests/ui_tests/test_regression.py @@ -16,11 +16,13 @@ import allure import pytest -from adcm_client.objects import Cluster, Service, Component, Provider, Task, ADCMClient +from adcm_client.objects import ADCMClient, Cluster, Component, Provider, Service, Task +from adcm_pytest_plugin.steps.actions import ( + run_cluster_action_and_assert_result, + run_provider_action_and_assert_result, +) from adcm_pytest_plugin.utils import get_data_dir, wait_until_step_succeeds -from adcm_pytest_plugin.steps.actions import run_cluster_action_and_assert_result, run_provider_action_and_assert_result from coreapi.exceptions import ErrorMessage - from tests.conftest import TEST_USER_CREDENTIALS from tests.functional.tools import AnyADCMObject, get_object_represent from tests.ui_tests.app.app import ADCMTest @@ -59,21 +61,23 @@ def provider(self, sdk_client_fs) -> Provider: return provider @pytest.fixture() - def login_as_custom_user(self, app_fs, user_sdk): # pylint: disable=unused-argument + def _login_as_custom_user(self, app_fs, user_sdk): """Login as test user""" username, password = TEST_USER_CREDENTIALS login_over_api(app_fs, {'username': username, 'password': password}) @pytest.fixture() - def view_import_on_cluster(self, sdk_client_fs, cluster, user): + def _view_import_on_cluster(self, sdk_client_fs, cluster, user): """Grant permission to view import on cluster to new user""" role = sdk_client_fs.role_create( - name='Wrapper role', display_name='Wrapper', child=[{'id': sdk_client_fs.role(name='View imports').id}] + name='Wrapper role', + display_name='Wrapper', + child=[{'id': sdk_client_fs.role(name='View imports').id}], ) sdk_client_fs.policy_create(name='Policy for cluster', role=role, objects=[cluster], user=[user]) @allure.issue(url='https://arenadata.atlassian.net/browse/ADCM-2563') - @pytest.mark.usefixtures('login_as_custom_user', 'view_import_on_cluster') + @pytest.mark.usefixtures('_login_as_custom_user', '_view_import_on_cluster') def test_access_to_detailed_page(self, app_fs, cluster): """ Test that user has access to detailed object page when there's __main_info in config, @@ -89,7 +93,7 @@ def test_access_to_detailed_page(self, app_fs, cluster): ) @allure.issue(url='https://arenadata.atlassian.net/browse/ADCM-2676') - @pytest.mark.usefixtures('login_to_adcm_over_api') + @pytest.mark.usefixtures('_login_to_adcm_over_api') def test_main_info_update(self, app_fs, cluster, provider): """Test that update of __main_info config field is displayed correctly on object detailed page""" change_main_info_action = 'change_main_info' @@ -97,7 +101,13 @@ def test_main_info_update(self, app_fs, cluster, provider): component = service.component() host = provider.host() - pages = (ClusterMainPage, ServiceMainPage, ComponentMainPage, ProviderMainPage, HostMainPage) + pages = ( + ClusterMainPage, + ServiceMainPage, + ComponentMainPage, + ProviderMainPage, + HostMainPage, + ) adcm_objects = (cluster, service, component, provider, host) with allure.step( @@ -117,7 +127,11 @@ def test_main_info_update(self, app_fs, cluster, provider): self._check_main_info_after_config_save(app_fs, page_type, adcm_object) def _check_detailed_page_main_info( - self, app: ADCMTest, page_type: Type[BaseDetailedPage], adcm_object: AnyADCMObject, expected_text: str + self, + app: ADCMTest, + page_type: Type[BaseDetailedPage], + adcm_object: AnyADCMObject, + expected_text: str, ): """Check that detailed page contains the expected text from __main_info in description""" object_represent = get_object_represent(adcm_object) @@ -173,7 +187,7 @@ class TestAllowToTerminate: pytestmark = [ pytest.mark.parametrize('generic_bundle', ['action_termination_allowed'], indirect=True), - pytest.mark.usefixtures('login_to_adcm_over_api', '_bind_client', 'cluster'), + pytest.mark.usefixtures('_login_to_adcm_over_api', '_bind_client', 'cluster'), ] @pytest.fixture() diff --git a/tests/ui_tests/test_service_page.py b/tests/ui_tests/test_service_page.py index 9d609d988e..38ab5fd933 100644 --- a/tests/ui_tests/test_service_page.py +++ b/tests/ui_tests/test_service_page.py @@ -12,57 +12,45 @@ """UI tests for /service page""" import os -from typing import Tuple from collections import OrderedDict +from typing import Tuple import allure import pytest from _pytest.fixtures import SubRequest -from adcm_client.objects import ( - ADCMClient, - Bundle, -) -from adcm_client.objects import ( - Cluster, - Service, - Host, -) -from adcm_pytest_plugin import params -from adcm_pytest_plugin import utils +from adcm_client.objects import ADCMClient, Bundle, Cluster, Host, Service +from adcm_pytest_plugin import params, utils from tests.library.status import ADCMObjectStatusChanger from tests.ui_tests.app.app import ADCMTest from tests.ui_tests.app.page.admin.page import AdminIntroPage -from tests.ui_tests.app.page.cluster.page import ( - ClusterServicesPage, -) +from tests.ui_tests.app.page.cluster.page import ClusterServicesPage from tests.ui_tests.app.page.common.configuration.page import CONFIG_ITEMS from tests.ui_tests.app.page.common.group_config_list.page import GroupConfigRowInfo from tests.ui_tests.app.page.common.import_page.page import ImportItemInfo from tests.ui_tests.app.page.common.status.page import ( - SUCCESS_COLOR, NEGATIVE_COLOR, + SUCCESS_COLOR, + StatusRowInfo, ) -from tests.ui_tests.app.page.common.status.page import StatusRowInfo from tests.ui_tests.app.page.service.page import ( ServiceComponentPage, - ServiceStatusPage, -) -from tests.ui_tests.app.page.service.page import ( - ServiceMainPage, ServiceConfigPage, ServiceGroupConfigPage, ServiceImportPage, + ServiceMainPage, + ServiceStatusPage, ) from tests.ui_tests.test_cluster_list_page import ( + BUNDLE_COMMUNITY, + BUNDLE_IMPORT, + BUNDLE_REQUIRED_FIELDS, CLUSTER_NAME, - SERVICE_NAME, - PROVIDER_NAME, - HOST_NAME, COMPONENT_NAME, - BUNDLE_REQUIRED_FIELDS, - BUNDLE_IMPORT, - BUNDLE_COMMUNITY, + HOST_NAME, + PROVIDER_NAME, + SERVICE_NAME, ) +from tests.ui_tests.utils import create_few_groups BUNDLE_WITH_REQUIRED_COMPONENT = "cluster_required_hostcomponent" BUNDLE_WITH_REQUIRED_IMPORT = "cluster_required_import" @@ -70,8 +58,8 @@ BUNDLE_WITH_DESCRIPTION_FIELDS = "service_with_all_config_params" -# pylint: disable=redefined-outer-name,unused-argument,too-many-locals -pytestmark = pytest.mark.usefixtures("login_to_adcm_over_api") +# pylint: disable=redefined-outer-name,too-many-locals +pytestmark = pytest.mark.usefixtures("_login_to_adcm_over_api") # !===== Fixtures =====! @@ -305,7 +293,8 @@ def test_save_advanced_config_on_service_config_page(self, app_fs, sdk_client_fs @pytest.mark.skip("https://tracker.yandex.ru/ADCM-3017") @pytest.mark.parametrize( - "bundle_name", ["password_no_confirm_false_required_false", "password_no_confirm_true_required_false"] + "bundle_name", + ["password_no_confirm_false_required_false", "password_no_confirm_true_required_false"], ) def test_password_required_false_in_config_on_service_config_page(self, app_fs, sdk_client_fs, bundle_name): """Test password field on /cluster/{}/service/{}/config page""" @@ -367,7 +356,12 @@ def test_password_no_confirm_true_required_true_in_config_on_service_config_page def test_reset_config_in_row_on_service_config_page(self, app_fs, create_cluster_with_service): """Test config reset on /cluster/{}/service/{}/config page""" - params = {"row_name": "param1", "row_value_new": "test", "row_value_old": "", "config_name": "test_name"} + params = { + "row_name": "param1", + "row_value_new": "test", + "row_value_old": "", + "config_name": "test_name", + } cluster, service = create_cluster_with_service service_config_page = ServiceConfigPage(app_fs.driver, app_fs.adcm.url, cluster.id, service.id).open() @@ -548,7 +542,7 @@ def test_check_pagination_on_group_config_service_page(self, app_fs, create_clus cluster, service = create_cluster_with_service group_conf_page = ServiceGroupConfigPage(app_fs.driver, app_fs.adcm.url, cluster.id, service.id).open() - group_conf_page.group_config.create_few_groups(11) + create_few_groups(group_conf_page.group_config) group_conf_page.table.check_pagination(second_page_item_amount=1) diff --git a/tests/ui_tests/utils.py b/tests/ui_tests/utils.py index dcff8cdff5..1ef0817735 100644 --- a/tests/ui_tests/utils.py +++ b/tests/ui_tests/utils.py @@ -14,19 +14,20 @@ # pylint: disable=too-many-ancestors import os -from collections import UserDict from contextlib import contextmanager -from typing import Callable, TypeVar, Any, Union, Optional, Dict, Tuple, Sized +from typing import Any, Callable, Dict, Optional, Sized, Tuple, TypeVar, Union import allure import requests from adcm_client.objects import ADCMClient, Cluster from adcm_pytest_plugin.utils import random_string, wait_until_step_succeeds -from selenium.common.exceptions import StaleElementReferenceException, NoSuchElementException +from selenium.common.exceptions import ( + NoSuchElementException, + StaleElementReferenceException, +) from selenium.webdriver.remote.webdriver import WebDriver from selenium.webdriver.support import expected_conditions as EC from selenium.webdriver.support.ui import WebDriverWait as WDW - from tests.ui_tests.app.app import ADCMTest ValueType = TypeVar('ValueType') @@ -40,91 +41,6 @@ def _prepare_cluster(sdk_client: ADCMClient, path) -> Cluster: return cluster -class BundleObjectDefinition(UserDict): - """Data class for ADCM object""" - - def __init__(self, obj_type=None, name=None, version=None): - super().__init__() - self["type"] = obj_type - self["name"] = name - if version is not None: - self["version"] = version - - def _set_ui_option(self, option, value): - if "ui_options" not in self: - self["ui_options"] = {} - self["ui_options"][option] = value - - def set_advanced(self, value): - """set advanced property value""" - self._set_ui_option("advanced", value) - - @classmethod - def to_dict(cls, obj) -> dict: - """Represent object as dict""" - if isinstance(obj, cls): - obj = cls.to_dict(obj.data) - elif isinstance(obj, list): - for i, v in enumerate(obj): - obj[i] = cls.to_dict(v) - elif isinstance(obj, dict): - for k in obj: - obj[k] = cls.to_dict(obj[k]) - return obj - - -class ClusterDefinition(BundleObjectDefinition): - """Data class for cluster""" - - def __init__(self, name=None, version=None): - """Data class for cluster""" - super().__init__(obj_type="cluster", name=name, version=version) - - -class ServiceDefinition(BundleObjectDefinition): - """Data class for service""" - - def __init__(self, name=None, version=None): - super().__init__(obj_type="service", name=name, version=version) - - -class ProviderDefinition(BundleObjectDefinition): - """Data class for provider""" - - def __init__(self, name=None, version=None): - super().__init__(obj_type="provider", name=name, version=version) - - -class HostDefinition(BundleObjectDefinition): - """Data class for host""" - - def __init__(self, name=None, version=None): - super().__init__(obj_type="host", name=name, version=version) - - -class GroupDefinition(BundleObjectDefinition): - """Data class for group""" - - def __init__(self, name=None): - super().__init__(obj_type="group", name=name) - self["activatable"] = True - self["subs"] = [] - - def add_fields(self, *fields): - """Add fields to the object""" - for field in fields: - self["subs"].append(field) - return self - - -class FieldDefinition(BundleObjectDefinition): - """Data class for field""" - - def __init__(self, prop_type, prop_name=None): - super().__init__(obj_type=prop_type, name=prop_name) - self["required"] = False - - @allure.step('Wait for a new window after action') @contextmanager def wait_for_new_window(driver: WebDriver, wait_time: int = 10): @@ -325,3 +241,30 @@ def _check_rows_amount_is_changed(): assert len(get_all_rows()) != current_amount, "Amount of rows on the page hasn't changed" wait_until_step_succeeds(_check_rows_amount_is_changed, period=1, timeout=10) + + +# common steps + + +@allure.step("Prepare cluster and open config page") +def prepare_cluster_and_open_config_page(sdk_client: ADCMClient, path: os.PathLike, app): + """Upload bundle, create cluster and open config page""" + from tests.ui_tests.app.page.cluster.page import ( # pylint: disable=import-outside-toplevel + ClusterConfigPage, + ) + + bundle = sdk_client.upload_from_fs(path) + cluster = bundle.cluster_create(name=f"Test cluster {random_string()}") + config = ClusterConfigPage(app.driver, app.adcm.url, cluster.cluster_id).open() + config.wait_page_is_opened() + return cluster, config + + +@allure.step("Create 11 group configs") +def create_few_groups(group_config_list_page): + for i in range(10): + with group_config_list_page.wait_rows_change(): + group_config_list_page.create_group(name=f"Test name_{i}", description="Test description") + + group_config_list_page.create_group(name="Test name_10", description="Test description") + assert len(group_config_list_page.get_all_config_rows()) == 10, "There should be exactly 10 groups on 1st page" diff --git a/tests/upgrade_utils.py b/tests/upgrade_utils.py index 573590a5f1..8ade8ab669 100644 --- a/tests/upgrade_utils.py +++ b/tests/upgrade_utils.py @@ -15,10 +15,9 @@ from typing import Tuple import allure - -from version_utils import rpm from adcm_client.objects import ADCMClient from adcm_pytest_plugin.docker_utils import ADCM +from version_utils import rpm @allure.step("Check that ADCM version has been changed") diff --git a/web/src/adcm2.scss b/web/src/adcm2.scss index e9bd7726ba..f0d7134b04 100644 --- a/web/src/adcm2.scss +++ b/web/src/adcm2.scss @@ -15,6 +15,12 @@ mat-header-cell.list-control, mat-cell.list-control { width: 100px } +.text-ellipsed { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + /* BEGIN: Material header arrow crutch */ .mat-header-cell .mat-sort-header-container.mat-sort-header-sorted .mat-sort-header-arrow { opacity: 1 !important; diff --git a/web/src/app/abstract-directives/base-detail.abstract.directive.ts b/web/src/app/abstract-directives/base-detail.abstract.directive.ts index fb59af62a0..111c193f3c 100644 --- a/web/src/app/abstract-directives/base-detail.abstract.directive.ts +++ b/web/src/app/abstract-directives/base-detail.abstract.directive.ts @@ -94,7 +94,7 @@ export abstract class BaseDetailAbstractDirective extends SocketListenerDirectiv } = w.current; const { upgradable, upgrade, hostcomponent } = w.current as ICluster; const { log_files, objects } = w.current as Job; - const { provider_id } = w.current as Host; + const { provider_id, provider_name } = w.current as Host; this.currentName = name; this.actions = actions; @@ -122,6 +122,7 @@ export abstract class BaseDetailAbstractDirective extends SocketListenerDirectiv prototype_display_name, prototype_version, provider_id, + provider_name, bundle_id, hostcomponent, }; diff --git a/web/src/app/abstract-directives/list.directive.ts b/web/src/app/abstract-directives/list.directive.ts index 9c641ca604..74cc22a441 100644 --- a/web/src/app/abstract-directives/list.directive.ts +++ b/web/src/app/abstract-directives/list.directive.ts @@ -111,7 +111,6 @@ export abstract class ListDirective extends BaseDirective implements OnInit, OnD } maintenanceModeToggle($event: MouseEvent, row: any): void { - EventHelper.stopPropagation($event); this.listItemEvt.emit({ cmd: 'maintenanceModeToggle', row} ) } @@ -175,10 +174,25 @@ export abstract class ListDirective extends BaseDirective implements OnInit, OnD pageHandler(pageEvent: PageEvent): void { this.pageEvent.emit(pageEvent); - localStorage.setItem('limit', String(pageEvent.pageSize)); + const f = this.route.snapshot.paramMap.get('filter') || ''; const ordering = this.getSortParam(this.getSort()); - this.router.navigate(['./', { page: pageEvent.pageIndex, limit: pageEvent.pageSize, filter: f, ordering }], { + const ls = localStorage.getItem('list:param'); + let listParam = ls ? JSON.parse(ls) : null; + + listParam = { + ...listParam, + [this.type]: { + ...listParam?.[this.type], + limit: String(pageEvent.pageSize), + page: String(pageEvent.pageIndex), + filter: f, ordering + } + } + localStorage.setItem('list:param', JSON.stringify(listParam)); + localStorage.setItem('limit', String(pageEvent.pageSize)); + + this.router.navigate(['./', listParam?.[this.type]], { relativeTo: this.route, }); } diff --git a/web/src/app/admin/admin.module.ts b/web/src/app/admin/admin.module.ts index 730da27cb9..ee7f246ec6 100644 --- a/web/src/app/admin/admin.module.ts +++ b/web/src/app/admin/admin.module.ts @@ -20,10 +20,10 @@ import { UsersComponent } from './users/users.component'; import { RbacGroupFormModule } from '../components/rbac/group-form/rbac-group-form.module'; import { RbacUserFormModule } from '../components/rbac/user-form/rbac-user-form.module'; import { RbacRoleFormModule } from '../components/rbac/role-form/rbac-role-form.module'; - import { GroupsComponent } from './groups/groups.component'; import { RolesComponent } from './roles/roles.component'; import { PoliciesComponent } from './policies/policies.component'; +import { AuditOperationsComponent } from './audit-operations/audit-operations.component'; import { AdwpListModule } from '@adwp-ui/widgets'; import { MatToolbarModule } from '@angular/material/toolbar'; import { MatSidenavModule } from '@angular/material/sidenav'; @@ -35,6 +35,10 @@ import { MatButtonModule } from '@angular/material/button'; import { MatIconModule } from '@angular/material/icon'; import { RbacPolicyFormModule } from '../components/rbac/policy-form/rbac-policy-form.module'; import { ConfigurationModule } from '../shared/configuration/configuration.module'; +import { + RbacAuditOperationsHistoryFormComponent +} from "../components/rbac/audit-operations-history-form/rbac-audit-operations-history-form.component"; +import { AuditLoginComponent } from './audit-login/audit-login.component'; const routes: Routes = [ { @@ -71,6 +75,14 @@ const routes: Routes = [ { path: 'policies', component: PoliciesComponent, + }, + { + path: 'audit/operations', + component: AuditOperationsComponent, + }, + { + path: 'audit/logins', + component: AuditLoginComponent, } ], }, @@ -103,6 +115,9 @@ const routes: Routes = [ GroupsComponent, RolesComponent, PoliciesComponent, + AuditOperationsComponent, + RbacAuditOperationsHistoryFormComponent, + AuditLoginComponent ], }) export class AdminModule { diff --git a/web/src/app/admin/audit-login/audit-login.component.html b/web/src/app/admin/audit-login/audit-login.component.html new file mode 100644 index 0000000000..cfc02daf12 --- /dev/null +++ b/web/src/app/admin/audit-login/audit-login.component.html @@ -0,0 +1,13 @@ + + + diff --git a/web/src/app/admin/audit-login/audit-login.component.scss b/web/src/app/admin/audit-login/audit-login.component.scss new file mode 100644 index 0000000000..3490a52051 --- /dev/null +++ b/web/src/app/admin/audit-login/audit-login.component.scss @@ -0,0 +1,10 @@ +:host { + flex: 1; + width: 100%; +} + +.controls { + position: absolute; + right: 40px; + top: 16px; +} diff --git a/web/src/app/admin/audit-login/audit-login.component.ts b/web/src/app/admin/audit-login/audit-login.component.ts new file mode 100644 index 0000000000..dbeb58606d --- /dev/null +++ b/web/src/app/admin/audit-login/audit-login.component.ts @@ -0,0 +1,114 @@ +import { Component, ComponentRef, ViewChild } from '@angular/core'; +import { ADD_SERVICE_PROVIDER } from "../../shared/add-component/add-service-model"; +import { IColumns } from "@adwp-ui/widgets"; +import { TypeName } from "../../core/types"; +import { ListService } from "../../shared/components/list/list.service"; +import { Store } from "@ngrx/store"; +import { SocketState } from "../../core/store"; +import { ActivatedRoute, Router } from "@angular/router"; +import { MatDialog } from "@angular/material/dialog"; +import { RbacEntityListDirective } from "../../abstract-directives/rbac-entity-list.directive"; +import { RbacAuditLoginModel } from "../../models/rbac/rbac-audit-login.model"; +import { AddButtonComponent } from "../../shared/add-component"; +import { RbacAuditLoginService } from "../../services/rbac-audit-login.service"; +import { BehaviorSubject } from "rxjs"; +import { IFilter } from "../../shared/configuration/tools/filter/filter.component"; +import { DateHelper } from "../../helpers/date-helper"; +import { WrapperColumnComponent } from "../../components/columns/wrapper-column/wrapper-column.component"; + +@Component({ + selector: 'app-audit-login', + templateUrl: './audit-login.component.html', + styleUrls: ['./audit-login.component.scss'], + providers: [ + { provide: ADD_SERVICE_PROVIDER, useExisting: RbacAuditLoginService } + ], +}) +export class AuditLoginComponent extends RbacEntityListDirective { + @ViewChild(AddButtonComponent) addButton: AddButtonComponent; + + listColumns = [ + { + label: 'Login', + type: 'component', + headerClassName: 'width30pr', + className: 'width30pr', + component: WrapperColumnComponent, + instanceTaken: (componentRef: ComponentRef) => { + componentRef.instance.type = ['text-substr']; + componentRef.instance.customColumnName = 'login_details/username' + }, + }, + { + label: 'Result', + type: 'component', + headerClassName: 'width30pr', + className: 'width30pr', + component: WrapperColumnComponent, + instanceTaken: (componentRef: ComponentRef) => { + componentRef.instance.type = ['color']; + componentRef.instance.customColumnName = 'login_result'; + } + }, + { + label: 'Login time', + sort: 'login_time', + className: 'width30pr action_date', + headerClassName: 'width30pr action_date', + value: (row) => DateHelper.short(row.login_time), + } + ] as IColumns; + + type: TypeName = 'audit_login'; + filterParams$: BehaviorSubject = new BehaviorSubject({}); + + auditLoginFilters: IFilter[] = [ + { + id: 1, name: 'login', display_name: 'Login', filter_field: 'username', filter_type: 'input', + }, + { + id: 2, name: 'login_result', display_name: 'Result', filter_field: 'login_result', filter_type: 'list', + options: [ + {id: 1, name: 'account disabled', display_name: 'Account disabled', value: 'account disabled'}, + {id: 2, name: 'success', display_name: 'Success', value: 'success'}, + {id: 3, name: 'user not found', display_name: 'User not found', value: 'user not found'}, + {id: 4, name: 'wrong password', display_name: 'Wrong password', value: 'wrong password'}, + ] + }, + { + id: 3, name: 'login_time', display_name: 'Login time', filter_field: 'login_time', filter_type: 'datepicker', + }, + ]; + + constructor( + protected service: ListService, + protected store: Store, + public route: ActivatedRoute, + public router: Router, + public dialog: MatDialog, + protected entityService: RbacAuditLoginService, + ) { + super(service, store, route, router, dialog, entityService); + } + + ngOnInit() { + super.ngOnInit(); + + this.filterParams$.subscribe((params) => { + const filter_params = this.baseListDirective.listParams; + + if (filter_params) { + filter_params['params'] = { ...params }; + this.router.navigate(['./', filter_params['params']], { + relativeTo: this.route, + replaceUrl: true, + }); + } + }) + } + + getTitle(row: RbacAuditLoginModel): string { + return row.login_details.username; + } + +} diff --git a/web/src/app/admin/audit-operations/audit-operations.component.html b/web/src/app/admin/audit-operations/audit-operations.component.html new file mode 100644 index 0000000000..0fcf3ad2bb --- /dev/null +++ b/web/src/app/admin/audit-operations/audit-operations.component.html @@ -0,0 +1,13 @@ + + + diff --git a/web/src/app/admin/audit-operations/audit-operations.component.scss b/web/src/app/admin/audit-operations/audit-operations.component.scss new file mode 100644 index 0000000000..a8ab930fdd --- /dev/null +++ b/web/src/app/admin/audit-operations/audit-operations.component.scss @@ -0,0 +1,62 @@ +:host { + flex: 1; + width: 100%; + + ::ng-deep { + .object_name, + .operation_name, + .action_date, + .username, + .username_header, + .table-end { + flex-grow: 0; + } + + .object_type { + min-width: 80px; + } + + .object_name { + flex-basis: 200px; + width: 200px; + min-width: 50px; + } + + .operation_name { + flex-basis: 320px; + width: 320px; + min-width: 110px; + } + + .operation_type, + .operation_result { + flex-basis: 100px; + width: 100px; + min-width: 70px; + } + + .action_date { + flex-basis: 205px; + width: 205px; + min-width: 205px; + margin-left: 10px; + } + + .username { + width: 75px; + flex-basis: 75px; + min-width: 75px; + } + + .table-end { + width: 50px; + flex-basis: 50px; + } + } +} + +.controls { + position: absolute; + right: 40px; + top: 16px; +} diff --git a/web/src/app/admin/audit-operations/audit-operations.component.ts b/web/src/app/admin/audit-operations/audit-operations.component.ts new file mode 100644 index 0000000000..ba3fd4dbb7 --- /dev/null +++ b/web/src/app/admin/audit-operations/audit-operations.component.ts @@ -0,0 +1,188 @@ +import { Component, ComponentRef, Type, ViewChild } from '@angular/core'; +import { ADD_SERVICE_PROVIDER } from "../../shared/add-component/add-service-model"; +import { IColumns } from "@adwp-ui/widgets"; +import { TypeName } from "../../core/types"; +import { ListService } from "../../shared/components/list/list.service"; +import { Store } from "@ngrx/store"; +import { SocketState } from "../../core/store"; +import { ActivatedRoute, Router } from "@angular/router"; +import { MatDialog } from "@angular/material/dialog"; +import { RbacEntityListDirective } from "../../abstract-directives/rbac-entity-list.directive"; +import { RbacAuditOperationsModel } from "../../models/rbac/rbac-audit-operations.model"; +import { AddButtonComponent } from "../../shared/add-component"; +import { RbacAuditOperationsService } from "../../services/rbac-audit-operations.service"; +import { + RbacAuditOperationsHistoryFormComponent +} from "../../components/rbac/audit-operations-history-form/rbac-audit-operations-history-form.component"; +import { BehaviorSubject } from "rxjs"; +import { IFilter } from "../../shared/configuration/tools/filter/filter.component"; +import { HistoryColumnComponent } from "../../components/columns/history-column/history-column.component"; +import { DateHelper } from "../../helpers/date-helper"; +import { WrapperColumnComponent } from "../../components/columns/wrapper-column/wrapper-column.component"; + +@Component({ + selector: 'app-audit-operations', + templateUrl: './audit-operations.component.html', + styleUrls: ['./audit-operations.component.scss'], + providers: [ + { provide: ADD_SERVICE_PROVIDER, useExisting: RbacAuditOperationsService } + ], +}) +export class AuditOperationsComponent extends RbacEntityListDirective { + @ViewChild(AddButtonComponent) addButton: AddButtonComponent; + + listColumns = [ + { + label: 'Object type', + headerClassName: 'object_type', + className: 'object_type', + value: (row) => row.object_type, + }, + { + label: 'Object name', + type: 'component', + headerClassName: 'object_name', + className: 'object_name object_name_content', + value: (row) => row.object_name, + component: WrapperColumnComponent, + instanceTaken: (componentRef: ComponentRef) => { + componentRef.instance.type = ['text-substr']; + } + }, + { + label: 'Operation name', + headerClassName: 'operation_name', + className: 'operation_name', + value: (row) => row.operation_name, + }, + { + label: 'Operation type', + type: 'component', + headerClassName: ' operation_type', + className: 'operation_type', + component: WrapperColumnComponent, + value: (row) => row.operation_type, + instanceTaken: (componentRef: ComponentRef) => { + componentRef.instance.type = ['color']; + } + }, + { + label: 'Operation result', + type: 'component', + headerClassName: 'operation_result', + className: 'operation_result', + component: WrapperColumnComponent, + value: (row) => row.operation_result, + instanceTaken: (componentRef: ComponentRef) => { + componentRef.instance.type = ['color']; + } + }, + { + label: 'Operation time', + sort: 'operation_time', + className: 'action_date', + headerClassName: 'action_date', + value: (row) => DateHelper.short(row.operation_time), + }, + { + label: 'Username', + type: 'component', + headerClassName: 'username', + className: 'username', + value: (row) => row.username, + component: WrapperColumnComponent, + instanceTaken: (componentRef: ComponentRef) => { + componentRef.instance.type = ['text-substr']; + } + }, + { + label: '', + type: 'component', + headerClassName: 'table-end', + className: 'table-end', + component: HistoryColumnComponent, + } + + ] as IColumns; + + type: TypeName = 'audit_operations'; + filterParams$: BehaviorSubject = new BehaviorSubject({}); + + auditOperationsFilters: IFilter[] = [ + { + id: 1, name: 'username', display_name: 'Username', filter_field: 'username', filter_type: 'input', + }, + { + id: 2, name: 'object_name', display_name: 'Object name', filter_field: 'object_name', filter_type: 'input', + }, + { + id: 3, name: 'object_type', display_name: 'Object type', filter_field: 'object_type', filter_type: 'list', + options: [ + {id: 1, name: 'adcm', display_name: 'ADCM', value: 'adcm'}, + {id: 2, name: 'bundle', display_name: 'Bundle', value: 'bundle'}, + {id: 3, name: 'cluster', display_name: 'Cluster', value: 'cluster'}, + {id: 4, name: 'component', display_name: 'Component', value: 'component'}, + {id: 5, name: 'group', display_name: 'Group', value: 'group'}, + {id: 6, name: 'host', display_name: 'Host', value: 'host'}, + {id: 7, name: 'policy', display_name: 'Policy', value: 'policy'}, + {id: 8, name: 'provider', display_name: 'Provider', value: 'provider'}, + {id: 9, name: 'role', display_name: 'Role', value: 'role'}, + {id: 10, name: 'service', display_name: 'Service', value: 'service'}, + {id: 11, name: 'user', display_name: 'User', value: 'user'}, + ] + }, + { + id: 4, name: 'operation_type', display_name: 'Operation type', filter_field: 'operation_type', filter_type: 'list', + options: [ + {id: 1, name: 'create', display_name: 'Create', value: 'create'}, + {id: 2, name: 'update', display_name: 'Update', value: 'update'}, + {id: 3, name: 'delete', display_name: 'Delete', value: 'delete'}, + ] + }, + { + id: 5, name: 'operation_result', display_name: 'Operation result', filter_field: 'operation_result', filter_type: 'list', + options: [ + {id: 1, name: 'success', display_name: 'Success', value: 'success'}, + {id: 2, name: 'fail', display_name: 'Fail', value: 'fail'}, + {id: 3, name: 'denied', display_name: 'Denied', value: 'denied'}, + ] + }, + { + id: 6, name: 'operation_time', display_name: 'Operation time', filter_field: 'operation_time', filter_type: 'datepicker', + }, + ]; + + component: Type = RbacAuditOperationsHistoryFormComponent; + + constructor( + protected service: ListService, + protected store: Store, + public route: ActivatedRoute, + public router: Router, + public dialog: MatDialog, + protected entityService: RbacAuditOperationsService, + ) { + super(service, store, route, router, dialog, entityService); + } + + ngOnInit() { + super.ngOnInit(); + + this.filterParams$.subscribe((params) => { + const filter_params = this.baseListDirective.listParams; + + if (filter_params) { + filter_params['params'] = { ...params }; + this.router.navigate(['./', filter_params['params']], { + relativeTo: this.route, + replaceUrl: true, + }); + } + }) + } + + getTitle(row: RbacAuditOperationsModel): string { + return row.object_name; + } + +} diff --git a/web/src/app/admin/pattern.component.ts b/web/src/app/admin/pattern.component.ts index 3ccd32fa62..b0bb35e712 100644 --- a/web/src/app/admin/pattern.component.ts +++ b/web/src/app/admin/pattern.component.ts @@ -16,8 +16,7 @@ import { exhaustMap, filter, map, switchMap } from 'rxjs/operators'; import { BaseDirective } from '@adwp-ui/widgets'; import { ApiService } from '@app/core/api'; import { getProfileSelector, settingsSave, State } from '@app/core/store'; -import { IConfig } from '@app/shared/configuration/types'; -import { BaseEntity } from "../core/types"; +import { IConfig,ISettingsListResponse } from '@app/shared/configuration/types'; import { Observable } from "rxjs"; @Component({ @@ -58,6 +57,8 @@ export class PatternComponent extends BaseDirective implements OnInit, OnDestroy { url: 'groups', title: 'Groups' }, { url: 'roles', title: 'Roles' }, { url: 'policies', title: 'Policies' }, + { url: 'audit/operations', title: 'Audit operations' }, + { url: 'audit/logins', title: 'Audit logins' } ]; data = { '/admin': { title: 'Hi there!', crumbs: [{ path: '/admin/', name: 'intro' }] }, @@ -67,6 +68,8 @@ export class PatternComponent extends BaseDirective implements OnInit, OnDestroy '/admin/groups': { title: 'Group list', crumbs: [{ path: '/admin/groups', name: 'groups' }] }, '/admin/roles': { title: 'Role list', crumbs: [{ path: '/admin/roles', name: 'roles' }] }, '/admin/policies': { title: 'Policy list', crumbs: [{ path: '/admin/policies', name: 'policies' }] }, + '/admin/audit/operations': { title: 'Audit operations', crumbs: [{ path: '/admin/audit/operations', name: 'audit operations' }] }, + '/admin/audit/logins': { title: 'Audit logins', crumbs: [{ path: '/admin/audit/logins', name: 'audit logins' }] } }; constructor(private store: Store, private api: ApiService, private router: Router) { @@ -75,8 +78,8 @@ export class PatternComponent extends BaseDirective implements OnInit, OnDestroy ngOnInit() { this.actionsUrl$ = this.api.root.pipe( - switchMap((root) => this.api.get(root.adcm)), - map((adcm) => `/api/v1/adcm/${adcm[0].id}/action/`)); + switchMap((root) => this.api.get(root.adcm)), + map((adcm) => `/api/v1/adcm/${adcm.results[0]?.id}/action/`)); this.getContext(this.router.routerState.snapshot.url); diff --git a/web/src/app/admin/settings.component.ts b/web/src/app/admin/settings.component.ts index 8d001572c2..066a6e77b6 100644 --- a/web/src/app/admin/settings.component.ts +++ b/web/src/app/admin/settings.component.ts @@ -16,8 +16,8 @@ import { map, switchMap } from 'rxjs/operators'; import { ApiService } from '@app/core/api'; import { settingsSave, State } from '@app/core/store'; -import { BaseEntity } from '@app/core/types/api'; import { DynamicEvent } from '@app/shared/directives'; +import { ISettingsListResponse } from '@app/shared/configuration/types'; @Component({ selector: 'app-settings', @@ -31,8 +31,8 @@ export class SettingsComponent implements OnInit { ngOnInit() { this.set$ = this.api.root.pipe( - switchMap((root) => this.api.get(root.adcm)), - map((adcm) => adcm[0]), + switchMap((root) => this.api.get(root.adcm)), + map((adcm) => adcm.results[0]), ); } diff --git a/web/src/app/admin/users/users.component.html b/web/src/app/admin/users/users.component.html index 1dbd58f7cc..c9791a0b02 100644 --- a/web/src/app/admin/users/users.component.html +++ b/web/src/app/admin/users/users.component.html @@ -8,11 +8,11 @@ - + imple ] as IColumns; type: TypeName = 'user' - filteredData$: BehaviorSubject = new BehaviorSubject(null); + filterParams$: BehaviorSubject = new BehaviorSubject({}); userFilters: IFilter[] = [ { - id: 1, name: 'status', display_name: 'Status', filter_field: 'is_active', + id: 1, name: 'status', display_name: 'Status', filter_field: 'is_active', filter_type: 'list', options: [ {id: 1, name: 'active', display_name: 'Active', value: true}, {id: 2, name: 'inactive', display_name: 'Inactive', value: false}, ] }, { - id: 2, name: 'type', display_name: 'Type', filter_field: 'type', + id: 2, name: 'type', display_name: 'Type', filter_field: 'type', filter_type: 'list', options: [ {id: 1, name: 'local', display_name: 'Local', value: 'local'}, {id: 2, name: 'ldap', display_name: 'Ldap', value: 'ldap'}, @@ -104,6 +104,22 @@ export class UsersComponent extends RbacEntityListDirective imple super(service, store, route, router, dialog, entityService); } + ngOnInit() { + super.ngOnInit(); + + this.filterParams$.subscribe((params) => { + const filter_params = this.baseListDirective.listParams; + + if (filter_params) { + filter_params['params'] = { ...params }; + this.router.navigate(['./', filter_params['params']], { + relativeTo: this.route, + replaceUrl: true, + }); + } + }) + } + getTitle(row: RbacUserModel): string { return row.username; } diff --git a/web/src/app/components/cluster/host/cluster-host.component.ts b/web/src/app/components/cluster/host/cluster-host.component.ts index 5d2b71bfac..54b022468d 100644 --- a/web/src/app/components/cluster/host/cluster-host.component.ts +++ b/web/src/app/components/cluster/host/cluster-host.component.ts @@ -39,7 +39,7 @@ export class ClusterHostComponent extends ConcernListDirective { ListFactory.statusColumn(this), ListFactory.actionsButton(this), ListFactory.configColumn(this), - ListFactory.maintenanceModeColumn(this), + ListFactory.maintenanceModeColumn(this, 'host'), { type: 'buttons', className: 'list-control', diff --git a/web/src/app/components/cluster/services/services.component.ts b/web/src/app/components/cluster/services/services.component.ts index cde02cfe62..eb765f9afc 100644 --- a/web/src/app/components/cluster/services/services.component.ts +++ b/web/src/app/components/cluster/services/services.component.ts @@ -43,6 +43,7 @@ export class ServicesComponent extends ConcernListDirective { ListFactory.actionsButton(this), ListFactory.importColumn(this), ListFactory.configColumn(this), + ListFactory.maintenanceModeColumn(this, 'service'), ListFactory.deleteColumn(this), ] as IColumns; diff --git a/web/src/app/components/columns/download-button-column/download-button-column.component.html b/web/src/app/components/columns/download-button-column/download-button-column.component.html new file mode 100644 index 0000000000..67b426d28f --- /dev/null +++ b/web/src/app/components/columns/download-button-column/download-button-column.component.html @@ -0,0 +1,5 @@ +
+ +
diff --git a/web/src/app/components/columns/download-button-column/download-button-column.component.scss b/web/src/app/components/columns/download-button-column/download-button-column.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/web/src/app/components/columns/download-button-column/download-button-column.component.ts b/web/src/app/components/columns/download-button-column/download-button-column.component.ts new file mode 100644 index 0000000000..d28a9faf87 --- /dev/null +++ b/web/src/app/components/columns/download-button-column/download-button-column.component.ts @@ -0,0 +1,21 @@ +import { Component, OnInit } from '@angular/core'; +import { AuthService } from "@app/core/auth/auth.service"; + +@Component({ + selector: 'app-download-button-column', + templateUrl: './download-button-column.component.html', + styleUrls: ['./download-button-column.component.scss'] +}) +export class DownloadButtonColumnComponent implements OnInit { + + url: string; + tooltip: string; + + constructor(private auth: AuthService) {} + + ngOnInit(): void {} + + download() { + location.href = this.url; + } +} diff --git a/web/src/app/components/columns/history-column/history-column.component.html b/web/src/app/components/columns/history-column/history-column.component.html new file mode 100644 index 0000000000..e5e37f7581 --- /dev/null +++ b/web/src/app/components/columns/history-column/history-column.component.html @@ -0,0 +1,5 @@ +
+ +
diff --git a/web/src/app/components/columns/history-column/history-column.component.scss b/web/src/app/components/columns/history-column/history-column.component.scss new file mode 100644 index 0000000000..9cf5a8dea6 --- /dev/null +++ b/web/src/app/components/columns/history-column/history-column.component.scss @@ -0,0 +1,17 @@ +:host { + display: flex; + flex: 1; + width: 100px; + justify-content: center; +} + +.history-button { + display: flex; + width: 24px !important; + min-width: unset !important; + color: rgba(255, 255, 255, 0.3); +} + +.has-history { + color: #78909c !important; +} diff --git a/web/src/app/components/columns/history-column/history-column.component.ts b/web/src/app/components/columns/history-column/history-column.component.ts new file mode 100644 index 0000000000..97f7a68e9c --- /dev/null +++ b/web/src/app/components/columns/history-column/history-column.component.ts @@ -0,0 +1,55 @@ +import { Component, OnInit } from '@angular/core'; +import { MatDialog, MatDialogConfig } from "@angular/material/dialog"; +import { DialogComponent } from "@app/shared/components"; +import { + RbacAuditOperationsHistoryFormComponent +} from "@app/components/rbac/audit-operations-history-form/rbac-audit-operations-history-form.component"; + +@Component({ + selector: 'app-history-column', + templateUrl: './history-column.component.html', + styleUrls: ['./history-column.component.scss'] +}) +export class HistoryColumnComponent implements OnInit { + + row: any; + + constructor(private dialog: MatDialog) { } + + ngOnInit(): void {} + + hasChangesHistory() { + return Object.keys(this?.row?.object_changes)?.length !== 0; + } + + show(event) { + if (this.hasChangesHistory()) { + this.prepare(); + } + event.preventDefault(); + event.stopPropagation(); + } + + prepare(): void { + let dialogModel: MatDialogConfig + const maxWidth = '1000px'; + const width = '1000px'; + const title = 'Operation detail'; + + + dialogModel = { + width, + maxWidth, + data: { + title, + model: { + row: this.row, + }, + component: RbacAuditOperationsHistoryFormComponent, + controls: ['Cancel'], + }, + }; + + this.dialog.open(DialogComponent, dialogModel) + } +} diff --git a/web/src/app/components/columns/name-edit-column/name-edit-column-field.component.ts b/web/src/app/components/columns/name-edit-column/name-edit-column-field.component.ts new file mode 100644 index 0000000000..3454c93a6c --- /dev/null +++ b/web/src/app/components/columns/name-edit-column/name-edit-column-field.component.ts @@ -0,0 +1,65 @@ +// 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 { Component, Input, OnInit } from '@angular/core'; +import { BaseDirective } from '@adwp-ui/widgets'; +import { FormControl } from "@angular/forms"; +import { debounceTime } from "rxjs/operators"; +import { editColumnValues } from "@app/components/columns/name-edit-column/name-edit-column.component"; + +@Component({ + selector: 'name-edit-column-field', + template: ` + +
+ + {{ column_rules?.modal_placeholder }} + + Please enter a valid {{ column_rules?.entity_type }} name + +
+
+ `, + styles: [` + .form { + min-width: 150px; + max-width: 500px; + width: 100%; + } + + .full-width { + width: 100%; + } + `] +}) +export class NameEditColumnFieldComponent extends BaseDirective implements OnInit { + @Input() model: any; + + row: any; + form: FormControl; + column_rules: editColumnValues; + + ngOnInit() { + this.row = this.model.row; + this.form = this.model.form; + + this.form.valueChanges + .pipe(debounceTime(500)) + .subscribe(newValue => { + this.form.markAsTouched(); + this.form.setValue(newValue, {emitEvent: false}); + }); + } + + checkValidity() { + return this.form.invalid; + } +} diff --git a/web/src/app/components/columns/name-edit-column/name-edit-column.component.html b/web/src/app/components/columns/name-edit-column/name-edit-column.component.html new file mode 100644 index 0000000000..51b2a8e9fc --- /dev/null +++ b/web/src/app/components/columns/name-edit-column/name-edit-column.component.html @@ -0,0 +1,6 @@ +
{{ row[column.sort] }}
+
+ +
diff --git a/web/src/app/components/columns/name-edit-column/name-edit-column.component.scss b/web/src/app/components/columns/name-edit-column/name-edit-column.component.scss new file mode 100644 index 0000000000..a11495d09c --- /dev/null +++ b/web/src/app/components/columns/name-edit-column/name-edit-column.component.scss @@ -0,0 +1,27 @@ +:host { + display: flex; + flex-grow: 1; + align-items: center; + justify-content: space-between; + width: 50% +} + +.name { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.rename-button { + display: none; + margin-left: 10px; + width: 24px !important; + min-width: unset !important; +} + +.editIcon { + font-size: 20px !important; + height: 20px !important; + width: 20px !important; + vertical-align: text-bottom; +} diff --git a/web/src/app/components/columns/name-edit-column/name-edit-column.component.ts b/web/src/app/components/columns/name-edit-column/name-edit-column.component.ts new file mode 100644 index 0000000000..3491a69c68 --- /dev/null +++ b/web/src/app/components/columns/name-edit-column/name-edit-column.component.ts @@ -0,0 +1,102 @@ +import { Component, OnInit } from '@angular/core'; +import { AdwpCellComponent } from "@adwp-ui/widgets"; +import { MatDialog, MatDialogConfig } from "@angular/material/dialog"; +import { DialogComponent } from "@app/shared/components"; +import { NameEditColumnFieldComponent } from "@app/components/columns/name-edit-column/name-edit-column-field.component"; +import { filter } from "rxjs/operators"; +import { FormControl, Validators } from "@angular/forms"; +import { ListService } from "@app/shared/components/list/list.service"; + +export interface editColumnValues { + modal_placeholder: string; + entity_type: string; + regex: any; +} + +@Component({ + selector: 'app-name-edit-column', + templateUrl: './name-edit-column.component.html', + styleUrls: ['./name-edit-column.component.scss'] +}) +export class NameEditColumnComponent implements AdwpCellComponent, OnInit { + + row: any; + column: any; + form: FormControl; + entity: string; + + constructor(private dialog: MatDialog, protected service: ListService) {} + + ngOnInit() { + this.form = new FormControl(this.row[this.column.sort], + [ + Validators.required, + Validators.maxLength(253), + Validators.pattern(new RegExp(this.column?.column_rules?.regex)) + ]); + this.entity = this.column?.column_rules?.entity_type; + } + + isEditable() { + switch (this.entity) { + case 'cluster': + return this.row.state === 'created'; + case 'host': + return this.row.cluster_id === null && this.row.state === 'created'; + } + } + + rename(event) { + this.prepare(); + event.preventDefault(); + event.stopPropagation(); + } + + prepare(): void { + let dialogModel: MatDialogConfig + const maxWidth = '1400px'; + const width = '500px'; + const title = `Edit ${ this.entity }`; + + this.form.setValue(this.row[this.column.sort]); + + dialogModel = { + width, + maxWidth, + data: { + title, + model: { + row: this.row, + column: this.column.sort, + form: this.form, + column_rules: this.column?.column_rules + }, + component: NameEditColumnFieldComponent, + controls: ['Save', 'Cancel'], + disabled: this.getFormStatus, + }, + }; + + this.dialog + .open(DialogComponent, dialogModel) + .beforeClosed() + .pipe(filter((save) => save)) + .subscribe(() => { + this.service[`rename${this.titleCase(this.entity)}`](this.column.sort, this.form.value, this.row.id) + .subscribe((value) => { + if (value) { + const colName = this.column.sort; + this.row[colName] = value[colName]; + } + }); + }); + } + + getFormStatus = (value) => { + return value.form.invalid; + } + + titleCase(string){ + return string[0].toUpperCase() + string.slice(1).toLowerCase(); + } +} diff --git a/web/src/app/components/columns/wrapper-column/wrapper-column.component.html b/web/src/app/components/columns/wrapper-column/wrapper-column.component.html new file mode 100644 index 0000000000..221e12a371 --- /dev/null +++ b/web/src/app/components/columns/wrapper-column/wrapper-column.component.html @@ -0,0 +1 @@ +
{{ columnNameNested ? row[nestedColumnName[0]][nestedColumnName[1]] : row[columnName] }}
diff --git a/web/src/app/components/columns/wrapper-column/wrapper-column.component.scss b/web/src/app/components/columns/wrapper-column/wrapper-column.component.scss new file mode 100644 index 0000000000..f2e45c35ec --- /dev/null +++ b/web/src/app/components/columns/wrapper-column/wrapper-column.component.scss @@ -0,0 +1,25 @@ +:host { + display: flex; + flex-grow: 1; + align-items: center; + justify-content: space-between; + width: 50% +} + +.text-ellipsed { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.green { + color: #00e676; +} + +.orange { + color: #ff8c00; +} + +.red { + color: #ff8a80; +} diff --git a/web/src/app/components/columns/wrapper-column/wrapper-column.component.ts b/web/src/app/components/columns/wrapper-column/wrapper-column.component.ts new file mode 100644 index 0000000000..e9713082d7 --- /dev/null +++ b/web/src/app/components/columns/wrapper-column/wrapper-column.component.ts @@ -0,0 +1,63 @@ +import { Component, Input, OnInit } from '@angular/core'; + +/* +* Component wrapper for columns +* If you need to change color of column value - use type 'color' +* If you need to make sure that long names will be trimmed to the column size - use type 'text-substr' +*/ + +@Component({ + selector: 'app-wrapper-column', + templateUrl: './wrapper-column.component.html', + styleUrls: ['./wrapper-column.component.scss'] +}) +export class WrapperColumnComponent implements OnInit { + + @Input() type: string[]; + + row: any; + column: any; + customColumnName: string; + red: string[] = ['delete', 'fail', 'user not found']; + orange: string[] = ['update', 'denied', 'wrong password', 'account disabled']; + green: string[] = ['create', 'success']; + + constructor() { } + + get columnName(): string { + return this.customColumnName || this.column?.label?.toLowerCase()?.replace(' ', '_'); + } + + get nestedColumnName(): string[] { + return this.customColumnName?.split('/'); + } + + get columnNameNested(): boolean { + return this.customColumnName?.includes('/'); + } + + ngOnInit(): void {} + + getWrapperClass() { + return this.type.map(value => { + switch(value) { + case 'color': + return this.getColorClass(); + case 'text-substr': + return 'text-ellipsed'; + } + }).join(' '); + } + + getColorClass() { + const value = this.row[this.columnName]; + + if (this.red.includes(value)) { + return 'red'; + } else if (this.orange.includes(value)) { + return 'orange'; + } else if (this.green.includes(value)) { + return 'green'; + } + } +} diff --git a/web/src/app/components/host/host-list/host-list.component.scss b/web/src/app/components/host/host-list/host-list.component.scss new file mode 100644 index 0000000000..9f71ea3b81 --- /dev/null +++ b/web/src/app/components/host/host-list/host-list.component.scss @@ -0,0 +1,19 @@ +:host { + flex: 1; +} + +::ng-deep mat-row { + + &:hover { + + mat-cell { + + button { + + &.rename-button.editable { + display: flex !important; + } + } + } + } +} diff --git a/web/src/app/components/host/host-list/host-list.component.ts b/web/src/app/components/host/host-list/host-list.component.ts index 36cdd263da..59f7d8ef8c 100644 --- a/web/src/app/components/host/host-list/host-list.component.ts +++ b/web/src/app/components/host/host-list/host-list.component.ts @@ -39,7 +39,7 @@ import { ApiService } from '@app/core/api'; (changeSort)="onChangeSort($event)" >
`, - styles: [':host { flex: 1; }'], + styleUrls: ['./host-list.component.scss'], }) export class HostListComponent extends ConcernListDirective { @@ -98,7 +98,7 @@ export class HostListComponent extends ConcernListDirective { ListFactory.statusColumn(this), ListFactory.actionsButton(this), ListFactory.configColumn(this), - ListFactory.maintenanceModeColumn(this), + ListFactory.maintenanceModeColumn(this, 'host'), ListFactory.deleteColumn(this), ] as IColumns; @@ -114,5 +114,4 @@ export class HostListComponent extends ConcernListDirective { ) { super(service, store, route, router, dialog, concernService); } - } diff --git a/web/src/app/components/maintenance-mode-button/maintenance-mode-button.component.html b/web/src/app/components/maintenance-mode-button/maintenance-mode-button.component.html index b9b21d5bf8..5bef053669 100644 --- a/web/src/app/components/maintenance-mode-button/maintenance-mode-button.component.html +++ b/web/src/app/components/maintenance-mode-button/maintenance-mode-button.component.html @@ -1,9 +1,9 @@ -
+
-
+
- + + + + + + + `, styles: [` @@ -138,4 +145,8 @@ export class DialogComponent implements OnInit { _isUserInactive() { return this.data?.model?.value?.is_active === false; } + + _getDisabledValue() { + return typeof this.data?.disabled === 'function' ? this.data?.disabled(this.data?.model) : this.data?.disabled; + } } diff --git a/web/src/app/shared/components/list/base-list.directive.ts b/web/src/app/shared/components/list/base-list.directive.ts index 90d8c331d6..19453c2a72 100644 --- a/web/src/app/shared/components/list/base-list.directive.ts +++ b/web/src/app/shared/components/list/base-list.directive.ts @@ -9,7 +9,6 @@ // 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 { MatDialog } from '@angular/material/dialog'; import { ParamMap } from '@angular/router'; import { select, Store } from '@ngrx/store'; @@ -17,7 +16,6 @@ import { filter, mergeMap, switchMap, takeUntil, tap } from 'rxjs/operators'; import { IListResult } from '@adwp-ui/widgets'; import { Sort } from '@angular/material/sort'; import { Observable, Subject } from 'rxjs'; - import { clearMessages, EventMessage, getMessage, SocketState } from '@app/core/store'; import { Bundle, EmmitRow, Entities, Host as AdcmHost, TypeName } from '@app/core/types'; import { DialogComponent } from '@app/shared/components'; @@ -26,7 +24,6 @@ import { ListService } from './list.service'; import { ListDirective } from '@app/abstract-directives/list.directive'; import { ICluster } from '@app/models/cluster'; - const TemporaryEntityNameConverter = (currentName: Partial): string => { if (currentName === 'group_config') return 'group-config'; @@ -94,7 +91,6 @@ export class BaseListDirective { } routeListener(limit: number, page: number, ordering: string, params: ParamMap) { - this.parent.paginator.pageSize = limit; if (page === 0) { this.parent.paginator.firstPage(); @@ -117,7 +113,7 @@ export class BaseListDirective { this.takeUntil(), filter((p) => this.checkParam(p)) ) - .subscribe((p) => this.routeListener(+p.get('limit') || 10, +p.get('page'), p.get('ordering'), p)); + .subscribe((p) => this.routeListener(+p.get('limit'), +p.get('page'), p.get('ordering'), p)); } init(): void { @@ -139,9 +135,12 @@ export class BaseListDirective { checkParam(p: ParamMap): boolean { const listParamStr = localStorage.getItem('list:param'); - if (!p.keys.length && listParamStr) { + + if (!p?.keys?.length && listParamStr) { const json = JSON.parse(listParamStr); + if (json[this.typeName]) { + delete json[this.typeName]?.page; this.parent.router.navigate(['./', json[this.typeName]], { relativeTo: this.parent.route, replaceUrl: true, @@ -194,9 +193,14 @@ export class BaseListDirective { } } - refresh(id?: number) { + refresh(id?: number, filter_params?: ParamMap) { if (id) this.parent.current = { id }; - this.service.getList(this.listParams, this.typeName).subscribe((list: IListResult) => { + if (!filter_params) { + let ls = localStorage.getItem('list:param'); + let filters = ls ? JSON.parse(ls) : {}; + if (filters[this.typeName]) this.listParams['params'] = { ...this.listParams['params'], ...filters[this.typeName] } + } + this.service.getList(filter_params || this.listParams, this.typeName).subscribe((list: IListResult) => { if (this.reload) { this.reload(list); } @@ -224,9 +228,7 @@ export class BaseListDirective { onLoad() {} maintenanceModeToggle(row) { - this.service.setMaintenanceMode(row) - .pipe(this.takeUntil()) - .subscribe(() => {}); + this.service.setMaintenanceMode(row).subscribe(); } license() { diff --git a/web/src/app/shared/components/list/list.service.ts b/web/src/app/shared/components/list/list.service.ts index 22e6dac57d..8af1af6633 100644 --- a/web/src/app/shared/components/list/list.service.ts +++ b/web/src/app/shared/components/list/list.service.ts @@ -13,7 +13,6 @@ import { Injectable } from '@angular/core'; import { convertToParamMap, ParamMap, Params } from '@angular/router'; import { map, switchMap, tap } from 'rxjs/operators'; import { Observable } from 'rxjs'; - import { environment } from '@env/environment'; import { ApiService } from '@app/core/api'; import { ClusterService } from '@app/core/services/cluster.service'; @@ -48,8 +47,11 @@ export class ListService implements IListService { getList(p: ParamMap, typeName: TypeName): Observable> { const listParamStr = localStorage.getItem('list:param'); - if (p?.keys.length) { + + if (p?.keys?.length > 0) { const param = p.keys.reduce((a, c) => ({ ...a, [c]: p.get(c) }), {}); + delete param['page']; + if (listParamStr) { const json = JSON.parse(listParamStr); json[typeName] = param; @@ -80,6 +82,12 @@ export class ListService implements IListService { case 'policy': params = { ...params['params'], 'expand': 'child,role,user,group,object', 'built_in': 'false' }; return this.api.getList(`${environment.apiRoot}rbac/policy/`, convertToParamMap(params)); + case 'audit_operations': + params = { ...params['params'], 'expand': null }; + return this.api.getList(`${environment.apiRoot}audit/operation`, convertToParamMap(params)); + case 'audit_login': + params = { ...params['params'], 'expand': null }; + return this.api.getList(`${environment.apiRoot}audit/login`, convertToParamMap(params)); default: return this.api.root.pipe(switchMap((root) => this.api.getList(root[this.current.typeName], p))); } @@ -120,6 +128,14 @@ export class ListService implements IListService { } setMaintenanceMode(row: Entities) { - return this.api.patch(`/api/v1/host/${row.id}/`, { maintenance_mode: row['maintenance_mode'] }); + return this.api.post(`/api/v1/${row['type']}/${row.id}/maintenance-mode/`, { maintenance_mode: row['maintenance_mode'] }); + } + + renameHost(column: string, value: any, id: number) { + return this.api.patch(`/api/v1/host/${id}/`, { [column]: value }); + } + + renameCluster(column: string, value: any, id: number) { + return this.api.patch(`/api/v1/cluster/${id}/`, { [column]: value }); } } diff --git a/web/src/app/shared/configuration/configuration.module.ts b/web/src/app/shared/configuration/configuration.module.ts index a9a2a9a2a2..6e50647862 100644 --- a/web/src/app/shared/configuration/configuration.module.ts +++ b/web/src/app/shared/configuration/configuration.module.ts @@ -12,20 +12,7 @@ import { CommonModule } from '@angular/common'; import { NgModule } from '@angular/core'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; -import { MatButtonModule } from '@angular/material/button'; -import { MatCheckboxModule } from '@angular/material/checkbox'; -import { MatOptionModule } from '@angular/material/core'; -import { MatExpansionModule } from '@angular/material/expansion'; -import { MatFormFieldModule } from '@angular/material/form-field'; -import { MatIconModule } from '@angular/material/icon'; -import { MatInputModule } from '@angular/material/input'; -import { MatListModule } from '@angular/material/list'; -import { MatMenuModule } from "@angular/material/menu"; -import { MatSelectModule } from '@angular/material/select'; -import { MatSlideToggleModule } from '@angular/material/slide-toggle'; -import { MatToolbarModule } from '@angular/material/toolbar'; -import { MatTooltipModule } from '@angular/material/tooltip'; - +import { MaterialModule } from "@app/shared/material.module"; import { FormElementsModule } from '../form-elements/form-elements.module'; import { StuffModule } from '../stuff.module'; import { FieldService } from './services/field.service'; @@ -51,22 +38,7 @@ import { ConfigAttributeNames } from '@app/shared/configuration/attributes/attri import { GroupKeysWrapperComponent } from '@app/shared/configuration/attributes/attributes/group-keys/group-keys-wrapper.component'; import { FilterComponent } from "@app/shared/configuration/tools/filter/filter.component"; import { FilterListComponent } from "@app/shared/configuration/tools/filter/filter-list/filter-list.component"; - -const material = [ - MatIconModule, - MatInputModule, - MatButtonModule, - MatSelectModule, - MatOptionModule, - MatCheckboxModule, - MatTooltipModule, - MatToolbarModule, - MatFormFieldModule, - MatExpansionModule, - MatSlideToggleModule, - MatListModule, - MatMenuModule, -]; +import { ServerFilterComponent } from './tools/server-filter/server-filter.component'; @NgModule({ declarations: [ @@ -83,6 +55,7 @@ const material = [ SchemeComponent, RootComponent, ItemComponent, + ServerFilterComponent ], imports: [ CommonModule, @@ -90,7 +63,7 @@ const material = [ ReactiveFormsModule, StuffModule, FormElementsModule, - ...material, + MaterialModule, AdwpListModule, AddingModule, ConfigGroupModule, @@ -110,7 +83,7 @@ const material = [ } }), ], - exports: [ConfigComponent, ConfigFieldsComponent, FilterComponent, FilterListComponent], + exports: [ConfigComponent, ConfigFieldsComponent, FilterComponent, FilterListComponent, ServerFilterComponent], providers: [FieldService, YspecService, SchemeService, ConfigService], }) export class ConfigurationModule { diff --git a/web/src/app/shared/configuration/services/field.service.spec.ts b/web/src/app/shared/configuration/services/field.service.spec.ts index 238605eff9..9c01fd3394 100644 --- a/web/src/app/shared/configuration/services/field.service.spec.ts +++ b/web/src/app/shared/configuration/services/field.service.spec.ts @@ -368,6 +368,11 @@ describe('Configuration fields service', () => { const result2 = service.parseValue(output2, source); expect(result2).toEqual({ field: null }); + + const output3: IOutput = { field: { field1: [] } }; + + const result3 = service.parseValue(output3, source); + expect(result3).toEqual({ field: null }); }); it('parseValue for structure should return list', () => { diff --git a/web/src/app/shared/configuration/services/field.service.ts b/web/src/app/shared/configuration/services/field.service.ts index e5e2314cc1..1cc0e815be 100644 --- a/web/src/app/shared/configuration/services/field.service.ts +++ b/web/src/app/shared/configuration/services/field.service.ts @@ -313,7 +313,7 @@ export class FieldService { const runYspecParse = (v: any, f: Partial) => ((!v || !Object.keys(v).length) && !f.value ? f.value : this.runYspec(v, f.limits.rules)); - const replaceEmptyObjectWithNull = (v: any): string => ((Array.isArray(v) && v?.length === 0) || JSON.stringify(v) === '{}') ? null : v + const replaceEmptyObjectWithNull = (v: any): string => ((Array.isArray(v) && v?.length === 0) || JSON.stringify(v) === '{}' || this.emptyArrayInside(v)) ? null : v const runParse = (v: IOutput, parentName?: string): IOutput => { const runByValue = (p: IOutput, c: string) => { @@ -391,4 +391,11 @@ export class FieldService { return value; } + + emptyArrayInside(object: Object): boolean { + if (object) { + const keys = Object.keys(object); + return keys?.length === 1 && Array.isArray(object[keys[0]]) && object[keys[0]]?.length === 0; + } + } } diff --git a/web/src/app/shared/configuration/tools/filter/filter.component.html b/web/src/app/shared/configuration/tools/filter/filter.component.html new file mode 100644 index 0000000000..4622f6fbf5 --- /dev/null +++ b/web/src/app/shared/configuration/tools/filter/filter.component.html @@ -0,0 +1,48 @@ +
+
+ filter_list + +
+ +
+ + + + + + + {{ p.display_name }} + + + + + + + + {{ filter.display_name }} + + + + + + + Invalid start date + Invalid end date + + + + + + + + +
+
diff --git a/web/src/app/shared/configuration/tools/filter/filter.component.scss b/web/src/app/shared/configuration/tools/filter/filter.component.scss index e5a8962ab4..7939689e01 100644 --- a/web/src/app/shared/configuration/tools/filter/filter.component.scss +++ b/web/src/app/shared/configuration/tools/filter/filter.component.scss @@ -18,3 +18,34 @@ mat-form-field { margin: 0 10px; font-size: 14px; } + +::ng-deep .filter-field.datepicker { + & > .mat-form-field-wrapper { + & > .mat-form-field-underline { + display: none !important; + } + } + + .datepicker-form { + .mat-form-field-suffix { + left: -20px; + } + + .mat-form-field-infix { + width: 170px; + margin-right: 16px; + } + } +} + +::ng-deep .mat-calendar-body-in-range { + &:before { + background: #00e676 !important; + opacity: 0.3; + } +} + +::ng-deep .mat-calendar-body-selected { + color: #333 !important; + background: #00e676 !important; +} diff --git a/web/src/app/shared/configuration/tools/filter/filter.component.ts b/web/src/app/shared/configuration/tools/filter/filter.component.ts index 8300bb3338..61f1bc47e4 100644 --- a/web/src/app/shared/configuration/tools/filter/filter.component.ts +++ b/web/src/app/shared/configuration/tools/filter/filter.component.ts @@ -25,7 +25,8 @@ export interface IFilter { name: string, display_name: string, filter_field: string, - options: IFilterOption[], + filter_type: FilterType, + options?: IFilterOption[], active?: boolean, } @@ -36,44 +37,18 @@ export interface IFilterOption { value: any, } +type FilterType = 'list' | 'input' | 'datepicker'; + @Component({ selector: 'app-filter', - template: ` -
-
- filter_list - -
- -
- - - - - {{ p.display_name }} - - - - - - -
-
- `, + templateUrl: './filter.component.html', styleUrls: ['./filter.component.scss'], }) export class FilterComponent extends BaseDirective implements OnInit, OnDestroy { filterForm = new FormGroup({}); availableFilters: any[]; activeFilters: number[] = []; + filtersByType = {}; backupData: any; freezeBackupData: boolean = false; externalChanges: boolean = false; @@ -95,8 +70,13 @@ export class FilterComponent extends BaseDirective implements OnInit, OnDestroy name: filter.name, display_name: filter.display_name, filter_field: filter.filter_field, + filter_type: filter.filter_type })); + this.availableFilters.forEach((i: IFilter) => { + this.filtersByType[i.filter_field] = i.filter_type; + }) + this.externalData.subscribe((values: any) => { this.externalChanges = true; this.freezeBackupData = false; @@ -120,7 +100,9 @@ export class FilterComponent extends BaseDirective implements OnInit, OnDestroy } clear(filter, event: any) { - this.filterForm.get(filter).setValue(undefined); + if (this.filtersByType[filter] === 'datepicker') { + this.filterForm.get(filter).setValue({start: undefined, end: undefined}); + } else this.filterForm.get(filter).setValue(undefined); this.innerData.next(this.backupData); event.preventDefault(); event.stopPropagation(); @@ -132,30 +114,77 @@ export class FilterComponent extends BaseDirective implements OnInit, OnDestroy event.preventDefault(); } + setDate(event) { + if (event.value) { + event.value.setHours(23, 59, 59, 999); + this.applyFilters(); + } + } + applyFilters() { const filters = this.filterForm.value; - Object.keys(filters).forEach((f) => { + + if (Object.keys(filters).filter((f) => { if (filters[f] === '' || filters[f] === undefined) { delete filters[f]; - } - }); - const data = this.backupData?.results?.filter((item) => { + return false; + } else return true; + }).length === 0) { + this.innerData.next(this.backupData); + return; + } + + let data = this.backupData?.results?.filter((item) => { for (let key in filters) { - if (item[key] === undefined || item[key] !== filters[key]) { - return false; + if (this.filtersByType[key] === 'list') { + if (item[key] === undefined || item[key] !== filters[key]) { + return false; + } } } return true; }); + if (this.filters.some((f) => f.filter_type === 'input' && filters[f.filter_field])) { + data = data.filter((item) => { + return Object.keys(filters).filter((f) => this.filtersByType[f] === 'input').every((i) => { + if (i.includes('/')) { + let nestedKey = i.split('/'); + + if (item[nestedKey[0]][nestedKey[1]] !== undefined && + item[nestedKey[0]][nestedKey[1]] !== null && + item[nestedKey[0]][nestedKey[1]] !== '' && + item[nestedKey[0]][nestedKey[1]].toLowerCase().includes(filters[i].toLowerCase())) { + return true; + } + } else { + if (item[i] !== undefined && item[i] !== null && item[i] !== '' && item[i].toLowerCase().includes(filters[i].toLowerCase())) { + return true; + } + } + }) + }) + } + + if (this.filters.some((f) => f.filter_type === 'datepicker' && filters[f.filter_field].end)) { + data = data.filter((item) => { + return Object.keys(filters).filter((f) => this.filtersByType[f] === 'datepicker').every((i) => { + if (item[i] !== undefined && item[i] !== null && (filters[i].start < new Date(item[i]) && new Date(item[i]) < filters[i].end)) { + return true; + } + }) + }) + } + + let count = this.activeFilters.length === 0 ? this.backupData.count : data.count; this.freezeBackupData = true; - this.innerData.next({...this.backupData, count: data.length, results: data}); + this.innerData.next({...this.backupData, count, results: data}); } clearButtonVisible(field) { const value = this.filterForm?.getRawValue()[field]; - return value || (typeof value === 'boolean' && !value); + return this.filtersByType[field] !== 'datepicker' && (value || (typeof value === 'boolean' && !value)); } toggleFilters(filter) { @@ -164,7 +193,16 @@ export class FilterComponent extends BaseDirective implements OnInit, OnDestroy this.filterForm.removeControl(filter.filter_field); } else { this.activeFilters.push(filter.id); - this.filterForm.addControl(filter.filter_field, new FormControl('')) + if (filter.filter_type === 'datepicker') { + this.filterForm.addControl(filter.filter_field, new FormGroup({ + start: new FormControl(new Date()), + end: new FormControl(new Date()), + })); + } else this.filterForm.addControl(filter.filter_field, new FormControl('')) } } + + datepickerGroup(controlName): FormGroup { + return this.filterForm.get(controlName) as FormGroup; + } } diff --git a/web/src/app/shared/configuration/tools/server-filter/server-filter.component.scss b/web/src/app/shared/configuration/tools/server-filter/server-filter.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/web/src/app/shared/configuration/tools/server-filter/server-filter.component.ts b/web/src/app/shared/configuration/tools/server-filter/server-filter.component.ts new file mode 100644 index 0000000000..63af44a703 --- /dev/null +++ b/web/src/app/shared/configuration/tools/server-filter/server-filter.component.ts @@ -0,0 +1,159 @@ +import { Component, Input, OnInit } from '@angular/core'; +import { FilterComponent, IFilter } from "@app/shared/configuration/tools/filter/filter.component"; +import { BehaviorSubject } from "rxjs"; +import { FormControl, FormGroup } from "@angular/forms"; + +@Component({ + selector: 'app-server-filter', + templateUrl: '../filter/filter.component.html', + styleUrls: ['../filter/filter.component.scss'] +}) +export class ServerFilterComponent extends FilterComponent implements OnInit { + @Input() filterParams$: BehaviorSubject; + @Input() entity: string; + + filterArr: string[]; + + constructor() { + super(); + } + + ngOnInit() { + this.availableFilters = this.filterList.map((filter: IFilter) => ({ + id: filter.id, + name: filter.name, + display_name: filter.display_name, + filter_field: filter.filter_field, + filter_type: filter.filter_type + })); + + this.filterArr = this.filterList.map((filter: IFilter) => (filter.name)); + + this.availableFilters.forEach((i: IFilter) => { + this.filtersByType[i.filter_field] = i.filter_type; + }) + + const listParam = localStorage.getItem('list:param'); + + if (listParam) { + const json = JSON.parse(listParam); + + if (json[this.entity]) { + this.manageDatepickerValue(json); + Object.keys(json[this.entity]).forEach((name) => { + if (!this.filterArr.includes(name)) return; + + this.toggleFilters(this.availableFilters.find((f) => f.name === name)); + this.filterForm.get(name).setValue(json[this.entity][name]); + }); + } + + this.applyFilters(); + } + } + + applyFilters(): void { + const filters = Object.entries(this.filterForm.value).reduce((res, [filterKey, filterVal]) => { + if (filterVal === '' || filterVal === undefined) { + return res; + } + + if (this.filtersByType[filterKey] === 'datepicker' && filterVal['start'] && filterVal['end']) { + return { + ...res, + [`${filterKey}_after`]: filterVal['start'].toISOString(), + [`${filterKey}_before`]: filterVal['end'].toISOString() + } + } + + return { + ...res, + [filterKey]: filterVal + } + }, {}) + + this.localStorageUpdate(filters); + } + + toggleFilters(filter): void { + if (this.activeFilters.includes(filter?.id)) { + this.activeFilters = this.activeFilters.filter((f) => f !== filter?.id); + this.localStorageCleaning(filter); + this.filterForm.removeControl(filter?.filter_field); + } else if (filter) { + this.activeFilters.push(filter?.id); + if (filter?.filter_type === 'datepicker') { + this.filterForm.addControl(filter.filter_field, new FormGroup({ + start: new FormControl(new Date()), + end: new FormControl(new Date()), + })); + } else if (filter) this.filterForm.addControl(filter?.filter_field, new FormControl('')) + } + } + + clear(filter, event: any) { + if (this.filtersByType[filter] === 'datepicker') { + this.filterForm.get(filter).setValue({start: undefined, end: undefined}); + } else this.filterForm.get(filter).setValue(undefined); + + this.applyFilters(); + } + + removeFilter(filter, event) { + this.toggleFilters(filter); + this.applyFilters(); + } + + localStorageCleaning(filter): void { + const listParamStr = localStorage.getItem('list:param'); + + if (listParamStr) { + const json = JSON.parse(listParamStr); + + if (json[this.entity]) { + delete json[this.entity][filter.filter_field]; + + this.manageDatepickerValue(json, true); + + if (Object.keys(json[this.entity]).length === 0) { + delete json[this.entity]; + } + } + + if (Object.keys(json).length === 0) { + localStorage.removeItem('list:param'); + } else localStorage.setItem('list:param', JSON.stringify(json)); + } + } + + localStorageUpdate(filters) { + const listParamStr = localStorage.getItem('list:param'); + const json = listParamStr ? JSON.parse(listParamStr) : null; + + let listParam = { + ...json, + [this.entity]: { + limit: json?.[this.entity]?.limit || '', + filter: json?.[this.entity]?.filter || '', + ordering: json?.[this.entity]?.ordering || '', + ...filters, + } + } + + localStorage.setItem('list:param', JSON.stringify(listParam)); + this.filterParams$.next(listParam[this.entity]); + } + + manageDatepickerValue(json: Object, deleteMode?: boolean) { + Object.keys(json[this.entity]).filter((name) => name.includes('_after') || name.includes('_before')).forEach((date) => { + const dateProp = date.replace(/_after|_before/gi, ''); + const period = date.includes('_after') ? 'start' : 'end'; + + if (!deleteMode) { + json[this.entity][dateProp] = {...json[this.entity][dateProp], [period]: new Date(json[this.entity][date])}; + } + + delete json[this.entity][date]; + }) + } +} diff --git a/web/src/app/shared/configuration/types.ts b/web/src/app/shared/configuration/types.ts index 383d88a44a..e83d107354 100644 --- a/web/src/app/shared/configuration/types.ts +++ b/web/src/app/shared/configuration/types.ts @@ -11,6 +11,7 @@ // limitations under the License. import { IYspec } from './yspec/yspec.service'; import { TFormOptions } from './services/field.service'; +import { BaseEntity } from '@app/core/types'; export type stateType = 'created' | 'locked'; @@ -180,4 +181,11 @@ export interface IFieldOptions extends IFormOptions, ICanGroup { compare: ICompare[]; } +export interface ISettingsListResponse { + count: 1; + next: null; + previous: null; + results: BaseEntity[]; +} + //#endregion diff --git a/web/src/app/shared/details/left-menu-items/log-menu-item/log-menu-item.component.spec.ts b/web/src/app/shared/details/left-menu-items/log-menu-item/log-menu-item.component.spec.ts index f25ff74899..830902601d 100644 --- a/web/src/app/shared/details/left-menu-items/log-menu-item/log-menu-item.component.spec.ts +++ b/web/src/app/shared/details/left-menu-items/log-menu-item/log-menu-item.component.spec.ts @@ -1,6 +1,10 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { LogMenuItemComponent } from './log-menu-item.component'; +import { AuthService } from "@app/core/auth/auth.service"; +import { ApiService } from "@app/core/api"; +import { HttpClient, HttpHandler } from "@angular/common/http"; +import { Store, StoreModule } from "@ngrx/store"; describe('LogMenuItemComponent', () => { let component: LogMenuItemComponent; @@ -8,7 +12,9 @@ describe('LogMenuItemComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - declarations: [ LogMenuItemComponent ] + declarations: [ LogMenuItemComponent ], + imports: [ StoreModule.forRoot({}) ], + providers: [ AuthService, ApiService, HttpClient, HttpHandler, Store ] }) .compileComponents(); }); diff --git a/web/src/app/shared/details/left-menu-items/log-menu-item/log-menu-item.component.ts b/web/src/app/shared/details/left-menu-items/log-menu-item/log-menu-item.component.ts index 4b7d815e89..465182a2f6 100644 --- a/web/src/app/shared/details/left-menu-items/log-menu-item/log-menu-item.component.ts +++ b/web/src/app/shared/details/left-menu-items/log-menu-item/log-menu-item.component.ts @@ -2,6 +2,7 @@ import { Component } from '@angular/core'; import { MenuItemAbstractDirective } from '@app/abstract-directives/menu-item.abstract.directive'; import { BaseEntity, Job } from '@app/core/types'; +import { AuthService } from "@app/core/auth/auth.service"; @Component({ selector: 'app-log-menu-item', @@ -25,6 +26,10 @@ import { BaseEntity, Job } from '@app/core/types'; }) export class LogMenuItemComponent extends MenuItemAbstractDirective { + constructor(private auth: AuthService) { + super(); + } + download() { if (this.data?.logId) { const file = (this.entity as Job).log_files.find(job => job.id === this.data.logId); diff --git a/web/src/app/shared/details/subtitle.component.ts b/web/src/app/shared/details/subtitle.component.ts index 37ffe41d28..15e58cf3cd 100644 --- a/web/src/app/shared/details/subtitle.component.ts +++ b/web/src/app/shared/details/subtitle.component.ts @@ -21,8 +21,9 @@ import { IDetails } from '@app/models/details'; - {{ cur.prototype_display_name || cur.prototype_name }} - {{ cur.prototype_version }} + {{ cur.provider_name || '' }} + {{ cur.typeName === 'host' ? '' : cur.prototype_display_name || cur.prototype_name }} + {{ cur.typeName === 'host' ? '' : cur.prototype_version }} diff --git a/web/src/app/shared/material.module.ts b/web/src/app/shared/material.module.ts index 27f387ae98..c4ca99fc4d 100644 --- a/web/src/app/shared/material.module.ts +++ b/web/src/app/shared/material.module.ts @@ -36,6 +36,8 @@ import { MatTableModule } from '@angular/material/table'; import { MatToolbarModule } from '@angular/material/toolbar'; import { MatTooltipModule } from '@angular/material/tooltip'; import { MatButtonToggleModule } from '@angular/material/button-toggle'; +import { MatDatepickerModule } from "@angular/material/datepicker"; +import { MatNativeDateModule } from "@angular/material/core"; @NgModule({ exports: [ @@ -65,6 +67,8 @@ import { MatButtonToggleModule } from '@angular/material/button-toggle'; MatSortModule, MatSliderModule, MatButtonToggleModule, + MatDatepickerModule, + MatNativeDateModule, ], }) export class MaterialModule {} diff --git a/web/src/app/shared/shared.module.ts b/web/src/app/shared/shared.module.ts index 2d7109eab4..4c448214aa 100644 --- a/web/src/app/shared/shared.module.ts +++ b/web/src/app/shared/shared.module.ts @@ -15,7 +15,6 @@ import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { RouterModule } from '@angular/router'; import { TranslateModule } from '@ngx-translate/core'; import { MatTreeModule } from '@angular/material/tree'; - import { AddingModule } from './add-component/adding.module'; import { UpgradeMasterComponent } from "@app/shared/components/upgrades/master/master.component"; import { @@ -62,7 +61,14 @@ import { RbacRoleService } from '@app/services/rbac-role.service'; import { RbacPolicyService } from '@app/services/rbac-policy.service'; import { DynamicModule } from '@app/shared/directives/dynamic/dynamic.module'; import { RbacObjectCandidateService } from '@app/services/rbac-object-candidate.service'; -import {UpgradeMasterConfigComponent} from "@app/shared/components/upgrades/master/upgrade-master-config.component"; +import { UpgradeMasterConfigComponent } from "@app/shared/components/upgrades/master/upgrade-master-config.component"; +import { RbacAuditOperationsService } from "@app/services/rbac-audit-operations.service"; +import { HistoryColumnComponent } from "@app/components/columns/history-column/history-column.component"; +import { WrapperColumnComponent } from "@app/components/columns/wrapper-column/wrapper-column.component"; +import { + DownloadButtonColumnComponent +} from "@app/components/columns/download-button-column/download-button-column.component"; +import { RbacAuditLoginService } from "@app/services/rbac-audit-login.service"; @NgModule({ imports: [ @@ -112,6 +118,9 @@ import {UpgradeMasterConfigComponent} from "@app/shared/components/upgrades/mast StatusTreeComponent, EntityStatusToStatusTreePipe, StatusTreeLinkPipe, + HistoryColumnComponent, + WrapperColumnComponent, + DownloadButtonColumnComponent, ], exports: [ FormsModule, @@ -148,6 +157,9 @@ import {UpgradeMasterConfigComponent} from "@app/shared/components/upgrades/mast StatusTreeComponent, EntityStatusToStatusTreePipe, StatusTreeLinkPipe, + HistoryColumnComponent, + WrapperColumnComponent, + DownloadButtonColumnComponent, ], providers: [ JobService, @@ -156,8 +168,9 @@ import {UpgradeMasterConfigComponent} from "@app/shared/components/upgrades/mast RbacGroupService, RbacRoleService, RbacPolicyService, - RbacObjectCandidateService + RbacAuditOperationsService, + RbacAuditLoginService, + RbacObjectCandidateService, ], }) -export class SharedModule { -} +export class SharedModule {}