diff --git a/.dockerignore b/.dockerignore
index a04e5421e3..ec48d00c68 100644
--- a/.dockerignore
+++ b/.dockerignore
@@ -1,32 +1,12 @@
*/.git*
-*@tmp/
-.dockerignore
-.git*
-.git/
-.pytest_cache
-Dockerfile*
-Jenkinsfile
-Makefile
-README.md
-allure-*/
-build-adcm.sh
-ui-tests.sh
-func-tests.sh
-data/
-docs_adcm/
-go/Makefile
-go/pkg/
-go/src/
-html/
-latex/
-make_html.sh
-make_pdf.sh
-pdf/
-pylintrc
-requirements-test.txt
-runserver.sh
-server.sh
-tests/
-var/cluster.db
-web
+*
+!COPYRIGHT
+!LICENSE
+!conf
+!config.json
+!go/bin/runstatus
+!os
+!python
+!requirements*
!web/build_static.sh
+!wwwroot
diff --git a/.npmcheckignore b/.npmcheckignore
deleted file mode 100644
index 273e126243..0000000000
--- a/.npmcheckignore
+++ /dev/null
@@ -1,2 +0,0 @@
-# all expressions in one line separate by whitespace
-# example: @angular/* core-js
diff --git a/Dockerfile_base b/Dockerfile_base
deleted file mode 100644
index c1eefd1228..0000000000
--- a/Dockerfile_base
+++ /dev/null
@@ -1,14 +0,0 @@
-FROM python:3-alpine
-
-COPY requirements.txt /
-
-RUN apk add --update --no-cache --virtual .pynacl_deps build-base python3-dev libffi-dev openssl-dev linux-headers libxslt-dev \
-&& apk add --update --no-cache libffi openssl libxslt libstdc++ \
-&& apk add --update --no-cache nginx sshpass runit openssh-keygen openssh-client git dcron logrotate curl rsync \
-&& pip install --no-cache-dir -r /requirements.txt \
-&& rm /requirements.txt \
-&& apk del git \
-&& apk del .pynacl_deps \
-&& rm /etc/nginx/conf.d/default.conf
-
-CMD ["/bin/sh"]
diff --git a/Makefile b/Makefile
index 97b2640aaf..ef3057d5ef 100644
--- a/Makefile
+++ b/Makefile
@@ -1,7 +1,12 @@
# Set number of threads
BRANCH_NAME ?= $(shell git rev-parse --abbrev-ref HEAD)
-ADCMBASE_IMAGE ?= arenadata/adcmbase
-ADCMBASE_TAG ?= 20200812154141
+
+ADCMBASE_IMAGE ?= hub.arenadata.io/adcm/base
+ADCMBASE_TAG ?= 20210317134752
+
+APP_IMAGE ?= hub.adsw.io/adcm/adcm
+APP_TAG ?= $(subst /,_,$(BRANCH_NAME))
+
SELENOID_HOST ?= 10.92.2.65
SELENOID_PORT ?= 4444
@@ -29,11 +34,16 @@ buildjs: ## Build client side js/html/css in directory wwwroot
@docker run -i --rm -v $(CURDIR)/wwwroot:/wwwroot -v $(CURDIR)/web:/code -w /code node:12-alpine ./build.sh
buildbase: ## Build base image for ADCM's container. That is alpine with all packages.
- @docker build --pull=true -f Dockerfile_base --no-cache=true -t $(ADCMBASE_IMAGE):$$(date '+%Y%m%d%H%M%S') -t $(ADCMBASE_IMAGE):latest .
+ cd assemble/base && docker build --pull=true --no-cache=true \
+ -t $(ADCMBASE_IMAGE):$$(date '+%Y%m%d%H%M%S') -t $(ADCMBASE_IMAGE):latest \
+ .
build: describe buildss buildjs ## Build final docker image and all depended targets except baseimage.
- @docker build --no-cache=true --pull=true -t ci.arenadata.io/adcm:$(subst /,_,$(BRANCH_NAME)) .
-
+ @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) \
+ .
##################################################
# T E S T S
@@ -49,29 +59,29 @@ unittests: ## Run unittests
pytest: ## Run functional tests
docker pull ci.arenadata.io/functest:3.8.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 ADCM_TAG=$(subst /,_,$(BRANCH_NAME)) -e ADCMPATH=/adcm/ -e PYTHONPATH=${PYTHONPATH}:python/ \
+ -e BUILD_TAG=${BUILD_TAG} -e ADCMPATH=/adcm/ -e PYTHONPATH=${PYTHONPATH}:python/ \
-e SELENOID_HOST="${SELENOID_HOST}" -e SELENOID_PORT="${SELENOID_PORT}" \
- ci.arenadata.io/functest:3.8.6.slim.buster-x64 /bin/sh -e ./pytest.sh
+ ci.arenadata.io/functest:3.8.6.slim.buster-x64 /bin/sh -e \
+ ./pytest.sh --adcm-image='hub.adsw.io/adcm/adcm:$(subst /,_,$(BRANCH_NAME))'
pytest_release: ## Run functional tests on release
docker pull ci.arenadata.io/functest:3.8.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 -w /adcm/ \
- -e BUILD_TAG=${BUILD_TAG} -e ADCM_TAG=$(subst /,_,$(BRANCH_NAME)) -e ADCMPATH=/adcm/ -e PYTHONPATH=${PYTHONPATH}:python/ \
+ -e BUILD_TAG=${BUILD_TAG} -e ADCMPATH=/adcm/ -e PYTHONPATH=${PYTHONPATH}:python/ \
-e SELENOID_HOST="${SELENOID_HOST}" -e SELENOID_PORT="${SELENOID_PORT}" \
- ci.arenadata.io/functest:3.8.6.slim.buster.firefox-x64 /bin/sh -e ./pytest.sh --firefox
+ ci.arenadata.io/functest:3.8.6.slim.buster.firefox-x64 /bin/sh -e \
+ ./pytest.sh --firefox --adcm-image='hub.adsw.io/adcm/adcm:$(subst /,_,$(BRANCH_NAME))'
ng_tests: ## Run Angular tests
docker pull ci.arenadata.io/functest:3.8.6.slim.buster-x64
- docker run -i --rm -v $(CURDIR)/:/adcm -w /adcm/web/src ci.arenadata.io/functest:3.8.6.slim.buster-x64 /bin/sh -c "export CHROME_BIN=/usr/bin/google-chrome; npm install && ng test --watch=false"
+ docker run -i --rm -v $(CURDIR)/:/adcm -w /adcm/web ci.arenadata.io/functest:3.8.6.slim.buster-x64 ./ng_test.sh
linters : ## Run linters
docker pull ci.arenadata.io/pr-builder:3-x64
docker run -i --rm -v $(CURDIR)/:/source -w /source ci.arenadata.io/pr-builder:3-x64 /linters.sh shellcheck pylint pep8
npm_check: ## Run npm-check
- docker pull ci.arenadata.io/functest:3.8.6.slim.buster-x64
- docker run -i --rm -v $(CURDIR)/:/adcm -w /adcm/web/src ci.arenadata.io/functest:3.8.6.slim.buster-x64 \
- /bin/bash -c 'npm i --production && { ignore=`cat ../../.npmcheckignore | grep -v "#"`\; npm-check --production --skip-unused --ignore $$ignore || true; } && npm audit'
+ docker run -i --rm -v $(CURDIR)/wwwroot:/wwwroot -v $(CURDIR)/web:/code -w /code node:12-alpine ./npm_check.sh
django_tests : ## Run django tests.
docker pull $(ADCMBASE_IMAGE):$(ADCMBASE_TAG)
diff --git a/Dockerfile b/assemble/app/Dockerfile
similarity index 73%
rename from Dockerfile
rename to assemble/app/Dockerfile
index 3e6ce57a43..e12887be3e 100644
--- a/Dockerfile
+++ b/assemble/app/Dockerfile
@@ -1,8 +1,10 @@
-FROM arenadata/adcmbase:20200812154141
+ARG ADCMBASE_IMAGE
+ARG ADCMBASE_TAG
+FROM $ADCMBASE_IMAGE:$ADCMBASE_TAG
COPY . /adcm/
-RUN cp -r /adcm/os/* / && rm -rf /adcm/os; cp -r /adcm/python/ansible/* /usr/local/lib/python3.8/site-packages/ansible/ && rm -rf /adcm/python/ansible && rmdir /var/log/nginx;
+RUN cp -r /adcm/os/* / && rm -rf /adcm/os; cp -r /adcm/python/ansible/* /usr/local/lib/python3.?/site-packages/ansible/ && rm -rf /adcm/python/ansible && rmdir /var/log/nginx;
# Secret_key is mandatory for build_static procedure,
# but should not be hardcoded in the image.
diff --git a/assemble/base/Dockerfile b/assemble/base/Dockerfile
new file mode 100644
index 0000000000..0abf47ace7
--- /dev/null
+++ b/assemble/base/Dockerfile
@@ -0,0 +1,8 @@
+FROM python:3.9-alpine
+
+COPY requirements-base.txt /
+COPY build.sh /
+
+RUN /build.sh
+
+CMD ["/bin/sh"]
diff --git a/assemble/base/build.sh b/assemble/base/build.sh
new file mode 100755
index 0000000000..60c692a05a
--- /dev/null
+++ b/assemble/base/build.sh
@@ -0,0 +1,11 @@
+#!/usr/bin/env sh
+set -eu
+
+apk add --update --no-cache --virtual .pynacl_deps build-base python3-dev libffi-dev openssl-dev linux-headers libxslt-dev rust cargo
+apk add --update --no-cache libffi openssl libxslt libstdc++
+apk add --update --no-cache nginx sshpass runit openssh-keygen openssh-client git dcron logrotate curl rsync
+pip install --no-cache-dir -r /requirements-base.txt
+rm /requirements-base.txt
+apk del git
+apk del .pynacl_deps
+rm /etc/nginx/conf.d/default.conf
diff --git a/assemble/base/requirements-base.txt b/assemble/base/requirements-base.txt
new file mode 100644
index 0000000000..d3329b893f
--- /dev/null
+++ b/assemble/base/requirements-base.txt
@@ -0,0 +1,26 @@
+# ansible==2.8.3
+git+https://github.com/arenadata/ansible.git@v2.8.8-p4
+coreapi
+django
+django-cors-headers
+django-filter
+django-rest-swagger
+djangorestframework
+django-background-tasks
+social-auth-app-django
+git+git://github.com/arenadata/django-generate-secret-key.git
+jinja2
+markdown
+mitogen
+pyyaml
+ruyaml
+toml
+uwsgi
+version_utils
+ruyaml
+yspec
+# Custom bundle libs
+apache-libcloud
+jmespath
+lxml
+pycrypto
diff --git a/conf/adcm/config.yaml b/conf/adcm/config.yaml
index 4c7aa8d1be..6541fc5fd5 100644
--- a/conf/adcm/config.yaml
+++ b/conf/adcm/config.yaml
@@ -2,19 +2,13 @@
type: adcm
name: ADCM
- version: 1.2
+ version: 1.3
config:
- name: "global"
display_name: "Global Options"
type: "group"
subs:
- - name: "send_stats"
- display_name: "Send Anonymous Statistics"
- description: |
- We will send anonymous statistic about number of bundles your use and number of hosts and clusters, but without any config or names.
- type: boolean
- default: true
- name: "adcm_url"
display_name: "ADCM's URL"
description: |
diff --git a/config.json b/config.json
index da1abc3aaa..a1de37fea3 100644
--- a/config.json
+++ b/config.json
@@ -1,4 +1,4 @@
{
- "version": "2020.04.24.11",
- "commit_id": "c827ccf"
+ "version": "2021.05.12.14",
+ "commit_id": "20b2b8b3"
}
diff --git a/data/download/adb.2.6.tar b/data/download/adb.2.6.tar
index eb35425f1b..ea27596189 100644
Binary files a/data/download/adb.2.6.tar and b/data/download/adb.2.6.tar differ
diff --git a/os/etc/nginx/conf.d/adcm.conf b/os/etc/nginx/http.d/adcm.conf
similarity index 100%
rename from os/etc/nginx/conf.d/adcm.conf
rename to os/etc/nginx/http.d/adcm.conf
diff --git a/pylintrc b/pylintrc
index ad84e6006e..8808e493ff 100644
--- a/pylintrc
+++ b/pylintrc
@@ -51,7 +51,7 @@ confidence=
# no Warning level messages displayed, use"--disable=all --enable=classes
# --disable=W"
-disable=missing-docstring,invalid-name,no-self-use,abstract-method,unused-argument,no-else-return,duplicate-code,fixme,no-member,too-few-public-methods,wrong-import-order,useless-import-alias
+disable=missing-docstring,invalid-name,no-self-use,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
diff --git a/pytest.sh b/pytest.sh
index f8a5182561..5218126b77 100755
--- a/pytest.sh
+++ b/pytest.sh
@@ -18,9 +18,9 @@ pip3 install -r requirements-test.txt
find . -name "*.pyc" -type f -delete
find . -name "__pycache__" -type d -delete
{ # try
- pytest tests/ui_tests tests/functional -s -v -n auto --maxfail 30 \
- --showlocals --alluredir ./allure-results/ --durations=20 \
- --reruns 2 --remote-executor-host "$SELENOID_HOST" --timeout=360 "$@" &&
+ pytest tests/functional tests/ui_tests -s -v -n auto --maxfail 30 \
+ --showlocals --alluredir ./allure-results/ --durations=20 -p allure_pytest \
+ --reruns 2 --remote-executor-host "$SELENOID_HOST" --timeout=1080 "$@" &&
chmod -R o+xw allure-results
} || { # catch
chmod -R o+xw allure-results
diff --git a/python/ansible/plugins/action/adcm_config.py b/python/ansible/plugins/action/adcm_config.py
index 4b9017f472..5ff865bebd 100644
--- a/python/ansible/plugins/action/adcm_config.py
+++ b/python/ansible/plugins/action/adcm_config.py
@@ -19,8 +19,16 @@
sys.path.append('/adcm/python')
import adcm.init_django
-from cm.ansible_plugin import ContextActionModule
-import cm.adcm_config
+from cm.ansible_plugin import (
+ ContextActionModule,
+ set_cluster_config,
+ set_service_config,
+ set_service_config_by_id,
+ set_host_config,
+ set_provider_config,
+ set_component_config,
+ set_component_config_by_name,
+)
ANSIBLE_METADATA = {'metadata_version': '1.1', 'supported_by': 'Arenadata'}
@@ -83,12 +91,12 @@
class ActionModule(ContextActionModule):
- _VALID_ARGS = frozenset(('type', 'key', 'value', 'service_name', 'host_id'))
+ _VALID_ARGS = frozenset(('type', 'key', 'value', 'service_name', 'component_name', 'host_id'))
_MANDATORY_ARGS = ('type', 'key', 'value')
def _do_cluster(self, task_vars, context):
res = self._wrap_call(
- cm.adcm_config.set_cluster_config,
+ set_cluster_config,
context['cluster_id'],
self._task.args["key"],
self._task.args["value"]
@@ -98,7 +106,7 @@ def _do_cluster(self, task_vars, context):
def _do_service_by_name(self, task_vars, context):
res = self._wrap_call(
- cm.adcm_config.set_service_config,
+ set_service_config,
context['cluster_id'],
self._task.args["service_name"],
self._task.args["key"],
@@ -109,7 +117,7 @@ def _do_service_by_name(self, task_vars, context):
def _do_service(self, task_vars, context):
res = self._wrap_call(
- cm.adcm_config.set_service_config_by_id,
+ set_service_config_by_id,
context['cluster_id'],
context['service_id'],
self._task.args["key"],
@@ -120,7 +128,7 @@ def _do_service(self, task_vars, context):
def _do_host(self, task_vars, context):
res = self._wrap_call(
- cm.adcm_config.set_host_config,
+ set_host_config,
context['host_id'],
self._task.args["key"],
self._task.args["value"]
@@ -131,7 +139,7 @@ def _do_host(self, task_vars, context):
def _do_host_from_provider(self, task_vars, context):
# TODO: Check that host is in provider
res = self._wrap_call(
- cm.adcm_config.set_host_config,
+ set_host_config,
self._task.args['host_id'],
self._task.args["key"],
self._task.args["value"]
@@ -141,10 +149,33 @@ def _do_host_from_provider(self, task_vars, context):
def _do_provider(self, task_vars, context):
res = self._wrap_call(
- cm.adcm_config.set_provider_config,
+ set_provider_config,
context['provider_id'],
self._task.args["key"],
self._task.args["value"]
)
res['value'] = self._task.args["value"]
return res
+
+ def _do_component_by_name(self, task_vars, context):
+ res = self._wrap_call(
+ set_component_config_by_name,
+ context['cluster_id'],
+ context['service_id'],
+ self._task.args['component_name'],
+ self._task.args.get('service_name', None),
+ self._task.args['key'],
+ self._task.args['value']
+ )
+ res['value'] = self._task.args['value']
+ return res
+
+ def _do_component(self, task_vars, context):
+ res = self._wrap_call(
+ set_component_config,
+ context['component_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_hc.py b/python/ansible/plugins/action/adcm_hc.py
index f771b3dbbf..708205d47b 100644
--- a/python/ansible/plugins/action/adcm_hc.py
+++ b/python/ansible/plugins/action/adcm_hc.py
@@ -54,8 +54,7 @@
sys.path.append('/adcm/python')
import adcm.init_django
-import cm.api
-from cm.ansible_plugin import get_object_id_from_context
+from cm.ansible_plugin import get_object_id_from_context, change_hc
from cm.errors import AdcmEx
from cm.logger import log
@@ -87,7 +86,7 @@ def run(self, tmp=None, task_vars=None):
raise AnsibleError('Invalid operation arguments: %s' % op)
try:
- cm.api.change_hc(job_id, cluster_id, ops)
+ change_hc(job_id, cluster_id, ops)
except AdcmEx as e:
raise AnsibleError(e.code + ": " + e.msg) from e
diff --git a/python/ansible/plugins/action/adcm_state.py b/python/ansible/plugins/action/adcm_state.py
index e4e7aa67f0..c177a945b2 100644
--- a/python/ansible/plugins/action/adcm_state.py
+++ b/python/ansible/plugins/action/adcm_state.py
@@ -18,13 +18,15 @@
sys.path.append('/adcm/python')
import adcm.init_django
-from cm.ansible_plugin import ContextActionModule
-from cm.api import (
+from cm.ansible_plugin import (
+ ContextActionModule,
set_cluster_state,
set_host_state,
set_service_state,
set_service_state_by_id,
- set_provider_state
+ set_provider_state,
+ set_component_state_by_name,
+ set_component_state
)
from cm.status_api import Event
@@ -85,7 +87,7 @@
class ActionModule(ContextActionModule):
TRANSFERS_FILES = False
- _VALID_ARGS = frozenset(('type', 'service_name', 'state', 'host_id'))
+ _VALID_ARGS = frozenset(('type', 'service_name', 'component_name', 'state', 'host_id'))
_MANDATORY_ARGS = ('type', 'state')
def _do_cluster(self, task_vars, context):
@@ -146,3 +148,24 @@ def _do_provider(self, task_vars, context):
event.send_state()
res['state'] = self._task.args["state"]
return res
+
+ def _do_component_by_name(self, task_vars, context):
+ res = self._wrap_call(
+ set_component_state_by_name,
+ context['cluster_id'],
+ context['service_id'],
+ self._task.args['component_name'],
+ self._task.args.get('service_name', None),
+ self._task.args['state']
+ )
+ res['state'] = self._task.args['state']
+ return res
+
+ def _do_component(self, task_vars, context):
+ res = self._wrap_call(
+ set_component_state,
+ context['component_id'],
+ self._task.args['state'],
+ )
+ res['state'] = self._task.args['state']
+ return res
diff --git a/python/ansible/plugins/lookup/adcm_config.py b/python/ansible/plugins/lookup/adcm_config.py
index da5e0a6323..4ee387b175 100644
--- a/python/ansible/plugins/lookup/adcm_config.py
+++ b/python/ansible/plugins/lookup/adcm_config.py
@@ -25,8 +25,14 @@
sys.path.append('/adcm/python')
import adcm.init_django
-import cm.adcm_config
from cm.logger import log
+from cm.ansible_plugin import (
+ set_service_config,
+ set_service_config_by_id,
+ set_cluster_config,
+ set_provider_config,
+ set_host_config,
+)
DOCUMENTATION = """
@@ -77,11 +83,9 @@ def run(self, terms, variables=None, **kwargs): # pylint: disable=too-many-bra
raise AnsibleError('there is no cluster in hostvars')
cluster = variables['cluster']
if 'service_name' in kwargs:
- res = cm.adcm_config.set_service_config(
- cluster['id'], kwargs['service_name'], terms[1], terms[2]
- )
+ res = set_service_config(cluster['id'], kwargs['service_name'], terms[1], terms[2])
elif 'job' in variables and 'service_id' in variables['job']:
- res = cm.adcm_config.set_service_config_by_id(
+ res = set_service_config_by_id(
cluster['id'], variables['job']['service_id'], terms[1], terms[2]
)
else:
@@ -91,16 +95,16 @@ def run(self, terms, variables=None, **kwargs): # pylint: disable=too-many-bra
if 'cluster' not in variables:
raise AnsibleError('there is no cluster in hostvars')
cluster = variables['cluster']
- res = cm.adcm_config.set_cluster_config(cluster['id'], terms[1], terms[2])
+ res = set_cluster_config(cluster['id'], terms[1], terms[2])
elif terms[0] == 'provider':
if 'provider' not in variables:
raise AnsibleError('there is no host provider in hostvars')
provider = variables['provider']
- res = cm.adcm_config.set_provider_config(provider['id'], terms[1], terms[2])
+ res = set_provider_config(provider['id'], terms[1], terms[2])
elif terms[0] == 'host':
if 'adcm_hostid' not in variables:
raise AnsibleError('there is no adcm_hostid in hostvars')
- res = cm.adcm_config.set_host_config(variables['adcm_hostid'], terms[1], terms[2])
+ res = set_host_config(variables['adcm_hostid'], terms[1], terms[2])
else:
raise AnsibleError('unknown object type: %s' % terms[0])
diff --git a/python/ansible/plugins/lookup/adcm_state.py b/python/ansible/plugins/lookup/adcm_state.py
index ba79415bfb..83da81ebe9 100644
--- a/python/ansible/plugins/lookup/adcm_state.py
+++ b/python/ansible/plugins/lookup/adcm_state.py
@@ -25,10 +25,16 @@
import sys
sys.path.append('/adcm/python')
import adcm.init_django
-import cm.api
import cm.status_api
from cm.logger import log
from cm.status_api import Event
+from cm.ansible_plugin import (
+ set_service_state,
+ set_service_state_by_id,
+ set_cluster_state,
+ set_provider_state,
+ set_host_state
+)
DOCUMENTATION = """
@@ -79,11 +85,9 @@ def run(self, terms, variables=None, **kwargs): # pylint: disable=too-many-bra
raise AnsibleError('there is no cluster in hostvars')
cluster = variables['cluster']
if 'service_name' in kwargs:
- res = cm.api.set_service_state(
- cluster['id'], kwargs['service_name'], terms[1]
- )
+ res = set_service_state(cluster['id'], kwargs['service_name'], terms[1])
elif 'job' in variables and 'service_id' in variables['job']:
- res = cm.api.set_service_state_by_id(
+ res = set_service_state_by_id(
cluster['id'], variables['job']['service_id'], terms[1]
)
else:
@@ -93,16 +97,16 @@ def run(self, terms, variables=None, **kwargs): # pylint: disable=too-many-bra
if 'cluster' not in variables:
raise AnsibleError('there is no cluster in hostvars')
cluster = variables['cluster']
- res = cm.api.set_cluster_state(cluster['id'], terms[1])
+ res = set_cluster_state(cluster['id'], terms[1])
elif terms[0] == 'provider':
if 'provider' not in variables:
raise AnsibleError('there is no provider in hostvars')
provider = variables['provider']
- res = cm.api.set_provider_state(provider['id'], terms[1], event)
+ res = set_provider_state(provider['id'], terms[1], event)
elif terms[0] == 'host':
if 'adcm_hostid' not in variables:
raise AnsibleError('there is no adcm_hostid in hostvars')
- res = cm.api.set_host_state(variables['adcm_hostid'], terms[1])
+ res = set_host_state(variables['adcm_hostid'], terms[1])
else:
raise AnsibleError('unknown object type: %s' % terms[0])
event.send_state()
diff --git a/python/api/action/serializers.py b/python/api/action/serializers.py
index 5b707980da..e4512a6f5e 100644
--- a/python/api/action/serializers.py
+++ b/python/api/action/serializers.py
@@ -30,6 +30,17 @@ def get_url(self, obj, view_name, request, 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:
+ 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()
@@ -46,10 +57,11 @@ class StackActionSerializer(serializers.Serializer):
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)
class ActionSerializer(StackActionSerializer):
- url = ActionDetailURL(read_only=True, view_name='object-action-details')
+ url = HostActionDetailURL(read_only=True, view_name='object-action-details')
class ActionShort(serializers.Serializer):
@@ -100,4 +112,16 @@ def get_subs(self, obj):
class ActionDetailSerializer(StackActionDetailSerializer):
- run = ActionDetailURL(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)
+ conf = ConfigSerializerUI(action_conf, many=True, context=self.context, read_only=True)
+ return {'attr': attr, 'config': conf.data}
diff --git a/python/api/action/views.py b/python/api/action/views.py
index 3dcf3dea1e..8105dc4e45 100644
--- a/python/api/action/views.py
+++ b/python/api/action/views.py
@@ -10,38 +10,18 @@
# See the License for the specific language governing permissions and
# limitations under the License.
-from django.db import models
from rest_framework.response import Response
from api.api_views import (
- ListView, GenericAPIPermView, ActionFilter, create, check_obj, filter_actions
+ ListView, DetailViewRO, GenericAPIPermView, ActionFilter, create, check_obj, filter_actions
+)
+from api.job.serializers import RunTaskSerializer
+from cm.models import (
+ Host, ClusterObject, ServiceComponent, Action, TaskLog, HostComponent, get_model_by_type
)
-from api.job_serial import RunTaskSerializer
-from cm.errors import AdcmApiEx
-from cm.models import ADCM, Cluster, HostProvider, Host, ClusterObject, ServiceComponent
-from cm.models import Action, TaskLog
from . import serializers
-def get_action_objects(object_type):
- if object_type == 'adcm':
- return ADCM.objects.all()
- if object_type == 'cluster':
- return Cluster.objects.all()
- elif object_type == 'provider':
- return HostProvider.objects.all()
- elif object_type == 'service':
- return ClusterObject.objects.all()
- elif object_type == 'component':
- return ServiceComponent.objects.all()
- elif object_type == 'host':
- return Host.objects.all()
- else:
- # This function should return a QuerySet, this is necessary for the correct
- # construction of the schema.
- return Cluster.objects.all()
-
-
def get_object_type_id(**kwargs):
object_type = kwargs.get('object_type')
object_id = kwargs.get(f'{object_type}_id')
@@ -51,67 +31,95 @@ def get_object_type_id(**kwargs):
def get_obj(**kwargs):
object_type, object_id, action_id = get_object_type_id(**kwargs)
- objects = get_action_objects(object_type)
- try:
- obj = objects.get(id=object_id)
- except models.ObjectDoesNotExist:
- errors = {
- 'adcm': 'ADCM_NOT_FOUND',
- 'cluster': 'CLUSTER_NOT_FOUND',
- 'provider': 'PROVIDER_NOT_FOUND',
- 'host': 'HOST_NOT_FOUND',
- 'service': 'SERVICE_NOT_FOUND',
- 'component': 'COMPONENT_NOT_FOUND',
- }
- raise AdcmApiEx(errors[object_type]) from None
+ model = get_model_by_type(object_type)
+ obj = model.obj.get(id=object_id)
return obj, action_id
-def get_selector(obj):
+def get_selector(obj, action):
selector = {obj.prototype.type: obj.id}
if obj.prototype.type == 'service':
selector['cluster'] = obj.cluster.id
if obj.prototype.type == 'component':
selector['cluster'] = obj.cluster.id
- selector['component'] = obj.service.id
+ selector['service'] = obj.service.id
+ if isinstance(obj, Host) and action.host_action:
+ if action.prototype.type == 'component':
+ component = ServiceComponent.obj.get(prototype=action.prototype)
+ selector['component'] = component.id
+ if action.prototype.type == 'service':
+ service = ClusterObject.obj.get(prototype=action.prototype)
+ selector['service'] = service.id
+ if obj.cluster is not None:
+ selector['cluster'] = obj.cluster.id
return selector
class ActionList(ListView):
queryset = Action.objects.all()
serializer_class = serializers.ActionSerializer
- serializer_class_ui = serializers.ActionDetailSerializer
+ serializer_class_ui = serializers.ActionUISerializer
filterset_class = ActionFilter
filterset_fields = ('name', 'button', 'button_is_null')
- def get(self, request, *args, **kwargs):
+ def get(self, request, *args, **kwargs): # pylint: disable=too-many-locals
"""
List all actions of a specified object
"""
- obj, _ = get_obj(**kwargs)
- actions = filter_actions(obj, self.filter_queryset(
- self.get_queryset().filter(prototype=obj.prototype)
- ))
+ if kwargs['object_type'] == 'host':
+ host, _ = get_obj(object_type='host', host_id=kwargs['host_id'])
+ actions = set(filter_actions(host, self.filter_queryset(
+ self.get_queryset().filter(prototype=host.prototype)
+ )))
+ obj = host
+ objects = {'host': host}
+ hcs = HostComponent.objects.filter(host_id=kwargs['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)
+ for obj in [cluster, service, component]:
+ actions.update(filter_actions(obj, self.filter_queryset(
+ self.get_queryset().filter(
+ prototype=obj.prototype, host_action=True))))
+ else:
+ if host.cluster is not None:
+ actions.update(filter_actions(host.cluster, self.filter_queryset(
+ self.get_queryset().filter(
+ prototype=host.cluster.prototype, host_action=True))))
+ else:
+ obj, _ = get_obj(**kwargs)
+ actions = filter_actions(obj, self.filter_queryset(
+ self.get_queryset().filter(prototype=obj.prototype, host_action=False)
+ ))
+ objects = {obj.prototype.type: obj}
serializer_class = self.select_serializer(request)
serializer = serializer_class(
- actions, many=True, context={'request': request, 'object': obj}
+ actions, many=True, context={'request': request, 'objects': objects, 'obj': obj}
)
return Response(serializer.data)
-class ActionDetail(GenericAPIPermView):
+class ActionDetail(DetailViewRO):
queryset = Action.objects.all()
serializer_class = serializers.ActionDetailSerializer
+ serializer_class_ui = serializers.ActionUISerializer
def get(self, request, *args, **kwargs):
"""
Show specified action
"""
obj, action_id = get_obj(**kwargs)
- action = check_obj(
- Action, {'prototype': obj.prototype, 'id': action_id}, 'ACTION_NOT_FOUND'
+ action = check_obj(Action, {'id': action_id}, 'ACTION_NOT_FOUND')
+ if isinstance(obj, Host) and action.host_action:
+ objects = {'host': obj}
+ else:
+ objects = {action.prototype.type: obj}
+ serializer_class = self.select_serializer(request)
+ serializer = serializer_class(
+ action, context={'request': request, 'objects': objects, 'obj': obj}
)
- serializer = self.serializer_class(action, context={'request': request, 'object': obj})
return Response(serializer.data)
@@ -124,9 +132,7 @@ def post(self, request, *args, **kwargs):
Ran specified action
"""
obj, action_id = get_obj(**kwargs)
- selector = get_selector(obj)
- action = check_obj(
- Action, {'prototype': obj.prototype, 'id': action_id}, 'ACTION_NOT_FOUND'
- )
+ action = check_obj(Action, {'id': action_id}, 'ACTION_NOT_FOUND')
+ selector = get_selector(obj, action)
serializer = self.serializer_class(data=request.data, context={'request': request})
return create(serializer, action_id=action.id, selector=selector)
diff --git a/tests/functional/test_stacks_data/toml_parser_error/__init__.py b/python/api/adcm/__init__.py
similarity index 100%
rename from tests/functional/test_stacks_data/toml_parser_error/__init__.py
rename to python/api/adcm/__init__.py
diff --git a/python/api/adcm/serializers.py b/python/api/adcm/serializers.py
new file mode 100644
index 0000000000..82fafced8b
--- /dev/null
+++ b/python/api/adcm/serializers.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 rest_framework import serializers
+from api.api_views import hlink
+from api.api_views import CommonAPIURL
+
+
+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 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')
+
+ def get_prototype_version(self, obj):
+ return obj.prototype.version
diff --git a/python/api/adcm/urls.py b/python/api/adcm/urls.py
new file mode 100644
index 0000000000..fde536eb7d
--- /dev/null
+++ b/python/api/adcm/urls.py
@@ -0,0 +1,25 @@
+# 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 path, include
+from . import views
+
+
+urlpatterns = [
+ path('', views.AdcmList.as_view(), name='adcm'),
+ path('/', include([
+ path('', views.AdcmDetail.as_view(), name='adcm-details'),
+ path('config/', include('api.config.urls'), {'object_type': 'adcm'}),
+ path('action/', include('api.action.urls'), {'object_type': 'adcm'}),
+ ])),
+]
diff --git a/python/api/adcm/views.py b/python/api/adcm/views.py
new file mode 100644
index 0000000000..4355db6abf
--- /dev/null
+++ b/python/api/adcm/views.py
@@ -0,0 +1,37 @@
+# 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 cm.models import ADCM
+from api.api_views import DetailViewRO, ListView
+from . import serializers
+
+
+class AdcmList(ListView):
+ """
+ get:
+ List adcm object
+ """
+ queryset = ADCM.objects.all()
+ serializer_class = serializers.AdcmSerializer
+ serializer_class_ui = serializers.AdcmDetailSerializer
+
+
+class AdcmDetail(DetailViewRO):
+ """
+ get:
+ Show adcm object
+ """
+ queryset = ADCM.objects.all()
+ serializer_class = serializers.AdcmDetailSerializer
+ lookup_field = 'id'
+ lookup_url_kwarg = 'adcm_id'
+ error_code = 'ADCM_NOT_FOUND'
diff --git a/python/api/api_views.py b/python/api/api_views.py
index 09f8351e53..adba7541e7 100644
--- a/python/api/api_views.py
+++ b/python/api/api_views.py
@@ -29,20 +29,17 @@
import cm.upgrade
import cm.config as config
+from cm.errors import AdcmEx
from cm.models import Action, ConfigLog, PrototypeConfig
-from cm.errors import AdcmApiEx
from cm.logger import log
-def check_obj(model, req, error):
+def check_obj(model, req, error=None):
if isinstance(req, dict):
kw = req
else:
kw = {'id': req}
- try:
- return model.objects.get(**kw)
- except ObjectDoesNotExist:
- raise AdcmApiEx(error) from None
+ return model.obj.get(**kw)
def hlink(view, lookup, lookup_url):
@@ -87,12 +84,14 @@ def get_upgradable_func(self, obj):
return bool(cm.upgrade.get_upgrade(obj))
-def get_api_url_kwargs(obj, request):
+def get_api_url_kwargs(obj, request, no_obj_type=False):
obj_type = obj.prototype.type
kwargs = {
- 'object_type': obj_type,
f'{obj_type}_id': obj.id,
}
+ # Do not include object_type in kwargs if no_obj_type == True
+ if not no_obj_type:
+ kwargs['object_type'] = obj_type
if obj_type == 'service':
if 'cluster' in request.path:
kwargs['cluster_id'] = obj.cluster.id
@@ -100,8 +99,11 @@ def get_api_url_kwargs(obj, request):
if 'cluster' in request.path:
kwargs['cluster_id'] = obj.cluster.id
elif obj_type == 'component':
- kwargs['service_id'] = obj.service.id
- kwargs['cluster_id'] = obj.cluster.id
+ if 'cluster' in request.path:
+ kwargs['service_id'] = obj.service.id
+ kwargs['cluster_id'] = obj.cluster.id
+ elif 'service' in request.path:
+ kwargs['service_id'] = obj.service.id
return kwargs
@@ -111,6 +113,12 @@ def get_url(self, obj, view_name, request, format): # pylint: disable=redefine
return reverse(view_name, kwargs=kwargs, request=request, format=format)
+class ObjectURL(serializers.HyperlinkedIdentityField):
+ def get_url(self, obj, view_name, request, format): # pylint: disable=redefined-builtin
+ kwargs = get_api_url_kwargs(obj, request, True)
+ return reverse(view_name, kwargs=kwargs, request=request, format=format)
+
+
class UrlField(serializers.HyperlinkedIdentityField):
def get_kwargs(self, obj):
return {}
@@ -177,12 +185,9 @@ def fix_ordering(field, view):
fix = fix.replace('cluster_', 'cluster__')
if view.__class__.__name__ not in ('BundleList',):
fix = fix.replace('version', 'version_order')
- if view.__class__.__name__ == 'ClusterServiceList':
+ if view.__class__.__name__ in ['ServiceListView', 'ComponentListView']:
if 'display_name' in fix:
fix = fix.replace('display_name', 'prototype__display_name')
- elif view.__class__.__name__ == 'ServiceComponentList':
- if 'display_name' in fix:
- fix = fix.replace('display_name', 'component__display_name')
return fix
@@ -262,7 +267,7 @@ def get_page(self, obj, request, context=None):
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 AdcmApiEx('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):
@@ -278,15 +283,15 @@ def get_page(self, obj, request, context=None):
return Response(obj)
msg = 'Response is too long, use paginated request'
- raise AdcmApiEx('TOO_LONG', msg=msg, args=self.get_paged_link())
+ raise AdcmEx('TOO_LONG', msg=msg, args=self.get_paged_link())
- def get(self, request):
+ def get(self, request, *args, **kwargs):
obj = self.filter_queryset(self.get_queryset())
return self.get_page(obj, request)
class PageViewAdd(PageView):
- def post(self, request):
+ def post(self, request, *args, **kwargs):
serializer_class = self.select_serializer(request)
serializer = serializer_class(data=request.data, context={'request': request})
return create(serializer)
@@ -317,7 +322,7 @@ def check_obj(self, kw_req):
try:
return self.get_queryset().get(**kw_req)
except ObjectDoesNotExist:
- raise AdcmApiEx(self.error_code) from None
+ raise AdcmEx(self.error_code) from None
def get_object(self):
lookup_url_kwarg = self.lookup_url_kwarg or self.lookup_field
diff --git a/tests/functional/test_stacks_data/toml_parser_error/services/__init__.py b/python/api/cluster/__init__.py
similarity index 100%
rename from tests/functional/test_stacks_data/toml_parser_error/services/__init__.py
rename to python/api/cluster/__init__.py
diff --git a/python/api/cluster_serial.py b/python/api/cluster/serializers.py
similarity index 52%
rename from python/api/cluster_serial.py
rename to python/api/cluster/serializers.py
index 5abd30fb9d..6cdccf887d 100644
--- a/python/api/cluster_serial.py
+++ b/python/api/cluster/serializers.py
@@ -16,14 +16,15 @@
import cm.api
import cm.job
import cm.status_api
-from cm.api import safe_api
from cm.logger import log # pylint: disable=unused-import
-from cm.errors import AdcmApiEx, AdcmEx
+from cm.errors import AdcmEx
from cm.models import Action, Cluster, Host, Prototype, ServiceComponent
from api.api_views import check_obj, hlink, filter_actions, get_upgradable_func
-from api.api_views import UrlField, CommonAPIURL
+from api.api_views import UrlField, CommonAPIURL, ObjectURL
from api.action.serializers import ActionShort
+from api.component.serializers import ComponentDetailSerializer
+from api.host.serializers import HostSerializer
def get_cluster_id(obj):
@@ -42,10 +43,7 @@ class ClusterSerializer(serializers.Serializer):
url = hlink('cluster-details', 'id', 'cluster_id')
def validate_prototype_id(self, prototype_id):
- cluster = check_obj(
- Prototype, {'id': prototype_id, 'type': 'cluster'}, "PROTOTYPE_NOT_FOUND"
- )
- return cluster
+ return check_obj(Prototype, {'id': prototype_id, 'type': 'cluster'})
def create(self, validated_data):
try:
@@ -54,12 +52,8 @@ def create(self, validated_data):
validated_data.get('name'),
validated_data.get('description', ''),
)
- except Prototype.DoesNotExist:
- raise AdcmApiEx('PROTOTYPE_NOT_FOUND') from None
except IntegrityError:
- raise AdcmApiEx("CLUSTER_CONFLICT") from None
- except AdcmEx as e:
- raise AdcmApiEx(e.code, e.msg, e.http_code) from e
+ raise AdcmEx("CLUSTER_CONFLICT") from None
def update(self, instance, validated_data):
instance.name = validated_data.get('name', instance.name)
@@ -68,7 +62,7 @@ def update(self, instance, validated_data):
instance.save()
except IntegrityError:
msg = 'cluster with name "{}" already exists'.format(instance.name)
- raise AdcmApiEx("CLUSTER_CONFLICT", msg) from None
+ raise AdcmEx("CLUSTER_CONFLICT", msg) from None
return instance
@@ -79,8 +73,8 @@ class ClusterDetailSerializer(ClusterSerializer):
edition = serializers.CharField(read_only=True)
license = serializers.CharField(read_only=True)
action = CommonAPIURL(view_name='object-action')
- service = hlink('cluster-service', 'id', 'cluster_id')
- host = hlink('cluster-host', 'id', 'cluster_id')
+ service = ObjectURL(view_name='service')
+ host = ObjectURL(view_name='host')
hostcomponent = hlink('host-component', 'id', 'cluster_id')
status = serializers.SerializerMethodField()
status_url = hlink('cluster-status', 'id', 'cluster_id')
@@ -92,7 +86,7 @@ class ClusterDetailSerializer(ClusterSerializer):
prototype = hlink('cluster-type-details', 'prototype_id', 'prototype_id')
def get_issue(self, obj):
- return cm.issue.get_issue(obj)
+ return cm.issue.aggregate_issues(obj)
def get_status(self, obj):
return cm.status_api.get_cluster_status(obj.id)
@@ -123,90 +117,6 @@ def get_prototype_display_name(self, obj):
return obj.prototype.display_name
-class ClusterHostUrlField(UrlField):
- def get_kwargs(self, obj):
- return {'cluster_id': obj.cluster.id, 'host_id': obj.id}
-
-
-class ClusterHostSerializer(serializers.Serializer):
- state = serializers.CharField(read_only=True)
- # stack = serializers.JSONField(read_only=True)
- cluster_id = serializers.IntegerField(read_only=True)
- fqdn = serializers.CharField(read_only=True)
- id = serializers.IntegerField(help_text='host id', read_only=True)
- host_id = serializers.IntegerField(source='id')
- prototype_id = serializers.IntegerField(read_only=True)
- provider_id = serializers.IntegerField(read_only=True)
- url = ClusterHostUrlField(read_only=True, view_name='cluster-host-details')
-
-
-class ClusterHostDetailSerializer(ClusterHostSerializer):
- issue = serializers.SerializerMethodField()
- cluster_url = hlink('cluster-details', 'cluster_id', 'cluster_id')
- status = serializers.SerializerMethodField()
- monitoring = serializers.CharField(read_only=True)
- host_url = hlink('host-details', 'id', 'host_id')
- config = CommonAPIURL(view_name='object-config')
- action = CommonAPIURL(view_name='object-action')
-
- def get_issue(self, obj):
- return cm.issue.get_issue(obj)
-
- def get_status(self, obj):
- return cm.status_api.get_host_status(obj.id)
-
-
-class ClusterHostAddSerializer(ClusterHostDetailSerializer):
- host_id = serializers.IntegerField(source='id')
-
- def create(self, validated_data):
- cluster = check_obj(Cluster, validated_data.get('cluster_id'), "CLUSTER_NOT_FOUND")
- host = check_obj(Host, validated_data.get('id'), "HOST_NOT_FOUND")
- try:
- cm.api.add_host_to_cluster(cluster, host)
- except AdcmEx as e:
- raise AdcmApiEx(e.code, e.msg, e.http_code) from e
- return host
-
-
-class ClusterHostUISerializer(ClusterHostDetailSerializer):
- actions = serializers.SerializerMethodField()
- upgradable = serializers.SerializerMethodField()
- prototype_version = serializers.SerializerMethodField()
- prototype_name = serializers.SerializerMethodField()
- prototype_display_name = serializers.SerializerMethodField()
- provider_name = serializers.SerializerMethodField()
- get_upgradable = get_upgradable_func
-
- def get_actions(self, obj):
- act_set = Action.objects.filter(prototype=obj.prototype)
- self.context['object'] = obj
- self.context['host_id'] = obj.id
- actions = ActionShort(
- filter_actions(obj, act_set), many=True, context=self.context
- )
- return actions.data
-
- def get_prototype_version(self, obj):
- return obj.prototype.version
-
- def get_prototype_name(self, obj):
- return obj.prototype.name
-
- def get_prototype_display_name(self, obj):
- return obj.prototype.display_name
-
- def get_provider_version(self, obj):
- if obj.provider:
- return obj.provider.prototype.version
- return None
-
- def get_provider_name(self, obj):
- if obj.provider:
- return obj.provider.name
- return None
-
-
class StatusSerializer(serializers.Serializer):
id = serializers.IntegerField(read_only=True)
component_id = serializers.IntegerField(read_only=True)
@@ -242,7 +152,7 @@ def get_kwargs(self, obj):
component = serializers.CharField(help_text='component name')
component_id = serializers.IntegerField(read_only=True, help_text='component id')
state = serializers.CharField(read_only=True, required=False)
- url = MyUrlField(read_only=True, view_name='host-component-details')
+ url = MyUrlField(read_only=True, view_name='host-comp-details')
host_url = hlink('host-details', 'host_id', 'host_id')
def to_representation(self, instance):
@@ -263,7 +173,7 @@ class HostComponentUISerializer(serializers.Serializer):
def get_host(self, obj):
hosts = Host.objects.filter(cluster=self.context.get('cluster'))
- return ClusterHostSerializer(hosts, many=True, context=self.context).data
+ return HostSerializer(hosts, many=True, context=self.context).data
def get_component(self, obj):
comps = ServiceComponent.objects.filter(cluster=self.context.get('cluster'))
@@ -275,131 +185,22 @@ class HostComponentSaveSerializer(serializers.Serializer):
def validate_hc(self, hc):
if not hc:
- raise AdcmApiEx('INVALID_INPUT', 'hc field is required')
+ raise AdcmEx('INVALID_INPUT', 'hc field is required')
if not isinstance(hc, list):
- raise AdcmApiEx('INVALID_INPUT', 'hc field should be a 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 AdcmApiEx('INVALID_INPUT', msg.format(key))
+ raise AdcmEx('INVALID_INPUT', msg.format(key))
return hc
def create(self, validated_data):
hc = validated_data.get('hc')
- return safe_api(cm.api.add_hc, (self.context.get('cluster'), hc))
-
-
-class ClusterServiceUrlField(UrlField):
- def get_kwargs(self, obj):
- return {'cluster_id': obj.cluster.id, 'service_id': obj.id}
-
-
-class ClusterServiceSerializer(serializers.Serializer):
- id = serializers.IntegerField(read_only=True)
- cluster_id = serializers.IntegerField(read_only=True)
- name = serializers.CharField(read_only=True)
- display_name = serializers.CharField(read_only=True)
- state = serializers.CharField(read_only=True)
- url = ClusterServiceUrlField(read_only=True, view_name='cluster-service-details')
- prototype_id = serializers.IntegerField(help_text='id of service prototype')
-
- def validate_prototype_id(self, prototype_id):
- service = check_obj(
- Prototype, {'id': prototype_id, 'type': 'service'}, "PROTOTYPE_NOT_FOUND"
- )
- return service
-
- def create(self, validated_data):
- try:
- return cm.api.add_service_to_cluster(
- self.context.get('cluster'),
- validated_data.get('prototype_id'),
- )
- except IntegrityError:
- raise AdcmApiEx('SERVICE_CONFLICT') from None
- except AdcmEx as e:
- raise AdcmApiEx(e.code, e.msg, e.http_code) from e
+ return cm.api.add_hc(self.context.get('cluster'), hc)
-class ClusterServiceDetailSerializer(ClusterServiceSerializer):
- prototype_id = serializers.IntegerField(read_only=True)
- # stack = serializers.JSONField(read_only=True)
- description = serializers.CharField(read_only=True)
- bundle_id = serializers.IntegerField(read_only=True)
- issue = serializers.SerializerMethodField()
- status = serializers.SerializerMethodField()
- monitoring = serializers.CharField(read_only=True)
- config = CommonAPIURL(view_name='object-config')
- action = CommonAPIURL(view_name='object-action')
- component = ClusterServiceUrlField(read_only=True, view_name='cluster-service-component')
- imports = ClusterServiceUrlField(read_only=True, view_name='cluster-service-import')
- bind = ClusterServiceUrlField(read_only=True, view_name='cluster-service-bind')
- prototype = hlink('service-type-details', 'prototype_id', 'prototype_id')
-
- def get_issue(self, obj):
- return cm.issue.get_issue(obj)
-
- def get_status(self, obj):
- return cm.status_api.get_service_status(obj.cluster.id, obj.id)
-
-
-class ClusterServiceUISerializer(ClusterServiceDetailSerializer):
- actions = serializers.SerializerMethodField()
- components = serializers.SerializerMethodField()
- name = serializers.CharField(read_only=True)
- version = serializers.CharField(read_only=True)
- config = CommonAPIURL(view_name='object-config')
- action = CommonAPIURL(view_name='object-action')
-
- def get_actions(self, obj):
- act_set = Action.objects.filter(prototype=obj.prototype)
- self.context['object'] = 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 ServiceComponentDetailSerializer(comps, many=True, context=self.context).data
-
-
-class ServiceComponentSerializer(serializers.Serializer):
- class MyUrlField(UrlField):
- def get_kwargs(self, obj):
- return {
- 'cluster_id': obj.cluster.id,
- 'service_id': obj.service.id,
- 'component_id': obj.id,
- }
-
- id = serializers.IntegerField(read_only=True)
- name = serializers.CharField(read_only=True)
- prototype_id = serializers.SerializerMethodField()
- display_name = serializers.CharField(read_only=True)
- description = serializers.CharField(read_only=True)
- state = serializers.CharField(read_only=True)
- url = MyUrlField(read_only=True, view_name='cluster-service-component-details')
-
- def get_prototype_id(self, obj):
- return obj.prototype.id
-
-
-class ServiceComponentDetailSerializer(ServiceComponentSerializer):
- constraint = serializers.JSONField(read_only=True)
- requires = serializers.JSONField(read_only=True)
- bound_to = serializers.JSONField(read_only=True)
- monitoring = serializers.CharField(read_only=True)
- status = serializers.SerializerMethodField()
- config = CommonAPIURL(view_name='object-config')
- action = CommonAPIURL(view_name='object-action')
-
- def get_status(self, obj):
- return cm.status_api.get_component_status(obj.id)
-
-
-class HCComponentSerializer(ServiceComponentDetailSerializer):
+class HCComponentSerializer(ComponentDetailSerializer):
service_id = serializers.IntegerField(read_only=True)
service_name = serializers.SerializerMethodField()
service_display_name = serializers.SerializerMethodField()
@@ -422,7 +223,7 @@ def get_requires(self, obj):
def process_requires(req_list):
for c in req_list:
- comp = Prototype.objects.get(
+ comp = Prototype.obj.get(
type='component',
name=c['component'],
parent__name=c['service'],
@@ -500,18 +301,6 @@ def get_import_service_name(self, obj):
return None
-class ServiceBindSerializer(BindSerializer):
- class MyUrlField(UrlField):
- def get_kwargs(self, obj):
- return {
- 'bind_id': obj.id,
- 'cluster_id': obj.cluster.id,
- 'service_id': obj.service.id,
- }
-
- url = MyUrlField(read_only=True, view_name='cluster-service-bind-details')
-
-
class ClusterBindSerializer(BindSerializer):
class MyUrlField(UrlField):
def get_kwargs(self, obj):
@@ -531,51 +320,20 @@ class DoBindSerializer(serializers.Serializer):
export_cluster_prototype_name = serializers.CharField(read_only=True)
def create(self, validated_data):
- export_cluster = check_obj(
- Cluster, validated_data.get('export_cluster_id'), "CLUSTER_NOT_FOUND"
+ export_cluster = check_obj(Cluster, validated_data.get('export_cluster_id'))
+ return cm.api.bind(
+ validated_data.get('cluster'),
+ None,
+ export_cluster,
+ validated_data.get('export_service_id', 0)
)
- try:
- return cm.api.bind(
- validated_data.get('cluster'),
- None,
- export_cluster,
- validated_data.get('export_service_id', 0)
- )
- except AdcmEx as e:
- raise AdcmApiEx(e.code, e.msg, e.http_code) from e
-
-
-class DoServiceBindSerializer(serializers.Serializer):
- id = serializers.IntegerField(read_only=True)
- export_cluster_id = serializers.IntegerField()
- export_service_id = serializers.IntegerField()
- export_cluster_name = serializers.CharField(read_only=True)
- export_service_name = serializers.CharField(read_only=True)
- export_cluster_prototype_name = serializers.CharField(read_only=True)
-
- def create(self, validated_data):
- export_cluster = check_obj(
- Cluster, validated_data.get('export_cluster_id'), "CLUSTER_NOT_FOUND"
- )
- try:
- return cm.api.bind(
- validated_data.get('cluster'),
- validated_data.get('service'),
- export_cluster,
- validated_data.get('export_service_id')
- )
- except AdcmEx as e:
- raise AdcmApiEx(e.code, e.msg, e.http_code) from e
class PostImportSerializer(serializers.Serializer):
bind = serializers.JSONField()
def create(self, validated_data):
- try:
- bind = validated_data.get('bind')
- cluster = self.context.get('cluster')
- service = self.context.get('service')
- return cm.api.multi_bind(cluster, service, bind)
- except AdcmEx as e:
- raise AdcmApiEx(e.code, e.msg, e.http_code, e.adds) from e
+ bind = validated_data.get('bind')
+ cluster = self.context.get('cluster')
+ service = self.context.get('service')
+ return cm.api.multi_bind(cluster, service, bind)
diff --git a/python/api/cluster/urls.py b/python/api/cluster/urls.py
new file mode 100644
index 0000000000..49e88c1be2
--- /dev/null
+++ b/python/api/cluster/urls.py
@@ -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.
+
+from django.urls import path, include
+from . import views
+
+urlpatterns = [
+ path('', views.ClusterList.as_view(), name='cluster'),
+ path('/', include([
+ path('', views.ClusterDetail.as_view(), name='cluster-details'),
+ path('import/', views.ClusterImport.as_view(), name='cluster-import'),
+ path('status/', views.StatusList.as_view(), name='cluster-status'),
+ path('serviceprototype/', views.ClusterBundle.as_view(), name='cluster-service-prototype'),
+ path('service/', include('api.service.urls')),
+ path('host/', include('api.host.cluster_urls')),
+ path('action/', include('api.action.urls'), {'object_type': 'cluster'}),
+ path('config/', include('api.config.urls'), {'object_type': 'cluster'}),
+ path('bind/', include([
+ path('', views.ClusterBindList.as_view(), name='cluster-bind'),
+ path('/', views.ClusterBindDetail.as_view(), name='cluster-bind-details'),
+ ])),
+ path('upgrade/', include([
+ path('', views.ClusterUpgrade.as_view(), name='cluster-upgrade'),
+ path('/', include([
+ path('', views.ClusterUpgradeDetail.as_view(), name='cluster-upgrade-details'),
+ path('do/', views.DoClusterUpgrade.as_view(), name='do-cluster-upgrade'),
+ ])),
+ ])),
+ path('hostcomponent/', include([
+ path('', views.HostComponentList.as_view(), name='host-component'),
+ path('/', views.HostComponentDetail.as_view(), name='host-comp-details'),
+ ])),
+ ])),
+]
diff --git a/python/api/cluster/views.py b/python/api/cluster/views.py
new file mode 100644
index 0000000000..4bd4ffc420
--- /dev/null
+++ b/python/api/cluster/views.py
@@ -0,0 +1,313 @@
+# 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 itertools import chain
+
+from rest_framework import status
+from rest_framework.response import Response
+
+import cm.job
+import cm.api
+import cm.bundle
+import cm.status_api
+from cm.errors import AdcmEx
+from cm.models import Cluster, HostComponent, Prototype
+from cm.models import ClusterObject, Upgrade, ClusterBind
+from cm.logger import log # pylint: disable=unused-import
+
+import api.serializers
+from api.api_views import create, update, check_obj, GenericAPIPermView
+from api.api_views import ListView, PageView, PageViewAdd, InterfaceView, DetailViewDelete
+from . import serializers
+
+
+def get_obj_conf(cluster_id, service_id):
+ cluster = check_obj(Cluster, cluster_id)
+ if service_id:
+ co = check_obj(ClusterObject, {'cluster': cluster, 'id': service_id})
+ obj = co
+ else:
+ obj = cluster
+ if not obj:
+ raise AdcmEx('CONFIG_NOT_FOUND', "this object has no config")
+ if not obj.config:
+ raise AdcmEx('CONFIG_NOT_FOUND', "this object has no config")
+ return obj
+
+
+class ClusterList(PageViewAdd):
+ """
+ get:
+ List of all existing clusters
+
+ post:
+ Create new cluster
+ """
+ queryset = Cluster.objects.all()
+ serializer_class = serializers.ClusterSerializer
+ serializer_class_ui = serializers.ClusterUISerializer
+ serializer_class_post = serializers.ClusterDetailSerializer
+ filterset_fields = ('name', 'prototype_id')
+ ordering_fields = ('name', 'state', 'prototype__display_name', 'prototype__version_order')
+
+
+class ClusterDetail(DetailViewDelete):
+ """
+ get:
+ Show cluster
+ """
+ queryset = Cluster.objects.all()
+ serializer_class = serializers.ClusterDetailSerializer
+ serializer_class_ui = serializers.ClusterUISerializer
+ lookup_field = 'id'
+ lookup_url_kwarg = 'cluster_id'
+ error_code = 'CLUSTER_NOT_FOUND'
+
+ def patch(self, request, *args, **kwargs):
+ """
+ Edit cluster
+ """
+ obj = self.get_object()
+ serializer = self.serializer_class(
+ obj, data=request.data, partial=True, context={'request': request}
+ )
+ return update(serializer)
+
+ def delete(self, request, *args, **kwargs):
+ """
+ Remove cluster
+ """
+ cluster = self.get_object()
+ cm.api.delete_cluster(cluster)
+ return Response(status=status.HTTP_204_NO_CONTENT)
+
+
+class ClusterBundle(ListView):
+ queryset = Prototype.objects.filter(type='service')
+ serializer_class = api.stack.serializers.ServiceSerializer
+ serializer_class_ui = api.stack.serializers.BundleServiceUISerializer
+
+ def get(self, request, cluster_id): # pylint: disable=arguments-differ
+ """
+ List all services of specified cluster of bundle
+ """
+ cluster = check_obj(Cluster, cluster_id)
+ bundle = self.get_queryset().filter(bundle=cluster.prototype.bundle)
+ shared = self.get_queryset().filter(shared=True).exclude(bundle=cluster.prototype.bundle)
+ serializer_class = self.select_serializer(request)
+ serializer = serializer_class(
+ list(chain(bundle, shared)), many=True, context={'request': request, 'cluster': cluster}
+ )
+ return Response(serializer.data)
+
+
+class ClusterImport(ListView):
+ queryset = Prototype.objects.all()
+ serializer_class = api.stack.serializers.ImportSerializer
+ post_serializer = serializers.PostImportSerializer
+
+ def get(self, request, cluster_id): # pylint: disable=arguments-differ
+ """
+ List all imports avaliable for specified cluster
+ """
+ cluster = check_obj(Cluster, cluster_id)
+ res = cm.api.get_import(cluster)
+ return Response(res)
+
+ def post(self, request, cluster_id): # pylint: disable=arguments-differ
+ """
+ Update bind for cluster
+ """
+ cluster = check_obj(Cluster, cluster_id)
+ serializer = self.post_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(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
+
+
+class ClusterBindList(ListView):
+ queryset = ClusterBind.objects.all()
+ serializer_class = serializers.ClusterBindSerializer
+
+ def get_serializer_class(self):
+ if self.request and self.request.method == 'POST':
+ return serializers.DoBindSerializer
+ else:
+ return serializers.ClusterBindSerializer
+
+ def get(self, request, cluster_id): # pylint: disable=arguments-differ
+ """
+ List all binds of specified cluster
+ """
+ cluster = check_obj(Cluster, cluster_id)
+ obj = self.get_queryset().filter(cluster=cluster, service=None)
+ serializer = self.get_serializer_class()(obj, many=True, context={'request': request})
+ return Response(serializer.data)
+
+ def post(self, request, cluster_id):
+ """
+ Bind two clusters
+ """
+ cluster = check_obj(Cluster, cluster_id)
+ serializer = self.get_serializer_class()(data=request.data, context={'request': request})
+ return create(serializer, cluster=cluster)
+
+
+class ClusterBindDetail(DetailViewDelete):
+ queryset = ClusterBind.objects.all()
+ serializer_class = serializers.BindSerializer
+
+ def get_obj(self, cluster_id, bind_id):
+ cluster = check_obj(Cluster, cluster_id)
+ return check_obj(ClusterBind, {'cluster': cluster, 'id': bind_id})
+
+ def get(self, request, cluster_id, bind_id): # pylint: disable=arguments-differ
+ """
+ Show specified bind of specified cluster
+ """
+ obj = self.get_obj(cluster_id, bind_id)
+ serializer = self.serializer_class(obj, context={'request': request})
+ return Response(serializer.data)
+
+ def delete(self, request, cluster_id, bind_id): # pylint: disable=arguments-differ
+ """
+ Unbind specified bind of specified cluster
+ """
+ bind = self.get_obj(cluster_id, bind_id)
+ cm.api.unbind(bind)
+ return Response(status=status.HTTP_204_NO_CONTENT)
+
+
+class ClusterUpgrade(PageView):
+ queryset = Upgrade.objects.all()
+ serializer_class = api.serializers.UpgradeLinkSerializer
+
+ def get(self, request, cluster_id): # pylint: disable=arguments-differ
+ """
+ List all avaliable upgrades for specified cluster
+ """
+ cluster = check_obj(Cluster, cluster_id)
+ obj = cm.upgrade.get_upgrade(cluster, self.get_ordering(request, self.queryset, self))
+ serializer = self.serializer_class(obj, many=True, context={
+ 'cluster_id': cluster.id, 'request': request
+ })
+ return Response(serializer.data)
+
+
+class ClusterUpgradeDetail(ListView):
+ queryset = Upgrade.objects.all()
+ serializer_class = api.serializers.UpgradeLinkSerializer
+
+ def get(self, request, cluster_id, upgrade_id): # pylint: disable=arguments-differ
+ """
+ List all avaliable upgrades for specified cluster
+ """
+ cluster = check_obj(Cluster, cluster_id)
+ obj = self.get_queryset().get(id=upgrade_id)
+ serializer = self.serializer_class(obj, context={
+ 'cluster_id': cluster.id, 'request': request
+ })
+ return Response(serializer.data)
+
+
+class DoClusterUpgrade(GenericAPIPermView):
+ queryset = Upgrade.objects.all()
+ serializer_class = api.serializers.DoUpgradeSerializer
+
+ def post(self, request, cluster_id, upgrade_id):
+ """
+ Do upgrade specified cluster
+ """
+ cluster = check_obj(Cluster, cluster_id)
+ serializer = self.serializer_class(data=request.data, context={'request': request})
+ return create(serializer, upgrade_id=int(upgrade_id), obj=cluster)
+
+
+class StatusList(GenericAPIPermView):
+ queryset = HostComponent.objects.all()
+ serializer_class = serializers.StatusSerializer
+
+ def get(self, request, cluster_id):
+ """
+ Show all hosts and components in a specified cluster
+ """
+ cluster = check_obj(Cluster, cluster_id)
+ obj = self.get_queryset().filter(cluster=cluster)
+ serializer = self.serializer_class(obj, many=True, context={'request': request})
+ return Response(serializer.data)
+
+
+class HostComponentList(GenericAPIPermView, InterfaceView):
+ queryset = HostComponent.objects.all()
+ serializer_class = serializers.HostComponentSerializer
+ serializer_class_ui = serializers.HostComponentUISerializer
+
+ def get_serializer_class(self):
+ if self.request and self.request.method == 'POST':
+ return serializers.HostComponentSaveSerializer
+ return self.serializer_class
+
+ def get(self, request, cluster_id):
+ """
+ Show host <-> component map in a specified cluster
+ """
+ cluster = check_obj(Cluster, cluster_id)
+ hc = self.get_queryset().filter(cluster=cluster)
+ serializer_class = self.select_serializer(request)
+ if self.for_ui(request):
+ ui_hc = HostComponent()
+ ui_hc.hc = hc
+ serializer = serializer_class(ui_hc, context={'request': request, 'cluster': cluster})
+ else:
+ serializer = serializer_class(hc, many=True, context={'request': request})
+ return Response(serializer.data)
+
+ def post(self, request, cluster_id):
+ """
+ Create new mapping service:component <-> host in a specified cluster.
+ """
+ cluster = check_obj(Cluster, cluster_id)
+ save_serializer = self.get_serializer_class()
+ serializer = save_serializer(data=request.data, context={
+ 'request': request, 'cluster': cluster,
+ })
+ if serializer.is_valid():
+ hc_list = serializer.save()
+ responce_serializer = self.serializer_class(
+ hc_list, many=True, context={'request': request}
+ )
+ return Response(responce_serializer.data, status.HTTP_201_CREATED)
+ return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
+
+
+class HostComponentDetail(GenericAPIPermView):
+ queryset = HostComponent.objects.all()
+ serializer_class = serializers.HostComponentSerializer
+
+ def get_obj(self, cluster_id, hs_id):
+ cluster = check_obj(Cluster, cluster_id)
+ return check_obj(
+ HostComponent,
+ {'id': hs_id, 'cluster': cluster},
+ 'HOSTSERVICE_NOT_FOUND'
+ )
+
+ def get(self, request, cluster_id, hs_id):
+ """
+ Show host <-> component link in a specified cluster
+ """
+ obj = self.get_obj(cluster_id, hs_id)
+ serializer = self.serializer_class(obj, context={'request': request})
+ return Response(serializer.data)
diff --git a/python/api/cluster_views.py b/python/api/cluster_views.py
deleted file mode 100644
index 7d487030f9..0000000000
--- a/python/api/cluster_views.py
+++ /dev/null
@@ -1,545 +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.
-
-from itertools import chain
-
-from rest_framework import status
-from rest_framework.response import Response
-
-import cm.job
-import cm.api
-import cm.bundle
-import cm.status_api
-from cm.errors import AdcmApiEx, AdcmEx
-from cm.models import Cluster, Host, HostComponent, Prototype, ServiceComponent
-from cm.models import ClusterObject, Upgrade, ClusterBind
-from cm.logger import log # pylint: disable=unused-import
-
-import api.serializers
-import api.cluster_serial
-import api.stack_serial
-from api.api_views import create, update, check_obj, GenericAPIPermView
-from api.api_views import ListView, PageView, PageViewAdd, InterfaceView
-from api.api_views import DetailViewRO, DetailViewDelete
-
-
-def get_obj_conf(cluster_id, service_id):
- cluster = check_obj(Cluster, cluster_id, 'CLUSTER_NOT_FOUND')
- if service_id:
- co = check_obj(
- ClusterObject, {'cluster': cluster, 'id': service_id}, 'SERVICE_NOT_FOUND'
- )
- obj = co
- else:
- obj = cluster
- if not obj:
- raise AdcmApiEx('CONFIG_NOT_FOUND', "this object has no config")
- if not obj.config:
- raise AdcmApiEx('CONFIG_NOT_FOUND', "this object has no config")
- return obj
-
-
-class ClusterList(PageViewAdd):
- """
- get:
- List of all existing clusters
-
- post:
- Create new cluster
- """
- queryset = Cluster.objects.all()
- serializer_class = api.cluster_serial.ClusterSerializer
- serializer_class_ui = api.cluster_serial.ClusterUISerializer
- serializer_class_post = api.cluster_serial.ClusterDetailSerializer
- filterset_fields = ('name', 'prototype_id')
- ordering_fields = ('name', 'state', 'prototype__display_name', 'prototype__version_order')
-
-
-class ClusterDetail(DetailViewDelete):
- """
- get:
- Show cluster
- """
- queryset = Cluster.objects.all()
- serializer_class = api.cluster_serial.ClusterDetailSerializer
- serializer_class_ui = api.cluster_serial.ClusterUISerializer
- lookup_field = 'id'
- lookup_url_kwarg = 'cluster_id'
- error_code = 'CLUSTER_NOT_FOUND'
-
- def patch(self, request, *args, **kwargs):
- """
- Edit cluster
- """
- obj = self.get_object()
- serializer = self.serializer_class(
- obj, data=request.data, partial=True, context={'request': request}
- )
- return update(serializer)
-
- def delete(self, request, *args, **kwargs):
- """
- Remove cluster
- """
- cluster = self.get_object()
- cm.api.delete_cluster(cluster)
- return Response(status=status.HTTP_204_NO_CONTENT)
-
-
-class ClusterHostList(PageView):
- queryset = Host.objects.all()
- serializer_class = api.cluster_serial.ClusterHostSerializer
- post_serializer = api.cluster_serial.ClusterHostAddSerializer
- serializer_class_ui = api.cluster_serial.ClusterHostUISerializer
- filterset_fields = ('fqdn', 'prototype_id', 'provider_id')
- ordering_fields = (
- 'fqdn', 'state', 'provider__name', 'prototype__display_name', 'prototype__version_order'
- )
-
- def get(self, request, cluster_id): # pylint: disable=arguments-differ
- """
- List all hosts of a specified cluster
- """
- cluster = check_obj(Cluster, cluster_id, 'CLUSTER_NOT_FOUND')
- obj = self.filter_queryset(self.get_queryset().filter(cluster=cluster))
- return self.get_page(obj, request, {'cluster_id': cluster_id})
-
- def post(self, request, cluster_id):
- check_obj(Cluster, cluster_id, 'CLUSTER_NOT_FOUND')
- serializer = self.post_serializer(data=request.data, context={'request': request})
- return create(serializer, cluster_id=cluster_id)
-
-
-class ClusterHostDetail(ListView):
- queryset = Host.objects.all()
- serializer_class = api.cluster_serial.ClusterHostDetailSerializer
- serializer_class_ui = api.cluster_serial.ClusterHostUISerializer
-
- def check_host(self, cluster, host):
- if host.cluster != cluster:
- msg = "Host #{} doesn't belong to cluster #{}".format(host.id, cluster.id)
- raise AdcmApiEx('FOREIGN_HOST', msg)
-
- def get_obj(self, cluster_id, host_id):
- cluster = check_obj(Cluster, cluster_id, 'CLUSTER_NOT_FOUND')
- host = check_obj(Host, host_id, 'HOST_NOT_FOUND')
- self.check_host(cluster, host)
- return host
-
- def get(self, request, cluster_id, host_id): # pylint: disable=arguments-differ
- """
- Show host of cluster
- """
- obj = self.get_obj(cluster_id, host_id)
- serializer_class = self.select_serializer(request)
- serializer = serializer_class(obj, context={'request': request, 'cluster_id': cluster_id})
- return Response(serializer.data)
-
- def delete(self, request, cluster_id, host_id):
- """
- Remove host from cluster
- """
- host = self.get_obj(cluster_id, host_id)
- try:
- cm.api.remove_host_from_cluster(host)
- except AdcmEx as e:
- raise AdcmApiEx(e.code, e.msg, e.http_code) from e
- return Response(status=status.HTTP_204_NO_CONTENT)
-
-
-class ClusterBundle(ListView):
- queryset = Prototype.objects.filter(type='service')
- serializer_class = api.stack_serial.ServiceSerializer
- serializer_class_ui = api.stack_serial.BundleServiceUISerializer
-
- def get(self, request, cluster_id): # pylint: disable=arguments-differ
- """
- List all services of specified cluster of bundle
- """
- cluster = check_obj(Cluster, cluster_id, 'CLUSTER_NOT_FOUND')
- bundle = self.get_queryset().filter(bundle=cluster.prototype.bundle)
- shared = self.get_queryset().filter(shared=True).exclude(bundle=cluster.prototype.bundle)
- serializer_class = self.select_serializer(request)
- serializer = serializer_class(
- list(chain(bundle, shared)), many=True, context={'request': request, 'cluster': cluster}
- )
- return Response(serializer.data)
-
-
-class ClusterImport(ListView):
- queryset = Prototype.objects.all()
- serializer_class = api.stack_serial.ImportSerializer
- post_serializer = api.cluster_serial.PostImportSerializer
-
- def get(self, request, cluster_id): # pylint: disable=arguments-differ
- """
- List all imports avaliable for specified cluster
- """
- cluster = check_obj(Cluster, cluster_id, 'CLUSTER_NOT_FOUND')
- res = cm.api.get_import(cluster)
- return Response(res)
-
- def post(self, request, cluster_id): # pylint: disable=arguments-differ
- """
- Update bind for cluster
- """
- cluster = check_obj(Cluster, cluster_id, 'CLUSTER_NOT_FOUND')
- serializer = self.post_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(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
-
-
-class ClusterServiceImport(ListView):
- queryset = Prototype.objects.all()
- serializer_class = api.stack_serial.ImportSerializer
- post_serializer = api.cluster_serial.PostImportSerializer
-
- def get(self, request, cluster_id, service_id): # pylint: disable=arguments-differ
- """
- List all imports avaliable for specified service in cluster
- """
- cluster = check_obj(Cluster, cluster_id, 'CLUSTER_NOT_FOUND')
- service = check_obj(
- ClusterObject, {'cluster': cluster, 'id': service_id}, 'SERVICE_NOT_FOUND'
- )
- res = cm.api.get_import(cluster, service)
- return Response(res)
-
- def post(self, request, cluster_id, service_id): # pylint: disable=arguments-differ
- """
- Update bind for service in cluster
- """
- cluster = check_obj(Cluster, cluster_id, 'CLUSTER_NOT_FOUND')
- service = check_obj(
- ClusterObject, {'cluster': cluster, 'id': service_id}, 'SERVICE_NOT_FOUND'
- )
- serializer = self.post_serializer(data=request.data, context={
- 'request': request, 'cluster': cluster, 'service': service
- })
- if serializer.is_valid():
- res = serializer.create(serializer.validated_data)
- return Response(res, status.HTTP_200_OK)
- return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
-
-
-class ClusterBindList(ListView):
- queryset = ClusterBind.objects.all()
- serializer_class = api.cluster_serial.ClusterBindSerializer
-
- def get_serializer_class(self):
- if self.request and self.request.method == 'POST':
- return api.cluster_serial.DoBindSerializer
- else:
- return api.cluster_serial.ClusterBindSerializer
-
- def get(self, request, cluster_id): # pylint: disable=arguments-differ
- """
- List all binds of specified cluster
- """
- cluster = check_obj(Cluster, cluster_id, 'CLUSTER_NOT_FOUND')
- obj = self.get_queryset().filter(cluster=cluster, service=None)
- serializer = self.get_serializer_class()(obj, many=True, context={'request': request})
- return Response(serializer.data)
-
- def post(self, request, cluster_id):
- """
- Bind two clusters
- """
- cluster = check_obj(Cluster, cluster_id, 'CLUSTER_NOT_FOUND')
- serializer = self.get_serializer_class()(data=request.data, context={'request': request})
- return create(serializer, cluster=cluster)
-
-
-class ClusterServiceBind(ListView):
- queryset = ClusterBind.objects.all()
- serializer_class = api.cluster_serial.ServiceBindSerializer
-
- def get_serializer_class(self):
- if self.request and self.request.method == 'POST':
- return api.cluster_serial.DoServiceBindSerializer
- else:
- return api.cluster_serial.ServiceBindSerializer
-
- def get(self, request, cluster_id, service_id): # pylint: disable=arguments-differ
- """
- List all binds of specified service in cluster
- """
- cluster = check_obj(Cluster, cluster_id, 'CLUSTER_NOT_FOUND')
- service = check_obj(
- ClusterObject, {'cluster': cluster, 'id': service_id}, 'SERVICE_NOT_FOUND'
- )
- obj = self.get_queryset().filter(cluster=cluster, service=service)
- serializer = self.get_serializer_class()(obj, many=True, context={'request': request})
- return Response(serializer.data)
-
- def post(self, request, cluster_id, service_id):
- """
- Bind two services
- """
- cluster = check_obj(Cluster, cluster_id, 'CLUSTER_NOT_FOUND')
- service = check_obj(
- ClusterObject, {'cluster': cluster, 'id': service_id}, 'SERVICE_NOT_FOUND'
- )
- serializer = self.get_serializer_class()(data=request.data, context={'request': request})
- return create(serializer, cluster=cluster, service=service)
-
-
-class ClusterServiceBindDetail(DetailViewDelete):
- queryset = ClusterBind.objects.all()
- serializer_class = api.cluster_serial.BindSerializer
-
- def get_obj(self, cluster_id, service_id, bind_id):
- cluster = check_obj(Cluster, cluster_id, 'CLUSTER_NOT_FOUND')
- if service_id:
- check_obj(
- ClusterObject, {'cluster': cluster, 'id': service_id}, 'SERVICE_NOT_FOUND'
- )
- return check_obj(ClusterBind, {'cluster': cluster, 'id': bind_id}, 'BIND_NOT_FOUND')
-
- def get(self, request, cluster_id, bind_id, service_id=None): # pylint: disable=arguments-differ
- """
- Show specified bind of specified service in cluster
- """
- obj = self.get_obj(cluster_id, service_id, bind_id)
- serializer = self.serializer_class(obj, context={'request': request})
- return Response(serializer.data)
-
- def delete(self, request, cluster_id, bind_id, service_id=None): # pylint: disable=arguments-differ
- """
- Unbind specified bind of specified service in cluster
- """
- bind = self.get_obj(cluster_id, service_id, bind_id)
- cm.api.unbind(bind)
- return Response(status=status.HTTP_204_NO_CONTENT)
-
-
-class ClusterUpgrade(PageView):
- queryset = Upgrade.objects.all()
- serializer_class = api.serializers.UpgradeLinkSerializer
-
- def get(self, request, cluster_id): # pylint: disable=arguments-differ
- """
- List all avaliable upgrades for specified cluster
- """
- cluster = check_obj(Cluster, cluster_id, 'CLUSTER_NOT_FOUND')
- obj = cm.upgrade.get_upgrade(cluster, self.get_ordering(request, self.queryset, self))
- serializer = self.serializer_class(obj, many=True, context={
- 'cluster_id': cluster.id, 'request': request
- })
- return Response(serializer.data)
-
-
-class ClusterUpgradeDetail(ListView):
- queryset = Upgrade.objects.all()
- serializer_class = api.serializers.UpgradeLinkSerializer
-
- def get(self, request, cluster_id, upgrade_id): # pylint: disable=arguments-differ
- """
- List all avaliable upgrades for specified cluster
- """
- cluster = check_obj(Cluster, cluster_id, 'CLUSTER_NOT_FOUND')
- obj = self.get_queryset().get(id=upgrade_id)
- serializer = self.serializer_class(obj, context={
- 'cluster_id': cluster.id, 'request': request
- })
- return Response(serializer.data)
-
-
-class DoClusterUpgrade(GenericAPIPermView):
- queryset = Upgrade.objects.all()
- serializer_class = api.serializers.DoUpgradeSerializer
-
- def post(self, request, cluster_id, upgrade_id):
- """
- Do upgrade specified cluster
- """
- cluster = check_obj(Cluster, cluster_id, 'CLUSTER_NOT_FOUND')
- serializer = self.serializer_class(data=request.data, context={'request': request})
- return create(serializer, upgrade_id=int(upgrade_id), obj=cluster)
-
-
-class ClusterServiceList(PageView):
- queryset = ClusterObject.objects.all()
- serializer_class = api.cluster_serial.ClusterServiceSerializer
- serializer_class_ui = api.cluster_serial.ClusterServiceUISerializer
- ordering_fields = ('state', 'prototype__display_name', 'prototype__version_order')
-
- def get(self, request, cluster_id): # pylint: disable=arguments-differ
- """
- List all services of a specified cluster
- """
- cluster = check_obj(Cluster, cluster_id, 'CLUSTER_NOT_FOUND')
- obj = self.filter_queryset(self.get_queryset().filter(cluster=cluster))
- return self.get_page(obj, request, {'cluster_id': cluster_id})
-
- def post(self, request, cluster_id):
- """
- Add service to specified cluster
- """
- cluster = check_obj(Cluster, cluster_id, 'CLUSTER_NOT_FOUND')
- serializer = self.serializer_class(data=request.data, context={
- 'request': request, 'cluster': cluster,
- })
- return create(serializer, id=cluster_id)
-
-
-class ClusterServiceDetail(DetailViewRO):
- queryset = ClusterObject.objects.all()
- serializer_class = api.cluster_serial.ClusterServiceDetailSerializer
- serializer_class_ui = api.cluster_serial.ClusterServiceUISerializer
-
- def get(self, request, cluster_id, service_id): # pylint: disable=arguments-differ
- """
- Show service in a specified cluster
- """
- cluster = check_obj(Cluster, cluster_id, 'CLUSTER_NOT_FOUND')
- service = check_obj(
- ClusterObject, {'id': service_id, 'cluster': cluster}, 'SERVICE_NOT_FOUND'
- )
- serial_class = self.select_serializer(request)
- serializer = serial_class(service, context={'request': request, 'cluster_id': cluster_id})
- return Response(serializer.data)
-
- def delete(self, request, cluster_id, service_id):
- """
- Remove service from cluster
- """
- cluster = check_obj(Cluster, cluster_id, 'CLUSTER_NOT_FOUND')
- service = check_obj(
- ClusterObject, {'id': service_id, 'cluster': cluster}, 'SERVICE_NOT_FOUND'
- )
- try:
- cm.api.delete_service(service)
- except AdcmEx as e:
- raise AdcmApiEx(e.code, e.msg, e.http_code) from e
- return Response(status=status.HTTP_204_NO_CONTENT)
-
-
-class ServiceComponentList(PageView):
- queryset = ServiceComponent.objects.all()
- serializer_class = api.cluster_serial.ServiceComponentSerializer
- serializer_class_ui = api.cluster_serial.ServiceComponentDetailSerializer
- ordering_fields = ('component__display_name',)
-
- def get(self, request, cluster_id, service_id): # pylint: disable=arguments-differ
- """
- Show componets of service in a specified cluster
- """
- cluster = check_obj(Cluster, cluster_id, 'CLUSTER_NOT_FOUND')
- co = check_obj(
- ClusterObject, {'cluster': cluster, 'id': service_id}, 'SERVICE_NOT_FOUND'
- )
- obj = self.filter_queryset(self.get_queryset().filter(cluster=cluster, service=co))
- return self.get_page(obj, request)
-
-
-class ServiceComponentDetail(GenericAPIPermView):
- queryset = ServiceComponent.objects.all()
- serializer_class = api.cluster_serial.ServiceComponentDetailSerializer
-
- def get(self, request, cluster_id, service_id, component_id):
- """
- Show specified componet of service in a specified cluster
- """
- cluster = check_obj(Cluster, cluster_id, 'CLUSTER_NOT_FOUND')
- co = check_obj(
- ClusterObject, {'cluster': cluster, 'id': service_id}, 'SERVICE_NOT_FOUND'
- )
- obj = check_obj(
- ServiceComponent,
- {'cluster': cluster, 'service': co, 'id': component_id},
- 'COMPONENT_NOT_FOUND'
- )
- serializer = self.serializer_class(obj, context={'request': request})
- return Response(serializer.data)
-
-
-class StatusList(GenericAPIPermView):
- queryset = HostComponent.objects.all()
- serializer_class = api.cluster_serial.StatusSerializer
-
- def get(self, request, cluster_id):
- """
- Show all hosts and components in a specified cluster
- """
- cluster = check_obj(Cluster, cluster_id, 'CLUSTER_NOT_FOUND')
- obj = self.get_queryset().filter(cluster=cluster)
- serializer = self.serializer_class(obj, many=True, context={'request': request})
- return Response(serializer.data)
-
-
-class HostComponentList(GenericAPIPermView, InterfaceView):
- queryset = HostComponent.objects.all()
- serializer_class = api.cluster_serial.HostComponentSerializer
- serializer_class_ui = api.cluster_serial.HostComponentUISerializer
-
- def get_serializer_class(self):
- if self.request and self.request.method == 'POST':
- return api.cluster_serial.HostComponentSaveSerializer
- return self.serializer_class
-
- def get(self, request, cluster_id):
- """
- Show host <-> component map in a specified cluster
- """
- cluster = check_obj(Cluster, cluster_id, 'CLUSTER_NOT_FOUND')
- hc = self.get_queryset().filter(cluster=cluster)
- serializer_class = self.select_serializer(request)
- if self.for_ui(request):
- ui_hc = HostComponent()
- ui_hc.hc = hc
- serializer = serializer_class(ui_hc, context={'request': request, 'cluster': cluster})
- else:
- serializer = serializer_class(hc, many=True, context={'request': request})
- return Response(serializer.data)
-
- def post(self, request, cluster_id):
- """
- Create new mapping service:component <-> host in a specified cluster.
- """
- cluster = check_obj(Cluster, cluster_id, 'CLUSTER_NOT_FOUND')
- save_serializer = self.get_serializer_class()
- serializer = save_serializer(data=request.data, context={
- 'request': request, 'cluster': cluster,
- })
- if serializer.is_valid():
- hc_list = serializer.save()
- responce_serializer = self.serializer_class(
- hc_list, many=True, context={'request': request}
- )
- return Response(responce_serializer.data, status.HTTP_201_CREATED)
- return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
-
-
-class HostComponentDetail(GenericAPIPermView):
- queryset = HostComponent.objects.all()
- serializer_class = api.cluster_serial.HostComponentSerializer
-
- def get_obj(self, cluster_id, hs_id):
- cluster = check_obj(Cluster, cluster_id, 'CLUSTER_NOT_FOUND')
- return check_obj(
- HostComponent,
- {'id': hs_id, 'cluster': cluster},
- 'HOSTSERVICE_NOT_FOUND'
- )
-
- def get(self, request, cluster_id, hs_id):
- """
- Show host <-> component link in a specified cluster
- """
- obj = self.get_obj(cluster_id, hs_id)
- serializer = self.serializer_class(obj, context={'request': request})
- return Response(serializer.data)
diff --git a/tests/functional/test_stacks_data/toml_parser_error/services/simple_service/__init__.py b/python/api/component/__init__.py
similarity index 100%
rename from tests/functional/test_stacks_data/toml_parser_error/services/simple_service/__init__.py
rename to python/api/component/__init__.py
diff --git a/python/api/component/serializers.py b/python/api/component/serializers.py
new file mode 100644
index 0000000000..3ff07de086
--- /dev/null
+++ b/python/api/component/serializers.py
@@ -0,0 +1,76 @@
+# 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=redefined-builtin
+
+from rest_framework import serializers
+from rest_framework.reverse import reverse
+
+from api.api_views import hlink, filter_actions, get_api_url_kwargs, CommonAPIURL
+from api.action.serializers import ActionShort
+
+from cm import issue
+from cm import status_api
+from cm.models import Action
+
+
+class ComponentObjectUrlField(serializers.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 ComponentSerializer(serializers.Serializer):
+ id = serializers.IntegerField(read_only=True)
+ cluster_id = serializers.IntegerField(read_only=True)
+ service_id = serializers.IntegerField(read_only=True)
+ name = serializers.CharField(read_only=True)
+ display_name = serializers.CharField(read_only=True)
+ description = serializers.CharField(read_only=True)
+ state = serializers.CharField(read_only=True)
+ prototype_id = serializers.IntegerField(required=True, help_text='id of component prototype')
+ url = ComponentObjectUrlField(read_only=True, view_name='component-details')
+
+
+class ComponentDetailSerializer(ComponentSerializer):
+ constraint = serializers.JSONField(read_only=True)
+ requires = serializers.JSONField(read_only=True)
+ bound_to = serializers.JSONField(read_only=True)
+ bundle_id = serializers.IntegerField(read_only=True)
+ monitoring = serializers.CharField(read_only=True)
+ status = serializers.SerializerMethodField()
+ issue = serializers.SerializerMethodField()
+ action = CommonAPIURL(read_only=True, view_name='object-action')
+ config = CommonAPIURL(read_only=True, view_name='object-config')
+ prototype = hlink('component-type-details', 'prototype_id', 'prototype_id')
+
+ def get_issue(self, obj):
+ return issue.aggregate_issues(obj)
+
+ def get_status(self, obj):
+ return status_api.get_component_status(obj.id)
+
+
+class ComponentUISerializer(ComponentDetailSerializer):
+ actions = serializers.SerializerMethodField()
+ version = serializers.SerializerMethodField()
+
+ def get_actions(self, obj):
+ act_set = Action.objects.filter(prototype=obj.prototype)
+ self.context['object'] = obj
+ self.context['component_id'] = obj.id
+ actions = filter_actions(obj, act_set)
+ acts = ActionShort(actions, many=True, context=self.context)
+ return acts.data
+
+ def get_version(self, obj):
+ return obj.prototype.version
diff --git a/python/api/component/urls.py b/python/api/component/urls.py
new file mode 100644
index 0000000000..550d496238
--- /dev/null
+++ b/python/api/component/urls.py
@@ -0,0 +1,25 @@
+# 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 path, include
+from . import views
+
+
+urlpatterns = [
+ path('', views.ComponentListView.as_view(), name='component'),
+ path('/', include([
+ path('', views.ComponentDetailView.as_view(), name='component-details'),
+ path('config/', include('api.config.urls'), {'object_type': 'component'}),
+ path('action/', include('api.action.urls'), {'object_type': 'component'}),
+ ])),
+]
diff --git a/python/api/component/views.py b/python/api/component/views.py
new file mode 100644
index 0000000000..78c2a72042
--- /dev/null
+++ b/python/api/component/views.py
@@ -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.
+
+from rest_framework.response import Response
+
+from api.api_views import PageView, check_obj, DetailViewRO
+from cm.models import ServiceComponent, ClusterObject, Cluster
+from . import serializers
+
+
+class ComponentListView(PageView):
+ queryset = ServiceComponent.objects.all()
+ serializer_class = serializers.ComponentSerializer
+ serializer_class_ui = serializers.ComponentUISerializer
+ filterset_fields = ('cluster_id', 'service_id')
+ ordering_fields = ('state', 'prototype__display_name', 'prototype__version_order')
+
+ def get(self, request, *args, **kwargs):
+ """
+ List all components
+ """
+ queryset = self.get_queryset()
+ if 'cluster_id' in kwargs:
+ cluster = check_obj(Cluster, kwargs['cluster_id'], 'CLUSTER_NOT_FOUND')
+ co = check_obj(
+ ClusterObject, {'cluster': cluster, 'id': kwargs['service_id']}, 'SERVICE_NOT_FOUND'
+ )
+ queryset = self.get_queryset().filter(cluster=cluster, service=co)
+ elif 'service_id' in kwargs:
+ co = check_obj(ClusterObject, {'id': kwargs['service_id']}, 'SERVICE_NOT_FOUND')
+ queryset = self.get_queryset().filter(service=co)
+ return self.get_page(self.filter_queryset(queryset), request)
+
+
+class ComponentDetailView(DetailViewRO):
+ queryset = ServiceComponent.objects.all()
+ serializer_class = serializers.ComponentDetailSerializer
+ serializer_class_ui = serializers.ComponentUISerializer
+
+ def get(self, request, *args, **kwargs):
+ """
+ Show component
+ """
+ component = check_obj(
+ ServiceComponent, {'id': kwargs['component_id']}, 'COMPONENT_NOT_FOUND'
+ )
+ serial_class = self.select_serializer(request)
+ serializer = serial_class(component, context={'request': request})
+ return Response(serializer.data)
diff --git a/python/api/config/serializers.py b/python/api/config/serializers.py
index a697f19eca..40475a7df5 100644
--- a/python/api/config/serializers.py
+++ b/python/api/config/serializers.py
@@ -19,7 +19,6 @@
import cm.adcm_config
from cm.adcm_config import ui_config, restore_cluster_config
from cm.api import update_obj_config
-from cm.errors import AdcmEx, AdcmApiEx
from api.api_views import get_api_url_kwargs, CommonAPIURL
@@ -46,17 +45,14 @@ class ObjectConfigSerializer(serializers.Serializer):
class ObjectConfigUpdateSerializer(ObjectConfigSerializer):
def update(self, instance, validated_data):
- try:
- conf = validated_data.get('config')
- attr = validated_data.get('attr', {})
- desc = validated_data.get('description', '')
- cl = update_obj_config(instance.obj_ref, conf, attr, desc)
- if validated_data.get('ui'):
- cl.config = ui_config(validated_data.get('obj'), cl)
- if hasattr(instance.obj_ref, 'adcm'):
- logrotate.run()
- except AdcmEx as e:
- raise AdcmApiEx(e.code, e.msg, e.http_code, e.adds) from e
+ conf = validated_data.get('config')
+ attr = validated_data.get('attr', {})
+ desc = validated_data.get('description', '')
+ cl = update_obj_config(instance.obj_ref, conf, attr, desc)
+ if validated_data.get('ui'):
+ cl.config = ui_config(validated_data.get('obj'), cl)
+ if hasattr(instance.obj_ref, 'adcm'):
+ logrotate.run()
return cl
@@ -64,15 +60,11 @@ class ObjectConfigRestoreSerializer(ObjectConfigSerializer):
config = serializers.JSONField(read_only=True)
def update(self, instance, validated_data):
- try:
- cc = restore_cluster_config(
- instance.obj_ref,
- instance.id,
- validated_data.get('description', instance.description)
- )
- except AdcmEx as e:
- raise AdcmApiEx(e.code, e.msg, e.http_code) from e
- return cc
+ return restore_cluster_config(
+ instance.obj_ref,
+ instance.id,
+ validated_data.get('description', instance.description)
+ )
class ConfigHistorySerializer(ObjectConfigSerializer):
diff --git a/python/api/config/views.py b/python/api/config/views.py
index cb53bf3445..ee616c537c 100644
--- a/python/api/config/views.py
+++ b/python/api/config/views.py
@@ -10,16 +10,12 @@
# See the License for the specific language governing permissions and
# limitations under the License.
-from django.db import models
from rest_framework.response import Response
from api.api_views import ListView, GenericAPIPermView, create, update, check_obj
from cm.adcm_config import ui_config
-from cm.errors import AdcmApiEx
-from cm.models import (
- ADCM, Cluster, HostProvider, Host, ClusterObject, ServiceComponent, ConfigLog, ObjectConfig
-)
+from cm.models import get_model_by_type, ConfigLog, ObjectConfig
from . import serializers
@@ -31,46 +27,13 @@ def get_config_version(objconf, version):
ver = objconf.current
else:
ver = version
- try:
- cl = ConfigLog.objects.get(obj_ref=objconf, id=ver)
- except ConfigLog.DoesNotExist:
- raise AdcmApiEx('CONFIG_NOT_FOUND', "config version doesn't exist") from None
+ cl = ConfigLog.obj.get(obj_ref=objconf, id=ver)
return cl
-def get_objects_for_config(object_type):
- if object_type == 'adcm':
- return ADCM.objects.all()
- if object_type == 'cluster':
- return Cluster.objects.all()
- elif object_type == 'provider':
- return HostProvider.objects.all()
- elif object_type == 'service':
- return ClusterObject.objects.all()
- elif object_type == 'component':
- return ServiceComponent.objects.all()
- elif object_type == 'host':
- return Host.objects.all()
- else:
- # This function should return a QuerySet, this is necessary for the correct
- # construction of the schema.
- return Cluster.objects.all()
-
-
-def get_obj(objects, object_type, object_id):
- try:
- obj = objects.get(id=object_id)
- except models.ObjectDoesNotExist:
- errors = {
- 'adcm': 'ADCM_NOT_FOUND',
- 'cluster': 'CLUSTER_NOT_FOUND',
- 'provider': 'PROVIDER_NOT_FOUND',
- 'host': 'HOST_NOT_FOUND',
- 'service': 'SERVICE_NOT_FOUND',
- 'component': 'COMPONENT_NOT_FOUND',
- }
- raise AdcmApiEx(errors[object_type]) from None
-
+def get_obj(object_type, object_id):
+ model = get_model_by_type(object_type)
+ obj = model.obj.get(id=object_id)
if object_type == 'provider':
object_type = 'hostprovider'
if object_type == 'service':
@@ -78,7 +41,7 @@ def get_obj(objects, object_type, object_id):
if object_type == 'component':
object_type = 'servicecomponent'
oc = check_obj(ObjectConfig, {object_type: obj}, 'CONFIG_NOT_FOUND')
- cl = ConfigLog.objects.get(obj_ref=oc, id=oc.current)
+ cl = ConfigLog.obj.get(obj_ref=oc, id=oc.current)
return obj, oc, cl
@@ -89,17 +52,19 @@ def get_object_type_id_version(**kwargs):
return object_type, object_id, version
+def get_queryset(self):
+ return get_model_by_type(self.object_type).objects.all()
+
+
class ConfigView(ListView):
serializer_class = serializers.HistoryCurrentPreviousConfigSerializer
+ get_queryset = get_queryset
object_type = None
- def get_queryset(self):
- return get_objects_for_config(self.object_type)
-
def get(self, request, *args, **kwargs):
object_type, object_id, _ = get_object_type_id_version(**kwargs)
self.object_type = object_type
- obj, _, _ = get_obj(self.get_queryset(), object_type, object_id)
+ obj, _, _ = get_obj(object_type, object_id)
serializer = self.serializer_class(
self.get_queryset().get(id=obj.id), context={'request': request, 'object': obj})
return Response(serializer.data)
@@ -108,15 +73,13 @@ def get(self, request, *args, **kwargs):
class ConfigHistoryView(ListView):
serializer_class = serializers.ConfigHistorySerializer
update_serializer = serializers.ObjectConfigUpdateSerializer
+ get_queryset = get_queryset
object_type = None
- def get_queryset(self):
- return get_objects_for_config(self.object_type)
-
def get(self, request, *args, **kwargs):
object_type, object_id, _ = get_object_type_id_version(**kwargs)
self.object_type = object_type
- obj, _, _ = get_obj(self.get_queryset(), object_type, object_id)
+ obj, _, _ = get_obj(object_type, object_id)
cl = ConfigLog.objects.filter(obj_ref=obj.config).order_by('-id')
serializer = self.serializer_class(
cl, many=True, context={'request': request, 'object': obj}
@@ -126,7 +89,7 @@ def get(self, request, *args, **kwargs):
def post(self, request, *args, **kwargs):
object_type, object_id, _ = get_object_type_id_version(**kwargs)
self.object_type = object_type
- obj, _, cl = get_obj(self.get_queryset(), object_type, object_id)
+ obj, _, cl = get_obj(object_type, object_id)
serializer = self.update_serializer(
cl, data=request.data, context={'request': request, 'object': obj}
)
@@ -135,15 +98,13 @@ def post(self, request, *args, **kwargs):
class ConfigVersionView(ListView):
serializer_class = serializers.ObjectConfigSerializer
+ get_queryset = get_queryset
object_type = None
- def get_queryset(self):
- return get_objects_for_config(self.object_type)
-
def get(self, request, *args, **kwargs):
object_type, object_id, version = get_object_type_id_version(**kwargs)
self.object_type = object_type
- obj, oc, _ = get_obj(self.get_queryset(), object_type, object_id)
+ obj, oc, _ = get_obj(object_type, object_id)
cl = get_config_version(oc, version)
if self.for_ui(request):
cl.config = ui_config(obj, cl)
@@ -153,15 +114,13 @@ def get(self, request, *args, **kwargs):
class ConfigHistoryRestoreView(GenericAPIPermView):
serializer_class = serializers.ObjectConfigRestoreSerializer
+ get_queryset = get_queryset
object_type = None
- def get_queryset(self):
- return get_objects_for_config(self.object_type)
-
def patch(self, request, *args, **kwargs):
object_type, object_id, version = get_object_type_id_version(**kwargs)
self.object_type = object_type
- _, oc, _ = get_obj(self.get_queryset(), object_type, object_id)
+ _, oc, _ = get_obj(object_type, object_id)
cl = get_config_version(oc, version)
serializer = self.serializer_class(cl, data=request.data, context={'request': request})
return update(serializer)
diff --git a/python/api/host/__init__.py b/python/api/host/__init__.py
new file mode 100644
index 0000000000..824dd6c8fe
--- /dev/null
+++ b/python/api/host/__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/host/cluster_urls.py b/python/api/host/cluster_urls.py
new file mode 100644
index 0000000000..747f2b4322
--- /dev/null
+++ b/python/api/host/cluster_urls.py
@@ -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.
+
+
+from django.urls import path, include
+from . import views
+
+
+urlpatterns = [
+ path('', views.HostListCluster.as_view(), name='host'),
+ path('', include('api.host.host_urls')),
+]
diff --git a/python/api/host/host_urls.py b/python/api/host/host_urls.py
new file mode 100644
index 0000000000..57e3d43721
--- /dev/null
+++ b/python/api/host/host_urls.py
@@ -0,0 +1,24 @@
+# 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 path, include
+from . import views
+
+
+urlpatterns = [
+ path('/', include([
+ path('', views.HostDetail.as_view(), name='host-details'),
+ path('config/', include('api.config.urls'), {'object_type': 'host'}),
+ path('action/', include('api.action.urls'), {'object_type': 'host'}),
+ ])),
+]
diff --git a/python/api/host/provider_urls.py b/python/api/host/provider_urls.py
new file mode 100644
index 0000000000..4bfeeea181
--- /dev/null
+++ b/python/api/host/provider_urls.py
@@ -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.
+
+
+from django.urls import path, include
+from . import views
+
+
+urlpatterns = [
+ path('', views.HostListProvider.as_view(), name='host'),
+ path('', include('api.host.host_urls')),
+]
diff --git a/python/api/host/serializers.py b/python/api/host/serializers.py
new file mode 100644
index 0000000000..38b2239971
--- /dev/null
+++ b/python/api/host/serializers.py
@@ -0,0 +1,136 @@
+# 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 import IntegrityError
+from rest_framework import serializers
+
+import cm
+from cm.errors import AdcmEx
+from cm.models import Cluster, Host, HostProvider, Prototype, Action
+from api.api_views import hlink, check_obj, filter_actions, CommonAPIURL, ObjectURL
+from api.action.serializers import ActionShort
+
+
+class HostSerializer(serializers.Serializer):
+ id = serializers.IntegerField(read_only=True)
+ cluster_id = serializers.IntegerField(read_only=True)
+ prototype_id = serializers.IntegerField(help_text='id of host type')
+ provider_id = serializers.IntegerField()
+ fqdn = serializers.CharField(help_text='fully qualified domain name')
+ description = serializers.CharField(required=False)
+ state = serializers.CharField(read_only=True)
+ url = ObjectURL(read_only=True, view_name='host-details')
+
+ def get_issue(self, obj):
+ return cm.issue.aggregate_issues(obj)
+
+ def validate_prototype_id(self, prototype_id):
+ return check_obj(Prototype, {'id': prototype_id, 'type': 'host'})
+
+ def validate_provider_id(self, provider_id):
+ return check_obj(HostProvider, provider_id)
+
+ def validate_fqdn(self, name):
+ return cm.stack.validate_name(name, 'Host name')
+
+ def create(self, validated_data):
+ try:
+ return cm.api.add_host(
+ validated_data.get('prototype_id'),
+ validated_data.get('provider_id'),
+ validated_data.get('fqdn'),
+ validated_data.get('description', '')
+ )
+ except IntegrityError:
+ raise AdcmEx("HOST_CONFLICT", "duplicate host") from None
+
+
+class HostDetailSerializer(HostSerializer):
+ # stack = serializers.JSONField(read_only=True)
+ issue = serializers.SerializerMethodField()
+ bundle_id = serializers.IntegerField(read_only=True)
+ status = serializers.SerializerMethodField()
+ config = CommonAPIURL(view_name='object-config')
+ action = CommonAPIURL(view_name='object-action')
+ prototype = hlink('host-type-details', 'prototype_id', 'prototype_id')
+
+ def get_issue(self, obj):
+ return cm.issue.aggregate_issues(obj)
+
+ def get_status(self, obj):
+ return cm.status_api.get_host_status(obj.id)
+
+
+class ClusterHostSerializer(HostSerializer):
+ host_id = serializers.IntegerField(source='id')
+ prototype_id = serializers.IntegerField(read_only=True)
+ provider_id = serializers.IntegerField(read_only=True)
+ fqdn = serializers.CharField(read_only=True)
+
+ def create(self, validated_data):
+ cluster = check_obj(Cluster, self.context.get('cluster_id'))
+ host = check_obj(Host, validated_data.get('id'))
+ cm.api.add_host_to_cluster(cluster, host)
+ return host
+
+
+class ProvideHostSerializer(HostSerializer):
+ prototype_id = serializers.IntegerField(read_only=True)
+ provider_id = serializers.IntegerField(read_only=True)
+
+ def create(self, validated_data):
+ provider = check_obj(HostProvider, self.context.get('provider_id'))
+ proto = Prototype.obj.get(bundle=provider.prototype.bundle, type='host')
+ try:
+ return cm.api.add_host(
+ proto,
+ provider,
+ validated_data.get('fqdn'),
+ validated_data.get('description', '')
+ )
+ except IntegrityError:
+ raise AdcmEx("HOST_CONFLICT", "duplicate host") from None
+
+
+class HostUISerializer(HostDetailSerializer):
+ actions = serializers.SerializerMethodField()
+ cluster_name = serializers.SerializerMethodField()
+ prototype_version = serializers.SerializerMethodField()
+ prototype_name = serializers.SerializerMethodField()
+ prototype_display_name = serializers.SerializerMethodField()
+ provider_name = serializers.SerializerMethodField()
+
+ def get_actions(self, obj):
+ act_set = Action.objects.filter(prototype=obj.prototype)
+ self.context['object'] = obj
+ self.context['host_id'] = obj.id
+ actions = ActionShort(filter_actions(obj, act_set), many=True, context=self.context)
+ return actions.data
+
+ def get_cluster_name(self, obj):
+ if obj.cluster:
+ return obj.cluster.name
+ return None
+
+ def get_prototype_version(self, obj):
+ return obj.prototype.version
+
+ def get_prototype_name(self, obj):
+ return obj.prototype.name
+
+ def get_prototype_display_name(self, obj):
+ return obj.prototype.display_name
+
+ def get_provider_name(self, obj):
+ if obj.provider:
+ return obj.provider.name
+ return None
diff --git a/python/api/host/urls.py b/python/api/host/urls.py
new file mode 100644
index 0000000000..afbfe804a7
--- /dev/null
+++ b/python/api/host/urls.py
@@ -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.
+
+
+from django.urls import path, include
+from . import views
+
+
+urlpatterns = [
+ path('', views.HostList.as_view(), name='host'),
+ path('', include('api.host.host_urls')),
+]
diff --git a/python/api/host/views.py b/python/api/host/views.py
new file mode 100644
index 0000000000..1e2695b143
--- /dev/null
+++ b/python/api/host/views.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.
+
+from django_filters import rest_framework as drf_filters
+from rest_framework.response import Response
+from rest_framework import status
+
+import cm
+from cm.errors import AdcmEx
+from cm.models import Cluster, HostProvider, Host
+from api.api_views import PageView, DetailViewDelete, create, check_obj
+from . import serializers
+
+
+class HostFilter(drf_filters.FilterSet):
+ cluster_is_null = drf_filters.BooleanFilter(field_name='cluster_id', lookup_expr='isnull')
+ provider_is_null = drf_filters.BooleanFilter(field_name='provider_id', lookup_expr='isnull')
+
+ class Meta:
+ model = Host
+ fields = ['cluster_id', 'prototype_id', 'provider_id', 'fqdn']
+
+
+class HostList(PageView):
+ """
+ get:
+ List all hosts
+
+ post:
+ Create new host
+ """
+ queryset = Host.objects.all()
+ serializer_class = serializers.HostSerializer
+ serializer_class_ui = serializers.HostUISerializer
+ filterset_class = HostFilter
+ filterset_fields = (
+ 'cluster_id', 'prototype_id', 'provider_id', 'fqdn', 'cluster_is_null', 'provider_is_null'
+ ) # just for documentation
+ ordering_fields = (
+ 'fqdn', 'state', 'provider__name', 'cluster__name',
+ 'prototype__display_name', 'prototype__version_order',
+ )
+
+ def get(self, request, *args, **kwargs):
+ """
+ List all hosts
+ """
+ queryset = self.get_queryset()
+ if 'cluster_id' in kwargs: # List cluster hosts
+ cluster = check_obj(Cluster, kwargs['cluster_id'])
+ queryset = self.get_queryset().filter(cluster=cluster)
+ if 'provider_id' in kwargs: # List provider hosts
+ provider = check_obj(HostProvider, kwargs['provider_id'])
+ queryset = self.get_queryset().filter(provider=provider)
+ return self.get_page(self.filter_queryset(queryset), request)
+
+ def post(self, request, *args, **kwargs):
+ """
+ Create host
+ """
+ serializer = self.serializer_class(data=request.data, context={
+ 'request': request,
+ 'cluster_id': kwargs.get('cluster_id', None),
+ 'provider_id': kwargs.get('provider_id', None)
+ })
+ return create(serializer)
+
+
+class HostListProvider(HostList):
+ serializer_class = serializers.ProvideHostSerializer
+
+
+class HostListCluster(HostList):
+ serializer_class = serializers.ClusterHostSerializer
+
+
+def check_host(host, cluster):
+ if host.cluster != cluster:
+ msg = "Host #{} doesn't belong to cluster #{}".format(host.id, cluster.id)
+ raise AdcmEx('FOREIGN_HOST', msg)
+
+
+class HostDetail(DetailViewDelete):
+ """
+ get:
+ Show host
+ """
+ queryset = Host.objects.all()
+ serializer_class = serializers.HostDetailSerializer
+ serializer_class_ui = serializers.HostUISerializer
+ lookup_field = 'id'
+ lookup_url_kwarg = 'host_id'
+ error_code = 'HOST_NOT_FOUND'
+
+ def get(self, request, host_id, **kwargs): # pylint: disable=arguments-differ)
+ host = check_obj(Host, host_id)
+ if 'cluster_id' in kwargs:
+ cluster = check_obj(Cluster, kwargs['cluster_id'])
+ check_host(host, cluster)
+ serial_class = self.select_serializer(request)
+ serializer = serial_class(host, context={'request': request})
+ return Response(serializer.data)
+
+ def delete(self, request, host_id, **kwargs): # pylint: disable=arguments-differ
+ """
+ Delete host
+ """
+ host = check_obj(Host, host_id, 'HOST_NOT_FOUND')
+ if 'cluster_id' in kwargs:
+ # Remove host from cluster
+ cluster = check_obj(Cluster, kwargs['cluster_id'])
+ check_host(host, cluster)
+ cm.api.remove_host_from_cluster(host)
+ else:
+ # Delete host (and all corresponding host services:components)
+ cm.api.delete_host(host)
+ return Response(status=status.HTTP_204_NO_CONTENT)
diff --git a/python/api/job/__init__.py b/python/api/job/__init__.py
new file mode 100644
index 0000000000..824dd6c8fe
--- /dev/null
+++ b/python/api/job/__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/job_serial.py b/python/api/job/serializers.py
similarity index 71%
rename from python/api/job_serial.py
rename to python/api/job/serializers.py
index 11b2a9899a..466307bbf8 100644
--- a/python/api/job_serial.py
+++ b/python/api/job/serializers.py
@@ -22,29 +22,11 @@
import cm.stack
import cm.status_api
import cm.config as config
-from cm.errors import AdcmEx, AdcmApiEx
-from cm.models import (
- Action, SubAction, JobLog, HostProvider, Host, Cluster, ClusterObject, ServiceComponent
-)
-
+from cm.errors import AdcmEx
+from cm.models import JobLog, HostProvider, Host, Cluster, ClusterObject, ServiceComponent
from api.api_views import hlink
-def get_job_action(obj):
- try:
- act = Action.objects.get(id=obj.action_id)
- return {
- 'name': act.name,
- 'display_name': act.display_name,
- 'prototype_id': act.prototype.id,
- 'prototype_name': act.prototype.name,
- 'prototype_version': act.prototype.version,
- 'prototype_type': act.prototype.type,
- }
- except Action.DoesNotExist:
- return None
-
-
def get_job_objects(obj):
resp = []
selector = obj.selector
@@ -77,28 +59,48 @@ def get_job_objects(obj):
return resp
-def get_job_object_type(obj):
- try:
- action = Action.objects.get(id=obj.action_id)
- return action.prototype.type
- except Action.DoesNotExist:
+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
+def get_action_url(self, obj):
+ if not obj.action_id:
+ return None
+ return reverse(
+ 'action-details', kwargs={'action_id': obj.action_id}, request=self.context['request']
+ )
+
+
class DataField(serializers.CharField):
def to_representation(self, value):
return value
+class JobAction(serializers.Serializer):
+ 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(serializers.Serializer):
id = serializers.IntegerField(read_only=True)
name = serializers.CharField(read_only=True)
- display_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(serializers.Serializer):
id = serializers.IntegerField(read_only=True)
@@ -118,12 +120,7 @@ class TaskSerializer(TaskListSerializer):
hc = serializers.JSONField(required=False)
hosts = serializers.JSONField(required=False)
verbose = serializers.BooleanField(required=False)
- action_url = serializers.HyperlinkedIdentityField(
- read_only=True,
- view_name='action-details',
- lookup_field='action_id',
- lookup_url_kwarg='action_id'
- )
+ action_url = serializers.SerializerMethodField()
action = serializers.SerializerMethodField()
objects = serializers.SerializerMethodField()
jobs = serializers.SerializerMethodField()
@@ -132,11 +129,12 @@ class TaskSerializer(TaskListSerializer):
cancel = hlink('task-cancel', 'id', 'task_id')
object_type = serializers.SerializerMethodField()
+ get_action_url = get_action_url
+
def get_terminatable(self, obj):
- try:
- action = Action.objects.get(id=obj.action_id)
- allow_to_terminate = action.allow_to_terminate
- except Action.DoesNotExist:
+ if obj.action:
+ allow_to_terminate = obj.action.allow_to_terminate
+ else:
allow_to_terminate = False
# pylint: disable=simplifiable-if-statement
if allow_to_terminate and obj.status in [config.Job.CREATED, config.Job.RUNNING]:
@@ -146,53 +144,34 @@ def get_terminatable(self, obj):
return False
def get_jobs(self, obj):
- task_jobs = JobLog.objects.filter(task_id=obj.id)
- for job in task_jobs:
- if job.sub_action_id:
- try:
- sub = SubAction.objects.get(id=job.sub_action_id)
- job.display_name = sub.display_name
- job.name = sub.name
- except SubAction.DoesNotExist:
- job.display_name = None
- job.name = None
- else:
- try:
- action = Action.objects.get(id=job.action_id)
- job.display_name = action.display_name
- job.name = action.name
- except Action.DoesNotExist:
- job.display_name = None
- job.name = None
- jobs = JobShort(task_jobs, many=True, context=self.context)
- return jobs.data
+ return JobShort(obj.joblog_set, many=True, context=self.context).data
def get_action(self, obj):
- return get_job_action(obj)
+ return JobAction(obj.action, context=self.context).data
def get_objects(self, obj):
return get_job_objects(obj)
def get_object_type(self, obj):
- return get_job_object_type(obj)
+ if obj.action:
+ return obj.action.prototype.type
+ else:
+ return None
class RunTaskSerializer(TaskSerializer):
def create(self, validated_data):
- try:
- obj = cm.job.start_task(
- validated_data.get('action_id'),
- validated_data.get('selector'),
- validated_data.get('config', {}),
- validated_data.get('attr', {}),
- validated_data.get('hc', []),
- validated_data.get('hosts', []),
- validated_data.get('verbose', False)
- )
- obj.jobs = JobLog.objects.filter(task_id=obj.id)
- return obj
- except AdcmEx as e:
- raise AdcmApiEx(e.code, e.msg, e.http_code, e.adds) from e
+ obj = cm.job.start_task(
+ validated_data.get('action_id'),
+ validated_data.get('selector'),
+ validated_data.get('config', {}),
+ validated_data.get('attr', {}),
+ validated_data.get('hc', []),
+ validated_data.get('hosts', []),
+ validated_data.get('verbose', False)
+ )
+ obj.jobs = JobLog.objects.filter(task_id=obj.id)
+ return obj
class TaskPostSerializer(RunTaskSerializer):
@@ -201,7 +180,7 @@ class TaskPostSerializer(RunTaskSerializer):
def validate_selector(self, selector):
if not isinstance(selector, dict):
- raise AdcmApiEx('JSON_ERROR', 'selector should be a map')
+ raise AdcmEx('JSON_ERROR', 'selector should be a map')
return selector
@@ -224,25 +203,14 @@ class JobSerializer(JobListSerializer):
selector = serializers.JSONField(required=False)
log_dir = serializers.CharField(read_only=True)
log_files = DataField(read_only=True)
- action_url = hlink('action-details', 'action_id', 'action_id')
- task_url = hlink('task-details', 'id', 'task_id')
+ action_url = serializers.SerializerMethodField()
+ task_url = hlink('task-details', 'task_id', 'task_id')
+
+ get_display_name = get_job_display_name
+ get_action_url = get_action_url
def get_action(self, obj):
- return get_job_action(obj)
-
- def get_display_name(self, obj):
- if obj.sub_action_id:
- try:
- sub = SubAction.objects.get(id=obj.sub_action_id)
- return sub.display_name
- except SubAction.DoesNotExist:
- return None
- else:
- try:
- action = Action.objects.get(id=obj.action_id)
- return action.display_name
- except Action.DoesNotExist:
- return None
+ return JobAction(obj.action, context=self.context).data
def get_objects(self, obj):
return get_job_objects(obj)
@@ -263,7 +231,7 @@ def _get_ansible_content(self, obj):
content = f.read()
except FileNotFoundError:
msg = f'File "{obj.name}-{obj.type}.{obj.format}" not found'
- raise AdcmApiEx('LOG_NOT_FOUND', msg) from None
+ raise AdcmEx('LOG_NOT_FOUND', msg) from None
return content
def get_content(self, obj):
diff --git a/python/api/job/task_urls.py b/python/api/job/task_urls.py
new file mode 100644
index 0000000000..7f1db4ff9d
--- /dev/null
+++ b/python/api/job/task_urls.py
@@ -0,0 +1,24 @@
+# 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 path, include
+from . import views
+
+
+urlpatterns = [
+ path('', views.Task.as_view(), name='task'),
+ path('/', include([
+ path('', views.TaskDetail.as_view(), name='task-details'),
+ path('restart/', views.TaskReStart.as_view(), name='task-restart'),
+ path('cancel/', views.TaskCancel.as_view(), name='task-cancel'),
+ ])),
+]
diff --git a/python/api/job/urls.py b/python/api/job/urls.py
new file mode 100644
index 0000000000..c85aa92d0a
--- /dev/null
+++ b/python/api/job/urls.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 path, include
+from . import views
+
+
+urlpatterns = [
+ path('', views.JobList.as_view(), name='job'),
+ path('/', include([
+ path('', views.JobDetail.as_view(), name='job-details'),
+ path('log/', include([
+ path('', views.LogStorageListView.as_view(), name='log-list'),
+ path('/', include([
+ path('', views.LogStorageView.as_view(), name='log-storage'),
+ path('download/', views.download_log_file, name='download-log'),
+ ])),
+ path(
+ '///',
+ views.LogFile.as_view(),
+ name='log-file'
+ ),
+ ])),
+ ])),
+]
diff --git a/python/api/job_views.py b/python/api/job/views.py
similarity index 58%
rename from python/api/job_views.py
rename to python/api/job/views.py
index 4a2ddffed2..bae27f39e6 100644
--- a/python/api/job_views.py
+++ b/python/api/job/views.py
@@ -21,13 +21,10 @@
import cm.config as config
from api.api_views import DetailViewRO, create, check_obj, PageView
-from api.job_serial import (
- JobSerializer, JobListSerializer, LogStorageSerializer, LogStorageListSerializer, LogSerializer
-)
-from api.job_serial import TaskSerializer, TaskListSerializer, TaskPostSerializer
-from cm.errors import AdcmEx, AdcmApiEx
+from cm.errors import AdcmEx
from cm.job import get_log, restart_task, cancel_task
from cm.models import JobLog, TaskLog, LogStorage
+from . import serializers
class JobList(PageView):
@@ -36,15 +33,15 @@ class JobList(PageView):
List all jobs
"""
queryset = JobLog.objects.order_by('-id')
- serializer_class = JobListSerializer
- serializer_class_ui = JobSerializer
+ serializer_class = serializers.JobListSerializer
+ serializer_class_ui = serializers.JobSerializer
filterset_fields = ('action_id', 'task_id', 'pid', 'status', 'start_date', 'finish_date')
ordering_fields = ('status', 'start_date', 'finish_date')
class JobDetail(GenericAPIView):
queryset = JobLog.objects.all()
- serializer_class = JobSerializer
+ serializer_class = serializers.JobSerializer
def get(self, request, job_id):
"""
@@ -79,7 +76,7 @@ def get(self, request, job_id):
class LogStorageListView(PageView):
queryset = LogStorage.objects.all()
- serializer_class = LogStorageListSerializer
+ serializer_class = serializers.LogStorageListSerializer
filterset_fields = ('name', 'type', 'format')
ordering_fields = ('id', 'name')
@@ -90,57 +87,52 @@ def get(self, request, job_id): # pylint: disable=arguments-differ
class LogStorageView(GenericAPIView):
queryset = LogStorage.objects.all()
- serializer_class = LogStorageSerializer
+ serializer_class = serializers.LogStorageSerializer
def get(self, request, job_id, log_id):
+ job = JobLog.obj.get(id=job_id)
try:
- job = JobLog.objects.get(id=job_id)
- try:
- log_storage = LogStorage.objects.get(id=log_id, job=job)
- except LogStorage.DoesNotExist:
- raise AdcmApiEx(
- 'LOG_NOT_FOUND', f'log {log_id} not found for job {job_id}') from None
- serializer = self.serializer_class(log_storage, context={'request': request})
- return Response(serializer.data)
- except AdcmEx as e:
- raise AdcmApiEx(e.code, e.msg, e.http_code) from e
+ log_storage = LogStorage.objects.get(id=log_id, job=job)
+ except LogStorage.DoesNotExist:
+ raise AdcmEx(
+ 'LOG_NOT_FOUND', f'log {log_id} not found for job {job_id}'
+ ) from None
+ serializer = self.serializer_class(log_storage, context={'request': request})
+ return Response(serializer.data)
def download_log_file(request, job_id, log_id):
- try:
- job = JobLog.objects.get(id=job_id)
- log_storage = LogStorage.objects.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
- except AdcmEx as e:
- raise AdcmApiEx(e.code, e.msg, e.http_code) from e
+ 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
class LogFile(GenericAPIView):
queryset = LogStorage.objects.all()
- serializer_class = LogSerializer
+ serializer_class = serializers.LogSerializer
def get(self, request, job_id, tag, level, log_type):
"""
@@ -152,12 +144,9 @@ def get(self, request, job_id, tag, level, log_type):
_type = 'check'
tag = 'ansible'
- ls = LogStorage.objects.get(job_id=job_id, name=tag, type=_type, format=log_type)
- try:
- serializer = self.serializer_class(ls, context={'request': request})
- return Response(serializer.data)
- except AdcmEx as e:
- raise AdcmApiEx(e.code, e.msg, e.http_code) from e
+ ls = LogStorage.obj.get(job_id=job_id, name=tag, type=_type, format=log_type)
+ serializer = self.serializer_class(ls, context={'request': request})
+ return Response(serializer.data)
class Task(PageView):
@@ -166,9 +155,9 @@ class Task(PageView):
List all tasks
"""
queryset = TaskLog.objects.order_by('-id')
- serializer_class = TaskListSerializer
- serializer_class_ui = TaskSerializer
- post_serializer = TaskPostSerializer
+ serializer_class = serializers.TaskListSerializer
+ serializer_class_ui = serializers.TaskSerializer
+ post_serializer = serializers.TaskPostSerializer
filterset_fields = ('action_id', 'pid', 'status', 'start_date', 'finish_date')
ordering_fields = ('status', 'start_date', 'finish_date')
@@ -187,38 +176,27 @@ class TaskDetail(DetailViewRO):
Show task
"""
queryset = TaskLog.objects.all()
- serializer_class = TaskSerializer
+ serializer_class = serializers.TaskSerializer
lookup_field = 'id'
lookup_url_kwarg = 'task_id'
error_code = 'TASK_NOT_FOUND'
- def get_object(self):
- task = super().get_object()
- task.jobs = JobLog.objects.filter(task_id=task.id)
- return task
-
class TaskReStart(GenericAPIView):
queryset = TaskLog.objects.all()
- serializer_class = TaskSerializer
+ serializer_class = serializers.TaskSerializer
def put(self, request, task_id):
task = check_obj(TaskLog, task_id, 'TASK_NOT_FOUND')
- try:
- restart_task(task)
- except AdcmEx as e:
- raise AdcmApiEx(e.code, e.msg, e.http_code) from e
+ restart_task(task)
return Response(status=status.HTTP_200_OK)
class TaskCancel(GenericAPIView):
queryset = TaskLog.objects.all()
- serializer_class = TaskSerializer
+ serializer_class = serializers.TaskSerializer
def put(self, request, task_id):
task = check_obj(TaskLog, task_id, 'TASK_NOT_FOUND')
- try:
- cancel_task(task)
- except AdcmEx as e:
- raise AdcmApiEx(e.code, e.msg, e.http_code) from e
+ cancel_task(task)
return Response(status=status.HTTP_200_OK)
diff --git a/python/api/provider/__init__.py b/python/api/provider/__init__.py
new file mode 100644
index 0000000000..824dd6c8fe
--- /dev/null
+++ b/python/api/provider/__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/provider/serializers.py b/python/api/provider/serializers.py
new file mode 100644
index 0000000000..ab85fde80b
--- /dev/null
+++ b/python/api/provider/serializers.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.
+
+from django.db import IntegrityError
+from rest_framework import serializers
+
+import cm
+from cm.errors import AdcmEx
+from cm.models import Action, Prototype
+
+from api.api_views import hlink, check_obj, filter_actions, get_upgradable_func
+from api.api_views import CommonAPIURL, ObjectURL
+from api.serializers import UpgradeSerializer, UrlField
+from api.action.serializers import ActionShort
+
+
+class ProviderSerializer(serializers.Serializer):
+ id = serializers.IntegerField(read_only=True)
+ name = serializers.CharField()
+ prototype_id = serializers.IntegerField()
+ description = serializers.CharField(required=False)
+ state = serializers.CharField(read_only=True)
+ url = hlink('provider-details', 'id', 'provider_id')
+
+ def validate_prototype_id(self, prototype_id):
+ proto = check_obj(
+ Prototype, {'id': prototype_id, 'type': 'provider'}, "PROTOTYPE_NOT_FOUND"
+ )
+ return proto
+
+ def create(self, validated_data):
+ try:
+ return cm.api.add_host_provider(
+ validated_data.get('prototype_id'),
+ validated_data.get('name'),
+ validated_data.get('description', '')
+ )
+ except IntegrityError:
+ raise AdcmEx("PROVIDER_CONFLICT") from None
+
+
+class ProviderDetailSerializer(ProviderSerializer):
+ issue = serializers.SerializerMethodField()
+ edition = serializers.CharField(read_only=True)
+ license = serializers.CharField(read_only=True)
+ bundle_id = serializers.IntegerField(read_only=True)
+ prototype = hlink('provider-type-details', 'prototype_id', 'prototype_id')
+ config = CommonAPIURL(view_name='object-config')
+ action = CommonAPIURL(view_name='object-action')
+ upgrade = hlink('provider-upgrade', 'id', 'provider_id')
+ host = ObjectURL(read_only=True, view_name='host')
+
+ def get_issue(self, obj):
+ return cm.issue.aggregate_issues(obj)
+
+
+class ProviderUISerializer(ProviderDetailSerializer):
+ actions = serializers.SerializerMethodField()
+ prototype_version = serializers.SerializerMethodField()
+ prototype_name = serializers.SerializerMethodField()
+ prototype_display_name = serializers.SerializerMethodField()
+ upgradable = serializers.SerializerMethodField()
+ get_upgradable = get_upgradable_func
+
+ def get_actions(self, obj):
+ act_set = Action.objects.filter(prototype=obj.prototype)
+ self.context['object'] = obj
+ self.context['provider_id'] = obj.id
+ actions = ActionShort(filter_actions(obj, act_set), many=True, context=self.context)
+ return actions.data
+
+ def get_prototype_version(self, obj):
+ return obj.prototype.version
+
+ def get_prototype_name(self, obj):
+ return obj.prototype.name
+
+ def get_prototype_display_name(self, obj):
+ return obj.prototype.display_name
+
+
+class UpgradeProviderSerializer(UpgradeSerializer):
+ class MyUrlField(UrlField):
+ def get_kwargs(self, obj):
+ return {'provider_id': self.context['provider_id'], 'upgrade_id': obj.id}
+
+ url = MyUrlField(read_only=True, view_name='provider-upgrade-details')
+ do = MyUrlField(read_only=True, view_name='do-provider-upgrade')
diff --git a/python/api/provider/urls.py b/python/api/provider/urls.py
new file mode 100644
index 0000000000..2f90813d17
--- /dev/null
+++ b/python/api/provider/urls.py
@@ -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.
+
+from django.urls import path, include
+from . import views
+
+
+urlpatterns = [
+ path('', views.ProviderList.as_view(), name='provider'),
+ path('/', include([
+ path('', views.ProviderDetail.as_view(), name='provider-details'),
+ path('host/', include('api.host.provider_urls')),
+ path('action/', include('api.action.urls'), {'object_type': 'provider'}),
+ path('config/', include('api.config.urls'), {'object_type': 'provider'}),
+ path('upgrade/', include([
+ path('', views.ProviderUpgrade.as_view(), name='provider-upgrade'),
+ path('/', include([
+ path('', views.ProviderUpgradeDetail.as_view(), name='provider-upgrade-details'),
+ path('do/', views.DoProviderUpgrade.as_view(), name='do-provider-upgrade'),
+ ])),
+ ])),
+ ])),
+]
diff --git a/python/api/provider/views.py b/python/api/provider/views.py
new file mode 100644
index 0000000000..2c52d5d8e1
--- /dev/null
+++ b/python/api/provider/views.py
@@ -0,0 +1,104 @@
+# 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 import status
+from rest_framework.response import Response
+
+import cm
+from cm.models import HostProvider, Upgrade
+
+from api.api_views import create, check_obj, ListView, PageView, PageViewAdd, DetailViewRO
+from api.api_views import GenericAPIPermView
+import api.serializers
+from . import serializers
+
+
+class ProviderList(PageViewAdd):
+ """
+ get:
+ List all host providers
+
+ post:
+ Create new host provider
+ """
+ queryset = HostProvider.objects.all()
+ serializer_class = serializers.ProviderSerializer
+ serializer_class_ui = serializers.ProviderUISerializer
+ serializer_class_post = serializers.ProviderDetailSerializer
+ filterset_fields = ('name', 'prototype_id')
+ ordering_fields = ('name', 'state', 'prototype__display_name', 'prototype__version_order')
+
+
+class ProviderDetail(DetailViewRO):
+ """
+ get:
+ Show host provider
+ """
+ queryset = HostProvider.objects.all()
+ serializer_class = serializers.ProviderDetailSerializer
+ serializer_class_ui = serializers.ProviderUISerializer
+ lookup_field = 'id'
+ lookup_url_kwarg = 'provider_id'
+ error_code = 'PROVIDER_NOT_FOUND'
+
+ def delete(self, request, provider_id): # pylint: disable=arguments-differ
+ """
+ Remove host provider
+ """
+ provider = check_obj(HostProvider, provider_id, 'PROVIDER_NOT_FOUND')
+ cm.api.delete_host_provider(provider)
+ return Response(status=status.HTTP_204_NO_CONTENT)
+
+
+class ProviderUpgrade(PageView):
+ queryset = Upgrade.objects.all()
+ serializer_class = serializers.UpgradeProviderSerializer
+
+ def get(self, request, provider_id): # pylint: disable=arguments-differ
+ """
+ List all avaliable upgrades for specified host provider
+ """
+ provider = check_obj(HostProvider, provider_id, 'PROVIDER_NOT_FOUND')
+ obj = cm.upgrade.get_upgrade(provider, self.get_ordering(request, self.queryset, self))
+ serializer = self.serializer_class(obj, many=True, context={
+ 'provider_id': provider.id, 'request': request
+ })
+ return Response(serializer.data)
+
+
+class ProviderUpgradeDetail(ListView):
+ queryset = Upgrade.objects.all()
+ serializer_class = serializers.UpgradeProviderSerializer
+
+ def get(self, request, provider_id, upgrade_id): # pylint: disable=arguments-differ
+ """
+ List all avaliable upgrades for specified host provider
+ """
+ provider = check_obj(HostProvider, provider_id, 'PROVIDER_NOT_FOUND')
+ obj = self.get_queryset().get(id=upgrade_id)
+ serializer = self.serializer_class(obj, context={
+ 'provider_id': provider.id, 'request': request
+ })
+ return Response(serializer.data)
+
+
+class DoProviderUpgrade(GenericAPIPermView):
+ queryset = Upgrade.objects.all()
+ serializer_class = api.serializers.DoUpgradeSerializer
+
+ def post(self, request, provider_id, upgrade_id):
+ """
+ Do upgrade specified host provider
+ """
+ provider = check_obj(HostProvider, provider_id, 'PROVIDER_NOT_FOUND')
+ serializer = self.serializer_class(data=request.data, context={'request': request})
+ return create(serializer, upgrade_id=int(upgrade_id), obj=provider)
diff --git a/python/api/serializers.py b/python/api/serializers.py
index 03a7ea5b1c..b940959b4e 100644
--- a/python/api/serializers.py
+++ b/python/api/serializers.py
@@ -1,33 +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.
import django.contrib.auth
import rest_framework.authtoken.serializers
-from django.contrib.auth.models import User, Group
-from django.db import IntegrityError, transaction
from rest_framework import serializers
-from rest_framework.authtoken.models import Token
import cm.job
import cm.stack
import cm.status_api
import cm.adcm_config
-from cm.api import safe_api
-from cm.errors import AdcmApiEx, AdcmEx
-from cm.models import Action, Prototype, UserProfile, Upgrade, HostProvider, Role
+from cm.errors import AdcmEx
+from cm.models import Upgrade
-from api.api_views import check_obj, hlink, filter_actions, get_upgradable_func
-from api.api_views import UrlField, CommonAPIURL
-from api.action.serializers import ActionShort
+from api.api_views import check_obj, hlink
+from api.api_views import UrlField
class AuthSerializer(rest_framework.authtoken.serializers.AuthTokenSerializer):
@@ -37,7 +21,7 @@ def validate(self, attrs):
password=attrs.get('password')
)
if not user:
- raise AdcmApiEx('AUTH_ERROR', 'Wrong user or password')
+ raise AdcmEx('AUTH_ERROR', 'Wrong user or password')
attrs['user'] = user
return attrs
@@ -46,410 +30,10 @@ class LogOutSerializer(serializers.Serializer):
pass
-class PermSerializer(serializers.Serializer):
- name = serializers.CharField()
- codename = serializers.CharField()
- app_label = serializers.SerializerMethodField()
- model = serializers.SerializerMethodField()
-
- def get_app_label(self, obj):
- return obj.content_type.app_label
-
- def get_model(self, obj):
- return obj.content_type.model
-
-
-class RoleSerializer(serializers.Serializer):
- id = serializers.IntegerField(read_only=True)
- name = serializers.CharField(read_only=True)
- description = serializers.CharField(read_only=True)
- url = hlink('role-details', 'id', 'role_id')
-
-
-class RoleDetailSerializer(RoleSerializer):
- permissions = PermSerializer(many=True, read_only=True)
-
-
-class GroupSerializer(serializers.Serializer):
- name = serializers.CharField()
- url = hlink('group-details', 'name', 'name')
- change_role = hlink('change-group-role', 'name', 'name')
-
- @transaction.atomic
- def create(self, validated_data):
- try:
- return Group.objects.create(name=validated_data.get('name'))
- except IntegrityError:
- raise AdcmApiEx("GROUP_CONFLICT", 'group already exists') from None
-
-
-class GroupDetailSerializer(GroupSerializer):
- permissions = PermSerializer(many=True, read_only=True)
- role = RoleSerializer(many=True, source='role_set')
-
-
-class UserSerializer(serializers.Serializer):
- username = serializers.CharField()
- password = serializers.CharField(write_only=True)
- url = hlink('user-details', 'username', 'username')
- change_group = hlink('add-user-group', 'username', 'username')
- change_password = hlink('user-passwd', 'username', 'username')
- change_role = hlink('change-user-role', 'username', 'username')
- is_superuser = serializers.BooleanField(required=False)
-
- @transaction.atomic
- def create(self, validated_data):
- try:
- user = User.objects.create_user(
- validated_data.get('username'),
- password=validated_data.get('password'),
- is_superuser=validated_data.get('is_superuser', True)
- )
- UserProfile.objects.create(login=validated_data.get('username'))
- return user
- except IntegrityError:
- raise AdcmApiEx("USER_CONFLICT", 'user already exists') from None
-
-
-class UserDetailSerializer(UserSerializer):
- user_permissions = PermSerializer(many=True)
- groups = GroupSerializer(many=True)
- role = RoleSerializer(many=True, source='role_set')
-
-
-class AddUser2GroupSerializer(serializers.Serializer):
- name = serializers.CharField()
-
- def update(self, user, validated_data): # pylint: disable=arguments-differ
- group = check_obj(Group, {'name': validated_data.get('name')}, 'GROUP_NOT_FOUND')
- group.user_set.add(user)
- return group
-
-
-class AddUserRoleSerializer(serializers.Serializer):
- role_id = serializers.IntegerField()
- name = serializers.CharField(read_only=True)
-
- def update(self, user, validated_data): # pylint: disable=arguments-differ
- role = check_obj(Role, {'id': validated_data.get('role_id')}, 'ROLE_NOT_FOUND')
- return safe_api(cm.api.add_user_role, (user, role))
-
-
-class AddGroupRoleSerializer(serializers.Serializer):
- role_id = serializers.IntegerField()
- name = serializers.CharField(read_only=True)
-
- def update(self, group, validated_data): # pylint: disable=arguments-differ
- role = check_obj(Role, {'id': validated_data.get('role_id')}, 'ROLE_NOT_FOUND')
- return safe_api(cm.api.add_group_role, (group, role))
-
-
-class UserPasswdSerializer(serializers.Serializer):
- token = serializers.CharField(read_only=True, source='key')
- password = serializers.CharField(write_only=True)
-
- @transaction.atomic
- def update(self, user, validated_data): # pylint: disable=arguments-differ
- user.set_password(validated_data.get('password'))
- user.save()
- token = Token.objects.get(user=user)
- token.delete()
- token.key = token.generate_key()
- token.user = user
- token.save()
- return token
-
-
-class ProfileDetailSerializer(serializers.Serializer):
- class MyUrlField(UrlField):
- def get_kwargs(self, obj):
- return {'username': obj.login}
-
- username = serializers.CharField(read_only=True, source='login')
- change_password = MyUrlField(read_only=True, view_name='profile-passwd')
- profile = serializers.JSONField()
-
- def validate_profile(self, raw):
- if isinstance(raw, str):
- raise AdcmApiEx('JSON_ERROR', 'profile should not be just one string')
- return raw
-
- def update(self, instance, validated_data):
- instance.profile = validated_data.get('profile', instance.profile)
- try:
- instance.save()
- except IntegrityError:
- raise AdcmApiEx("USER_CONFLICT") from None
- return instance
-
-
-class ProfileSerializer(ProfileDetailSerializer):
- username = serializers.CharField(source='login')
- url = hlink('profile-details', 'login', 'username')
-
- def create(self, validated_data):
- check_obj(User, {'username': validated_data.get('login')}, 'USER_NOT_FOUND')
- try:
- return UserProfile.objects.create(**validated_data)
- except IntegrityError:
- raise AdcmApiEx("USER_CONFLICT") from None
-
-
class EmptySerializer(serializers.Serializer):
id = serializers.IntegerField(read_only=True)
-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 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')
-
- def get_prototype_version(self, obj):
- return obj.prototype.version
-
-
-class ProviderSerializer(serializers.Serializer):
- id = serializers.IntegerField(read_only=True)
- name = serializers.CharField()
- prototype_id = serializers.IntegerField()
- description = serializers.CharField(required=False)
- state = serializers.CharField(read_only=True)
- url = hlink('provider-details', 'id', 'provider_id')
-
- def validate_prototype_id(self, prototype_id):
- proto = check_obj(
- Prototype, {'id': prototype_id, 'type': 'provider'}, "PROTOTYPE_NOT_FOUND"
- )
- return proto
-
- def create(self, validated_data):
- try:
- return cm.api.add_host_provider(
- validated_data.get('prototype_id'),
- validated_data.get('name'),
- validated_data.get('description', '')
- )
- except Prototype.DoesNotExist:
- raise AdcmApiEx('PROTOTYPE_NOT_FOUND') from None
- except IntegrityError:
- raise AdcmApiEx("PROVIDER_CONFLICT") from None
- except AdcmEx as e:
- raise AdcmApiEx(e.code, e.msg, e.http_code) from e
-
-
-class ProviderDetailSerializer(ProviderSerializer):
- issue = serializers.SerializerMethodField()
- edition = serializers.CharField(read_only=True)
- license = serializers.CharField(read_only=True)
- bundle_id = serializers.IntegerField(read_only=True)
- prototype = hlink('provider-type-details', 'prototype_id', 'prototype_id')
- config = CommonAPIURL(view_name='object-config')
- action = CommonAPIURL(view_name='object-action')
- upgrade = hlink('provider-upgrade', 'id', 'provider_id')
- host = hlink('provider-host', 'id', 'provider_id')
-
- def get_issue(self, obj):
- return cm.issue.get_issue(obj)
-
-
-class ProviderUISerializer(ProviderDetailSerializer):
- actions = serializers.SerializerMethodField()
- prototype_version = serializers.SerializerMethodField()
- prototype_name = serializers.SerializerMethodField()
- prototype_display_name = serializers.SerializerMethodField()
- upgradable = serializers.SerializerMethodField()
- get_upgradable = get_upgradable_func
-
- def get_actions(self, obj):
- act_set = Action.objects.filter(prototype=obj.prototype)
- self.context['object'] = obj
- self.context['provider_id'] = obj.id
- actions = ActionShort(filter_actions(obj, act_set), many=True, context=self.context)
- return actions.data
-
- def get_prototype_version(self, obj):
- return obj.prototype.version
-
- def get_prototype_name(self, obj):
- return obj.prototype.name
-
- def get_prototype_display_name(self, obj):
- return obj.prototype.display_name
-
-
-class HostSerializer(serializers.Serializer):
- id = serializers.IntegerField(read_only=True)
- cluster_id = serializers.IntegerField(read_only=True)
- prototype_id = serializers.IntegerField(help_text='id of host type')
- provider_id = serializers.IntegerField()
- fqdn = serializers.CharField(help_text='fully qualified domain name')
- description = serializers.CharField(required=False)
- state = serializers.CharField(read_only=True)
- url = hlink('host-details', 'id', 'host_id')
-
- def get_issue(self, obj):
- return cm.issue.get_issue(obj)
-
- def validate_prototype_id(self, prototype_id):
- proto = check_obj(
- Prototype, {'id': prototype_id, 'type': 'host'}, "PROTOTYPE_NOT_FOUND"
- )
- return proto
-
- def validate_provider_id(self, provider_id):
- provider = check_obj(HostProvider, provider_id, "PROVIDER_NOT_FOUND")
- return provider
-
- def validate_fqdn(self, name):
- try:
- return cm.stack.validate_name(name, 'Host name')
- except AdcmEx as e:
- raise AdcmApiEx(e.code, e.msg, e.http_code) from e
-
- def create(self, validated_data):
- try:
- return cm.api.add_host(
- validated_data.get('prototype_id'),
- validated_data.get('provider_id'),
- validated_data.get('fqdn'),
- validated_data.get('description', '')
- )
- except Prototype.DoesNotExist:
- raise AdcmApiEx('PROTOTYPE_NOT_FOUND') from None
- except IntegrityError:
- raise AdcmApiEx("HOST_CONFLICT", "duplicate host") from None
- except AdcmEx as e:
- raise AdcmApiEx(e.code, e.msg, e.http_code) from e
-
- def update(self, instance, validated_data):
- instance.cluster_id = validated_data.get('cluster_id')
- instance.save()
- return instance
-
-
-class HostDetailSerializer(HostSerializer):
- # stack = serializers.JSONField(read_only=True)
- issue = serializers.SerializerMethodField()
- bundle_id = serializers.IntegerField(read_only=True)
- status = serializers.SerializerMethodField()
- config = CommonAPIURL(view_name='object-config')
- action = CommonAPIURL(view_name='object-action')
- prototype = hlink('host-type-details', 'prototype_id', 'prototype_id')
-
- def get_issue(self, obj):
- return cm.issue.get_issue(obj)
-
- def get_status(self, obj):
- return cm.status_api.get_host_status(obj.id)
-
-
-class HostUISerializer(HostDetailSerializer):
- actions = serializers.SerializerMethodField()
- cluster_name = serializers.SerializerMethodField()
- prototype_version = serializers.SerializerMethodField()
- prototype_name = serializers.SerializerMethodField()
- prototype_display_name = serializers.SerializerMethodField()
- provider_name = serializers.SerializerMethodField()
-
- def get_actions(self, obj):
- act_set = Action.objects.filter(prototype=obj.prototype)
- self.context['object'] = obj
- self.context['host_id'] = obj.id
- actions = ActionShort(filter_actions(obj, act_set), many=True, context=self.context)
- return actions.data
-
- def get_cluster_name(self, obj):
- if obj.cluster:
- return obj.cluster.name
- return None
-
- def get_prototype_version(self, obj):
- return obj.prototype.version
-
- def get_prototype_name(self, obj):
- return obj.prototype.name
-
- def get_prototype_display_name(self, obj):
- return obj.prototype.display_name
-
- def get_provider_name(self, obj):
- if obj.provider:
- return obj.provider.name
- return None
-
-
-class ProviderHostSerializer(serializers.Serializer):
- id = serializers.IntegerField(read_only=True)
- cluster_id = serializers.IntegerField(read_only=True)
- prototype_id = serializers.IntegerField(required=False, read_only=True)
- provider_id = serializers.IntegerField(required=False, read_only=True)
- fqdn = serializers.CharField(help_text='fully qualified domain name')
- description = serializers.CharField(required=False)
- state = serializers.CharField(read_only=True)
- # stack = serializers.JSONField(read_only=True)
- url = hlink('host-details', 'id', 'host_id')
-
- def validate_fqdn(self, name):
- try:
- return cm.stack.validate_name(name, 'Host name')
- except AdcmEx as e:
- raise AdcmApiEx(e.code, e.msg, e.http_code) from e
-
- def create(self, validated_data):
- provider = validated_data.get('provider')
- try:
- proto = Prototype.objects.get(bundle=provider.prototype.bundle, type='host')
- except Prototype.DoesNotExist:
- raise AdcmApiEx('PROTOTYPE_NOT_FOUND') from None
- try:
- return cm.api.add_host(
- proto,
- self.context.get('provider'),
- validated_data.get('fqdn'),
- validated_data.get('description', '')
- )
- except IntegrityError:
- raise AdcmApiEx("HOST_CONFLICT", "duplicate host") from None
- except AdcmEx as e:
- raise AdcmApiEx(e.code, e.msg, e.http_code) from e
-
-
-class ActionSerializer(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)
-
-
-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 UpgradeSerializer(serializers.Serializer):
id = serializers.IntegerField(read_only=True)
name = serializers.CharField(required=False)
@@ -476,27 +60,10 @@ def get_kwargs(self, obj):
do = MyUrlField(read_only=True, view_name='do-cluster-upgrade')
-class UpgradeProviderSerializer(UpgradeSerializer):
- class MyUrlField(UrlField):
- def get_kwargs(self, obj):
- return {'provider_id': self.context['provider_id'], 'upgrade_id': obj.id}
-
- url = MyUrlField(read_only=True, view_name='provider-upgrade-details')
- do = MyUrlField(read_only=True, view_name='do-provider-upgrade')
-
-
class DoUpgradeSerializer(serializers.Serializer):
id = serializers.IntegerField(read_only=True)
upgradable = serializers.BooleanField(read_only=True)
def create(self, validated_data):
- try:
- upgrade = check_obj(Upgrade, validated_data.get('upgrade_id'), 'UPGRADE_NOT_FOUND')
- return cm.upgrade.do_upgrade(validated_data.get('obj'), upgrade)
- except AdcmEx as e:
- raise AdcmApiEx(e.code, e.msg, e.http_code) from e
-
-
-class StatsSerializer(serializers.Serializer):
- task = hlink('task-stats', 'id', 'task_id')
- job = hlink('job-stats', 'id', 'job_id')
+ upgrade = check_obj(Upgrade, validated_data.get('upgrade_id'), 'UPGRADE_NOT_FOUND')
+ return cm.upgrade.do_upgrade(validated_data.get('obj'), upgrade)
diff --git a/python/api/service/serializers.py b/python/api/service/serializers.py
index 0baef3f914..580980f14b 100644
--- a/python/api/service/serializers.py
+++ b/python/api/service/serializers.py
@@ -16,35 +16,18 @@
from rest_framework import serializers
from rest_framework.reverse import reverse
-from api.api_views import check_obj, filter_actions, CommonAPIURL
-from api.cluster_serial import BindSerializer
+from api.api_views import check_obj, filter_actions, CommonAPIURL, ObjectURL
+from api.cluster.serializers import BindSerializer
from api.action.serializers import ActionShort
+from api.component.serializers import ComponentUISerializer
from cm import issue
from cm import status_api
from cm.api import add_service_to_cluster, multi_bind, bind
-from cm.errors import AdcmApiEx, AdcmEx
+from cm.errors import AdcmEx
from cm.models import Prototype, Action, ServiceComponent, Cluster
-class ServiceObjectUrlField(serializers.HyperlinkedIdentityField):
- def get_url(self, obj, view_name, request, format):
- kwargs = {'service_id': obj.id}
- return reverse(view_name, kwargs=kwargs, request=request, format=format)
-
-
-class ServiceComponentDetailsUrlField(serializers.HyperlinkedIdentityField):
- def get_url(self, obj, view_name, request, format):
- kwargs = {'service_id': obj.service.id, 'component_id': obj.id}
- return reverse(view_name, kwargs=kwargs, request=request, format=format)
-
-
-class ServiceActionDetailsUrlField(serializers.HyperlinkedIdentityField):
- def get_url(self, obj, view_name, request, format):
- kwargs = {'service_id': self.context['service_id'], 'action_id': obj.id}
- return reverse(view_name, kwargs=kwargs, request=request, format=format)
-
-
class ServiceSerializer(serializers.Serializer):
id = serializers.IntegerField(read_only=True)
cluster_id = serializers.IntegerField(required=True)
@@ -52,7 +35,7 @@ class ServiceSerializer(serializers.Serializer):
display_name = serializers.CharField(read_only=True)
state = serializers.CharField(read_only=True)
prototype_id = serializers.IntegerField(required=True, help_text='id of service prototype')
- url = ServiceObjectUrlField(read_only=True, view_name='service-details')
+ url = ObjectURL(read_only=True, view_name='service-details')
def validate_prototype_id(self, prototype_id):
prototype = check_obj(
@@ -61,15 +44,22 @@ def validate_prototype_id(self, prototype_id):
def create(self, validated_data):
try:
- cluster = check_obj(
- Cluster, {'id': validated_data['cluster_id']}, 'CLUSTER_NOT_FOUND')
- prototype = check_obj(
- Prototype, {'id': validated_data['prototype_id']}, 'PROTOTYPE_NOT_FOUND')
+ cluster = check_obj(Cluster, validated_data['cluster_id'])
+ prototype = check_obj(Prototype, validated_data['prototype_id'])
return add_service_to_cluster(cluster, prototype)
except IntegrityError:
- raise AdcmApiEx('SERVICE_CONFLICT') from None
- except AdcmEx as e:
- raise AdcmApiEx(e.code, e.msg, e.http_code) from e
+ raise AdcmEx('SERVICE_CONFLICT') from None
+
+
+class ClusterServiceSerializer(ServiceSerializer):
+ cluster_id = serializers.IntegerField(read_only=True)
+
+ def create(self, validated_data):
+ try:
+ cluster = check_obj(Cluster, self.context.get('cluster_id'))
+ return add_service_to_cluster(cluster, validated_data['prototype_id'])
+ except IntegrityError:
+ raise AdcmEx('SERVICE_CONFLICT') from None
class ServiceDetailSerializer(ServiceSerializer):
@@ -81,42 +71,20 @@ class ServiceDetailSerializer(ServiceSerializer):
monitoring = serializers.CharField(read_only=True)
action = CommonAPIURL(read_only=True, view_name='object-action')
config = CommonAPIURL(read_only=True, view_name='object-config')
- component = ServiceObjectUrlField(read_only=True, view_name='service-component')
- imports = ServiceObjectUrlField(read_only=True, view_name='service-import')
- bind = ServiceObjectUrlField(read_only=True, view_name='service-bind')
+ 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 = serializers.HyperlinkedIdentityField(
view_name='service-type-details', lookup_field='prototype_id',
lookup_url_kwarg='prototype_id')
def get_issue(self, obj):
- return issue.get_issue(obj)
+ return issue.aggregate_issues(obj)
def get_status(self, obj):
return status_api.get_service_status(obj.cluster.id, obj.id)
-class ServiceComponentSerializer(serializers.Serializer):
- id = serializers.IntegerField(read_only=True)
- name = serializers.CharField(read_only=True)
- prototype_id = serializers.SerializerMethodField()
- display_name = serializers.CharField(read_only=True)
- description = serializers.CharField(read_only=True)
- url = ServiceComponentDetailsUrlField(read_only=True, view_name='service-component-details')
-
- def get_prototype_id(self, obj):
- return obj.prototype.id
-
-
-class ServiceComponentDetailSerializer(ServiceComponentSerializer):
- constraint = serializers.JSONField(read_only=True)
- requires = serializers.JSONField(read_only=True)
- monitoring = serializers.CharField(read_only=True)
- status = serializers.SerializerMethodField()
-
- def get_status(self, obj):
- return status_api.get_component_status(obj.id)
-
-
class ServiceUISerializer(ServiceDetailSerializer):
actions = serializers.SerializerMethodField()
components = serializers.SerializerMethodField()
@@ -135,29 +103,20 @@ def get_actions(self, obj):
def get_components(self, obj):
comps = ServiceComponent.objects.filter(service=obj, cluster=obj.cluster)
- return ServiceComponentDetailSerializer(comps, many=True, context=self.context).data
+ return ComponentUISerializer(comps, many=True, context=self.context).data
def get_version(self, obj):
return obj.prototype.version
-class ServiceComponentUrlField(serializers.HyperlinkedIdentityField):
- def get_url(self, obj, view_name, request, format):
- kwargs = {'service_id': obj.service.id, 'component_id': obj.id}
- return reverse(view_name, kwargs=kwargs, request=request, format=format)
-
-
class ImportPostSerializer(serializers.Serializer):
bind = serializers.JSONField()
def create(self, validated_data):
- try:
- binds = validated_data.get('bind')
- service = self.context.get('service')
- cluster = self.context.get('cluster')
- return multi_bind(cluster, service, binds)
- except AdcmEx as error:
- raise AdcmApiEx(error.code, error.msg, error.http_code, error.adds) from error
+ binds = validated_data.get('bind')
+ service = self.context.get('service')
+ cluster = self.context.get('cluster')
+ return multi_bind(cluster, service, binds)
class ServiceBindUrlFiels(serializers.HyperlinkedIdentityField):
@@ -179,15 +138,10 @@ class ServiceBindPostSerializer(serializers.Serializer):
export_cluster_prototype_name = serializers.CharField(read_only=True)
def create(self, validated_data):
- export_cluster = check_obj(
- Cluster, validated_data.get('export_cluster_id'), 'CLUSTER_NOT_FOUND'
+ export_cluster = check_obj(Cluster, validated_data.get('export_cluster_id'))
+ return bind(
+ validated_data.get('cluster'),
+ validated_data.get('service'),
+ export_cluster,
+ validated_data.get('export_service_id')
)
- try:
- return bind(
- validated_data.get('cluster'),
- validated_data.get('service'),
- export_cluster,
- validated_data.get('export_service_id')
- )
- except AdcmEx as error:
- raise AdcmApiEx(error.code, error.msg, error.http_code) from error
diff --git a/python/api/service/urls.py b/python/api/service/urls.py
index b2a6519594..b2854b0653 100644
--- a/python/api/service/urls.py
+++ b/python/api/service/urls.py
@@ -19,11 +19,7 @@
path('', views.ServiceListView.as_view(), name='service'),
path('/', include([
path('', views.ServiceDetailView.as_view(), name='service-details'),
- path('component/', include([
- path('', views.ServiceComponentListView.as_view(), name='service-component'),
- path('/', views.ServiceComponentDetailView.as_view(),
- name='service-component-details'),
- ])),
+ path('component/', include('api.component.urls')),
path('import/', views.ServiceImportView.as_view(), name='service-import'),
path('bind/', include([
path('', views.ServiceBindView.as_view(), name='service-bind'),
diff --git a/python/api/service/views.py b/python/api/service/views.py
index 4fc287bb30..634e47f1f4 100644
--- a/python/api/service/views.py
+++ b/python/api/service/views.py
@@ -10,26 +10,32 @@
# See the License for the specific language governing permissions and
# limitations under the License.
-from django.db import models
from rest_framework import status
from rest_framework.response import Response
from api.api_views import (
- PageView, create, check_obj, DetailViewRO, ListView, GenericAPIPermView, DetailViewDelete
+ PageView, create, check_obj, DetailViewRO, ListView, DetailViewDelete
)
-from api.stack_serial import ImportSerializer
-from api.cluster_serial import BindSerializer
+from api.stack.serializers import ImportSerializer
+from api.cluster.serializers import BindSerializer
from cm.api import delete_service, get_import, unbind
-from cm.errors import AdcmEx, AdcmApiEx
-from cm.models import ClusterObject, ServiceComponent, Prototype, ClusterBind
+from cm.models import Cluster, ClusterObject, Prototype, ClusterBind
from . import serializers
+def check_service(kwargs):
+ service = check_obj(ClusterObject, kwargs['service_id'])
+ if 'cluster_id' in kwargs:
+ check_obj(Cluster, kwargs['cluster_id'])
+ return service
+
+
class ServiceListView(PageView):
queryset = ClusterObject.objects.all()
serializer_class = serializers.ServiceSerializer
serializer_class_ui = serializers.ServiceUISerializer
+ serializer_class_cluster = serializers.ClusterServiceSerializer
filterset_fields = ('cluster_id', )
ordering_fields = ('state', 'prototype__display_name', 'prototype__version_order')
@@ -37,13 +43,22 @@ def get(self, request, *args, **kwargs):
"""
List all services
"""
- return self.get_page(self.filter_queryset(self.get_queryset()), request)
+ queryset = self.get_queryset()
+ if 'cluster_id' in kwargs:
+ cluster = check_obj(Cluster, kwargs['cluster_id'])
+ queryset = self.get_queryset().filter(cluster=cluster)
+ return self.get_page(self.filter_queryset(queryset), request)
def post(self, request, *args, **kwargs):
"""
Add service to cluster
"""
- serializer = self.serializer_class(data=request.data, context={'request': request})
+ serializer_class = self.serializer_class
+ if 'cluster_id' in kwargs:
+ serializer_class = self.serializer_class_cluster
+ serializer = serializer_class(data=request.data, context={
+ 'request': request, 'cluster_id': kwargs.get('cluster_id', None)}
+ )
return create(serializer)
@@ -56,7 +71,7 @@ def get(self, request, *args, **kwargs):
"""
Show service
"""
- service = check_obj(ClusterObject, {'id': kwargs['service_id']}, 'SERVICE_NOT_FOUND')
+ service = check_service(kwargs)
serial_class = self.select_serializer(request)
serializer = serial_class(service, context={'request': request})
return Response(serializer.data)
@@ -65,44 +80,11 @@ def delete(self, request, *args, **kwargs):
"""
Remove service from cluster
"""
- service = check_obj(ClusterObject, {'id': kwargs['service_id']}, 'SERVICE_NOT_FOUND')
- try:
- delete_service(service)
- except AdcmEx as error:
- raise AdcmApiEx(error.code, error.msg, error.http_code) from error
+ service = check_service(kwargs)
+ delete_service(service)
return Response(status=status.HTTP_204_NO_CONTENT)
-class ServiceComponentListView(PageView):
- queryset = ServiceComponent.objects.all()
- serializer_class = serializers.ServiceComponentSerializer
- serializer_class_ui = serializers.ServiceComponentDetailSerializer
- ordering_fields = ('component__display_name', )
-
- def get(self, request, *args, **kwargs):
- """
- Show components of service
- """
- service = check_obj(ClusterObject, {'id': kwargs['service_id']}, 'SERVICE_NOT_FOUND')
- components = self.filter_queryset(self.get_queryset().filter(service=service))
- return self.get_page(components, request)
-
-
-class ServiceComponentDetailView(GenericAPIPermView):
- queryset = ServiceComponent.objects.all()
- serializer_class = serializers.ServiceComponentDetailSerializer
-
- def get(self, request, service_id, component_id):
- """
- Show specified component of service
- """
- service = check_obj(ClusterObject, {'id': service_id}, 'SERVICE_NOT_FOUND')
- service_component = check_obj(
- ServiceComponent, {'id': component_id, 'service': service}, 'COMPONENT_NOT_FOUND')
- serializer = self.serializer_class(service_component, context={'request': request})
- return Response(serializer.data)
-
-
class ServiceImportView(ListView):
queryset = Prototype.objects.all()
serializer_class = ImportSerializer
@@ -112,21 +94,13 @@ def get(self, request, *args, **kwargs):
"""
List all imports available for specified service
"""
-
- service = check_obj(ClusterObject, {'id': kwargs['service_id']}, 'SERVICE_NOT_FOUND')
- try:
- cluster = service.cluster
- except models.ObjectDoesNotExist:
- raise AdcmApiEx('CLUSTER_NOT_FOUND') from None
-
+ service = check_service(kwargs)
+ cluster = service.cluster
return Response(get_import(cluster, service))
- def post(self, request, service_id):
- service = check_obj(ClusterObject, {'id': service_id}, 'SERVICE_NOT_FOUND')
- try:
- cluster = service.cluster
- except models.ObjectDoesNotExist:
- raise AdcmApiEx('CLUSTER_NOT_FOUND') from None
+ def post(self, request, **kwargs):
+ service = check_service(kwargs)
+ cluster = service.cluster
serializer = self.post_serializer_class(
data=request.data,
context={'request': request, 'cluster': cluster, 'service': service})
@@ -150,20 +124,17 @@ def get(self, request, *args, **kwargs):
"""
List all binds of service
"""
- service = check_obj(ClusterObject, {'id': kwargs['service_id']}, 'SERVICE_NOT_FOUND')
+ service = check_service(kwargs)
binds = self.get_queryset().filter(service=service)
serializer = self.get_serializer_class()(binds, many=True, context={'request': request})
return Response(serializer.data)
- def post(self, request, service_id):
+ def post(self, request, **kwargs):
"""
Bind two services
"""
- service = check_obj(ClusterObject, {'id': service_id}, 'SERVICE_NOT_FOUND')
- try:
- cluster = service.cluster
- except models.ObjectDoesNotExist:
- raise AdcmApiEx('CLUSTER_NOT_FOUND') from None
+ service = check_service(kwargs)
+ cluster = service.cluster
serializer = self.get_serializer_class()(data=request.data, context={'request': request})
return create(serializer, cluster=cluster, service=service)
@@ -172,19 +143,16 @@ class ServiceBindDetailView(DetailViewDelete):
queryset = ClusterBind.objects.all()
serializer_class = BindSerializer
- def get_obj(self, service_id, bind_id):
- service = check_obj(ClusterObject, service_id, 'SERVICE_NOT_FOUND')
- try:
- cluster = service.cluster
- except models.ObjectDoesNotExist:
- AdcmApiEx('CLUSTER_NOT_FOUND')
- return check_obj(ClusterBind, {'cluster': cluster, 'id': bind_id}, 'BIND_NOT_FOUND')
+ def get_obj(self, kwargs, bind_id):
+ service = check_service(kwargs)
+ cluster = service.cluster
+ return check_obj(ClusterBind, {'cluster': cluster, 'id': bind_id})
def get(self, request, *args, **kwargs):
"""
Show specified bind of service
"""
- bind = self.get_obj(kwargs['service_id'], kwargs['bind_id'])
+ bind = self.get_obj(kwargs, kwargs['bind_id'])
serializer = self.serializer_class(bind, context={'request': request})
return Response(serializer.data)
@@ -192,6 +160,6 @@ def delete(self, request, *args, **kwargs):
"""
Unbind specified bind of service
"""
- bind = self.get_obj(kwargs['service_id'], kwargs['bind_id'])
+ bind = self.get_obj(kwargs, kwargs['bind_id'])
unbind(bind)
return Response(status=status.HTTP_204_NO_CONTENT)
diff --git a/python/api/stack/__init__.py b/python/api/stack/__init__.py
new file mode 100644
index 0000000000..824dd6c8fe
--- /dev/null
+++ b/python/api/stack/__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/stack_serial.py b/python/api/stack/serializers.py
similarity index 100%
rename from python/api/stack_serial.py
rename to python/api/stack/serializers.py
diff --git a/python/api/stack/urls.py b/python/api/stack/urls.py
new file mode 100644
index 0000000000..3bb44e10eb
--- /dev/null
+++ b/python/api/stack/urls.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 django.urls import path, include
+from . import views
+
+
+PROTOTYPE_ID = '/'
+
+
+urlpatterns = [
+ path('', views.Stack.as_view(), name='stack'),
+ path('upload/', views.UploadBundle.as_view(), name='upload-bundle'),
+ path('load/', views.LoadBundle.as_view(), name='load-bundle'),
+ path('load/servicemap/', views.LoadServiceMap.as_view(), name='load-servicemap'),
+ 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'),
+ ])),
+]
diff --git a/python/api/stack_views.py b/python/api/stack/views.py
similarity index 72%
rename from python/api/stack_views.py
rename to python/api/stack/views.py
index 5ad2720eed..ae22a30b09 100644
--- a/python/api/stack_views.py
+++ b/python/api/stack/views.py
@@ -18,25 +18,24 @@
import cm.api
import cm.bundle
-from cm.errors import AdcmEx, AdcmApiEx
from cm.models import Bundle, Prototype, Action
from cm.models import PrototypeConfig, Upgrade, PrototypeExport
from cm.models import PrototypeImport
from cm.logger import log # pylint: disable=unused-import
-import api.serializers
-import api.stack_serial
from api.api_views import ListView, DetailViewRO, PageView, GenericAPIPermView, check_obj
+from api.action.serializers import StackActionSerializer
+from . import serializers
class CsrfOffSessionAuthentication(SessionAuthentication):
def enforce_csrf(self, request):
- return #
+ return
class Stack(GenericAPIPermView):
queryset = Prototype.objects.all()
- serializer_class = api.stack_serial.Stack
+ serializer_class = serializers.Stack
def get(self, request):
"""
@@ -49,7 +48,7 @@ def get(self, request):
class UploadBundle(GenericAPIPermView):
queryset = Bundle.objects.all()
- serializer_class = api.stack_serial.UploadBundle
+ serializer_class = serializers.UploadBundle
authentication_classes = (CsrfOffSessionAuthentication, TokenAuthentication)
parser_classes = (MultiPartParser,)
@@ -63,23 +62,20 @@ def post(self, request):
class LoadBundle(GenericAPIPermView):
queryset = Prototype.objects.all()
- serializer_class = api.stack_serial.LoadBundle
+ serializer_class = serializers.LoadBundle
def post(self, request):
"""
post:
Load bundle
"""
- try:
- serializer = self.serializer_class(data=request.data, context={'request': request})
- if serializer.is_valid():
- bundle = cm.bundle.load_bundle(serializer.validated_data.get('bundle_file'))
- srl = api.stack_serial.BundleSerializer(bundle, context={'request': request})
- return Response(srl.data)
- else:
- return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
- except AdcmEx as e:
- raise AdcmApiEx(e.code, e.msg, e.http_code) from e
+ serializer = self.serializer_class(data=request.data, context={'request': request})
+ if serializer.is_valid():
+ bundle = cm.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)
class BundleList(PageView):
@@ -88,7 +84,7 @@ class BundleList(PageView):
List all bundles
"""
queryset = Bundle.objects.exclude(hash='adcm')
- serializer_class = api.stack_serial.BundleSerializer
+ serializer_class = serializers.BundleSerializer
filterset_fields = ('name', 'version')
ordering_fields = ('name', 'version_order')
@@ -102,63 +98,51 @@ class BundleDetail(DetailViewRO):
Remove bundle
"""
queryset = Bundle.objects.all()
- serializer_class = api.stack_serial.BundleSerializer
+ serializer_class = serializers.BundleSerializer
lookup_field = 'id'
lookup_url_kwarg = 'bundle_id'
error_code = 'BUNDLE_NOT_FOUND'
def delete(self, request, bundle_id):
bundle = check_obj(Bundle, bundle_id, 'BUNDLE_NOT_FOUND')
- try:
- cm.bundle.delete_bundle(bundle)
- except AdcmEx as e:
- raise AdcmApiEx(e.code, e.msg, e.http_code) from e
+ cm.bundle.delete_bundle(bundle)
return Response(status=status.HTTP_204_NO_CONTENT)
class BundleUpdate(GenericAPIPermView):
queryset = Bundle.objects.all()
- serializer_class = api.stack_serial.BundleSerializer
+ serializer_class = serializers.BundleSerializer
def put(self, request, bundle_id):
"""
update bundle
"""
bundle = check_obj(Bundle, bundle_id, 'BUNDLE_NOT_FOUND')
- try:
- cm.bundle.update_bundle(bundle)
- serializer = self.serializer_class(bundle, context={'request': request})
- return Response(serializer.data)
- except AdcmEx as e:
- raise AdcmApiEx(e.code, e.msg, e.http_code, e.adds) from e
+ cm.bundle.update_bundle(bundle)
+ serializer = self.serializer_class(bundle, context={'request': request})
+ return Response(serializer.data)
class BundleLicense(GenericAPIPermView):
action = 'retrieve'
queryset = Bundle.objects.all()
- serializer_class = api.stack_serial.LicenseSerializer
+ serializer_class = serializers.LicenseSerializer
def get(self, request, bundle_id):
bundle = check_obj(Bundle, bundle_id, 'BUNDLE_NOT_FOUND')
- try:
- body = cm.api.get_license(bundle)
- url = reverse('accept-license', kwargs={'bundle_id': bundle.id}, request=request)
- return Response({'license': bundle.license, 'accept': url, 'text': body})
- except AdcmEx as e:
- raise AdcmApiEx(e.code, e.msg, e.http_code, e.adds) from e
+ body = cm.api.get_license(bundle)
+ url = reverse('accept-license', kwargs={'bundle_id': bundle.id}, request=request)
+ return Response({'license': bundle.license, 'accept': url, 'text': body})
class AcceptLicense(GenericAPIPermView):
queryset = Bundle.objects.all()
- serializer_class = api.stack_serial.LicenseSerializer
+ serializer_class = serializers.LicenseSerializer
def put(self, request, bundle_id):
bundle = check_obj(Bundle, bundle_id, 'BUNDLE_NOT_FOUND')
- try:
- cm.api.accept_license(bundle)
- return Response(status=status.HTTP_200_OK)
- except AdcmEx as e:
- raise AdcmApiEx(e.code, e.msg, e.http_code, e.adds) from e
+ cm.api.accept_license(bundle)
+ return Response(status=status.HTTP_200_OK)
class PrototypeList(PageView):
@@ -167,7 +151,7 @@ class PrototypeList(PageView):
List all stack prototypes
"""
queryset = Prototype.objects.all()
- serializer_class = api.stack_serial.PrototypeSerializer
+ serializer_class = serializers.PrototypeSerializer
filterset_fields = ('name', 'bundle_id', 'type')
ordering_fields = ('display_name', 'version_order')
@@ -178,7 +162,7 @@ class ServiceList(PageView):
List all stack services
"""
queryset = Prototype.objects.filter(type='service')
- serializer_class = api.stack_serial.ServiceSerializer
+ serializer_class = serializers.ServiceSerializer
filterset_fields = ('name', 'bundle_id')
ordering_fields = ('display_name', 'version_order')
@@ -189,7 +173,7 @@ class ServiceDetail(DetailViewRO):
Show stack service
"""
queryset = Prototype.objects.filter(type='service')
- serializer_class = api.stack_serial.ServiceDetailSerializer
+ serializer_class = serializers.ServiceDetailSerializer
lookup_field = 'id'
lookup_url_kwarg = 'prototype_id'
error_code = 'SERVICE_NOT_FOUND'
@@ -208,7 +192,7 @@ def get_object(self):
class ProtoActionDetail(GenericAPIPermView):
queryset = Action.objects.all()
- serializer_class = api.serializers.ActionSerializer
+ serializer_class = StackActionSerializer
def get(self, request, action_id):
"""
@@ -221,13 +205,13 @@ def get(self, request, action_id):
class ServiceProtoActionList(GenericAPIPermView):
queryset = Action.objects.filter(prototype__type='service')
- serializer_class = api.serializers.ActionSerializer
+ serializer_class = StackActionSerializer
- def get(self, request, service_id):
+ def get(self, request, prototype_id):
"""
List all actions of a specified service
"""
- obj = self.get_queryset().filter(prototype_id=service_id)
+ obj = self.get_queryset().filter(prototype_id=prototype_id)
serializer = self.serializer_class(obj, many=True, context={'request': request})
return Response(serializer.data)
@@ -238,7 +222,7 @@ class ComponentList(PageView):
List all stack components
"""
queryset = Prototype.objects.filter(type='component')
- serializer_class = api.stack_serial.ComponentTypeSerializer
+ serializer_class = serializers.ComponentTypeSerializer
filterset_fields = ('name', 'bundle_id')
ordering_fields = ('display_name', 'version_order')
@@ -249,7 +233,7 @@ class HostTypeList(PageView):
List all host types
"""
queryset = Prototype.objects.filter(type='host')
- serializer_class = api.stack_serial.HostTypeSerializer
+ serializer_class = serializers.HostTypeSerializer
filterset_fields = ('name', 'bundle_id')
ordering_fields = ('display_name', 'version_order')
@@ -260,7 +244,7 @@ class ProviderTypeList(PageView):
List all host providers types
"""
queryset = Prototype.objects.filter(type='provider')
- serializer_class = api.stack_serial.ProviderTypeSerializer
+ serializer_class = serializers.ProviderTypeSerializer
filterset_fields = ('name', 'bundle_id', 'display_name')
ordering_fields = ('display_name', 'version_order')
@@ -271,7 +255,7 @@ class ClusterTypeList(PageView):
List all cluster types
"""
queryset = Prototype.objects.filter(type='cluster')
- serializer_class = api.stack_serial.ClusterTypeSerializer
+ serializer_class = serializers.ClusterTypeSerializer
filterset_fields = ('name', 'bundle_id', 'display_name')
ordering_fields = ('display_name', 'version_order')
@@ -282,7 +266,7 @@ class AdcmTypeList(ListView):
List adcm root object prototypes
"""
queryset = Prototype.objects.filter(type='adcm')
- serializer_class = api.stack_serial.AdcmTypeSerializer
+ serializer_class = serializers.AdcmTypeSerializer
filterset_fields = ('bundle_id',)
@@ -292,7 +276,7 @@ class PrototypeDetail(DetailViewRO):
Show prototype
"""
queryset = Prototype.objects.all()
- serializer_class = api.stack_serial.PrototypeDetailSerializer
+ serializer_class = serializers.PrototypeDetailSerializer
lookup_field = 'id'
lookup_url_kwarg = 'prototype_id'
error_code = 'PROTOTYPE_NOT_FOUND'
@@ -317,7 +301,7 @@ class AdcmTypeDetail(PrototypeDetail):
Show adcm prototype
"""
queryset = Prototype.objects.filter(type='adcm')
- serializer_class = api.stack_serial.AdcmTypeDetailSerializer
+ serializer_class = serializers.AdcmTypeDetailSerializer
class ClusterTypeDetail(PrototypeDetail):
@@ -326,7 +310,7 @@ class ClusterTypeDetail(PrototypeDetail):
Show cluster prototype
"""
queryset = Prototype.objects.filter(type='cluster')
- serializer_class = api.stack_serial.ClusterTypeDetailSerializer
+ serializer_class = serializers.ClusterTypeDetailSerializer
class ComponentTypeDetail(PrototypeDetail):
@@ -335,7 +319,7 @@ class ComponentTypeDetail(PrototypeDetail):
Show component prototype
"""
queryset = Prototype.objects.filter(type='component')
- serializer_class = api.stack_serial.ComponentTypeDetailSerializer
+ serializer_class = serializers.ComponentTypeDetailSerializer
class HostTypeDetail(PrototypeDetail):
@@ -344,7 +328,7 @@ class HostTypeDetail(PrototypeDetail):
Show host prototype
"""
queryset = Prototype.objects.filter(type='host')
- serializer_class = api.stack_serial.HostTypeDetailSerializer
+ serializer_class = serializers.HostTypeDetailSerializer
class ProviderTypeDetail(PrototypeDetail):
@@ -353,12 +337,12 @@ class ProviderTypeDetail(PrototypeDetail):
Show host provider prototype
"""
queryset = Prototype.objects.filter(type='provider')
- serializer_class = api.stack_serial.ProviderTypeDetailSerializer
+ serializer_class = serializers.ProviderTypeDetailSerializer
class LoadServiceMap(GenericAPIPermView):
queryset = Prototype.objects.all()
- serializer_class = api.stack_serial.Stack
+ serializer_class = serializers.Stack
def put(self, request):
cm.status_api.load_service_map()
diff --git a/python/api/stats/__init__.py b/python/api/stats/__init__.py
new file mode 100644
index 0000000000..824dd6c8fe
--- /dev/null
+++ b/python/api/stats/__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/stats/serializers.py b/python/api/stats/serializers.py
new file mode 100644
index 0000000000..e132b1f09d
--- /dev/null
+++ b/python/api/stats/serializers.py
@@ -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.
+
+from rest_framework import serializers
+from api.api_views import hlink
+
+
+class StatsSerializer(serializers.Serializer):
+ task = hlink('task-stats', 'id', 'task_id')
+ job = hlink('job-stats', 'id', 'job_id')
diff --git a/python/api/stats/urls.py b/python/api/stats/urls.py
new file mode 100644
index 0000000000..e7bda83a64
--- /dev/null
+++ b/python/api/stats/urls.py
@@ -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.
+
+
+from django.urls import path
+from . import views
+
+
+urlpatterns = [
+ path('', views.Stats.as_view(), name='stats'),
+ path('task//', views.TaskStats.as_view(), name='task-stats'),
+ path('job//', views.JobStats.as_view(), name='job-stats'),
+
+]
diff --git a/python/api/stats/views.py b/python/api/stats/views.py
new file mode 100644
index 0000000000..05c9027096
--- /dev/null
+++ b/python/api/stats/views.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 rest_framework.response import Response
+
+import cm.config as config
+from cm.models import JobLog, TaskLog
+from api.serializers import EmptySerializer
+from api.api_views import GenericAPIPermView
+from . import serializers
+
+
+class Stats(GenericAPIPermView):
+ queryset = JobLog.objects.all()
+ serializer_class = serializers.StatsSerializer
+
+ def get(self, request):
+ """
+ Statistics
+ """
+ obj = JobLog(id=1)
+ serializer = self.serializer_class(obj, context={'request': request})
+ return Response(serializer.data)
+
+
+class JobStats(GenericAPIPermView):
+ queryset = JobLog.objects.all()
+ serializer_class = EmptySerializer
+
+ def get(self, request, job_id):
+ """
+ Show jobs stats
+ """
+ jobs = self.get_queryset().filter(id__gt=job_id)
+ 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(),
+ }
+ return Response(data)
+
+
+class TaskStats(GenericAPIPermView):
+ queryset = TaskLog.objects.all()
+ serializer_class = EmptySerializer
+
+ def get(self, request, task_id):
+ """
+ Show tasks stats
+ """
+ tasks = self.get_queryset().filter(id__gt=task_id)
+ 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(),
+ }
+ return Response(data)
diff --git a/python/api/urls.py b/python/api/urls.py
index 2ccb796e3f..596c330bc1 100644
--- a/python/api/urls.py
+++ b/python/api/urls.py
@@ -10,13 +10,12 @@
# See the License for the specific language governing permissions and
# limitations under the License.
-from django.urls import path, register_converter
-from django.conf.urls import include
+from django.urls import path, include, register_converter
from rest_framework_swagger.views import get_swagger_view
from rest_framework.schemas import get_schema_view
-from api import views, user_views, stack_views, cluster_views, docs, job_views
+from api import views, docs
register_converter(views.NameConverter, 'name')
@@ -24,271 +23,30 @@
schema_view = get_schema_view(title='ArenaData Chapel API')
-CLUSTER = 'cluster//'
-PROVIDER = 'provider//'
-HOST = 'host//'
-SERVICE = 'service//'
-
-
urlpatterns = [
path('info/', views.ADCMInfo.as_view(), name='adcm-info'),
+ path('stats/', include('api.stats.urls')),
+
path('token/', views.GetAuthToken.as_view(), name='token'),
path('logout/', views.LogOut.as_view(), name='logout'),
- path('user/', user_views.UserList.as_view(), name='user-list'),
- path('user//', user_views.UserDetail.as_view(), name='user-details'),
- path(
- 'user//role/', user_views.ChangeUserRole.as_view(), name='change-user-role'
- ),
- path('user//group/', user_views.AddUser2Group.as_view(), name='add-user-group'),
- path('user//password/', user_views.UserPasswd.as_view(), name='user-passwd'),
-
- path('group/', user_views.GroupList.as_view(), name='group-list'),
- path('group//', user_views.GroupDetail.as_view(), name='group-details'),
- path('group//role/', user_views.ChangeGroupRole.as_view(), name='change-group-role'),
-
- path('profile/', user_views.ProfileList.as_view(), name='profile-list'),
- path('profile//', user_views.ProfileDetail.as_view(), name='profile-details'),
- path(
- 'profile//password/', user_views.UserPasswd.as_view(), name='profile-passwd'
- ),
-
- path('role/', user_views.RoleList.as_view(), name='role-list'),
- path('role//', user_views.RoleDetail.as_view(), name='role-details'),
-
- path('stats/', views.Stats.as_view(), name='stats'),
- path('stats/task//', views.TaskStats.as_view(), name='task-stats'),
- path('stats/job//', views.JobStats.as_view(), name='job-stats'),
-
- path('stack/', stack_views.Stack.as_view(), name='stack'),
- path('stack/upload/', stack_views.UploadBundle.as_view(), name='upload-bundle'),
- path('stack/load/', stack_views.LoadBundle.as_view(), name='load-bundle'),
- path(
- 'stack/load/servicemap/',
- stack_views.LoadServiceMap.as_view(),
- name='load-servicemap'
- ),
- path('stack/bundle/', stack_views.BundleList.as_view(), name='bundle'),
- path(
- 'stack/bundle//',
- stack_views.BundleDetail.as_view(),
- name='bundle-details'
- ),
- path(
- 'stack/bundle//update/',
- stack_views.BundleUpdate.as_view(),
- name='bundle-update'
- ),
- path(
- 'stack/bundle//license/',
- stack_views.BundleLicense.as_view(),
- name='bundle-license'
- ),
- path(
- 'stack/bundle//license/accept/',
- stack_views.AcceptLicense.as_view(),
- name='accept-license'
- ),
- path(
- 'stack/action//',
- stack_views.ProtoActionDetail.as_view(),
- name='action-details'
- ),
- path('stack/prototype/', stack_views.PrototypeList.as_view(), name='prototype'),
- path('stack/service/', stack_views.ServiceList.as_view(), name='service-type'),
- path(
- 'stack/service//',
- stack_views.ServiceDetail.as_view(),
- name='service-type-details'
- ),
- path(
- 'stack/' + SERVICE + 'action/',
- stack_views.ServiceProtoActionList.as_view(),
- name='service-actions'
- ),
- path('stack/component/', stack_views.ComponentList.as_view(), name='component-type'),
- path(
- 'stack/component//',
- stack_views.ComponentTypeDetail.as_view(),
- name='component-type-details'
- ),
- path('stack/provider/', stack_views.ProviderTypeList.as_view(), name='provider-type'),
- path(
- 'stack/provider//',
- stack_views.ProviderTypeDetail.as_view(),
- name='provider-type-details'
- ),
- path('stack/host/', stack_views.HostTypeList.as_view(), name='host-type'),
- path(
- 'stack/host//',
- stack_views.HostTypeDetail.as_view(),
- name='host-type-details'
- ),
- path('stack/cluster/', stack_views.ClusterTypeList.as_view(), name='cluster-type'),
- path(
- 'stack/cluster//',
- stack_views.ClusterTypeDetail.as_view(),
- name='cluster-type-details'
- ),
- path('stack/adcm/', stack_views.AdcmTypeList.as_view(), name='adcm-type'),
- path(
- 'stack/adcm//',
- stack_views.AdcmTypeDetail.as_view(),
- name='adcm-type-details'
- ),
- path(
- 'stack/prototype//',
- stack_views.PrototypeDetail.as_view(),
- name='prototype-details'
- ),
+ path('user/', include('api.user.urls')),
+ path('group/', include('api.user.group_urls')),
+ path('role/', include('api.user.role_urls')),
+ path('profile/', include('api.user.profile_urls')),
- path('cluster/', cluster_views.ClusterList.as_view(), name='cluster'),
- path(CLUSTER, cluster_views.ClusterDetail.as_view(), name='cluster-details'),
- path(CLUSTER + 'host/', cluster_views.ClusterHostList.as_view(), name='cluster-host'),
- path(CLUSTER + 'import/', cluster_views.ClusterImport.as_view(), name='cluster-import'),
- path(CLUSTER + 'upgrade/', cluster_views.ClusterUpgrade.as_view(), name='cluster-upgrade'),
- path(CLUSTER + 'bind/', cluster_views.ClusterBindList.as_view(), name='cluster-bind'),
- path(
- CLUSTER + 'bind//',
- cluster_views.ClusterServiceBindDetail.as_view(),
- name='cluster-bind-details'
- ),
- path(
- CLUSTER + 'serviceprototype/',
- cluster_views.ClusterBundle.as_view(),
- name='cluster-service-prototype'
- ),
- path(
- CLUSTER + 'upgrade//',
- cluster_views.ClusterUpgradeDetail.as_view(),
- name='cluster-upgrade-details'
- ),
- path(
- CLUSTER + 'upgrade//do/',
- cluster_views.DoClusterUpgrade.as_view(),
- name='do-cluster-upgrade'
- ),
- path(
- CLUSTER + HOST, cluster_views.ClusterHostDetail.as_view(), name='cluster-host-details'
- ),
- path(
- CLUSTER + 'service/', cluster_views.ClusterServiceList.as_view(), name='cluster-service'
- ),
-
- path(CLUSTER + HOST + 'config/', include('api.config.urls'), {'object_type': 'host'}),
- path(CLUSTER + HOST + 'action/', include('api.action.urls'), {'object_type': 'host'}),
-
- path(CLUSTER + 'action/', include('api.action.urls'), {'object_type': 'cluster'}),
- path(
- CLUSTER + 'status/',
- cluster_views.StatusList.as_view(),
- name='cluster-status'
- ),
- path(
- CLUSTER + 'hostcomponent/',
- cluster_views.HostComponentList.as_view(),
- name='host-component'
- ),
- path(
- CLUSTER + 'hostcomponent//',
- cluster_views.HostComponentDetail.as_view(),
- name='host-component-details'
- ),
- path(
- CLUSTER + SERVICE,
- cluster_views.ClusterServiceDetail.as_view(),
- name='cluster-service-details'
- ),
- path(CLUSTER + SERVICE + 'action/', include('api.action.urls'), {'object_type': 'service'}),
- path(
- CLUSTER + SERVICE + 'component/',
- cluster_views.ServiceComponentList.as_view(),
- name='cluster-service-component'
- ),
- path(
- CLUSTER + SERVICE + 'component//',
- cluster_views.ServiceComponentDetail.as_view(),
- name='cluster-service-component-details'
- ),
- path(
- CLUSTER + SERVICE + 'component//config/',
- include('api.config.urls'),
- {'object_type': 'component'}
- ),
- path(
- CLUSTER + SERVICE + 'component//action/',
- include('api.action.urls'),
- {'object_type': 'component'}
- ),
- path(
- CLUSTER + SERVICE + 'import/',
- cluster_views.ClusterServiceImport.as_view(),
- name='cluster-service-import'
- ),
- path(
- CLUSTER + SERVICE + 'bind/',
- cluster_views.ClusterServiceBind.as_view(),
- name='cluster-service-bind'
- ),
- path(
- CLUSTER + SERVICE + 'bind//',
- cluster_views.ClusterServiceBindDetail.as_view(),
- name='cluster-service-bind-details'
- ),
-
- path(CLUSTER + 'config/', include('api.config.urls'), {'object_type': 'cluster'}),
- path(CLUSTER + SERVICE + 'config/', include('api.config.urls'), {'object_type': 'service'}),
+ path('stack/', include('api.stack.urls')),
+ path('cluster/', include('api.cluster.urls')),
path('service/', include('api.service.urls')),
+ path('component/', include('api.component.urls')),
+ path('provider/', include('api.provider.urls')),
+ path('host/', include('api.host.urls')),
+ path('adcm/', include('api.adcm.urls')),
- path('adcm/', views.AdcmList.as_view(), name='adcm'),
- path('adcm//', views.AdcmDetail.as_view(), name='adcm-details'),
- path('adcm//config/', include('api.config.urls'), {'object_type': 'adcm'}),
- path('adcm//action/', include('api.action.urls'), {'object_type': 'adcm'}),
-
- path('provider/', views.ProviderList.as_view(), name='provider'),
- path(PROVIDER, views.ProviderDetail.as_view(), name='provider-details'),
- path(PROVIDER + 'host/', views.ProviderHostList.as_view(), name='provider-host'),
- path(PROVIDER + 'action/', include('api.action.urls'), {'object_type': 'provider'}),
-
- path(PROVIDER + 'upgrade/', views.ProviderUpgrade.as_view(), name='provider-upgrade'),
- path(
- PROVIDER + 'upgrade//',
- views.ProviderUpgradeDetail.as_view(),
- name='provider-upgrade-details'
- ),
- path(
- PROVIDER + 'upgrade//do/',
- views.DoProviderUpgrade.as_view(),
- name='do-provider-upgrade'
- ),
- path(PROVIDER + 'config/', include('api.config.urls'), {'object_type': 'provider'}),
-
- path('host/', views.HostList.as_view(), name='host'),
- path(HOST, views.HostDetail.as_view(), name='host-details'),
-
- path(HOST + 'action/', include('api.action.urls'), {'object_type': 'host'}),
- path(HOST + 'config/', include('api.config.urls'), {'object_type': 'host'}),
-
- path('task/', job_views.Task.as_view(), name='task'),
- path('task//', job_views.TaskDetail.as_view(), name='task-details'),
- path('task//restart/', job_views.TaskReStart.as_view(), name='task-restart'),
- path('task//cancel/', job_views.TaskCancel.as_view(), name='task-cancel'),
+ path('task/', include('api.job.task_urls')),
+ path('job/', include('api.job.urls')),
- path('job/', job_views.JobList.as_view(), name='job'),
- path('job//', job_views.JobDetail.as_view(), name='job-details'),
- path('job//log/', job_views.LogStorageListView.as_view(), name='log-list'),
- path('job//log//',
- job_views.LogStorageView.as_view(),
- name='log-storage'),
- path('job//log//download/',
- job_views.download_log_file,
- name='download-log'),
- path(
- 'job//log////',
- job_views.LogFile.as_view(),
- name='log-file'
- ),
# path('docs/', include_docs_urls(title='ArenaData Chapel API')),
path('swagger/', swagger_view),
path('schema/', schema_view),
diff --git a/python/api/user/__init__.py b/python/api/user/__init__.py
new file mode 100644
index 0000000000..824dd6c8fe
--- /dev/null
+++ b/python/api/user/__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/user/group_urls.py b/python/api/user/group_urls.py
new file mode 100644
index 0000000000..cb6b855d98
--- /dev/null
+++ b/python/api/user/group_urls.py
@@ -0,0 +1,24 @@
+# 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 path, include
+from . import views
+
+
+urlpatterns = [
+ path('', views.GroupList.as_view(), name='group-list'),
+ path('/', include([
+ path('', views.GroupDetail.as_view(), name='group-details'),
+ path('role/', views.ChangeGroupRole.as_view(), name='change-group-role'),
+ ])),
+]
diff --git a/python/api/user/profile_urls.py b/python/api/user/profile_urls.py
new file mode 100644
index 0000000000..802b8a7d7b
--- /dev/null
+++ b/python/api/user/profile_urls.py
@@ -0,0 +1,24 @@
+# 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 path, include
+from . import views
+
+
+urlpatterns = [
+ path('', views.ProfileList.as_view(), name='profile-list'),
+ path('/', include([
+ path('', views.ProfileDetail.as_view(), name='profile-details'),
+ path('password/', views.UserPasswd.as_view(), name='profile-passwd'),
+ ])),
+]
diff --git a/python/api/user/role_urls.py b/python/api/user/role_urls.py
new file mode 100644
index 0000000000..8c44952200
--- /dev/null
+++ b/python/api/user/role_urls.py
@@ -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.
+
+
+from django.urls import path
+from . import views
+
+
+urlpatterns = [
+ path('', views.RoleList.as_view(), name='role-list'),
+ path('/', views.RoleDetail.as_view(), name='role-details'),
+]
diff --git a/python/api/user/serializers.py b/python/api/user/serializers.py
new file mode 100644
index 0000000000..333ffd0d97
--- /dev/null
+++ b/python/api/user/serializers.py
@@ -0,0 +1,171 @@
+# 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 import IntegrityError, transaction
+from django.contrib.auth.models import User, Group
+from rest_framework import serializers
+from rest_framework.authtoken.models import Token
+
+import cm
+from cm.errors import AdcmEx
+from cm.models import UserProfile, Role
+from api.api_views import check_obj, hlink
+from api.serializers import UrlField
+
+
+class PermSerializer(serializers.Serializer):
+ name = serializers.CharField()
+ codename = serializers.CharField()
+ app_label = serializers.SerializerMethodField()
+ model = serializers.SerializerMethodField()
+
+ def get_app_label(self, obj):
+ return obj.content_type.app_label
+
+ def get_model(self, obj):
+ return obj.content_type.model
+
+
+class RoleSerializer(serializers.Serializer):
+ id = serializers.IntegerField(read_only=True)
+ name = serializers.CharField(read_only=True)
+ description = serializers.CharField(read_only=True)
+ url = hlink('role-details', 'id', 'role_id')
+
+
+class RoleDetailSerializer(RoleSerializer):
+ permissions = PermSerializer(many=True, read_only=True)
+
+
+class GroupSerializer(serializers.Serializer):
+ name = serializers.CharField()
+ url = hlink('group-details', 'name', 'name')
+ change_role = hlink('change-group-role', 'name', 'name')
+
+ @transaction.atomic
+ def create(self, validated_data):
+ try:
+ return Group.objects.create(name=validated_data.get('name'))
+ except IntegrityError:
+ raise AdcmEx("GROUP_CONFLICT", 'group already exists') from None
+
+
+class GroupDetailSerializer(GroupSerializer):
+ permissions = PermSerializer(many=True, read_only=True)
+ role = RoleSerializer(many=True, source='role_set')
+
+
+class UserSerializer(serializers.Serializer):
+ username = serializers.CharField()
+ password = serializers.CharField(write_only=True)
+ url = hlink('user-details', 'username', 'username')
+ change_group = hlink('add-user-group', 'username', 'username')
+ change_password = hlink('user-passwd', 'username', 'username')
+ change_role = hlink('change-user-role', 'username', 'username')
+ is_superuser = serializers.BooleanField(required=False)
+
+ @transaction.atomic
+ def create(self, validated_data):
+ try:
+ user = User.objects.create_user(
+ validated_data.get('username'),
+ password=validated_data.get('password'),
+ is_superuser=validated_data.get('is_superuser', True)
+ )
+ UserProfile.objects.create(login=validated_data.get('username'))
+ return user
+ except IntegrityError:
+ raise AdcmEx("USER_CONFLICT", 'user already exists') from None
+
+
+class UserDetailSerializer(UserSerializer):
+ user_permissions = PermSerializer(many=True)
+ groups = GroupSerializer(many=True)
+ role = RoleSerializer(many=True, source='role_set')
+
+
+class AddUser2GroupSerializer(serializers.Serializer):
+ name = serializers.CharField()
+
+ def update(self, user, validated_data): # pylint: disable=arguments-differ
+ group = check_obj(Group, {'name': validated_data.get('name')}, 'GROUP_NOT_FOUND')
+ group.user_set.add(user)
+ return group
+
+
+class AddUserRoleSerializer(serializers.Serializer):
+ role_id = serializers.IntegerField()
+ name = serializers.CharField(read_only=True)
+
+ def update(self, user, validated_data): # pylint: disable=arguments-differ
+ role = check_obj(Role, {'id': validated_data.get('role_id')}, 'ROLE_NOT_FOUND')
+ return cm.api.add_user_role(user, role)
+
+
+class AddGroupRoleSerializer(serializers.Serializer):
+ role_id = serializers.IntegerField()
+ name = serializers.CharField(read_only=True)
+
+ def update(self, group, validated_data): # pylint: disable=arguments-differ
+ role = check_obj(Role, {'id': validated_data.get('role_id')}, 'ROLE_NOT_FOUND')
+ return cm.api.add_group_role(group, role)
+
+
+class UserPasswdSerializer(serializers.Serializer):
+ token = serializers.CharField(read_only=True, source='key')
+ password = serializers.CharField(write_only=True)
+
+ @transaction.atomic
+ def update(self, user, validated_data): # pylint: disable=arguments-differ
+ user.set_password(validated_data.get('password'))
+ user.save()
+ token = Token.obj.get(user=user)
+ token.delete()
+ token.key = token.generate_key()
+ token.user = user
+ token.save()
+ return token
+
+
+class ProfileDetailSerializer(serializers.Serializer):
+ class MyUrlField(UrlField):
+ def get_kwargs(self, obj):
+ return {'username': obj.login}
+
+ username = serializers.CharField(read_only=True, source='login')
+ change_password = MyUrlField(read_only=True, view_name='profile-passwd')
+ profile = serializers.JSONField()
+
+ def validate_profile(self, raw):
+ if isinstance(raw, str):
+ raise AdcmEx('JSON_ERROR', 'profile should not be just one string')
+ return raw
+
+ def update(self, instance, validated_data):
+ instance.profile = validated_data.get('profile', instance.profile)
+ try:
+ instance.save()
+ except IntegrityError:
+ raise AdcmEx("USER_CONFLICT") from None
+ return instance
+
+
+class ProfileSerializer(ProfileDetailSerializer):
+ username = serializers.CharField(source='login')
+ url = hlink('profile-details', 'login', 'username')
+
+ def create(self, validated_data):
+ check_obj(User, {'username': validated_data.get('login')}, 'USER_NOT_FOUND')
+ try:
+ return UserProfile.objects.create(**validated_data)
+ except IntegrityError:
+ raise AdcmEx("USER_CONFLICT") from None
diff --git a/python/api/user/urls.py b/python/api/user/urls.py
new file mode 100644
index 0000000000..9a58a63bff
--- /dev/null
+++ b/python/api/user/urls.py
@@ -0,0 +1,26 @@
+# 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 path, include
+from . import views
+
+
+urlpatterns = [
+ path('', views.UserList.as_view(), name='user-list'),
+ path('/', include([
+ path('', views.UserDetail.as_view(), name='user-details'),
+ path('role/', views.ChangeUserRole.as_view(), name='change-user-role'),
+ path('group/', views.AddUser2Group.as_view(), name='add-user-group'),
+ path('password/', views.UserPasswd.as_view(), name='user-passwd'),
+ ])),
+]
diff --git a/python/api/user_views.py b/python/api/user/views.py
similarity index 84%
rename from python/api/user_views.py
rename to python/api/user/views.py
index 1c778dd0c1..d726e6db6f 100644
--- a/python/api/user_views.py
+++ b/python/api/user/views.py
@@ -12,17 +12,28 @@
from django.db import transaction
from django.utils import timezone
+from django.core.exceptions import ObjectDoesNotExist
from django.contrib.auth.models import User, Group
from rest_framework import status
from rest_framework.response import Response
-import api.serializers
-from api.api_views import PageView, PageViewAdd, DetailViewRO, GenericAPIPermView, update, check_obj
+from api.api_views import PageView, PageViewAdd, DetailViewRO, GenericAPIPermView, update
import cm.api
-from cm.api import safe_api
+from cm.errors import AdcmEx
from cm.models import Role, UserProfile, DummyData
-from cm.errors import AdcmApiEx
+from . import serializers
+
+
+def check_obj(model, req, error=None):
+ if isinstance(req, dict):
+ kw = req
+ else:
+ kw = {'id': req}
+ try:
+ return model.objects.get(**kw)
+ except ObjectDoesNotExist:
+ raise AdcmEx(error) from None
@transaction.atomic
@@ -47,13 +58,13 @@ class UserList(PageViewAdd):
Create new user
"""
queryset = User.objects.all()
- serializer_class = api.serializers.UserSerializer
+ serializer_class = serializers.UserSerializer
ordering_fields = ('username',)
class UserDetail(GenericAPIPermView):
queryset = User.objects.all()
- serializer_class = api.serializers.UserDetailSerializer
+ serializer_class = serializers.UserDetailSerializer
def get(self, request, username):
"""
@@ -72,7 +83,7 @@ def delete(self, request, username):
class UserPasswd(GenericAPIPermView):
queryset = User.objects.all()
- serializer_class = api.serializers.UserPasswdSerializer
+ serializer_class = serializers.UserPasswdSerializer
def patch(self, request, username):
"""
@@ -85,7 +96,7 @@ def patch(self, request, username):
class AddUser2Group(GenericAPIPermView):
queryset = User.objects.all()
- serializer_class = api.serializers.AddUser2GroupSerializer
+ serializer_class = serializers.AddUser2GroupSerializer
def post(self, request, username):
"""
@@ -111,7 +122,7 @@ def delete(self, request, username):
class ChangeUserRole(GenericAPIPermView):
queryset = User.objects.all()
- serializer_class = api.serializers.AddUserRoleSerializer
+ serializer_class = serializers.AddUserRoleSerializer
def post(self, request, username):
"""
@@ -129,7 +140,7 @@ def delete(self, request, username):
serializer = self.serializer_class(data=request.data, context={'request': request})
serializer.is_valid(raise_exception=True)
role = check_obj(Role, {'id': serializer.data['role_id']}, 'ROLE_NOT_FOUND')
- safe_api(cm.api.remove_user_role, (user, role))
+ cm.api.remove_user_role(user, role)
return Response(status=status.HTTP_204_NO_CONTENT)
@@ -142,13 +153,13 @@ class GroupList(PageViewAdd):
Create new user group
"""
queryset = Group.objects.all()
- serializer_class = api.serializers.GroupSerializer
+ serializer_class = serializers.GroupSerializer
ordering_fields = ('name',)
class GroupDetail(GenericAPIPermView):
queryset = Group.objects.all()
- serializer_class = api.serializers.GroupDetailSerializer
+ serializer_class = serializers.GroupDetailSerializer
def get(self, request, name):
"""
@@ -169,7 +180,7 @@ def delete(self, request, name):
class ChangeGroupRole(GenericAPIPermView):
queryset = User.objects.all()
- serializer_class = api.serializers.AddGroupRoleSerializer
+ serializer_class = serializers.AddGroupRoleSerializer
def post(self, request, name):
"""
@@ -187,7 +198,7 @@ def delete(self, request, name):
serializer = self.serializer_class(data=request.data, context={'request': request})
serializer.is_valid(raise_exception=True)
role = check_obj(Role, {'id': serializer.data['role_id']}, 'ROLE_NOT_FOUND')
- safe_api(cm.api.remove_group_role, (group, role))
+ cm.api.remove_group_role(group, role)
return Response(status=status.HTTP_204_NO_CONTENT)
@@ -197,13 +208,13 @@ class RoleList(PageView):
List all existing roles
"""
queryset = Role.objects.all()
- serializer_class = api.serializers.RoleSerializer
+ serializer_class = serializers.RoleSerializer
ordering_fields = ('name',)
class RoleDetail(PageView):
queryset = Role.objects.all()
- serializer_class = api.serializers.RoleDetailSerializer
+ serializer_class = serializers.RoleDetailSerializer
def get(self, request, role_id): # pylint: disable=arguments-differ
"""
@@ -223,7 +234,7 @@ class ProfileList(PageViewAdd):
Create new user profile
"""
queryset = UserProfile.objects.all()
- serializer_class = api.serializers.ProfileSerializer
+ serializer_class = serializers.ProfileSerializer
ordering_fields = ('username',)
@@ -233,7 +244,7 @@ class ProfileDetail(DetailViewRO):
Show user profile
"""
queryset = UserProfile.objects.all()
- serializer_class = api.serializers.ProfileDetailSerializer
+ serializer_class = serializers.ProfileDetailSerializer
lookup_field = 'login'
lookup_url_kwarg = 'username'
error_code = 'USER_NOT_FOUND'
@@ -243,12 +254,9 @@ def get_object(self):
try:
up = UserProfile.objects.get(login=login)
except UserProfile.DoesNotExist:
- try:
- user = User.objects.get(username=login)
- up = UserProfile.objects.create(login=user.username)
- up.save()
- except User.DoesNotExist:
- raise AdcmApiEx('USER_NOT_FOUND') from None
+ user = User.obj.get(username=login)
+ up = UserProfile.objects.create(login=user.username)
+ up.save()
return up
def patch(self, request, *args, **kwargs):
diff --git a/python/api/views.py b/python/api/views.py
index d037dca596..442b84a06e 100644
--- a/python/api/views.py
+++ b/python/api/views.py
@@ -10,11 +10,8 @@
# See the License for the specific language governing permissions and
# limitations under the License.
-# pylint: disable=duplicate-except,attribute-defined-outside-init
-
import rest_framework
import django.contrib.auth
-from django_filters import rest_framework as drf_filters
from rest_framework import routers, status
from rest_framework.authtoken.models import Token
@@ -22,19 +19,11 @@
from rest_framework.response import Response
import api.serializers
-import api.cluster_views
import cm.api
-import cm.config as config
import cm.job
import cm.stack
import cm.status_api
-from cm.errors import AdcmEx, AdcmApiEx
-from cm.models import HostProvider, Host, ADCM, JobLog, TaskLog, Upgrade
from adcm.settings import ADCM_VERSION
-from api.api_views import (
- DetailViewRO, DetailViewDelete, ListView,
- PageView, PageViewAdd, GenericAPIPermView, create, check_obj
-)
class APIRoot(routers.APIRootView):
@@ -49,6 +38,7 @@ class APIRoot(routers.APIRootView):
'provider': 'provider',
'host': 'host',
'service': 'service',
+ 'component': 'component',
'job': 'job',
'stack': 'stack',
'stats': 'stats',
@@ -118,242 +108,3 @@ def get(self, request):
'adcm_version': ADCM_VERSION,
'google_oauth': cm.api.has_google_oauth()
})
-
-
-class AdcmList(ListView):
- """
- get:
- List adcm object
- """
- queryset = ADCM.objects.all()
- serializer_class = api.serializers.AdcmSerializer
- serializer_class_ui = api.serializers.AdcmDetailSerializer
-
-
-class AdcmDetail(DetailViewRO):
- """
- get:
- Show adcm object
- """
- queryset = ADCM.objects.all()
- serializer_class = api.serializers.AdcmDetailSerializer
- lookup_field = 'id'
- lookup_url_kwarg = 'adcm_id'
- error_code = 'ADCM_NOT_FOUND'
-
-
-class ProviderList(PageViewAdd):
- """
- get:
- List all host providers
-
- post:
- Create new host provider
- """
- queryset = HostProvider.objects.all()
- serializer_class = api.serializers.ProviderSerializer
- serializer_class_ui = api.serializers.ProviderUISerializer
- serializer_class_post = api.serializers.ProviderDetailSerializer
- filterset_fields = ('name', 'prototype_id')
- ordering_fields = ('name', 'state', 'prototype__display_name', 'prototype__version_order')
-
-
-class ProviderDetail(DetailViewDelete):
- """
- get:
- Show host provider
- """
- queryset = HostProvider.objects.all()
- serializer_class = api.serializers.ProviderDetailSerializer
- serializer_class_ui = api.serializers.ProviderUISerializer
- lookup_field = 'id'
- lookup_url_kwarg = 'provider_id'
- error_code = 'PROVIDER_NOT_FOUND'
-
- def delete(self, request, provider_id): # pylint: disable=arguments-differ
- """
- Remove host provider
- """
- provider = check_obj(HostProvider, provider_id, 'PROVIDER_NOT_FOUND')
- try:
- cm.api.delete_host_provider(provider)
- except AdcmEx as e:
- raise AdcmApiEx(e.code, e.msg, e.http_code) from e
- return Response(status=status.HTTP_204_NO_CONTENT)
-
-
-class ProviderHostList(PageView):
- """
- post:
- Create new host
- """
- queryset = Host.objects.all()
- serializer_class = api.serializers.ProviderHostSerializer
- serializer_class_ui = api.serializers.HostUISerializer
- filterset_fields = ('fqdn', 'cluster_id')
- ordering_fields = (
- 'fqdn', 'state', 'prototype__display_name', 'prototype__version_order'
- )
-
- def get(self, request, provider_id): # pylint: disable=arguments-differ
- """
- List all hosts of specified host provider
- """
- provider = check_obj(HostProvider, provider_id, 'PROVIDER_NOT_FOUND')
- obj = self.filter_queryset(self.get_queryset().filter(provider=provider))
- return self.get_page(obj, request)
-
- def post(self, request, provider_id):
- provider = check_obj(HostProvider, provider_id, 'PROVIDER_NOT_FOUND')
- serializer_class = self.select_serializer(request)
- serializer = serializer_class(
- data=request.data, context={'request': request, 'provider': provider}
- )
- return create(serializer, provider=provider)
-
-
-class HostFilter(drf_filters.FilterSet):
- cluster_is_null = drf_filters.BooleanFilter(field_name='cluster_id', lookup_expr='isnull')
- provider_is_null = drf_filters.BooleanFilter(field_name='provider_id', lookup_expr='isnull')
-
- class Meta:
- model = Host
- fields = ['cluster_id', 'prototype_id', 'provider_id', 'fqdn']
-
-
-class HostList(PageViewAdd):
- """
- get:
- List all hosts
-
- post:
- Create new host
- """
- queryset = Host.objects.all()
- serializer_class = api.serializers.HostSerializer
- serializer_class_ui = api.serializers.HostUISerializer
- serializer_class_post = api.serializers.HostDetailSerializer
- filterset_class = HostFilter
- filterset_fields = (
- 'cluster_id', 'prototype_id', 'provider_id', 'fqdn', 'cluster_is_null', 'provider_is_null'
- ) # just for documentation
- ordering_fields = (
- 'fqdn', 'state', 'provider__name', 'cluster__name',
- 'prototype__display_name', 'prototype__version_order',
- )
-
-
-class HostDetail(DetailViewDelete):
- """
- get:
- Show host
- """
- queryset = Host.objects.all()
- serializer_class = api.serializers.HostDetailSerializer
- serializer_class_ui = api.serializers.HostUISerializer
- lookup_field = 'id'
- lookup_url_kwarg = 'host_id'
- error_code = 'HOST_NOT_FOUND'
-
- def delete(self, request, host_id): # pylint: disable=arguments-differ
- """
- Remove host (and all corresponding host services:components)
- """
- host = check_obj(Host, host_id, 'HOST_NOT_FOUND')
- try:
- cm.api.delete_host(host)
- except AdcmEx as e:
- raise AdcmApiEx(e.code, e.msg, e.http_code) from e
- return Response(status=status.HTTP_204_NO_CONTENT)
-
-
-class Stats(GenericAPIPermView):
- queryset = JobLog.objects.all()
- serializer_class = api.serializers.StatsSerializer
-
- def get(self, request):
- """
- Statistics
- """
- obj = JobLog(id=1)
- serializer = self.serializer_class(obj, context={'request': request})
- return Response(serializer.data)
-
-
-class JobStats(GenericAPIPermView):
- queryset = JobLog.objects.all()
- serializer_class = api.serializers.EmptySerializer
-
- def get(self, request, job_id):
- """
- Show jobs stats
- """
- jobs = self.get_queryset().filter(id__gt=job_id)
- 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(),
- }
- return Response(data)
-
-
-class TaskStats(GenericAPIPermView):
- queryset = TaskLog.objects.all()
- serializer_class = api.serializers.EmptySerializer
-
- def get(self, request, task_id):
- """
- Show tasks stats
- """
- tasks = self.get_queryset().filter(id__gt=task_id)
- 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(),
- }
- return Response(data)
-
-
-class ProviderUpgrade(PageView):
- queryset = Upgrade.objects.all()
- serializer_class = api.serializers.UpgradeProviderSerializer
-
- def get(self, request, provider_id): # pylint: disable=arguments-differ
- """
- List all avaliable upgrades for specified host provider
- """
- provider = check_obj(HostProvider, provider_id, 'PROVIDER_NOT_FOUND')
- obj = cm.upgrade.get_upgrade(provider, self.get_ordering(request, self.queryset, self))
- serializer = self.serializer_class(obj, many=True, context={
- 'provider_id': provider.id, 'request': request
- })
- return Response(serializer.data)
-
-
-class ProviderUpgradeDetail(ListView):
- queryset = Upgrade.objects.all()
- serializer_class = api.serializers.UpgradeProviderSerializer
-
- def get(self, request, provider_id, upgrade_id): # pylint: disable=arguments-differ
- """
- List all avaliable upgrades for specified host provider
- """
- provider = check_obj(HostProvider, provider_id, 'PROVIDER_NOT_FOUND')
- obj = self.get_queryset().get(id=upgrade_id)
- serializer = self.serializer_class(obj, context={
- 'provider_id': provider.id, 'request': request
- })
- return Response(serializer.data)
-
-
-class DoProviderUpgrade(GenericAPIPermView):
- queryset = Upgrade.objects.all()
- serializer_class = api.serializers.DoUpgradeSerializer
-
- def post(self, request, provider_id, upgrade_id):
- """
- Do upgrade specified host provider
- """
- provider = check_obj(HostProvider, provider_id, 'PROVIDER_NOT_FOUND')
- serializer = self.serializer_class(data=request.data, context={'request': request})
- return create(serializer, upgrade_id=int(upgrade_id), obj=provider)
diff --git a/python/check_adcm_bundle.py b/python/check_adcm_bundle.py
new file mode 100755
index 0000000000..887b54d7c3
--- /dev/null
+++ b/python/check_adcm_bundle.py
@@ -0,0 +1,68 @@
+#!/usr/bin/env python3
+# 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
+import sys
+import shutil
+import tarfile
+import argparse
+
+import cm.config
+from check_adcm_config import check_config
+
+
+TMP_DIR = '/tmp/adcm_bundle_tmp'
+
+
+def untar(bundle_file):
+ if os.path.isdir(TMP_DIR):
+ shutil.rmtree(TMP_DIR)
+ tar = tarfile.open(bundle_file)
+ tar.extractall(path=TMP_DIR)
+ tar.close()
+
+
+def get_config_files(path):
+ conf_list = []
+ conf_files = ('config.yaml', 'config.yml')
+ for root, _, files in os.walk(path):
+ for conf_file in conf_files:
+ if conf_file in files:
+ conf_list.append(os.path.join(root, conf_file))
+ return conf_list
+
+
+def check_bundle(bundle_file, use_directory=False, verbose=False):
+ if not use_directory:
+ try:
+ untar(bundle_file)
+ except FileNotFoundError as e:
+ print(e)
+ sys.exit(1)
+ 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)
+
+
+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(
+ "-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
new file mode 100755
index 0000000000..cf19a32c87
--- /dev/null
+++ b/python/check_adcm_config.py
@@ -0,0 +1,68 @@
+#!/usr/bin/env python3
+# 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
+import sys
+import ruyaml
+import argparse
+
+import cm.config
+import cm.checker
+
+
+def check_config(data_file, schema_file, print_ok=True):
+ rules = ruyaml.round_trip_load(open(schema_file))
+ try:
+ # ruyaml.version_info=(0, 15, 0) # switch off duplicate key error
+ data = ruyaml.round_trip_load(open(data_file), 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}')
+ return 1
+ except (ruyaml.parser.ParserError, ruyaml.scanner.ScannerError, NotImplementedError) as e:
+ print(f'Config file "{data_file}" YAML Parser Error:')
+ print(f'{e}')
+ return 1
+
+ try:
+ cm.checker.check(data, rules)
+ if print_ok:
+ print(f'Config file "{data_file}" is OK')
+ return 0
+ except cm.checker.DataError as e:
+ print(f'File "{data_file}", error: {e}')
+ return 1
+ except cm.checker.SchemaError as e:
+ print(f'File "{schema_file}" error: {e}')
+ return 1
+ except cm.checker.FormatError as e:
+ print(f'Data File "{data_file}" Errors:')
+ print(f'\tline {e.line}: {e.message}')
+ if e.errors:
+ for ee in e.errors:
+ if 'Input data for' in ee.message:
+ continue
+ 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')
+ 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'))
+ sys.exit(r)
diff --git a/python/cm/adcm_config.py b/python/cm/adcm_config.py
index d6c8a02f28..9fe64004ff 100644
--- a/python/cm/adcm_config.py
+++ b/python/cm/adcm_config.py
@@ -16,20 +16,15 @@
import os
import yspec.checker
+from ansible.parsing.vault import VaultSecret, VaultAES256
from django.conf import settings
-from django.db import DEFAULT_DB_ALIAS, connections
-from django.db.migrations.executor import MigrationExecutor
from django.db.utils import OperationalError
-from ansible.parsing.vault import VaultSecret, VaultAES256
import cm.config as config
-from cm.errors import AdcmApiEx, AdcmEx
+import cm.variant
from cm.errors import raise_AdcmEx as err
from cm.logger import log
-from cm.models import (
- Cluster, Prototype, Host, HostProvider, ADCM, ClusterObject, ServiceComponent,
- PrototypeConfig, ObjectConfig, ConfigLog, HostComponent
-)
+from cm.models import ADCM, PrototypeConfig, ObjectConfig, ConfigLog
def proto_ref(proto):
@@ -77,7 +72,7 @@ def get_default(c, proto=None): # pylint: disable=too-many-branches
value = c.default
elif c.type == 'text':
value = c.default
- elif c.type == 'password':
+ elif c.type in ('password', 'secrettext'):
if c.default:
value = ansible_encrypt_and_format(c.default)
elif type_is_complex(c.type):
@@ -167,18 +162,6 @@ def load_social_auth():
return
except OperationalError:
return
- except AdcmEx as error:
- # This code handles the "JSON_DB_ERROR" error that occurs when
- # the "0057_auto_20200831_1055" migration is applied. In the "ADCM" object,
- # the "stack" field type was changed from "TextField" to "JSONField", so the "stack" field
- # contained an empty string, which is not a valid json format.
- # This error occurs due to the fact that when "manage.py migrate" is started, the "urls.py"
- # module is imported, in which the "load_social_auth()" function is called.
- if error.code == 'JSON_DB_ERROR':
- executor = MigrationExecutor(connections[DEFAULT_DB_ALIAS])
- if ('cm', '0057_auto_20200831_1055') not in executor.loader.applied_migrations:
- return
- raise error
try:
cl = ConfigLog.objects.get(obj_ref=adcm[0].config, id=adcm[0].config.current)
@@ -289,10 +272,7 @@ def is_new_default(key):
def restore_cluster_config(obj_conf, version, desc=''):
- try:
- cl = ConfigLog.objects.get(obj_ref=obj_conf, id=version)
- except ConfigLog.DoesNotExist:
- raise AdcmApiEx('CONFIG_NOT_FOUND', "config version doesn't exist") from None
+ cl = ConfigLog.obj.get(obj_ref=obj_conf, id=version)
obj_conf.previous = obj_conf.current
obj_conf.current = version
obj_conf.save()
@@ -387,11 +367,11 @@ def update_password(passwd):
for key in conf:
if 'type' in spec[key]:
- if spec[key]['type'] == 'password' and conf[key]:
+ if spec[key]['type'] in ('password', 'secrettext') and conf[key]:
conf[key] = update_password(conf[key])
else:
for subkey in conf[key]:
- if spec[key][subkey]['type'] == 'password' and conf[key][subkey]:
+ if spec[key][subkey]['type'] in ('password', 'secrettext') and conf[key][subkey]:
conf[key][subkey] = update_password(conf[key][subkey])
return conf
@@ -405,7 +385,7 @@ def process_config(obj, spec, old_conf): # pylint: disable=too-many-branches
if conf[key] is not None:
if spec[key]['type'] == 'file':
conf[key] = cook_file_type_name(obj, key, '')
- elif spec[key]['type'] == 'password':
+ elif spec[key]['type'] in ('password', 'secrettext'):
if config.ANSIBLE_VAULT_HEADER in conf[key]:
conf[key] = {'__ansible_vault': conf[key]}
elif conf[key]:
@@ -413,7 +393,7 @@ def process_config(obj, spec, old_conf): # pylint: disable=too-many-branches
if conf[key][subkey] is not None:
if spec[key][subkey]['type'] == 'file':
conf[key][subkey] = cook_file_type_name(obj, key, subkey)
- elif spec[key][subkey]['type'] == 'password':
+ elif spec[key][subkey]['type'] in ('password', 'secrettext'):
if config.ANSIBLE_VAULT_HEADER in conf[key][subkey]:
conf[key][subkey] = {'__ansible_vault': conf[key][subkey]}
return conf
@@ -427,132 +407,6 @@ def group_is_activatable(spec):
return False
-def get_cluster(obj):
- if obj.prototype.type == 'service':
- cluster = obj.cluster
- elif obj.prototype.type == 'host':
- cluster = obj.cluster
- elif obj.prototype.type == 'cluster':
- cluster = obj
- else:
- return None
- return cluster
-
-
-def variant_service_in_cluster(obj, args=None):
- out = []
- cluster = get_cluster(obj)
- if not cluster:
- return []
-
- for co in ClusterObject.objects.filter(cluster=cluster).order_by('prototype__name'):
- out.append(co.prototype.name)
- return out
-
-
-def variant_service_to_add(obj, args=None):
- out = []
- cluster = get_cluster(obj)
- if not cluster:
- return []
-
- for proto in Prototype.objects \
- .filter(bundle=cluster.prototype.bundle, type='service') \
- .exclude(id__in=ClusterObject.objects.filter(cluster=cluster).values('prototype')) \
- .order_by('name'):
- out.append(proto.name)
- return out
-
-
-def variant_host_in_cluster(obj, args=None):
- out = []
- cluster = get_cluster(obj)
- if not cluster:
- return []
-
- if args and 'service' in args:
- try:
- service = ClusterObject.objects.get(cluster=cluster, prototype__name=args['service'])
- except ClusterObject.DoesNotExist:
- return []
- if 'component' in args:
- try:
- comp = ServiceComponent.objects.get(
- cluster=cluster, service=service, component__name=args['component']
- )
- except ServiceComponent.DoesNotExist:
- return []
- 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'):
- out.append(hc.host.fqdn)
- return out
-
- for host in Host.objects.filter(cluster=cluster).order_by('fqdn'):
- out.append(host.fqdn)
- return out
-
-
-def variant_host_not_in_clusters(obj, args=None):
- out = []
- for host in Host.objects.filter(cluster=None).order_by('fqdn'):
- out.append(host.fqdn)
- return out
-
-
-VARIANT_FUNCTIONS = {
- 'host_in_cluster': variant_host_in_cluster,
- 'host_not_in_clusters': variant_host_not_in_clusters,
- 'service_in_cluster': variant_service_in_cluster,
- 'service_to_add': variant_service_to_add,
-}
-
-
-def get_builtin_variant(obj, func_name, args):
- if func_name not in VARIANT_FUNCTIONS:
- log.warning('unknown variant builtin function: %s', func_name)
- return None
- return VARIANT_FUNCTIONS[func_name](obj, args)
-
-
-def get_variant(obj, conf, limits):
- value = None
- source = limits['source']
- if source['type'] == 'config':
- skey = source['name'].split('/')
- if len(skey) == 1:
- value = conf[skey[0]]
- else:
- value = conf[skey[0]][skey[1]]
- elif source['type'] == 'builtin':
- value = get_builtin_variant(obj, source['name'], source.get('args', None))
- elif source['type'] == 'inline':
- value = source['value']
- return value
-
-
-def process_variant(obj, spec, conf):
- def set_variant(spec):
- limits = spec['limits']
- limits['source']['value'] = get_variant(obj, conf, limits)
- return limits
-
- for key in spec:
- if 'type' in spec[key]:
- if spec[key]['type'] == 'variant':
- spec[key]['limits'] = set_variant(spec[key])
- else:
- for subkey in spec[key]:
- if spec[key][subkey]['type'] == 'variant':
- spec[key][subkey]['limits'] = set_variant(spec[key][subkey])
-
-
def ui_config(obj, cl):
conf = []
_, spec, _, _ = get_prototype_config(obj.prototype)
@@ -570,7 +424,7 @@ def ui_config(obj, cl):
item['read_only'] = bool(config_is_ro(obj, key, spec[key].limits))
item['activatable'] = bool(group_is_activatable(spec[key]))
if item['type'] == 'variant':
- item['limits']['source']['value'] = get_variant(obj, obj_conf, limits)
+ item['limits']['source']['value'] = cm.variant.get_variant(obj, obj_conf, limits)
item['default'] = get_default(spec[key])
if key in flat_conf:
item['value'] = flat_conf[key]
@@ -588,7 +442,7 @@ def get_action_variant(obj, conf):
for c in conf:
if c.type != 'variant':
continue
- c.limits['source']['value'] = get_variant(obj, obj_conf, c.limits)
+ c.limits['source']['value'] = cm.variant.get_variant(obj, obj_conf, c.limits)
def config_is_ro(obj, key, limits):
@@ -643,7 +497,7 @@ def restore_read_only(obj, spec, conf, old_conf):
def check_json_config(proto, obj, new_conf, old_conf=None, attr=None):
spec, flat_spec, _, _ = get_prototype_config(proto)
check_attr(proto, attr, flat_spec)
- process_variant(obj, spec, new_conf)
+ cm.variant.process_variant(obj, spec, new_conf)
return check_config_spec(proto, obj, spec, flat_spec, new_conf, old_conf, attr)
@@ -797,7 +651,7 @@ def check_str(idx, v):
for k, v in value.items():
check_str(k, v)
- if spec['type'] in ('string', 'password', 'text'):
+ if spec['type'] in ('string', 'password', 'text', 'secrettext'):
if not isinstance(value, str):
err('CONFIG_VALUE_ERROR', tmpl2.format("should be string"))
if 'required' in spec and spec['required'] and value == '':
@@ -881,79 +735,16 @@ def replace_object_config(obj, key, subkey, value):
save_obj_config(obj.config, conf, cl.attr, 'ansible update')
-def set_cluster_config(cluster_id, keys, value):
- try:
- cluster = Cluster.objects.get(id=cluster_id)
- except Cluster.DoesNotExist:
- msg = 'Cluster # {} does not exist'
- err('CLUSTER_NOT_FOUND', msg.format(cluster_id))
- return set_object_config(cluster, keys, value)
-
-
-def set_host_config(host_id, keys, value):
- try:
- host = Host.objects.get(id=host_id)
- except Host.DoesNotExist:
- msg = 'Host # {} does not exist'
- err('HOST_NOT_FOUND', msg.format(host_id))
- return set_object_config(host, keys, value)
-
-
-def set_provider_config(provider_id, keys, value):
- try:
- provider = HostProvider.objects.get(id=provider_id)
- except HostProvider.DoesNotExist:
- msg = 'Host # {} does not exist'
- err('PROVIDER_NOT_FOUND', msg.format(provider_id))
- return set_object_config(provider, keys, value)
-
-
-def set_service_config(cluster_id, service_name, keys, value):
- try:
- cluster = Cluster.objects.get(id=cluster_id)
- except Cluster.DoesNotExist:
- msg = 'Cluster # {} does not exist'
- err('CLUSTER_NOT_FOUND', msg.format(cluster_id))
- try:
- proto = Prototype.objects.get(
- type='service', name=service_name, bundle=cluster.prototype.bundle
- )
- except Prototype.DoesNotExist:
- msg = 'Service "{}" does not exist'
- err('SERVICE_NOT_FOUND', msg.format(service_name))
- try:
- obj = ClusterObject.objects.get(cluster=cluster, prototype=proto)
- except ClusterObject.DoesNotExist:
- msg = '{} does not exist in cluster # {}'
- err('OBJECT_NOT_FOUND', msg.format(proto_ref(proto), cluster.id))
- return set_object_config(obj, keys, value)
-
-
-def set_service_config_by_id(cluster_id, service_id, keys, value):
- try:
- obj = ClusterObject.objects.get(
- id=service_id, cluster__id=cluster_id, prototype__type='service'
- )
- except ClusterObject.DoesNotExist:
- msg = 'service # {} does not exist in cluster # {}'
- err('OBJECT_NOT_FOUND', msg.format(service_id, cluster_id))
- return set_object_config(obj, keys, value)
-
-
def set_object_config(obj, keys, value):
proto = obj.prototype
- try:
- spl = keys.split('/')
- key = spl[0]
- if len(spl) == 1:
- subkey = ''
- else:
- subkey = spl[1]
- pconf = PrototypeConfig.objects.get(prototype=proto, action=None, name=key, subname=subkey)
- except PrototypeConfig.DoesNotExist:
- msg = '{} does not has config key "{}/{}"'
- err('CONFIG_NOT_FOUND', msg.format(proto_ref(proto), key, subkey))
+ spl = keys.split('/')
+ key = spl[0]
+ if len(spl) == 1:
+ subkey = ''
+ else:
+ subkey = spl[1]
+ pconf = PrototypeConfig.obj.get(prototype=proto, action=None, name=key, subname=subkey)
if pconf.type == 'group':
msg = 'You can not update config group "{}" for {}'
err('CONFIG_VALUE_ERROR', msg.format(key, obj_ref(obj)))
diff --git a/python/cm/adcm_schema.yaml b/python/cm/adcm_schema.yaml
new file mode 100644
index 0000000000..e531b96e71
--- /dev/null
+++ b/python/cm/adcm_schema.yaml
@@ -0,0 +1,797 @@
+---
+# Main config.yaml object should be a list of Objects
+root:
+ match: list
+ item: object
+
+###############################################################################
+# O B J E C T S
+###############################################################################
+# There are a number of object types: cluster, provider, host and service
+# All of them has the same struture but with small difference
+
+object:
+ match: dict_key_selection
+ selector: type
+ variants:
+ cluster: cluster_object
+ service: service_object
+ host: host_object
+ provider: provider_object
+ adcm: adcm_object
+
+common_object: &common_object
+ match: dict
+ items: &common_object_items
+ type: string
+ name: string
+ version: version_rule
+ display_name: string
+ description: string
+ edition: string
+ license: string
+ adcm_min_version: version_rule
+ config: config_obj
+ actions: actions_dict
+ required_items: &common_object_required_items
+ - type
+ - name
+ - version
+
+service_object:
+ <<: *common_object
+ items:
+ <<: *common_object_items
+ import: import_dict
+ export: export
+ shared: boolean
+ components: components_dict
+ required: boolean
+ monitoring: monitoring
+
+cluster_object:
+ <<: *common_object
+ items:
+ <<: *common_object_items
+ upgrade: upgrade_list
+ import: import_dict
+ export: export
+
+host_object:
+ <<: *common_object
+ items:
+ <<: *common_object_items
+
+provider_object:
+ <<: *common_object
+ items:
+ <<: *common_object_items
+ upgrade: upgrade_list
+
+adcm_object:
+ <<: *common_object
+ items:
+ <<: *common_object_items
+ upgrade: upgrade_list
+
+export:
+ match: one_of
+ variants:
+ - string
+ - list_of_string
+
+monitoring:
+ match: set
+ variants:
+ - active
+ - passive
+
+# Components
+components_dict:
+ match: dict
+ default_item: component_item
+
+component_item:
+ match: one_of
+ variants:
+ - none
+ - component_dict
+
+component_dict:
+ match: dict
+ items:
+ display_name: string
+ description: string
+ monitoring: monitoring
+ constraint: constraint_list
+ bound_to: bound_dict
+ params: json
+ requires: comp_req_list
+ config: config_obj
+ actions: actions_dict
+
+comp_req_list:
+ match: list
+ item: comp_req_item
+
+comp_req_item:
+ match: dict
+ items:
+ service: string
+ component: string
+ required_items:
+ - component
+
+constraint_list:
+ match: list
+ item: constraint_list_item
+
+constraint_list_item:
+ match: one_of
+ variants:
+ - integer
+ - constraint_variants
+
+constraint_variants:
+ match: set
+ variants:
+ - "+"
+ - odd
+
+bound_dict:
+ match: dict
+ items:
+ service: string
+ component: string
+ required_items:
+ - service
+ - component
+
+version_rule:
+ match: one_of
+ variants:
+ - integer
+ - string
+ - float
+
+# Upgrade Block
+upgrade_list:
+ match: list
+ item: upgrade_obj
+
+upgrade_obj:
+ match: dict
+ items:
+ name: version_rule
+ description: string
+ versions: version_dict
+ states: states_dict
+ from_edition: any_or_list
+ required_items:
+ - name
+ - versions
+
+version_dict:
+ match: dict
+ items:
+ min: version_rule
+ max: version_rule
+ min_strict: version_rule
+ max_strict: version_rule
+
+states_dict:
+ match: dict
+ items:
+ available: any_or_list
+ on_success: string
+ on_fail: string
+
+# Config block of object could be in two forms: dict or list
+config_obj:
+ match: one_of
+ variants:
+ - config_dict
+ - config_list
+
+## Config dict rules
+config_dict:
+ match: dict
+ default_item: config_dict_obj
+
+config_dict_obj:
+ match: one_of
+ variants:
+ - config_dict_sub
+ - config_dict_group
+
+config_dict_group:
+ match: dict
+ default_item: config_dict_sub
+
+config_dict_sub:
+ match: dict_key_selection
+ selector: type
+ variants:
+ boolean: config_dict_sub_boolean
+ integer: config_dict_sub_integer
+ float: config_dict_sub_float
+ string: config_dict_sub_string
+ password: config_dict_sub_string
+ secrettext: config_dict_sub_string
+ text: config_dict_sub_string
+ file: config_dict_sub_string
+ list: config_dict_sub_list
+ map: config_dict_sub_map
+ structure: config_dict_sub_structure
+ json: config_dict_sub_json
+ option: config_dict_sub_option
+ variant: config_dict_sub_variant
+
+## Common fields for config of dict type
+config_dict_sub_common: &config_dict_sub_common
+ match: dict
+ items: &config_dict_sub_items
+ type: string
+ read_only: any_or_list
+ writable: any_or_list
+ required: boolean
+ display_name: string
+ description: string_or_none
+ ui_options: json
+ required_items: &config_dict_sub_required
+ - type
+
+config_dict_sub_boolean:
+ <<: *config_dict_sub_common
+ items:
+ <<: *config_dict_sub_items
+ default: boolean
+
+config_dict_sub_integer:
+ <<: *config_dict_sub_common
+ items:
+ <<: *config_dict_sub_items
+ min: integer
+ max: integer
+ default: integer
+
+config_dict_sub_float:
+ <<: *config_dict_sub_common
+ items:
+ <<: *config_dict_sub_items
+ min: number
+ max: number
+ default: number
+
+config_dict_sub_string:
+ <<: *config_dict_sub_common
+ items:
+ <<: *config_dict_sub_items
+ default: string
+
+config_dict_sub_list:
+ <<: *config_dict_sub_common
+ items:
+ <<: *config_dict_sub_items
+ default: list_of_string
+
+config_dict_sub_map:
+ <<: *config_dict_sub_common
+ items:
+ <<: *config_dict_sub_items
+ default: map_string_string
+
+config_dict_sub_structure:
+ <<: *config_dict_sub_common
+ items:
+ <<: *config_dict_sub_items
+ default: json
+ yspec: string
+ required_items:
+ - yspec
+
+config_dict_sub_json:
+ <<: *config_dict_sub_common
+ items:
+ <<: *config_dict_sub_items
+ default: json
+
+config_dict_sub_option:
+ <<: *config_dict_sub_common
+ items:
+ <<: *config_dict_sub_items
+ option: map_string_any
+ default: base_type
+ required_items:
+ - option
+
+config_dict_sub_variant:
+ <<: *config_dict_sub_common
+ items:
+ <<: *config_dict_sub_items
+ source: variant_source
+ default: string
+
+## Config list rules
+config_list:
+ match: list
+ item: config_list_object
+
+config_list_object:
+ match: dict_key_selection
+ selector: type
+ variants:
+ group: config_list_group
+ boolean: config_list_boolean
+ integer: config_list_integer
+ float: config_list_float
+ string: config_list_string
+ password: config_list_string
+ secrettext: config_list_string
+ text: config_list_string
+ file: config_list_string
+ list: config_list_list
+ map: config_list_map
+ structure: config_list_structure
+ variant: config_list_variant
+ json: config_list_json
+ option: config_list_option
+
+## Common fields for config of list type
+config_list_common: &config_list_common
+ match: dict
+ items: &config_list_items
+ <<: *config_dict_sub_items
+ name: string
+ required_items: &config_list_required
+ - type
+ - name
+
+config_list_group:
+ match: dict
+ items:
+ <<: *config_list_items
+ subs: config_list_sub_list
+ activatable: boolean
+ active: boolean
+ required_items:
+ - name
+ - type
+ - subs
+
+config_list_sub_list:
+ match: list
+ item: config_sub_list_object
+
+config_sub_list_object:
+ match: dict_key_selection
+ selector: type
+ variants:
+ boolean: config_list_boolean
+ integer: config_list_integer
+ float: config_list_float
+ string: config_list_string
+ password: config_list_string
+ secrettext: config_list_string
+ text: config_list_string
+ file: config_list_string
+ list: config_list_list
+ map: config_list_map
+ structure: config_list_structure
+ variant: config_list_variant
+ json: config_list_json
+ option: config_list_option
+
+config_list_boolean:
+ <<: *config_list_common
+ items:
+ <<: *config_list_items
+ default: boolean
+
+config_list_string:
+ <<: *config_list_common
+ items:
+ <<: *config_list_items
+ default: string
+
+config_list_integer:
+ <<: *config_list_common
+ items:
+ <<: *config_list_items
+ min: integer
+ max: integer
+ default: integer
+
+config_list_float:
+ <<: *config_list_common
+ items:
+ <<: *config_list_items
+ min: number
+ max: number
+ default: number
+
+config_list_list:
+ <<: *config_list_common
+ items:
+ <<: *config_list_items
+ default: list_of_string
+
+config_list_json:
+ <<: *config_list_common
+ items:
+ <<: *config_list_items
+ default: json
+
+config_list_option:
+ <<: *config_list_common
+ items:
+ <<: *config_list_items
+ option: map_string_any
+ default: base_type
+ required_items:
+ - option
+
+config_list_map:
+ <<: *config_list_common
+ items:
+ <<: *config_list_items
+ default: map_string_string
+
+config_list_structure:
+ <<: *config_list_common
+ items:
+ <<: *config_list_items
+ default: json
+ yspec: string
+ required_items:
+ - yspec
+
+### Variant config type
+config_list_variant:
+ <<: *config_list_common
+ items:
+ <<: *config_list_items
+ default: string
+ source: variant_source
+ required_items:
+ - source
+
+variant_common:
+ match: dict
+ items: &varinat_items
+ type: string
+ strict: boolean
+
+variant_source:
+ match: dict_key_selection
+ selector: type
+ variants:
+ inline: variant_inline
+ config: variant_config
+ builtin: variant_builtin
+
+variant_inline:
+ match: dict
+ items:
+ <<: *varinat_items
+ value: list_of_string
+ required_items:
+ - value
+
+variant_config:
+ match: dict
+ items:
+ <<: *varinat_items
+ name: string
+ required_items:
+ - name
+
+variant_builtin:
+ match: dict_key_selection
+ selector: name
+ variants:
+ host: var_func_host
+ host_in_cluster: var_func_host_in_cluster
+ host_not_in_clusters: var_builtin_func
+ service_in_cluster: var_builtin_func
+ service_to_add: var_builtin_func
+
+var_builtin_func:
+ match: dict
+ items:
+ <<: *varinat_items
+ name: string
+
+var_func_host_in_cluster:
+ match: dict
+ items:
+ <<: *varinat_items
+ name: string
+ args: var_func_host_in_cluster_args
+
+var_func_host_in_cluster_args:
+ match: dict
+ items:
+ service: string
+ component: string
+ required_items:
+ - service
+
+#### Variant config type host function solver
+var_func_host:
+ match: dict
+ items:
+ <<: *varinat_items
+ name: string
+ args: vfh_predicate_item
+ required_items:
+ - name
+ - args
+
+vfh_args:
+ match: one_of
+ variants:
+ - vfh_predicate_list
+ - vfh_predicate_item
+
+vfh_predicate_list:
+ match: list
+ item: vfh_predicate_item
+
+vfh_predicate_item:
+ match: dict_key_selection
+ selector: predicate
+ variants:
+ and: vfh_predicate_and
+ or: vfh_predicate_and
+ in_cluster: vfh_predicate_in_cluster
+ in_service: vfh_predicate_in_service
+ in_component: vfh_predicate_in_component
+
+vfh_predicate_and:
+ match: dict
+ items:
+ predicate: vfh_predicates
+ args: vfh_predicate_list
+ required_items:
+ - predicate
+ - args
+
+vfh_predicate_in_cluster:
+ match: dict
+ items:
+ predicate: vfh_predicates
+ args: none
+ required_items:
+ - predicate
+ - args
+
+vfh_predicate_in_service:
+ match: dict
+ items:
+ predicate: vfh_predicates
+ args: vfh_in_service_args
+ required_items:
+ - predicate
+ - args
+
+vfh_in_service_args:
+ match: dict
+ items:
+ service: string
+ required_items:
+ - service
+
+vfh_predicate_in_component:
+ match: dict
+ items:
+ predicate: vfh_predicates
+ args: vfh_in_component_args
+ required_items:
+ - predicate
+ - args
+
+vfh_in_component_args:
+ match: dict
+ items:
+ service: string
+ component: string
+ required_items:
+ - service
+ - component
+
+vfh_predicates:
+ match: set
+ variants:
+ - and
+ - or
+ - in_service
+ - in_component
+ - in_cluster
+
+# Imports
+import_dict:
+ match: dict
+ default_item: import_dict_item
+
+import_dict_item:
+ match: dict
+ items:
+ versions: version_dict
+ required: boolean
+ multibind: boolean
+ default: list_of_string
+ required_items:
+ - versions
+
+# Actions. Actions could be in two forms: job or task
+actions_dict:
+ match: dict
+ default_item: action_item
+
+action_item:
+ match: dict_key_selection
+ selector: type
+ variants:
+ job: action_job_dict
+ task: action_task_dict
+
+common_action: &common_action
+ match: dict
+ items: &common_action_items
+ type: string
+ display_name: string
+ description: string
+ params: json
+ ui_options: json
+ button: string
+ allow_to_terminate: boolean
+ partial_execution: boolean
+ host_action: boolean
+ log_files: list_of_string
+ states: action_states_dict
+ hc_acl: action_hc_acl_list
+ config: config_obj
+
+## Task action
+action_task_dict:
+ match: dict
+ items:
+ <<: *common_action_items
+ scripts: task_list
+ required_items:
+ - type
+ - scripts
+
+task_list:
+ match: list
+ item: task_action
+
+task_action:
+ match: dict
+ items:
+ name: string
+ script: string
+ script_type: action_script_type
+ display_name: string
+ params: json
+ on_fail: string
+ required_items:
+ - name
+ - script
+ - script_type
+
+## Job action
+action_job_dict:
+ match: dict
+ items:
+ <<: *common_action_items
+ script_type: action_script_type
+ script: string
+ required_items:
+ - type
+ - script_type
+ - script
+
+action_hc_acl_list:
+ match: list
+ item: action_hc_acl_dict
+
+action_hc_acl_dict:
+ match: dict
+ items:
+ service: string
+ component: string
+ action: hc_acl_action
+ required_items:
+ - component
+ - action
+
+hc_acl_action:
+ match: set
+ variants:
+ - add
+ - remove
+
+action_script_type:
+ match: set
+ variants:
+ - ansible
+
+action_states_dict:
+ match: dict
+ items:
+ on_success: string
+ on_fail: string
+ available: any_or_list
+ required_items:
+ - available
+
+# Common types
+list_of_string:
+ match: list
+ item: string
+
+list_of_any:
+ match: list
+ item: base_type
+
+map_string_string:
+ match: dict
+ default_item: string
+
+map_string_any:
+ match: dict
+ default_item: base_type
+
+boolean:
+ match: bool
+
+string:
+ match: string
+
+integer:
+ match: int
+
+float:
+ match: float
+
+none:
+ match: none
+
+dict:
+ match: dict
+
+json:
+ match: any
+
+string_or_none:
+ match: one_of
+ variants:
+ - string
+ - none
+
+any_or_list:
+ match: one_of
+ variants:
+ - list_of_string
+ - literally_any_string
+
+literally_any_string:
+ match: set
+ variants:
+ - any
+
+number:
+ match: one_of
+ variants:
+ - integer
+ - float
+
+base_type:
+ match: one_of
+ variants:
+ - boolean
+ - string
+ - integer
+ - float
diff --git a/python/cm/ansible_plugin.py b/python/cm/ansible_plugin.py
index f93178ccde..3bb8313391 100644
--- a/python/cm/ansible_plugin.py
+++ b/python/cm/ansible_plugin.py
@@ -13,7 +13,14 @@
from ansible.errors import AnsibleError
from ansible.plugins.action import ActionBase
from ansible.utils.vars import merge_hash
+
import cm
+import cm.config as config
+from cm.errors import raise_AdcmEx as err
+from cm.api import push_obj, set_object_state, add_hc, get_hc
+from cm.adcm_config import set_object_config
+from cm.models import Cluster, ClusterObject, ServiceComponent, HostProvider, Host
+from cm.models import Prototype, Action, JobLog
MSG_NO_CONFIG = (
@@ -41,9 +48,10 @@
" Service state can be changed in service's actions only or in cluster's actions but"
" with using service_name arg. Bad Dobby!"
)
-MSG_MANDATORY_ARGS = "Type, key and value are mandatory. Bad Dobby!"
+MSG_MANDATORY_ARGS = "Arguments {} are mandatory. Bad Dobby!"
MSG_NO_ROUTE = "Incorrect combination of args. Bad Dobby!"
MSG_WRONG_SERVICE = "Do not try to change one service from another."
+MSG_NO_SERVICE_NAME = "You must specify service name in arguments."
def check_context_type(task_vars, *context_type, err_msg=None):
@@ -91,7 +99,7 @@ def _wrap_call(self, func, *args):
def _check_mandatory(self):
for arg in self._MANDATORY_ARGS:
if arg not in self._task.args:
- raise AnsibleError(MSG_MANDATORY_ARGS)
+ raise AnsibleError(MSG_MANDATORY_ARGS.format(self._MANDATORY_ARGS))
def _get_job_var(self, task_vars, name):
try:
@@ -111,7 +119,13 @@ def _do_service(self, task_vars, context):
def _do_host(self, task_vars, context):
raise NotImplementedError
- def run(self, tmp=None, task_vars=None):
+ def _do_component(self, task_vars, context):
+ raise NotImplementedError
+
+ def _do_component_by_name(self, task_vars, context):
+ raise NotImplementedError
+
+ def run(self, tmp=None, task_vars=None): # pylint: disable=too-many-branches
self._check_mandatory()
obj_type = self._task.args["type"]
@@ -164,8 +178,178 @@ def run(self, tmp=None, task_vars=None):
task_vars,
{'provider_id': self._get_job_var(task_vars, 'provider_id')}
)
+ elif obj_type == "component" and "component_name" in self._task.args:
+ check_context_type(task_vars, 'cluster', 'service', 'component')
+ context = task_vars['context']
+ if context['type'] == 'component':
+ res = self._do_component(
+ task_vars,
+ {'component_id': self._get_job_var(task_vars, 'component_id')}
+ )
+ else:
+ check_context_type(task_vars, 'cluster', 'service')
+ if context['type'] != 'service':
+ if 'service_name' not in self._task.args:
+ 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': task_vars['job'].get('service_id', None),
+ }
+ )
+ 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')}
+ )
else:
raise AnsibleError(MSG_NO_ROUTE)
result = super().run(tmp, task_vars)
return merge_hash(result, res)
+
+
+# Helper functions for ansible plugins
+
+
+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
+ )
+ else:
+ comp = ServiceComponent.obj.get(
+ cluster_id=cluster_id,
+ service__prototype__name=service_name,
+ prototype__name=component_name
+ )
+ return comp
+
+
+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
+ )
+ return ClusterObject.obj.get(cluster=cluster, prototype=proto)
+
+
+def set_cluster_state(cluster_id, state):
+ cluster = Cluster.obj.get(id=cluster_id)
+ return push_obj(cluster, state)
+
+
+def set_host_state(host_id, state):
+ host = Host.obj.get(id=host_id)
+ return push_obj(host, state)
+
+
+def set_component_state(component_id, state):
+ comp = ServiceComponent.obj.get(id=component_id)
+ return push_obj(comp, state)
+
+
+def set_component_state_by_name(cluster_id, service_id, component_name, service_name, state):
+ comp = get_component_by_name(cluster_id, service_id, component_name, service_name)
+ return push_obj(comp, state)
+
+
+def set_provider_state(provider_id, state, event):
+ provider = HostProvider.obj.get(id=provider_id)
+ if provider.state == config.Job.LOCKED:
+ return push_obj(provider, state)
+ else:
+ return set_object_state(provider, state, event)
+
+
+def set_service_state(cluster_id, service_name, state):
+ obj = get_service_by_name(cluster_id, service_name)
+ return push_obj(obj, state)
+
+
+def set_service_state_by_id(cluster_id, service_id, state):
+ obj = ClusterObject.obj.get(
+ id=service_id, cluster__id=cluster_id, prototype__type='service'
+ )
+ return push_obj(obj, state)
+
+
+def change_hc(job_id, cluster_id, operations): # pylint: disable=too-many-branches
+ '''
+ For use in ansible plugin adcm_hc
+ '''
+ 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')
+
+ 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'])
+ item = {
+ 'host_id': host.id,
+ 'service_id': service.id,
+ 'component_id': component.id,
+ }
+ 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':
+ 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))
+ else:
+ err('INVALID_INPUT', 'unknown hc action "{}"'.format(op['action']))
+
+ add_hc(cluster, hc)
+
+
+def set_cluster_config(cluster_id, keys, value):
+ cluster = Cluster.obj.get(id=cluster_id)
+ return set_object_config(cluster, keys, value)
+
+
+def set_host_config(host_id, keys, value):
+ host = Host.obj.get(id=host_id)
+ return set_object_config(host, keys, value)
+
+
+def set_provider_config(provider_id, keys, value):
+ provider = HostProvider.obj.get(id=provider_id)
+ return set_object_config(provider, keys, value)
+
+
+def set_service_config(cluster_id, service_name, keys, value):
+ obj = get_service_by_name(cluster_id, service_name)
+ return set_object_config(obj, keys, value)
+
+
+def set_service_config_by_id(cluster_id, service_id, keys, value):
+ obj = ClusterObject.obj.get(
+ id=service_id, cluster__id=cluster_id, prototype__type='service'
+ )
+ return set_object_config(obj, keys, value)
+
+
+def set_component_config_by_name(cluster_id, service_id, component_name, service_name, keys, value):
+ obj = get_component_by_name(cluster_id, service_id, component_name, service_name)
+ return set_object_config(obj, keys, value)
+
+
+def set_component_config(component_id, keys, value):
+ obj = ServiceComponent.obj.get(id=component_id)
+ return set_object_config(obj, keys, value)
diff --git a/python/cm/api.py b/python/cm/api.py
index d92bb5ce6b..9cdd039d66 100644
--- a/python/cm/api.py
+++ b/python/cm/api.py
@@ -26,13 +26,13 @@
proto_ref, obj_ref, prepare_social_auth, process_file_type, read_bundle_file,
get_prototype_config, init_object_config, save_obj_config, check_json_config
)
-from cm.errors import AdcmEx, AdcmApiEx
+from cm.errors import AdcmEx
from cm.errors import raise_AdcmEx as err
from cm.status_api import Event
from cm.models import (
Cluster, Prototype, Host, HostComponent, ADCM, ClusterObject,
ServiceComponent, ConfigLog, HostProvider, PrototypeImport, PrototypeExport,
- ClusterBind, Action, JobLog, DummyData, Role,
+ ClusterBind, DummyData, Role,
)
@@ -42,13 +42,6 @@ def check_proto_type(proto, check_type):
err('OBJ_TYPE_ERROR', msg.format(check_type, proto.type))
-def safe_api(func, args):
- try:
- return func(*args)
- except AdcmEx as e:
- raise AdcmApiEx(e.code, e.msg, e.http_code) from e
-
-
def add_cluster(proto, name, desc=''):
check_proto_type(proto, 'cluster')
check_license(proto.bundle)
@@ -58,7 +51,7 @@ def add_cluster(proto, name, desc=''):
cluster = Cluster(prototype=proto, name=name, config=obj_conf, description=desc)
cluster.save()
process_file_type(cluster, spec, conf)
- cm.issue.save_issue(cluster)
+ cm.issue.update_hierarchy_issues(cluster)
cm.status_api.post_event('create', 'cluster', cluster.id)
cm.status_api.load_service_map()
return cluster
@@ -86,7 +79,7 @@ def add_host(proto, provider, fqdn, desc='', lock=False):
host.stack = ['created']
set_object_state(host, config.Job.LOCKED, event)
process_file_type(host, spec, conf)
- cm.issue.save_issue(host)
+ cm.issue.update_hierarchy_issues(host)
event.send_state()
cm.status_api.post_event('create', 'host', host.id, 'provider', str(provider.id))
cm.status_api.load_service_map()
@@ -99,10 +92,7 @@ def add_provider_host(provider_id, fqdn, desc=''):
This is intended for use in adcm_add_host ansible plugin only
"""
- try:
- provider = HostProvider.objects.get(id=provider_id)
- except HostProvider.DoesNotExist:
- err('PROVIDER_NOT_FOUND', 'Host Provider with id #{} is not found'.format(provider_id))
+ provider = HostProvider.obj.get(id=provider_id)
proto = Prototype.objects.get(bundle=provider.prototype.bundle, type='host')
return add_host(proto, provider, fqdn, desc, lock=True)
@@ -116,7 +106,7 @@ def add_host_provider(proto, name, desc=''):
provider = HostProvider(prototype=proto, name=name, config=obj_conf, description=desc)
provider.save()
process_file_type(provider, spec, conf)
- cm.issue.save_issue(provider)
+ cm.issue.update_hierarchy_issues(provider)
cm.status_api.post_event('create', 'provider', provider.id)
return provider
@@ -141,8 +131,7 @@ def add_host_to_cluster(cluster, host):
with transaction.atomic():
host.cluster = cluster
host.save()
- cm.issue.save_issue(host)
- cm.issue.save_issue(cluster)
+ cm.issue.update_hierarchy_issues(host)
cm.status_api.post_event('add', 'host', host.id, 'cluster', str(cluster.id))
cm.status_api.load_service_map()
log.info('host #%s %s is added to cluster #%s %s', host.id, host.fqdn, cluster.id, cluster.name)
@@ -150,22 +139,13 @@ def add_host_to_cluster(cluster, host):
def get_cluster_and_host(cluster_id, fqdn, host_id):
- try:
- cluster = Cluster.objects.get(id=cluster_id)
- except Cluster.DoesNotExist:
- err('CLUSTER_NOT_FOUND', f'Cluster with id #{cluster_id} is not found')
+ cluster = Cluster.obj.get(id=cluster_id)
if fqdn:
- try:
- host = Host.objects.get(fqdn=fqdn)
- except Host.DoesNotExist:
- err('HOST_NOT_FOUND', f'Host "{fqdn}" is not found')
+ 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}")')
- try:
- host = Host.objects.get(id=host_id)
- except Host.DoesNotExist:
- err('HOST_NOT_FOUND', f'Host with id #{host_id} is not found')
+ host = Host.obj.get(id=host_id)
else:
err('HOST_NOT_FOUND', 'fqdn or host_id is mandatory args')
return (cluster, host)
@@ -210,10 +190,7 @@ def delete_host_by_id(host_id):
This is intended for use in adcm_delete_host ansible plugin only
"""
- try:
- host = Host.objects.get(id=host_id)
- except Host.DoesNotExist:
- err('HOST_NOT_FOUND', 'Host with id #{} is not found'.format(host_id))
+ host = Host.obj.get(id=host_id)
delete_host(host)
@@ -223,10 +200,7 @@ def delete_service_by_id(service_id):
This is intended for use in adcm_delete_service ansible plugin only
"""
- try:
- service = ClusterObject.objects.get(id=service_id)
- except ClusterObject.DoesNotExist:
- err('SERVICE_NOT_FOUND', 'Service with id #{} is not found'.format(service_id))
+ service = ClusterObject.obj.get(id=service_id)
service.delete()
cm.status_api.post_event('delete', 'service', service_id)
cm.status_api.load_service_map()
@@ -238,11 +212,7 @@ def delete_service_by_name(service_name, cluster_id):
This is intended for use in adcm_delete_service ansible plugin only
"""
- try:
- service = ClusterObject.objects.get(cluster__id=cluster_id, prototype__name=service_name)
- except ClusterObject.DoesNotExist:
- msg = 'Service with name "{}" not found in cluster #{}'
- err('SERVICE_NOT_FOUND', msg.format(service_name, cluster_id))
+ service = ClusterObject.obj.get(cluster__id=cluster_id, prototype__name=service_name)
service_id = service.id
service.delete()
cm.status_api.post_event('delete', 'service', service_id)
@@ -275,7 +245,7 @@ def remove_host_from_cluster(host):
with transaction.atomic():
host.cluster = None
host.save()
- cm.issue.save_issue(cluster)
+ cm.issue.update_hierarchy_issues(cluster)
cm.status_api.post_event('remove', 'host', host.id, 'cluster', str(cluster.id))
cm.status_api.load_service_map()
return host
@@ -290,7 +260,7 @@ def unbind(cbind):
with transaction.atomic():
DummyData.objects.filter(id=1).update(date=timezone.now())
cbind.delete()
- cm.issue.save_issue(cbind.cluster)
+ cm.issue.update_hierarchy_issues(cbind.cluster)
cm.status_api.post_event('delete', 'bind', cbind_id, 'cluster', str(cbind_cluster_id))
@@ -310,8 +280,7 @@ def add_service_to_cluster(cluster, proto):
cs.save()
add_components_to_service(cluster, cs)
process_file_type(cs, spec, conf)
- cm.issue.save_issue(cs)
- cm.issue.save_issue(cluster)
+ cm.issue.update_hierarchy_issues(cs)
cm.status_api.post_event('add', 'service', cs.id, 'cluster', str(cluster.id))
cm.status_api.load_service_map()
return cs
@@ -323,6 +292,7 @@ def add_components_to_service(cluster, service):
obj_conf = init_object_config(spec, conf, attr)
sc = ServiceComponent(cluster=cluster, service=service, prototype=comp, config=obj_conf)
sc.save()
+ cm.issue.update_hierarchy_issues(sc)
def add_user_role(user, role):
@@ -441,7 +411,7 @@ def update_obj_config(obj_conf, conf, attr, desc=''):
new_conf = check_json_config(proto, obj, conf, old_conf.config, attr)
with transaction.atomic():
cl = save_obj_config(obj_conf, new_conf, attr, desc)
- cm.issue.save_issue(obj)
+ cm.issue.update_hierarchy_issues(obj)
if hasattr(obj_conf, 'adcm'):
prepare_social_auth(new_conf)
cm.status_api.post_event('change_config', proto.type, obj.id, 'version', str(cl.id))
@@ -498,23 +468,9 @@ def check_sub(sub_key, sub_type, item):
host_comp_list = []
for item in hc_in:
- try:
- host = Host.objects.get(id=item['host_id'])
- except Host.DoesNotExist:
- msg = 'No host #{}'.format(item['host_id'])
- raise AdcmEx('HOST_NOT_FOUND', msg) from None
- try:
- service = ClusterObject.objects.get(id=item['service_id'], cluster=cluster)
- except ClusterObject.DoesNotExist:
- msg = 'No service #{} in {}'.format(item['service_id'], obj_ref(cluster))
- raise AdcmEx('SERVICE_NOT_FOUND', msg) from None
- try:
- comp = ServiceComponent.objects.get(
- id=item['component_id'], cluster=cluster, service=service
- )
- except ServiceComponent.DoesNotExist:
- msg = 'No component #{} in {} '.format(item['component_id'], obj_ref(service))
- raise AdcmEx('COMPONENT_NOT_FOUND', msg) from None
+ 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)
if not host.cluster:
msg = 'host #{} {} does not belong to any cluster'.format(host.id, host.fqdn)
raise AdcmEx("FOREIGN_HOST", msg)
@@ -544,7 +500,7 @@ def save_hc(cluster, host_comp_list):
hc.save()
result.append(hc)
cm.status_api.post_event('change_hostcomponentmap', 'cluster', cluster.id)
- cm.issue.save_issue(cluster)
+ cm.issue.update_hierarchy_issues(cluster)
cm.status_api.load_service_map()
return result
@@ -664,10 +620,7 @@ def get_bind_obj(cluster, service):
def multi_bind(cluster, service, bind_list): # pylint: disable=too-many-locals,too-many-statements
def get_pi(import_id, import_obj):
- try:
- pi = PrototypeImport.objects.get(id=import_id)
- except PrototypeImport.DoesNotExist:
- err('BIND_ERROR', 'Import with id #{} does not found'.format(import_id))
+ pi = PrototypeImport.obj.get(id=import_id)
if pi.prototype != import_obj.prototype:
msg = 'Import #{} does not belong to {}'
err('BIND_ERROR', msg.format(import_id, obj_ref(import_obj)))
@@ -676,11 +629,7 @@ def get_pi(import_id, import_obj):
def get_export_service(b, export_cluster):
export_co = None
if 'service_id' in b['export_id']:
- try:
- export_co = ClusterObject.objects.get(id=b['export_id']['service_id'])
- except ClusterObject.DoesNotExist:
- msg = 'export service with id #{} not found'
- err('BIND_ERROR', msg.format(b['export_id']['service_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)))
@@ -701,11 +650,7 @@ def cook_key(cluster, service):
new_bind = {}
for b in bind_list:
pi = get_pi(b['import_id'], import_obj)
- try:
- export_cluster = Cluster.objects.get(id=b['export_id']['cluster_id'])
- except Cluster.DoesNotExist:
- msg = 'export cluster with id #{} not found'
- err('BIND_ERROR', msg.format(b['export_id']['cluster_id']))
+ export_cluster = Cluster.obj.get(id=b['export_id']['cluster_id'])
export_obj = export_cluster
export_co = get_export_service(b, export_cluster)
if export_co:
@@ -748,9 +693,7 @@ def cook_key(cluster, service):
old_bind[key].delete()
log.info('unbind %s from %s', obj_ref(export_obj), obj_ref(import_obj))
- cm.issue.save_issue(cluster)
- if service:
- cm.issue.save_issue(service)
+ cm.issue.update_hierarchy_issues(cluster)
return get_import(cluster, service)
@@ -763,13 +706,9 @@ def bind(cluster, service, export_cluster, export_service_id): # pylint: disab
'''
export_service = None
if export_service_id:
- try:
- export_service = ClusterObject.objects.get(cluster=export_cluster, id=export_service_id)
- if not PrototypeExport.objects.filter(prototype=export_service.prototype):
- err('BIND_ERROR', '{} do not have exports'.format(obj_ref(export_service)))
- except ClusterObject.DoesNotExist:
- msg = 'service #{} does not exists or does not belong to cluster # {}'
- err('SERVICE_NOT_FOUND', msg.format(export_service_id, export_cluster.id))
+ export_service = ClusterObject.obj.get(cluster=export_cluster, id=export_service_id)
+ if not PrototypeExport.objects.filter(prototype=export_service.prototype):
+ err('BIND_ERROR', '{} do not have exports'.format(obj_ref(export_service)))
name = export_service.prototype.name
else:
if not PrototypeExport.objects.filter(prototype=export_cluster.prototype):
@@ -781,9 +720,7 @@ def bind(cluster, service, export_cluster, export_service_id): # pylint: disab
import_obj = service
try:
- pi = PrototypeImport.objects.get(prototype=import_obj.prototype, name=name)
- except PrototypeImport.DoesNotExist:
- err('BIND_ERROR', '{} does not have appropriate import'.format(obj_ref(import_obj)))
+ 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/')
@@ -831,7 +768,6 @@ def check_multi_bind(actual_import, cluster, service, export_cluster, export_ser
def push_obj(obj, state):
stack = obj.stack
-
if not stack:
stack = [state]
else:
@@ -847,124 +783,3 @@ def set_object_state(obj, state, event):
event.set_object_state(obj.prototype.type, obj.id, state)
log.info('set %s state to "%s"', obj_ref(obj), state)
return obj
-
-
-def set_cluster_state(cluster_id, state):
- try:
- cluster = Cluster.objects.get(id=cluster_id)
- except Cluster.DoesNotExist:
- msg = 'Cluster # {} does not exist'
- err('CLUSTER_NOT_FOUND', msg.format(cluster_id))
- return push_obj(cluster, state)
-
-
-def set_host_state(host_id, state):
- try:
- host = Host.objects.get(id=host_id)
- except Host.DoesNotExist:
- msg = 'Host # {} does not exist'
- err('HOST_NOT_FOUND', msg.format(host_id))
- return push_obj(host, state)
-
-
-def set_provider_state(provider_id, state, event):
- try:
- provider = HostProvider.objects.get(id=provider_id)
- except HostProvider.DoesNotExist:
- msg = 'Host Provider # {} does not exist'
- err('PROVIDER_NOT_FOUND', msg.format(provider_id))
- if provider.state == config.Job.LOCKED:
- return push_obj(provider, state)
- else:
- return set_object_state(provider, state, event)
-
-
-def set_service_state(cluster_id, service_name, state):
- try:
- cluster = Cluster.objects.get(id=cluster_id)
- except Cluster.DoesNotExist:
- msg = 'Cluster # {} does not exist'
- err('CLUSTER_NOT_FOUND', msg.format(cluster_id))
- try:
- proto = Prototype.objects.get(
- type='service',
- name=service_name,
- bundle=cluster.prototype.bundle
- )
- except Prototype.DoesNotExist:
- msg = 'Service "{}" does not exist'
- err('SERVICE_NOT_FOUND', msg.format(service_name))
- try:
- obj = ClusterObject.objects.get(cluster=cluster, prototype=proto)
- except ClusterObject.DoesNotExist:
- msg = '{} does not exist in cluster # {}'
- err('OBJECT_NOT_FOUND', msg.format(proto_ref(proto), cluster.id))
- return push_obj(obj, state)
-
-
-def set_service_state_by_id(cluster_id, service_id, state):
- try:
- obj = ClusterObject.objects.get(
- id=service_id, cluster__id=cluster_id, prototype__type='service'
- )
- except ClusterObject.DoesNotExist:
- msg = 'service # {} does not exist in cluster # {}'
- err('OBJECT_NOT_FOUND', msg.format(service_id, cluster_id))
- return push_obj(obj, state)
-
-
-def change_hc(job_id, cluster_id, operations): # pylint: disable=too-many-branches
- '''
- For use in ansible plugin adcm_hc
- '''
- 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')
-
- try:
- cluster = Cluster.objects.get(id=cluster_id)
- except Cluster.DoesNotExist:
- msg = 'Cluster # {} does not exist'
- err('CLUSTER_NOT_FOUND', msg.format(cluster_id))
-
- hc = get_hc(cluster)
- for op in operations:
- try:
- service = ClusterObject.objects.get(cluster=cluster, prototype__name=op['service'])
- except ClusterObject.DoesNotExist:
- msg = 'service "{}" does not exist in cluster #{}'
- err('SERVICE_NOT_FOUND', msg.format(op['service'], cluster.id))
- try:
- component = ServiceComponent.objects.get(
- cluster=cluster, service=service, prototype__name=op['component']
- )
- except ServiceComponent.DoesNotExist:
- msg = 'component "{}" does not exist in service "{}"'
- err('SERVICE_NOT_FOUND', msg.format(op['component'], service.prototype.name))
- try:
- host = Host.objects.get(cluster=cluster, fqdn=op['host'])
- except Host.DoesNotExist:
- msg = 'host "{}" does not exist in cluster #{}'
- err('HOST_NOT_FOUND', msg.format(op['host'], cluster.id))
- item = {
- 'host_id': host.id,
- 'service_id': service.id,
- 'component_id': component.id,
- }
- 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':
- 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))
- else:
- err('INVALID_INPUT', 'unknown hc action "{}"'.format(op['action']))
-
- add_hc(cluster, hc)
diff --git a/python/cm/bundle.py b/python/cm/bundle.py
index 71b08d586b..64a90d8e08 100644
--- a/python/cm/bundle.py
+++ b/python/cm/bundle.py
@@ -258,21 +258,11 @@ def check_component_requires(comp):
req_list = comp.requires
for i, item in enumerate(req_list):
if 'service' in item:
- try:
- service = StagePrototype.objects.get(name=item['service'], type='service')
- except StagePrototype.DoesNotExist:
- msg = 'Unknown service "{}" {}'
- err('COMPONENT_CONSTRAINT_ERROR', msg.format(item['service'], ref))
+ service = StagePrototype.obj.get(name=item['service'], type='service')
else:
- service = comp.prototype
- req_list[i]['service'] = comp.prototype.name
- try:
- req_comp = StagePrototype.objects.get(
- name=item['component'], type='component', parent=service
- )
- except StagePrototype.DoesNotExist:
- msg = 'Unknown component "{}" {}'
- err('COMPONENT_CONSTRAINT_ERROR', msg.format(item['component'], ref))
+ service = comp.parent
+ 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))
@@ -285,20 +275,8 @@ def check_bound_component(comp):
return
ref = 'in "bound_to" of component "{}" of {}'.format(comp.name, proto_ref(comp.parent))
bind = comp.bound_to
- try:
- service = StagePrototype.objects.get(name=bind['service'], type='service')
- except StagePrototype.DoesNotExist:
- msg = 'Unknown service "{}" {}'
- err('COMPONENT_CONSTRAINT_ERROR', msg.format(bind['service'], ref))
-
- try:
- bind_comp = StagePrototype.objects.get(
- name=bind['component'], type='component', parent=service
- )
- except StagePrototype.DoesNotExist:
- msg = 'Unknown component "{}" {}'
- err('COMPONENT_CONSTRAINT_ERROR', msg.format(bind['component'], ref))
-
+ 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))
@@ -310,6 +288,30 @@ def re_check_components():
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 args is None:
+ return
+ if isinstance(args, dict):
+ if 'predicate' not in args:
+ return
+ 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)
+
+
def re_check_config():
for c in StagePrototypeConfig.objects.filter(type='variant'):
ref = proto_ref(c.prototype)
@@ -333,17 +335,20 @@ def re_check_config():
elif lim['source']['type'] == 'builtin':
if not lim['source']['args']:
continue
+ 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']
try:
- 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']
try:
- StagePrototype.objects.get(type='component', name=comp, parent=c.prototype)
+ 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))
@@ -396,7 +401,7 @@ def copy_stage_actons(stage_actions, prototype):
('name', 'type', 'script', 'script_type', 'state_on_success',
'state_on_fail', 'state_available', 'params', 'log_files',
'hostcomponentmap', 'button', 'display_name', 'description', 'ui_options',
- 'allow_to_terminate', 'partial_execution')
+ 'allow_to_terminate', 'partial_execution', 'host_action')
)
Action.objects.bulk_create(actions)
@@ -529,14 +534,14 @@ def update_bundle_from_stage(bundle): # pylint: disable=too-many-locals,too-ma
'type', 'script', 'script_type', 'state_on_success',
'state_on_fail', 'state_available', 'params', 'log_files',
'hostcomponentmap', 'button', 'display_name', 'description', 'ui_options',
- 'allow_to_terminate', 'partial_execution'
+ 'allow_to_terminate', 'partial_execution', 'host_action'
))
except Action.DoesNotExist:
action = copy_obj(saction, Action, (
'name', 'type', 'script', 'script_type', 'state_on_success',
'state_on_fail', 'state_available', 'params', 'log_files',
'hostcomponentmap', 'button', 'display_name', 'description', 'ui_options',
- 'allow_to_terminate', 'partial_execution'
+ 'allow_to_terminate', 'partial_execution', 'host_action'
))
action.prototype = p
action.save()
diff --git a/python/cm/checker.py b/python/cm/checker.py
new file mode 100644
index 0000000000..883e3c4cc9
--- /dev/null
+++ b/python/cm/checker.py
@@ -0,0 +1,188 @@
+# 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
+# regarding copyright ownership. The ASF licenses this file
+# to you 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 ruyaml
+
+
+class FormatError(Exception):
+ def __init__(self, path, message, data=None, rule=None, parent=None, caused_by=None):
+ self.path = path
+ self.message = message
+ self.data = data
+ self.rule = rule
+ self.errors = caused_by
+ self.parent = parent
+ self.line = None
+ if isinstance(data, ruyaml.comments.CommentedBase):
+ self.line = data.lc.line
+ elif parent and isinstance(parent, ruyaml.comments.CommentedBase):
+ self.line = parent.lc.line
+ super().__init__(message)
+
+
+class SchemaError(Exception):
+ pass
+
+
+class DataError(Exception):
+ pass
+
+
+def check_type(data, data_type, path, rule=None, parent=None):
+ if not isinstance(data, data_type):
+ msg = 'Object should be a {}'.format(str(data_type))
+ if path:
+ last = path[-1]
+ msg = '{} "{}" should be a {}'.format(last[0], last[1], str(data_type))
+ raise FormatError(path, msg, data, rule, parent)
+
+
+def check_match_type(match, data, data_type, path, rule, parent=None):
+ if not isinstance(data, data_type):
+ msg = f'Input data for {match}, rule "{rule}" should be {str(data_type)}"'
+ raise FormatError(path, msg, data, rule, parent)
+
+
+def match_none(data, rules, rule, path, parent=None):
+ if data is not None:
+ msg = 'Object should be empty'
+ if path:
+ last = path[-1]
+ msg = '{} "{}" should be empty'.format(last[0], last[1])
+ raise FormatError(path, msg, data, rule, parent)
+
+
+def match_any(data, rules, rule, path, parent=None):
+ pass
+
+
+def match_list(data, rules, rule, path, parent=None):
+ 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)
+ return True
+
+
+def match_dict(data, rules, rule, path, parent=None):
+ check_match_type('match_dict', data, dict, path, rule, parent)
+ if 'required_items' in rules[rule]:
+ for i in rules[rule]['required_items']:
+ if i not in data:
+ raise FormatError(path, f'There is no required key "{i}" in map.', data, rule)
+ for k in data:
+ new_path = path + [('Value of map key', k)]
+ if 'items' in rules[rule] and k in rules[rule]['items']:
+ process_rule(data[k], rules, rules[rule]['items'][k], new_path, data)
+ elif 'default_item' in rules[rule]:
+ process_rule(data[k], rules, rules[rule]['default_item'], new_path, data)
+ else:
+ msg = f'Map key "{k}" is not allowed here (rule "{rule}")'
+ raise FormatError(path, msg, data, rule)
+
+
+def match_dict_key_selection(data, rules, rule, path, parent=None):
+ check_match_type('dict_key_selection', data, dict, path, rule, parent)
+ key = rules[rule]['selector']
+ if key not in data:
+ msg = f'There is no key "{key}" in map.'
+ raise FormatError(path, msg, data, rule, parent)
+ value = data[key]
+ if value in rules[rule]['variants']:
+ process_rule(data, rules, rules[rule]['variants'][value], path, parent)
+ elif 'default_variant' in rule:
+ process_rule(data, rules, rules[rule]['default_variant'], path, parent)
+ else:
+ msg = f'Value "{value}" is not allowed for map key "{key}".'
+ raise FormatError(path, msg, data, rule, parent)
+
+
+def match_one_of(data, rules, rule, path, parent=None):
+ errors = []
+ sub_errors = []
+ for obj in rules[rule]['variants']:
+ try:
+ process_rule(data, rules, obj, path, parent)
+ except FormatError as e:
+ if e.errors:
+ sub_errors += e.errors
+ errors.append(e)
+ if len(errors) == len(rules[rule]['variants']):
+ errors += sub_errors
+ msg = f'None of the variants for rule "{rule}" match'
+ raise FormatError(path, msg, data, rule, parent, caused_by=errors)
+
+
+def match_set(data, rules, rule, path, parent=None):
+ if data not in rules[rule]['variants']:
+ msg = f'Value "{data}" not in set {rules[rule]["variants"]}'
+ raise FormatError(path, msg, data, rule, parent=parent)
+
+
+def match_simple_type(obj_type):
+ def match(data, rules, rule, path, parent=None):
+ check_type(data, obj_type, path, rule, parent=parent)
+ return match
+
+
+MATCH = {
+ 'list': match_list,
+ 'dict': match_dict,
+ 'one_of': match_one_of,
+ 'dict_key_selection': match_dict_key_selection,
+ 'set': match_set,
+ 'string': match_simple_type(str),
+ 'bool': match_simple_type(bool),
+ 'int': match_simple_type(int),
+ 'float': match_simple_type(float),
+ 'none': match_none,
+ 'any': match_any,
+}
+
+
+def check_rule(rules):
+ if not isinstance(rules, dict):
+ return (False, 'YSpec should be a map')
+ if 'root' not in rules:
+ return (False, 'YSpec should has "root" key')
+ if 'match' not in rules['root']:
+ return (False, 'YSpec should has "match" subkey of "root" key')
+ return (True, '')
+
+
+def process_rule(data, rules, name, path=None, parent=None):
+ if path is None:
+ path = []
+ if name not in rules:
+ raise SchemaError(f"There is no rule {name} in schema.")
+ rule = rules[name]
+ if 'match' not in rule:
+ raise SchemaError(f"There is no mandatory match attr in rule {rule} in schema.")
+ match = rule['match']
+ if match not in MATCH:
+ raise SchemaError(f"Unknown match {match} from schema. Donno how to handle that.")
+
+ # print(f'process_rule: {MATCH[match].__name__} "{name}" data: {data}')
+ MATCH[match](data, rules, name, path=path, parent=parent)
+
+
+def check(data, rules):
+ if not isinstance(data, ruyaml.comments.CommentedBase):
+ raise DataError("You should use ruyaml.round_trip_load() to parse data yaml")
+ if not isinstance(rules, ruyaml.comments.CommentedBase):
+ raise SchemaError("You should use ruyaml.round_trip_load() to parse schema yaml")
+ process_rule(data, rules, 'root')
diff --git a/python/cm/errors.py b/python/cm/errors.py
index a1be347474..5787891f7a 100644
--- a/python/cm/errors.py
+++ b/python/cm/errors.py
@@ -23,6 +23,10 @@
'AUTH_ERROR': ("authenticate error", rfs.HTTP_409_CONFLICT, ERR),
'STACK_LOAD_ERROR': ("stack loading error", rfs.HTTP_409_CONFLICT, ERR),
+ 'NO_MODEL_ERROR_CODE': (
+ "django model doesn't has __error_code__ attribute", rfs.HTTP_404_NOT_FOUND, ERR
+ ),
+
'ADCM_NOT_FOUND': ("adcm object doesn't exist", rfs.HTTP_404_NOT_FOUND, ERR),
'BUNDLE_NOT_FOUND': ("bundle doesn't exist", rfs.HTTP_404_NOT_FOUND, ERR),
'CLUSTER_NOT_FOUND': ("cluster doesn't exist", rfs.HTTP_404_NOT_FOUND, ERR),
@@ -118,12 +122,13 @@
'CONFIG_KEY_ERROR': ("error in json config", rfs.HTTP_400_BAD_REQUEST, ERR),
'CONFIG_VALUE_ERROR': ("error in json config", rfs.HTTP_400_BAD_REQUEST, ERR),
'ATTRIBUTE_ERROR': ("error in attribute config", rfs.HTTP_400_BAD_REQUEST, ERR),
+ 'CONFIG_VARIANT_ERROR': ("error in config variant type", rfs.HTTP_400_BAD_REQUEST, ERR),
'TOO_LONG': ("response is too long", rfs.HTTP_400_BAD_REQUEST, WARN),
'NOT_IMPLEMENTED': ("not implemented yet", rfs.HTTP_501_NOT_IMPLEMENTED, ERR),
'NO_JOBS_RUNNING': ("no jobs running", rfs.HTTP_409_CONFLICT, ERR),
'BAD_QUERY_PARAMS': ("bad query params", rfs.HTTP_400_BAD_REQUEST),
- 'JSON_DB_ERROR': ("Not correct field format", rfs.HTTP_409_CONFLICT, ERR),
+ 'DUMP_LOAD_CLUSTER_ERROR': ("Dumping or Loading error", rfs.HTTP_409_CONFLICT),
}
@@ -141,7 +146,7 @@ def get_error(code):
return ('UNKNOWN_ERROR', msg, rfs.HTTP_501_NOT_IMPLEMENTED, CRIT)
-class AdcmEx(Exception):
+class AdcmEx(APIException):
def __init__(self, code, msg='', http_code='', args=''):
(err_code, err_msg, err_http_code, level) = get_error(code)
if msg != '':
@@ -149,23 +154,8 @@ def __init__(self, code, msg='', http_code='', args=''):
if http_code != '':
err_http_code = http_code
self.msg = err_msg
- self.code = err_code
- self.http_code = err_http_code
self.level = level
- self.adds = args
- super().__init__(err_msg)
-
- def __str__(self):
- return self.msg
-
-
-class AdcmApiEx(APIException):
- def __init__(self, code, msg='', http_code='', args=''):
- (err_code, err_msg, err_http_code, level) = get_error(code)
- if msg != '':
- err_msg = msg
- if http_code != '':
- err_http_code = http_code
+ self.code = err_code
self.status_code = err_http_code
detail = {
'code': err_code,
@@ -178,6 +168,9 @@ def __init__(self, code, msg='', http_code='', args=''):
detail['args'] = args
super().__init__(detail, err_http_code)
+ def __str__(self):
+ return self.msg
+
def raise_AdcmEx(code, msg='', args=''):
(_, err_msg, _, _) = get_error(code)
diff --git a/python/cm/hierarchy.py b/python/cm/hierarchy.py
new file mode 100644
index 0000000000..73c1c87aca
--- /dev/null
+++ b/python/cm/hierarchy.py
@@ -0,0 +1,198 @@
+# 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 Set, Optional, Tuple
+
+from cm.models import (
+ ADCMModel,
+ ClusterObject,
+ Host,
+ HostComponent,
+ ServiceComponent,
+)
+
+
+class HierarchyError(Exception):
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args)
+ self.msg = kwargs.get('msg') or args[0] if args else 'Hierarchy build error'
+
+
+class Node:
+ """
+ Node of hierarchy tree
+ Each node has zero to many parents and zero to many children
+ """
+ order = ('root', 'cluster', 'service', 'component', 'host', 'provider')
+
+ def __init__(self, value: Optional[ADCMModel]):
+ self.children = set()
+ if value is None: # tree virtual root
+ self.id = 0
+ self.type = 'root'
+ self.value = None
+ self.parents = tuple()
+ else:
+ if not hasattr(value, 'prototype'):
+ raise HierarchyError(f'Type <{type(value)}> is not part of hierarchy')
+ self.id = value.pk
+ self.type = value.prototype.type
+ self.value = value
+ self.parents = set()
+
+ def add_child(self, child: 'Node') -> None:
+ if child in self.parents or child == self:
+ raise HierarchyError("Hierarchy should not have cycles")
+ self.children.add(child)
+
+ def add_parent(self, parent: 'Node') -> None:
+ if parent in self.children or parent == self:
+ raise HierarchyError("Hierarchy should not have cycles")
+ self.parents.add(parent)
+
+ def get_parents(self) -> Set['Node']:
+ """Get own parents and all its ancestors"""
+ result = set(self.parents)
+ for parent in self.parents:
+ result.update(parent.get_parents())
+ return result
+
+ def get_children(self) -> Set['Node']:
+ """Get own children and all its descendants"""
+ result = set(self.children)
+ for child in self.children:
+ result.update(child.get_children())
+ return result
+
+ @staticmethod
+ def get_obj_key(obj) -> Tuple[str, int]:
+ """Make simple unique key for caching in tree"""
+ if obj is None:
+ return 'root', 0
+ return obj.prototype.type, obj.pk
+
+ @property
+ def key(self) -> Tuple[str, int]:
+ """Simple key unique in tree"""
+ return self.type, self.id
+
+ def __hash__(self):
+ return hash(self.key)
+
+ def __eq__(self, other):
+ return self.key == other.key
+
+
+class Tree:
+ """
+ Hierarchy tree class keep links and relations between its nodes like this:
+ common_virtual_root -> *cluster -> *service -> *component -> *host -> provider
+ """
+
+ def __init__(self, obj):
+ self.root = Node(value=None)
+ self._nodes = {self.root.key: self.root}
+ self.built_from = self._make_node(obj)
+ self._build_tree_up(self.built_from) # go to the root ...
+ self._build_tree_down(self.root) # ... and find all its children
+
+ def _make_node(self, obj) -> Node:
+ cached = self._nodes.get(Node.get_obj_key(obj))
+ if cached:
+ return cached
+ else:
+ node = Node(value=obj)
+ self._nodes[node.key] = node
+ return node
+
+ def _build_tree_down(self, node: Node) -> None:
+ if node.type == 'root':
+ children_values = [n.value for n in node.children]
+
+ if node.type == 'cluster':
+ 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()
+
+ elif node.type == 'component':
+ children_values = [
+ c.host for c in HostComponent.objects.filter(
+ cluster=node.value.service.cluster,
+ service=node.value.service,
+ component=node.value
+ ).select_related('host').all()
+ ]
+
+ elif node.type == 'host':
+ children_values = [node.value.provider]
+
+ elif node.type == 'provider':
+ children_values = []
+
+ for value in children_values:
+ child = self._make_node(value)
+ node.add_child(child)
+ child.add_parent(node)
+ self._build_tree_down(child)
+
+ def _build_tree_up(self, node: Node) -> None:
+ parent_values = []
+ if node.type == 'cluster':
+ parent_values = [None]
+ elif node.type == 'service':
+ parent_values = [node.value.cluster]
+ elif node.type == 'component':
+ parent_values = [node.value.service]
+ elif node.type == 'host':
+ parent_values = [
+ hc.component for hc in
+ HostComponent.objects.filter(host=node.value).select_related('component').all()
+ ]
+ elif node.type == 'provider':
+ parent_values = Host.objects.filter(provider=node.value).all()
+
+ for value in parent_values:
+ parent = self._make_node(value)
+ node.add_parent(parent)
+ parent.add_child(node)
+ self._build_tree_up(parent)
+
+ def get_node(self, obj) -> Node:
+ """Get tree node by its object"""
+ key = Node.get_obj_key(obj)
+ cached = self._nodes.get(key)
+ if cached:
+ return cached
+ else:
+ raise HierarchyError(f'Object {key} is not part of tree')
+
+ def get_directly_affected(self, node: Node) -> Set[Node]:
+ """Collect directly affected nodes for issues re-calc"""
+ result = {node}
+ result.update(node.get_parents())
+ result.update(node.get_children())
+ result.discard(self.root)
+ return result
+
+ def get_all_affected(self, node: Node) -> Set[Node]:
+ """Collect directly affected nodes and propagate effect back through affected hosts"""
+ directly_affected = self.get_directly_affected(node)
+ indirectly_affected = set()
+ for host_node in filter(lambda x: x.type == 'host', directly_affected):
+ indirectly_affected.update(host_node.get_parents())
+ result = indirectly_affected.union(directly_affected)
+ result.discard(self.root)
+ return result
diff --git a/python/cm/inventory.py b/python/cm/inventory.py
index f5ae82a6fe..2b7b29c047 100644
--- a/python/cm/inventory.py
+++ b/python/cm/inventory.py
@@ -203,7 +203,18 @@ def get_host(host_id):
return groups
-def prepare_job_inventory(selector, job_id, delta, action_host=None):
+def get_target_host(host_id):
+ host = Host.objects.get(id=host_id)
+ groups = {
+ 'target': {
+ 'hosts': get_hosts([host]),
+ 'vars': get_cluster_config(host.cluster.id)
+ }
+ }
+ return groups
+
+
+def prepare_job_inventory(selector, job_id, action, delta, action_host=None):
log.info('prepare inventory for job #%s, selector: %s', job_id, selector)
fd = open(os.path.join(config.RUN_DIR, f'{job_id}/inventory.json'), 'w')
inv = {'all': {'children': {}}}
@@ -212,6 +223,8 @@ def prepare_job_inventory(selector, job_id, delta, action_host=None):
inv['all']['children'].update(get_host_groups(selector['cluster'], delta, action_host))
if 'host' in selector:
inv['all']['children'].update(get_host(selector['host']))
+ if action.host_action:
+ inv['all']['children'].update(get_target_host(selector['host']))
if 'provider' in selector:
inv['all']['children'].update(get_provider_hosts(selector['provider'], action_host))
inv['all']['vars'] = get_provider_config(selector['provider'])
diff --git a/python/cm/issue.py b/python/cm/issue.py
index 348ea45e3e..d7a92f4c13 100644
--- a/python/cm/issue.py
+++ b/python/cm/issue.py
@@ -10,156 +10,135 @@
# See the License for the specific language governing permissions and
# limitations under the License.
-from cm.logger import log # pylint: disable=unused-import
+from collections import defaultdict
+
import cm.status_api
-from cm.errors import AdcmEx
-from cm.errors import raise_AdcmEx as err
from cm.adcm_config import proto_ref, obj_ref, get_prototype_config
-from cm.models import ConfigLog, Host, ClusterObject, Prototype, HostComponent
-from cm.models import PrototypeImport, ClusterBind
-
-
-def save_issue(obj):
- if obj.prototype.type == 'adcm':
- return
- obj.issue = check_issue(obj)
- obj.save()
- report_issue(obj)
-
-
-def report_issue(obj):
- issue = get_issue(obj)
- if issue_to_bool(issue):
- cm.status_api.post_event('clear_issue', obj.prototype.type, obj.id)
- else:
- cm.status_api.post_event('raise_issue', obj.prototype.type, obj.id, 'issue', issue)
-
-
-def check_issue(obj):
- disp = {
+from cm.errors import AdcmEx, raise_AdcmEx as err
+from cm.hierarchy import Tree
+from cm.logger import log
+from cm.models import (
+ ADCMModel,
+ ClusterBind,
+ ClusterObject,
+ ConfigLog,
+ Host,
+ HostComponent,
+ Prototype,
+ PrototypeImport,
+)
+
+
+class IssueReporter:
+ """Cache, compare, and report updated issues"""
+
+ def __init__(self, obj: ADCMModel):
+ self.tree = Tree(obj)
+ self._affected_nodes = self.tree.get_directly_affected(self.tree.built_from)
+ self._cache = {}
+ self._init_cache()
+
+ def _init_cache(self):
+ for node in self._affected_nodes:
+ self._cache[node.key] = aggregate_issues(node.value, self.tree)
+
+ def update_issues(self):
+ for node in self._affected_nodes:
+ obj = node.value
+ issue = check_for_issue(obj)
+ if obj.issue != issue:
+ obj.issue = issue
+ obj.save()
+
+ def report_changed(self):
+ for node in self._affected_nodes:
+ new_issue = aggregate_issues(node.value, self.tree)
+ old_issue = self._cache[node.key]
+ if new_issue != old_issue:
+ if issue_to_bool(new_issue):
+ cm.status_api.post_event('clear_issue', node.type, node.id)
+ else:
+ cm.status_api.post_event('raise_issue', node.type, node.id, 'issue', new_issue)
+
+
+def update_hierarchy_issues(obj: ADCMModel):
+ """Update issues on all directly connected objects"""
+ reporter = IssueReporter(obj)
+ reporter.update_issues()
+ reporter.report_changed()
+
+
+def check_for_issue(obj: ADCMModel):
+ type_check_map = {
'cluster': check_cluster_issue,
'service': check_service_issue,
- 'component': check_obj_issue,
- 'provider': check_obj_issue,
- 'host': check_obj_issue,
- 'adcm': check_adcm_issue,
+ 'component': check_config_issue,
+ 'provider': check_config_issue,
+ 'host': check_config_issue,
+ 'adcm': lambda x: {},
}
- if obj.prototype.type not in disp:
+ if obj.prototype.type not in type_check_map:
err('NOT_IMPLEMENTED', 'unknown object type')
- issue = disp[obj.prototype.type](obj)
+ issue = {k: v for k, v in type_check_map[obj.prototype.type](obj).items() if v is False}
log.debug('%s issue: %s', obj_ref(obj), issue)
return issue
def issue_to_bool(issue):
if isinstance(issue, dict):
- for key in issue:
- if not issue_to_bool(issue[key]):
- return False
+ return all(map(issue_to_bool, issue.values()))
elif isinstance(issue, list):
- for val in issue:
- if not issue_to_bool(val):
- return False
- elif not issue:
- return False
- return True
+ return all(map(issue_to_bool, issue))
+ else:
+ return bool(issue)
-def get_issue(obj): # pylint: disable=too-many-branches
- issue = obj.issue
- if obj.prototype.type == 'cluster':
- issue['service'] = []
- for co in ClusterObject.objects.filter(cluster=obj):
- service_iss = cook_issue(co, name_obj=co.prototype)
- if service_iss:
- issue['service'].append(service_iss)
- if not issue['service']:
- del issue['service']
- issue['host'] = []
- for host in Host.objects.filter(cluster=obj):
- host_iss = cook_issue(host, 'fqdn')
- provider_iss = cook_issue(host.provider)
- if host_iss:
- if provider_iss:
- host_iss['issue']['provider'] = provider_iss
- issue['host'].append(host_iss)
- elif provider_iss:
- issue['host'].append(cook_issue(host, 'fqdn', iss={'provider': [provider_iss]}))
- if not issue['host']:
- del issue['host']
-
- elif obj.prototype.type == 'service':
- cluster_iss = cook_issue(obj.cluster)
- if cluster_iss:
- issue['cluster'] = [cluster_iss]
-
- elif obj.prototype.type == 'component':
- cluster_iss = cook_issue(obj.cluster)
- if cluster_iss:
- issue['cluster'] = [cluster_iss]
- service_iss = cook_issue(obj.service)
- if service_iss:
- issue['service'] = [service_iss]
-
- elif obj.prototype.type == 'host':
- if obj.cluster:
- cluster_iss = cook_issue(obj.cluster)
- if cluster_iss:
- issue['cluster'] = [cluster_iss]
- if obj.provider:
- provider_iss = cook_issue(obj.provider)
- if provider_iss:
- issue['provider'] = [provider_iss]
- return issue
+def aggregate_issues(obj: ADCMModel, tree: Tree = None) -> dict:
+ """
+ Get own issue extended with issues of all nested objects
+ TODO: unify behavior for hosts (in `Host.serialized_issue`) and providers
+ """
+ issue = defaultdict(list)
+ issue.update(obj.issue)
+ if obj.prototype.type == 'provider':
+ return issue
-def cook_issue(obj, name='name', name_obj=None, iss=None):
- if not name_obj:
- name_obj = obj
- if not iss:
- if obj:
- iss = obj.issue
- else:
- iss = {}
- if iss:
- return {
- 'id': obj.id,
- 'name': getattr(name_obj, name),
- 'issue': iss,
- }
- return None
+ tree = tree or Tree(obj)
+ node = tree.get_node(obj)
+ for child in tree.get_directly_affected(node):
+ if child.key == node.key: # skip itself
+ continue
-def check_cluster_issue(cluster):
- issue = {}
- if not check_config(cluster):
- issue['config'] = False
- if not check_required_services(cluster):
- issue['required_service'] = False
- if not check_required_import(cluster):
- issue['required_import'] = False
- if not check_hc(cluster):
- issue['host_component'] = False
- return issue
+ if obj.prototype.type == 'provider':
+ continue
+ child_issue = child.value.serialized_issue
+ if child_issue:
+ issue[child.type].append(child_issue)
-def check_service_issue(service):
- issue = {}
- if not check_config(service):
- issue['config'] = False
- if not check_required_import(service.cluster, service):
- issue['required_import'] = False
return issue
-def check_obj_issue(obj):
- if not check_config(obj):
- return {'config': False}
- return {}
+def check_cluster_issue(cluster):
+ return {
+ 'config': check_config(cluster),
+ 'required_service': check_required_services(cluster),
+ 'required_import': check_required_import(cluster),
+ 'host_component': check_hc(cluster),
+ }
+
+
+def check_service_issue(service):
+ return {
+ 'config': check_config(service),
+ 'required_import': check_required_import(service.cluster, service)
+ }
-def check_adcm_issue(obj):
- return {}
+def check_config_issue(obj):
+ return {'config': check_config(obj)}
def check_config(obj): # pylint: disable=too-many-branches
diff --git a/python/cm/job.py b/python/cm/job.py
index e5c6c979fd..eb4359de13 100644
--- a/python/cm/job.py
+++ b/python/cm/job.py
@@ -28,10 +28,11 @@
from django.utils import timezone
import cm.config as config
-from cm import api, issue, inventory, adcm_config
+from cm import api, issue, inventory, adcm_config, variant
from cm.adcm_config import obj_ref, process_file_type
from cm.errors import raise_AdcmEx as err
from cm.inventory import get_obj_config, process_config_and_attr
+from cm.lock import lock_objects, unlock_objects
from cm.logger import log
from cm.models import (
Cluster, Action, SubAction, TaskLog, JobLog, CheckLog, Host, ADCM,
@@ -42,11 +43,7 @@
def start_task(action_id, selector, conf, attr, hc, hosts, verbose): # pylint: disable=too-many-locals
- try:
- action = Action.objects.get(id=action_id)
- except Action.DoesNotExist:
- err('ACTION_NOT_FOUND')
-
+ action = Action.obj.get(id=action_id)
obj, cluster, provider = check_task(action, selector, conf)
act_conf, spec = check_action_config(action, obj, conf, attr)
host_map, delta = check_hostcomponentmap(cluster, action, hc)
@@ -73,7 +70,7 @@ def start_task(action_id, selector, conf, attr, hc, hosts, verbose): # pylint:
def check_task(action, selector, conf):
obj, cluster, provider = get_action_context(action, selector)
check_action_state(action, obj)
- iss = issue.get_issue(obj)
+ iss = issue.aggregate_issues(obj)
if not issue.issue_to_bool(iss):
err('TASK_ERROR', 'action has issues', iss)
return obj, cluster, provider
@@ -89,10 +86,7 @@ def check_action_hosts(action, cluster, provider, hosts):
for host_id in hosts:
if not isinstance(host_id, int):
err('TASK_ERROR', f'host id should be integer ({host_id})')
- try:
- host = Host.objects.get(id=host_id)
- except Host.DoesNotExist:
- err('TASK_ERROR', f'Can not find host with id #{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.id}')
if provider and host.provider != provider:
@@ -102,6 +96,7 @@ def check_action_hosts(action, cluster, provider, hosts):
@transaction.atomic
def prepare_task(action, obj, selector, conf, attr, spec, old_hc, delta, host_map, cluster, # pylint: disable=too-many-locals
hosts, event, verbose):
+ DummyData.objects.filter(id=1).update(date=timezone.now())
lock_objects(obj, event)
if not attr:
@@ -118,7 +113,7 @@ def prepare_task(action, obj, selector, conf, attr, spec, old_hc, delta, host_ma
task = create_one_job_task(
action, selector, obj, conf, attr, old_hc, hosts, event, verbose
)
- _job = create_job(action, None, selector, event, task.id)
+ _job = create_job(action, None, selector, event, task)
if conf:
new_conf = process_config_and_attr(task, conf, attr, spec)
@@ -157,7 +152,7 @@ def cancel_task(task):
if task.status in [config.Job.FAILED, config.Job.ABORTED, config.Job.SUCCESS]:
err(*errors.get(task.status))
i = 0
- while not JobLog.objects.filter(task_id=task.id, status=config.Job.RUNNING) and i < 10:
+ while not JobLog.objects.filter(task=task, status=config.Job.RUNNING) and i < 10:
time.sleep(0.5)
i += 1
if i == 10:
@@ -207,118 +202,6 @@ def check_action_state(action, obj):
err('TASK_ERROR', 'action is disabled')
-def lock_obj(obj, event):
- stack = obj.stack
-
- if not stack:
- stack = [obj.state]
- elif stack[-1] != obj.state:
- stack.append(obj.state)
-
- log.debug('lock %s, stack: %s', obj_ref(obj), stack)
- obj.stack = stack
- api.set_object_state(obj, config.Job.LOCKED, event)
-
-
-def unlock_obj(obj, event):
- if obj.stack:
- stack = obj.stack
- else:
- log.warning('no stack in %s for unlock', obj_ref(obj))
- return
- try:
- state = stack.pop()
- except IndexError:
- log.warning('empty stack in %s for unlock', obj_ref(obj))
- return
- log.debug('unlock %s, stack: %s', obj_ref(obj), stack)
- obj.stack = stack
- api.set_object_state(obj, state, event)
-
-
-def lock_objects(obj, event):
- if isinstance(obj, ClusterObject):
- lock_obj(obj, event)
- lock_obj(obj.cluster, event)
- for host in Host.objects.filter(cluster=obj.cluster):
- lock_obj(host, event)
- elif isinstance(obj, Host):
- lock_obj(obj, event)
- if obj.cluster:
- lock_obj(obj.cluster, event)
- for service in ClusterObject.objects.filter(cluster=obj.cluster):
- lock_obj(service, event)
- elif isinstance(obj, HostProvider):
- lock_obj(obj, event)
- for host in Host.objects.filter(provider=obj):
- lock_obj(host, event)
- elif isinstance(obj, ADCM):
- lock_obj(obj, event)
- elif isinstance(obj, Cluster):
- lock_obj(obj, event)
- for service in ClusterObject.objects.filter(cluster=obj):
- lock_obj(service, event)
- for host in Host.objects.filter(cluster=obj):
- lock_obj(host, event)
- else:
- log.warning('lock_objects: unknown object type: %s', obj)
-
-
-def unlock_deleted_objects(job, event):
- if not job:
- log.warning('unlock_deleted_objects: no job')
- return
- selector = job.selector
- if 'cluster' in selector:
- cluster = Cluster.objects.get(id=selector['cluster'])
- unlock_objects(cluster, event)
-
-
-def unlock_objects(obj, event, job=None):
- if isinstance(obj, ClusterObject):
- unlock_obj(obj, event)
- unlock_obj(obj.cluster, event)
- for host in Host.objects.filter(cluster=obj.cluster):
- unlock_obj(host, event)
- elif isinstance(obj, Host):
- unlock_obj(obj, event)
- if obj.cluster:
- unlock_obj(obj.cluster, event)
- for service in ClusterObject.objects.filter(cluster=obj.cluster):
- unlock_obj(service, event)
- elif isinstance(obj, HostProvider):
- unlock_obj(obj, event)
- for host in Host.objects.filter(provider=obj):
- unlock_obj(host, event)
- elif isinstance(obj, ADCM):
- unlock_obj(obj, event)
- elif isinstance(obj, Cluster):
- unlock_obj(obj, event)
- for service in ClusterObject.objects.filter(cluster=obj):
- unlock_obj(service, event)
- for host in Host.objects.filter(cluster=obj):
- unlock_obj(host, event)
- elif obj is None:
- unlock_deleted_objects(job, event)
- else:
- log.warning('unlock_objects: unknown object type: %s', obj)
-
-
-def unlock_all(event):
- for obj in Cluster.objects.filter(state=config.Job.LOCKED):
- unlock_objects(obj, event)
- for obj in HostProvider.objects.filter(state=config.Job.LOCKED):
- unlock_objects(obj, event)
- for obj in ClusterObject.objects.filter(state=config.Job.LOCKED):
- unlock_objects(obj, event)
- for obj in Host.objects.filter(state=config.Job.LOCKED):
- unlock_objects(obj, event)
- for task in TaskLog.objects.filter(status=config.Job.RUNNING):
- set_task_status(task, config.Job.ABORTED, event)
- for job in JobLog.objects.filter(status=config.Job.RUNNING):
- set_job_status(job.id, config.Job.ABORTED, event)
-
-
def check_action_config(action, obj, conf, attr):
proto = action.prototype
spec, flat_spec, _, _ = adcm_config.get_prototype_config(proto, action)
@@ -331,7 +214,7 @@ def check_action_config(action, obj, conf, attr):
cl = ConfigLog.objects.get(obj_ref=obj.config, id=obj.config.current)
obj_conf = cl.config
adcm_config.check_attr(proto, attr, flat_spec)
- adcm_config.process_variant(obj, spec, obj_conf)
+ variant.process_variant(obj, spec, obj_conf)
new_conf = adcm_config.check_config_spec(proto, action, spec, flat_spec, conf, None, attr)
return new_conf, spec
@@ -419,70 +302,49 @@ def check_selector(selector, key):
def check_service_task(cluster_id, action):
+ cluster = Cluster.obj.get(id=cluster_id)
try:
- cluster = Cluster.objects.get(id=cluster_id)
- try:
- service = ClusterObject.objects.get(cluster=cluster, prototype=action.prototype)
- return service
- except ClusterObject.DoesNotExist:
- msg = (f'service #{action.prototype.id} for action '
- f'"{action.name}" is not installed in cluster #{cluster.id}')
- err('CLUSTER_SERVICE_NOT_FOUND', msg)
- except Cluster.DoesNotExist:
- err('CLUSTER_NOT_FOUND')
+ service = ClusterObject.objects.get(cluster=cluster, prototype=action.prototype)
+ return service
+ except ClusterObject.DoesNotExist:
+ msg = (f'service #{action.prototype.id} for action '
+ f'"{action.name}" is not installed in cluster #{cluster.id}')
+ err('CLUSTER_SERVICE_NOT_FOUND', msg)
def check_component_task(cluster_id, action):
+ cluster = Cluster.obj.get(id=cluster_id)
try:
- cluster = Cluster.objects.get(id=cluster_id)
- try:
- component = ServiceComponent.objects.get(cluster=cluster, prototype=action.prototype)
- return component
- except ServiceComponent.DoesNotExist:
- msg = (f'component #{action.prototype.id} for action '
- f'"{action.name}" is not installed in cluster #{cluster.id}')
- err('COMPONENT_NOT_FOUND', msg)
- except Cluster.DoesNotExist:
- err('CLUSTER_NOT_FOUND')
+ component = ServiceComponent.objects.get(cluster=cluster, prototype=action.prototype)
+ return component
+ except ServiceComponent.DoesNotExist:
+ msg = (f'component #{action.prototype.id} for action '
+ f'"{action.name}" is not installed in cluster #{cluster.id}')
+ err('COMPONENT_NOT_FOUND', msg)
def check_cluster(cluster_id):
- try:
- cluster = Cluster.objects.get(id=cluster_id)
- return cluster
- except Cluster.DoesNotExist:
- err('CLUSTER_NOT_FOUND')
+ return Cluster.obj.get(id=cluster_id)
def check_provider(provider_id):
- try:
- provider = HostProvider.objects.get(id=provider_id)
- return provider
- except HostProvider.DoesNotExist:
- err('PROVIDER_NOT_FOUND')
+ return HostProvider.obj.get(id=provider_id)
def check_adcm(adcm_id):
- try:
- adcm = ADCM.objects.get(id=adcm_id)
- return adcm
- except ADCM.DoesNotExist:
- err('ADCM_NOT_FOUND')
+ return ADCM.obj.get(id=adcm_id)
def check_host(host_id, selector):
- try:
- host = Host.objects.get(id=host_id)
- if 'cluster' in selector:
- if not host.cluster:
- msg = f'Host #{host_id} does not belong to any cluster'
- err('HOST_NOT_FOUND', msg)
- if host.cluster.id != selector['cluster']:
- msg = f'Host #{host_id} does not belong to cluster #{selector["cluster"]}'
- err('HOST_NOT_FOUND', msg)
- return host
- except Host.DoesNotExist:
- err('HOST_NOT_FOUND')
+ host = Host.obj.get(id=host_id)
+ if 'cluster' in selector:
+ if not host.cluster:
+ msg = f'Host #{host_id} does not belong to any cluster'
+ err('HOST_NOT_FOUND', msg)
+ if host.cluster.id != selector['cluster']:
+ msg = f'Host #{host_id} does not belong to cluster #{selector["cluster"]}'
+ err('HOST_NOT_FOUND', msg)
+ return host
def get_bundle_root(action):
@@ -502,11 +364,8 @@ def cook_script(action, sub_action):
def get_adcm_config():
- try:
- adcm = ADCM.objects.get()
- return get_obj_config(adcm)
- except ADCM.DoesNotExist:
- return {}
+ adcm = ADCM.obj.get()
+ return get_obj_config(adcm)
def get_new_hc(cluster):
@@ -552,7 +411,7 @@ def re_prepare_job(task, job):
def prepare_job(action, sub_action, selector, job_id, obj, conf, delta, hosts, verbose):
prepare_job_config(action, sub_action, selector, job_id, obj, conf, verbose)
- inventory.prepare_job_inventory(selector, job_id, delta, hosts)
+ inventory.prepare_job_inventory(selector, job_id, action, delta, hosts)
prepare_ansible_config(job_id, action, sub_action)
@@ -570,7 +429,7 @@ def prepare_context(selector):
if 'provider' in selector:
context['type'] = 'provider'
context['provider_id'] = selector['provider']
- if 'host' in selector:
+ if 'host' in selector and 'type' not in context:
context['type'] = 'host'
context['host_id'] = selector['host']
if 'adcm' in selector:
@@ -649,13 +508,13 @@ def prepare_job_config(action, sub_action, selector, job_id, obj, conf, verbose)
def create_task(action, selector, obj, conf, attr, hc, delta, hosts, event, verbose):
task = create_one_job_task(action, selector, obj, conf, attr, hc, hosts, event, verbose)
for sub in SubAction.objects.filter(action=action):
- _job = create_job(action, sub, selector, event, task.id)
+ _job = create_job(action, sub, selector, event, task)
return task
def create_one_job_task(action, selector, obj, conf, attr, hc, hosts, event, verbose):
task = TaskLog(
- action_id=action.id,
+ action=action,
object_id=obj.id,
selector=selector,
config=conf,
@@ -672,10 +531,10 @@ def create_one_job_task(action, selector, obj, conf, attr, hc, hosts, event, ver
return task
-def create_job(action, sub_action, selector, event, task_id=0):
+def create_job(action, sub_action, selector, event, task):
job = JobLog(
- task_id=task_id,
- action_id=action.id,
+ task=task,
+ action=action,
selector=selector,
log_files=action.log_files,
start_date=timezone.now(),
@@ -683,7 +542,7 @@ def create_job(action, sub_action, selector, event, task_id=0):
status=config.Job.CREATED
)
if sub_action:
- job.sub_action_id = sub_action.id
+ job.sub_action = sub_action
job.save()
LogStorage.objects.create(job=job, name='ansible', type='stdout', format='txt')
LogStorage.objects.create(job=job, name='ansible', type='stderr', format='txt')
@@ -692,18 +551,6 @@ def create_job(action, sub_action, selector, event, task_id=0):
return job
-def set_job_status(job_id, status, event, pid=0):
- JobLog.objects.filter(id=job_id).update(status=status, pid=pid, finish_date=timezone.now())
- event.set_job_status(job_id, status)
-
-
-def set_task_status(task, status, event):
- task.status = status
- task.finish_date = timezone.now()
- task.save()
- event.set_task_status(task.id, status)
-
-
def get_task_obj(context, obj_id):
def get_obj_safe(model, obj_id):
try:
@@ -711,7 +558,9 @@ def get_obj_safe(model, obj_id):
except model.DoesNotExist:
return None
- if context == 'service':
+ if context == 'component':
+ obj = get_obj_safe(ServiceComponent, obj_id)
+ elif context == 'service':
obj = get_obj_safe(ClusterObject, obj_id)
elif context == 'host':
obj = get_obj_safe(Host, obj_id)
@@ -784,6 +633,16 @@ def restore_hc(task, action, status):
api.save_hc(cluster, host_comp_list)
+def get_job_cluster(job):
+ """Get Job's cluster for unlocking in case linked objects were somehow deleted"""
+ if not job:
+ return
+
+ selector = job.selector
+ if 'cluster' in selector:
+ return Cluster.objects.get(id=selector['cluster'])
+
+
def finish_task(task, job, status):
action = Action.objects.get(id=task.action_id)
obj = get_task_obj(action.prototype.type, task.object_id)
@@ -793,7 +652,7 @@ def finish_task(task, job, status):
DummyData.objects.filter(id=1).update(date=timezone.now())
if state is not None:
set_action_state(action, task, obj, state)
- unlock_objects(obj, event, job)
+ unlock_objects(obj or get_job_cluster(job), event)
restore_hc(task, action, status)
set_task_status(task, status, event)
event.send_state()
@@ -833,21 +692,18 @@ def log_group_check(group, fail_msg, success_msg):
def log_check(job_id, group_data, check_data):
- try:
- job = JobLog.objects.get(id=job_id)
- if job.status != config.Job.RUNNING:
- err('JOB_NOT_FOUND', f'job #{job.id} has status "{job.status}", not "running"')
- except JobLog.DoesNotExist:
- err('JOB_NOT_FOUND', f'no job with id #{job_id}')
+ job = JobLog.obj.get(id=job_id)
+ if job.status != config.Job.RUNNING:
+ err('JOB_NOT_FOUND', f'job #{job.id} has status "{job.status}", not "running"')
group_title = group_data.pop('title')
if group_title:
- group, _ = GroupCheckLog.objects.get_or_create(job_id=job_id, title=group_title)
+ group, _ = GroupCheckLog.objects.get_or_create(job=job, title=group_title)
else:
group = None
- check_data.update({'job_id': job_id, 'group': group})
+ check_data.update({'job': job, 'group': group})
cl = CheckLog.objects.create(**check_data)
if group is not None:
@@ -882,9 +738,7 @@ def get_check_log(job_id):
def finish_check(job_id):
-
data = get_check_log(job_id)
-
if not data:
return
@@ -892,21 +746,18 @@ def finish_check(job_id):
LogStorage.objects.filter(job=job, name='ansible', type='check', format='json').update(
body=json.dumps(data))
- GroupCheckLog.objects.filter(job_id=job_id).delete()
- CheckLog.objects.filter(job_id=job_id).delete()
+ GroupCheckLog.objects.filter(job=job).delete()
+ CheckLog.objects.filter(job=job).delete()
def log_custom(job_id, name, log_format, body):
- try:
- job = JobLog.objects.get(id=job_id)
- l1 = LogStorage.objects.create(
- job=job, name=name, type='custom', format=log_format, body=body
- )
- post_event('add_job_log', 'job', job_id, {
- 'id': l1.id, 'type': l1.type, 'name': l1.name, 'format': l1.format,
- })
- except JobLog.DoesNotExist:
- err('JOB_NOT_FOUND', f'no job with id #{job_id}')
+ job = JobLog.obj.get(id=job_id)
+ l1 = LogStorage.objects.create(
+ job=job, name=name, type='custom', format=log_format, body=body
+ )
+ post_event('add_job_log', 'job', job_id, {
+ 'id': l1.id, 'type': l1.type, 'name': l1.name, 'format': l1.format,
+ })
def check_all_status():
@@ -967,7 +818,8 @@ def log_rotation():
def prepare_ansible_config(job_id, action, sub_action):
config_parser = ConfigParser()
config_parser['defaults'] = {
- 'stdout_callback': 'yaml'
+ 'stdout_callback': 'yaml',
+ 'callback_whitelist': 'profile_tasks',
}
adcm_object = ADCM.objects.get(id=1)
cl = ConfigLog.objects.get(obj_ref=adcm_object.config, id=adcm_object.config.current)
@@ -989,3 +841,22 @@ def prepare_ansible_config(job_id, action, sub_action):
with open(os.path.join(config.RUN_DIR, f'{job_id}/ansible.cfg'), 'w') as config_file:
config_parser.write(config_file)
+
+
+def set_task_status(task, status, event):
+ task.status = status
+ task.finish_date = timezone.now()
+ task.save()
+ event.set_task_status(task.id, status)
+
+
+def set_job_status(job_id, status, event, pid=0):
+ JobLog.objects.filter(id=job_id).update(status=status, pid=pid, finish_date=timezone.now())
+ event.set_job_status(job_id, status)
+
+
+def abort_all(event):
+ for task in TaskLog.objects.filter(status=config.Job.RUNNING):
+ set_task_status(task, config.Job.ABORTED, event)
+ for job in JobLog.objects.filter(status=config.Job.RUNNING):
+ set_job_status(job.id, config.Job.ABORTED, event)
diff --git a/python/cm/lock.py b/python/cm/lock.py
new file mode 100644
index 0000000000..124c4adf9e
--- /dev/null
+++ b/python/cm/lock.py
@@ -0,0 +1,80 @@
+# 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=too-many-branches,
+
+import cm.config as config
+from cm import api
+from cm.adcm_config import obj_ref
+from cm.hierarchy import Tree
+from cm.logger import log
+from cm.models import (
+ Cluster,
+ ClusterObject,
+ Host,
+ HostProvider,
+ ServiceComponent,
+)
+
+
+def _lock_obj(obj, event):
+ stack = obj.stack
+
+ if not stack:
+ stack = [obj.state]
+ elif stack[-1] != obj.state:
+ stack.append(obj.state)
+
+ log.debug('lock %s, stack: %s', obj_ref(obj), stack)
+ obj.stack = stack
+ api.set_object_state(obj, config.Job.LOCKED, event)
+
+
+def _unlock_obj(obj, event):
+ if obj.stack:
+ stack = obj.stack
+ else:
+ log.warning('no stack in %s for unlock', obj_ref(obj))
+ return
+ try:
+ state = stack.pop()
+ except IndexError:
+ log.warning('empty stack in %s for unlock', obj_ref(obj))
+ return
+ log.debug('unlock %s, stack: %s, state: %s', obj_ref(obj), stack, state)
+ obj.stack = stack
+ api.set_object_state(obj, state, event)
+
+
+def lock_objects(obj, event):
+ tree = Tree(obj)
+ for node in tree.get_all_affected(tree.built_from):
+ _lock_obj(node.value, event)
+
+
+def unlock_objects(obj, event):
+ tree = Tree(obj)
+ for node in tree.get_all_affected(tree.built_from):
+ _unlock_obj(node.value, event)
+
+
+def unlock_all(event):
+ for obj in Cluster.objects.filter(state=config.Job.LOCKED):
+ _unlock_obj(obj, event)
+ for obj in HostProvider.objects.filter(state=config.Job.LOCKED):
+ _unlock_obj(obj, event)
+ for obj in ClusterObject.objects.filter(state=config.Job.LOCKED):
+ _unlock_obj(obj, event)
+ for obj in ServiceComponent.objects.filter(state=config.Job.LOCKED):
+ _unlock_obj(obj, event)
+ for obj in Host.objects.filter(state=config.Job.LOCKED):
+ _unlock_obj(obj, event)
diff --git a/python/cm/management/__init__.py b/python/cm/management/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/python/cm/management/commands/__init__.py b/python/cm/management/commands/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/python/cm/management/commands/dumpcluster.py b/python/cm/management/commands/dumpcluster.py
new file mode 100644
index 0000000000..20060d9573
--- /dev/null
+++ b/python/cm/management/commands/dumpcluster.py
@@ -0,0 +1,362 @@
+# 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=too-many-locals
+
+import json
+import sys
+
+from django.conf import settings
+from django.core.management.base import BaseCommand
+
+from cm import models
+
+
+def serialize_datetime_fields(obj, fields=None):
+ """
+ Modifies fields of type datetime to ISO string
+
+ :param obj: Object in dictionary format
+ :type obj: dict
+ :param fields: List of fields in datetime format
+ :type fields: list
+ """
+ if fields is not None:
+ for field in fields:
+ obj[field] = obj[field].isoformat()
+
+
+def get_object(model, object_id, fields, datetime_fields=None):
+ """
+ The object is returned in dictionary format
+
+ :param model: Type object
+ :param object_id: Object ID
+ :type object_id: int
+ :param fields: List of fields
+ :type fields: tuple
+ :param datetime_fields: List of fields in datetime format
+ :type datetime_fields: list
+ :return: Object in dictionary format
+ :rtype: dict
+ """
+ obj = model.objects.values(*fields).get(id=object_id)
+ serialize_datetime_fields(obj, datetime_fields)
+ return obj
+
+
+def get_objects(model, fields, filters, datetime_fields=None):
+ objects = list(model.objects.filters(**filters).values(*fields))
+ for obj in objects:
+ serialize_datetime_fields(obj, datetime_fields)
+ return objects
+
+
+def get_bundle(prototype_id):
+ """
+ Returns bundle object in dictionary format
+
+ :param prototype_id: Prototype object ID
+ :type prototype_id: int
+ :return: Bundle object
+ :rtype: dict
+ """
+ fields = (
+ 'name',
+ 'version',
+ 'edition',
+ 'hash',
+ 'description'
+ )
+ prototype = models.Prototype.objects.get(id=prototype_id)
+ bundle = get_object(models.Bundle, prototype.bundle_id, fields)
+ return bundle
+
+
+def get_bundle_hash(prototype_id):
+ """
+ Returns the hash of the bundle
+
+ :param prototype_id: Object ID
+ :type prototype_id: int
+ :return: The hash of the bundle
+ :rtype: str
+ """
+ bundle = get_bundle(prototype_id)
+ return bundle['hash']
+
+
+def get_config(object_config_id):
+ """
+ Returns current and previous config
+
+ :param object_config_id:
+ :type object_config_id: int
+ :return: Current and previous config in dictionary format
+ :rtype: dict
+ """
+ fields = ('config', 'attr', 'date', 'description')
+ try:
+ object_config = models.ObjectConfig.objects.get(id=object_config_id)
+ except models.ObjectConfig.DoesNotExist:
+ return None
+ config = {}
+ for name in ['current', 'previous']:
+ _id = getattr(object_config, name)
+ if _id:
+ config[name] = get_object(models.ConfigLog, _id, fields, ['date'])
+ else:
+ config[name] = None
+ return config
+
+
+def get_cluster(cluster_id):
+ """
+ Returns cluster object in dictionary format
+
+ :param cluster_id: Object ID
+ :type cluster_id: int
+ :return: Cluster object
+ :rtype: dict
+ """
+ fields = (
+ 'id',
+ 'name',
+ 'description',
+ 'config',
+ 'state',
+ 'stack',
+ 'issue',
+ 'prototype',
+ )
+ cluster = get_object(models.Cluster, cluster_id, fields)
+ cluster['config'] = get_config(cluster['config'])
+ bundle = get_bundle(cluster.pop('prototype'))
+ cluster['bundle_hash'] = bundle['hash']
+ return cluster, bundle
+
+
+def get_provider(provider_id):
+ """
+ Returns provider object in dictionary format
+
+ :param provider_id: Object ID
+ :type provider_id: int
+ :return: Provider object
+ :rtype: dict
+ """
+ fields = (
+ 'id',
+ 'prototype',
+ 'name',
+ 'description',
+ 'config',
+ 'state',
+ 'stack',
+ 'issue',
+ )
+ provider = get_object(models.HostProvider, provider_id, fields)
+ provider['config'] = get_config(provider['config'])
+ bundle = get_bundle(provider.pop('prototype'))
+ provider['bundle_hash'] = bundle['hash']
+ return provider, bundle
+
+
+def get_host(host_id):
+ """
+ Returns host object in dictionary format
+
+ :param host_id: Object ID
+ :type host_id: int
+ :return: Host object
+ :rtype: dict
+ """
+ fields = (
+ 'id',
+ 'prototype',
+ 'fqdn',
+ 'description',
+ 'provider',
+ 'provider__name',
+ 'config',
+ 'state',
+ 'stack',
+ 'issue',
+ )
+ host = get_object(models.Host, host_id, fields)
+ host['config'] = get_config(host['config'])
+ host['bundle_hash'] = get_bundle_hash(host.pop('prototype'))
+ return host
+
+
+def get_service(service_id):
+ """
+ Returns service object in dictionary format
+
+ :param service_id: Object ID
+ :type service_id: int
+ :return: Service object
+ :rtype: dict
+ """
+ fields = (
+ 'id',
+ 'prototype',
+ 'prototype__name',
+ # 'service', # TODO: you need to remove the field from the ClusterObject model
+ 'config',
+ 'state',
+ 'stack',
+ 'issue',
+ )
+ service = get_object(models.ClusterObject, service_id, fields)
+ service['config'] = get_config(service['config'])
+ service['bundle_hash'] = get_bundle_hash(service.pop('prototype'))
+ return service
+
+
+def get_component(component_id):
+ """
+ Returns component object in dictionary format
+
+ :param component_id: Object ID
+ :type component_id: int
+ :return: Component object
+ :rtype: dict
+ """
+ fields = (
+ 'id',
+ 'prototype',
+ 'prototype__name',
+ 'service',
+ 'config',
+ 'state',
+ 'stack',
+ 'issue',
+ )
+ component = get_object(models.ServiceComponent, component_id, fields)
+ component['config'] = get_config(component['config'])
+ component['bundle_hash'] = get_bundle_hash(component.pop('prototype'))
+ return component
+
+
+def get_host_component(host_component_id):
+ """
+ Returns host_component object in dictionary format
+
+ :param host_component_id: Object ID
+ :type host_component_id: int
+ :return: HostComponent object
+ :rtype: dict
+ """
+ fields = (
+ 'cluster',
+ 'host',
+ 'service',
+ 'component',
+ 'state',
+ )
+ host_component = get_object(models.HostComponent, host_component_id, fields)
+ return host_component
+
+
+def dump(cluster_id, output):
+ """
+ Saving objects to file in JSON format
+
+ :param cluster_id: Object ID
+ :type cluster_id: int
+ :param output: Path to file
+ :type output: str
+ """
+ cluster, bundle = get_cluster(cluster_id)
+
+ data = {
+ 'ADCM_VERSION': settings.ADCM_VERSION,
+ 'bundles': {
+ bundle['hash']: bundle,
+ },
+ 'cluster': cluster,
+ 'hosts': [],
+ 'providers': [],
+ 'services': [],
+ 'components': [],
+ 'host_components': [],
+ }
+
+ provider_ids = set()
+
+ for host_obj in models.Host.objects.filter(cluster_id=cluster['id']):
+ host = get_host(host_obj.id)
+ provider_ids.add(host['provider'])
+ data['hosts'].append(host)
+
+ host_ids = [host['id'] for host in data['hosts']]
+
+ for provider_obj in models.HostProvider.objects.filter(id__in=provider_ids):
+ provider, bundle = get_provider(provider_obj.id)
+ data['providers'].append(provider)
+ data['bundles'][bundle['hash']] = bundle
+
+ for service_obj in models.ClusterObject.objects.filter(cluster_id=cluster['id']):
+ service = get_service(service_obj.id)
+ data['services'].append(service)
+
+ service_ids = [service['id'] for service in data['services']]
+
+ for component_obj in models.ServiceComponent.objects.filter(
+ cluster_id=cluster['id'], service_id__in=service_ids):
+ component = get_component(component_obj.id)
+ data['components'].append(component)
+
+ component_ids = [component['id'] for component in data['components']]
+
+ for host_component_obj in models.HostComponent.objects.filter(
+ 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)
+
+ result = json.dumps(data, indent=2)
+
+ if output is not None:
+ with open(output, 'w') as f:
+ f.write(result)
+ else:
+ sys.stdout.write(result)
+
+
+class Command(BaseCommand):
+ """
+ Command for dump cluster object to JSON format
+
+ Example:
+ manage.py dumpcluster --cluster_id 1 --output cluster.json
+ """
+ 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', required=True,
+ type=int, help='Cluster ID'
+ )
+ 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']
+ dump(cluster_id, output)
diff --git a/python/cm/management/commands/loadcluster.py b/python/cm/management/commands/loadcluster.py
new file mode 100644
index 0000000000..b966e05682
--- /dev/null
+++ b/python/cm/management/commands/loadcluster.py
@@ -0,0 +1,314 @@
+# 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=too-many-locals
+
+import json
+from datetime import datetime
+
+from django.conf import settings
+from django.core.management.base import BaseCommand
+from django.db.transaction import atomic
+
+from cm import models
+from cm.errors import AdcmEx
+
+
+def deserializer_datetime_fields(obj, fields=None):
+ """
+ Modifies fields of type ISO string to datetime type
+
+ :param obj: Object in dictionary format
+ :type obj: dict
+ :param fields: List of fields in ISO string format
+ :type fields: list
+ """
+ if obj is not None and fields is not None:
+ for field in fields:
+ obj[field] = datetime.fromisoformat(obj[field])
+
+
+def get_prototype(**kwargs):
+ """
+ Returns prototype object
+
+ :param kwargs: Parameters for finding a prototype
+ :return: Prototype object
+ :rtype: models.Prototype
+ """
+ bundle = models.Bundle.objects.get(hash=kwargs.pop('bundle_hash'))
+ prototype = models.Prototype.objects.get(bundle=bundle, **kwargs)
+ return prototype
+
+
+def create_config(config):
+ """
+ Creating current ConfigLog, previous ConfigLog and ObjectConfig objects
+
+ :param config: ConfigLog object in dictionary format
+ :type config: dict
+ :return: ObjectConfig object
+ :rtype: models.ObjectConfig
+ """
+ if config is not None:
+ current_config = config['current']
+ deserializer_datetime_fields(current_config, ['date'])
+ previous_config = config['previous']
+ deserializer_datetime_fields(previous_config, ['date'])
+
+ conf = models.ObjectConfig.objects.create(current=0, previous=0)
+
+ current = models.ConfigLog.objects.create(obj_ref=conf, **current_config)
+ current_id = current.id
+ if previous_config is not None:
+ previous = models.ConfigLog.objects.create(obj_ref=conf, **previous_config)
+ previous_id = previous.id
+ else:
+ previous_id = 0
+
+ conf.current = current_id
+ conf.previous = previous_id
+ conf.save()
+ return conf
+ else:
+ return None
+
+
+def create_cluster(cluster):
+ """
+ Creating Cluster object
+
+ :param cluster: Cluster object in dictionary format
+ :type cluster: dict
+ :return: Cluster object
+ :rtype: models.Cluster
+ """
+ prototype = get_prototype(bundle_hash=cluster.pop('bundle_hash'), type='cluster')
+ ex_id = cluster.pop('id')
+ config = create_config(cluster.pop('config'))
+ cluster = models.Cluster.objects.create(prototype=prototype, config=config, **cluster)
+ return ex_id, cluster
+
+
+def create_provider(provider):
+ """
+ Creating HostProvider object
+
+ :param provider: HostProvider object in dictionary format
+ :type provider: dict
+ :return: HostProvider object
+ :rtype: models.HostProvider
+ """
+ prototype = get_prototype(bundle_hash=provider.pop('bundle_hash'), type='provider')
+ ex_id = provider.pop('id')
+ config = create_config(provider.pop('config'))
+ provider = models.HostProvider.objects.create(prototype=prototype, config=config, **provider)
+ return ex_id, provider
+
+
+def create_host(host, cluster):
+ """
+ Creating Host object
+
+ :param host: Host object in dictionary format
+ :type host: dict
+ :param cluster: Cluster object
+ :type cluster: models.Cluster
+ :return: Host object
+ :rtype: models.Host
+ """
+ prototype = get_prototype(bundle_hash=host.pop('bundle_hash'), type='host')
+ ex_id = host.pop('id')
+ host.pop('provider')
+ config = create_config(host.pop('config'))
+ provider = models.HostProvider.objects.get(name=host.pop('provider__name'))
+ host = models.Host.objects.create(
+ prototype=prototype,
+ provider=provider,
+ config=config,
+ cluster=cluster,
+ **host,
+ )
+ return ex_id, host
+
+
+def create_service(service, cluster):
+ """
+ Creating Service object
+
+ :param service: ClusterObject object in dictionary format
+ :type service: dict
+ :param cluster: Cluster object
+ :type cluster: models.Cluster
+ :return: ClusterObject object
+ :rtype: models.ClusterObject
+ """
+ prototype = get_prototype(
+ bundle_hash=service.pop('bundle_hash'), type='service', name=service.pop('prototype__name')
+ )
+ ex_id = service.pop('id')
+ config = create_config(service.pop('config'))
+ service = models.ClusterObject.objects.create(
+ prototype=prototype,
+ cluster=cluster,
+ config=config,
+ **service
+ )
+ return ex_id, service
+
+
+def create_component(component, cluster, service):
+ """
+ Creating Component object
+
+ :param component: ServiceComponent object in dictionary format
+ :type component: dict
+ :param cluster: Cluster object
+ :type cluster: models.Cluster
+ :param service: Service object
+ :type service: models.ClusterObject
+ :return: Component object
+ :rtype: models.ServiceComponent
+ """
+ prototype = get_prototype(
+ bundle_hash=component.pop('bundle_hash'),
+ type='component',
+ name=component.pop('prototype__name'),
+ parent=service.prototype
+ )
+ ex_id = component.pop('id')
+ config = create_config(component.pop('config'))
+ component = models.ServiceComponent.objects.create(
+ prototype=prototype,
+ cluster=cluster,
+ service=service,
+ config=config,
+ **component
+ )
+ return ex_id, component
+
+
+def create_host_component(host_component, cluster, host, service, component):
+ """
+ Creating HostComponent object
+
+ :param host_component: HostComponent object in dictionary format
+ :type host_component: dict
+ :param cluster: Cluster object
+ :type cluster: models.Cluster
+ :param host: Host object
+ :type host: models.Host
+ :param service: Service object
+ :type service: models.ClusterObject
+ :param component: Component object
+ :type component: models.ServiceComponent
+ :return: HostComponent object
+ :rtype: models.HostComponent
+ """
+ host_component.pop('cluster')
+ host_component = models.HostComponent.objects.create(
+ cluster=cluster,
+ host=host,
+ service=service,
+ component=component,
+ **host_component
+ )
+ return host_component
+
+
+def check(data):
+ """
+ Checking cluster load
+
+ :param data: Data from file
+ :type data: dict
+ """
+ if settings.ADCM_VERSION != data['ADCM_VERSION']:
+ raise AdcmEx(
+ 'DUMP_LOAD_CLUSTER_ERROR',
+ msg=(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():
+ try:
+ models.Bundle.objects.get(hash=bundle_hash)
+ except models.Bundle.DoesNotExist as err:
+ raise AdcmEx(
+ 'DUMP_LOAD_CLUSTER_ERROR',
+ msg=f'Bundle "{bundle["name"]} {bundle["version"]}" not found') from err
+
+
+@atomic
+def load(file_path):
+ """
+ Loading and creating objects from JSON file
+
+ :param file_path: Path to JSON file
+ :type file_path: str
+ """
+ try:
+ with open(file_path, 'r') as f:
+ data = json.load(f)
+ except FileNotFoundError as err:
+ raise AdcmEx('DUMP_LOAD_CLUSTER_ERROR') from err
+
+ check(data)
+
+ _, cluster = create_cluster(data['cluster'])
+
+ for provider_data in data['providers']:
+ create_provider(provider_data)
+
+ ex_host_ids = {}
+ 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']:
+ 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']:
+ ex_component_id, component = create_component(
+ 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']:
+ 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')],
+ )
+
+
+class Command(BaseCommand):
+ """
+ Command for load cluster object from JSON file
+
+ Example:
+ manage.py loadcluster cluster.json
+ """
+ help = 'Load cluster object from JSON format'
+
+ def add_arguments(self, parser):
+ """Parsing command line arguments"""
+ parser.add_argument('file_path', nargs='?')
+
+ def handle(self, *args, **options):
+ """Handler method"""
+ file_path = options.get('file_path')
+ load(file_path)
diff --git a/python/cm/migrations/0057_auto_20200831_1055.py b/python/cm/migrations/0057_auto_20200831_1055.py
index f116622296..60ffbb9509 100644
--- a/python/cm/migrations/0057_auto_20200831_1055.py
+++ b/python/cm/migrations/0057_auto_20200831_1055.py
@@ -12,8 +12,9 @@
#
# Generated by Django 3.1 on 2020-08-31 10:55
+from django.db import migrations, models
+
import cm.models
-from django.db import migrations
def fix_default_json_fields_component(apps, schema_editor):
@@ -108,211 +109,211 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name='action',
name='hostcomponentmap',
- field=cm.models.JSONField(default=[]),
+ field=models.JSONField(default=list),
),
migrations.AlterField(
model_name='action',
name='log_files',
- field=cm.models.JSONField(default=[]),
+ field=models.JSONField(default=list),
),
migrations.AlterField(
model_name='action',
name='params',
- field=cm.models.JSONField(default={}),
+ field=models.JSONField(default=dict),
),
migrations.AlterField(
model_name='action',
name='state_available',
- field=cm.models.JSONField(default=[]),
+ field=models.JSONField(default=list),
),
migrations.AlterField(
model_name='action',
name='ui_options',
- field=cm.models.JSONField(default=None, null=True),
+ field=models.JSONField(default=None, null=True),
),
migrations.AlterField(
model_name='adcm',
name='issue',
- field=cm.models.JSONField(default={}),
+ field=models.JSONField(default=dict),
),
migrations.AlterField(
model_name='adcm',
name='stack',
- field=cm.models.JSONField(default=[]),
+ field=models.JSONField(default=list),
),
migrations.AlterField(
model_name='cluster',
name='issue',
- field=cm.models.JSONField(default={}),
+ field=models.JSONField(default=dict),
),
migrations.AlterField(
model_name='cluster',
name='stack',
- field=cm.models.JSONField(default=[]),
+ field=models.JSONField(default=list),
),
migrations.AlterField(
model_name='clusterobject',
name='issue',
- field=cm.models.JSONField(default={}),
+ field=models.JSONField(default=dict),
),
migrations.AlterField(
model_name='clusterobject',
name='stack',
- field=cm.models.JSONField(default=[]),
+ field=models.JSONField(default=list),
),
migrations.AlterField(
model_name='component',
name='constraint',
- field=cm.models.JSONField(default=[0, '+']),
+ field=models.JSONField(default=[0, '+']),
),
migrations.AlterField(
model_name='component',
name='params',
- field=cm.models.JSONField(default={}),
+ field=models.JSONField(default=dict),
),
migrations.AlterField(
model_name='component',
name='requires',
- field=cm.models.JSONField(default=[]),
+ field=models.JSONField(default=list),
),
migrations.AlterField(
model_name='host',
name='issue',
- field=cm.models.JSONField(default={}),
+ field=models.JSONField(default=dict),
),
migrations.AlterField(
model_name='host',
name='stack',
- field=cm.models.JSONField(default=[]),
+ field=models.JSONField(default=list),
),
migrations.AlterField(
model_name='hostprovider',
name='issue',
- field=cm.models.JSONField(default={}),
+ field=models.JSONField(default=dict),
),
migrations.AlterField(
model_name='hostprovider',
name='stack',
- field=cm.models.JSONField(default=[]),
+ field=models.JSONField(default=list),
),
migrations.AlterField(
model_name='joblog',
name='log_files',
- field=cm.models.JSONField(default=[]),
+ field=models.JSONField(default=list),
),
migrations.AlterField(
model_name='joblog',
name='selector',
- field=cm.models.JSONField(default={}),
+ field=models.JSONField(default=dict),
),
migrations.AlterField(
model_name='prototypeimport',
name='default',
- field=cm.models.JSONField(default=None, null=True),
+ field=models.JSONField(default=None, null=True),
),
migrations.AlterField(
model_name='stageaction',
name='hostcomponentmap',
- field=cm.models.JSONField(default=[]),
+ field=models.JSONField(default=list),
),
migrations.AlterField(
model_name='stageaction',
name='log_files',
- field=cm.models.JSONField(default=[]),
+ field=models.JSONField(default=list),
),
migrations.AlterField(
model_name='stageaction',
name='params',
- field=cm.models.JSONField(default={}),
+ field=models.JSONField(default=dict),
),
migrations.AlterField(
model_name='stageaction',
name='state_available',
- field=cm.models.JSONField(default=[]),
+ field=models.JSONField(default=list),
),
migrations.AlterField(
model_name='stageaction',
name='ui_options',
- field=cm.models.JSONField(default=None, null=True),
+ field=models.JSONField(default=None, null=True),
),
migrations.AlterField(
model_name='stagecomponent',
name='constraint',
- field=cm.models.JSONField(default=[0, '+']),
+ field=models.JSONField(default=[0, '+']),
),
migrations.AlterField(
model_name='stagecomponent',
name='params',
- field=cm.models.JSONField(default={}),
+ field=models.JSONField(default=dict),
),
migrations.AlterField(
model_name='stagecomponent',
name='requires',
- field=cm.models.JSONField(default=[]),
+ field=models.JSONField(default=list),
),
migrations.AlterField(
model_name='stageprototypeimport',
name='default',
- field=cm.models.JSONField(default=None, null=True),
+ field=models.JSONField(default=None, null=True),
),
migrations.AlterField(
model_name='stagesubaction',
name='params',
- field=cm.models.JSONField(default={}),
+ field=models.JSONField(default=dict),
),
migrations.AlterField(
model_name='stageupgrade',
name='from_edition',
- field=cm.models.JSONField(default=['community']),
+ field=models.JSONField(default=cm.models.get_default_from_edition),
),
migrations.AlterField(
model_name='stageupgrade',
name='state_available',
- field=cm.models.JSONField(default=[]),
+ field=models.JSONField(default=list),
),
migrations.AlterField(
model_name='subaction',
name='params',
- field=cm.models.JSONField(default={}),
+ field=models.JSONField(default=dict),
),
migrations.AlterField(
model_name='tasklog',
name='attr',
- field=cm.models.JSONField(default=None, null=True),
+ field=models.JSONField(default=None, null=True),
),
migrations.AlterField(
model_name='tasklog',
name='config',
- field=cm.models.JSONField(default=None, null=True),
+ field=models.JSONField(default=None, null=True),
),
migrations.AlterField(
model_name='tasklog',
name='hostcomponentmap',
- field=cm.models.JSONField(default=None, null=True),
+ field=models.JSONField(default=None, null=True),
),
migrations.AlterField(
model_name='tasklog',
name='hosts',
- field=cm.models.JSONField(default=None, null=True),
+ field=models.JSONField(default=None, null=True),
),
migrations.AlterField(
model_name='tasklog',
name='selector',
- field=cm.models.JSONField(default={}),
+ field=models.JSONField(default=dict),
),
migrations.AlterField(
model_name='upgrade',
name='from_edition',
- field=cm.models.JSONField(default=['community']),
+ field=models.JSONField(default=cm.models.get_default_from_edition),
),
migrations.AlterField(
model_name='upgrade',
name='state_available',
- field=cm.models.JSONField(default=[]),
+ field=models.JSONField(default=list),
),
migrations.AlterField(
model_name='userprofile',
name='profile',
- field=cm.models.JSONField(default=''),
+ field=models.JSONField(default=str),
),
]
diff --git a/python/cm/migrations/0058_encrypt_passwords.py b/python/cm/migrations/0058_encrypt_passwords.py
index 335146adf6..a8e98c5ab7 100644
--- a/python/cm/migrations/0058_encrypt_passwords.py
+++ b/python/cm/migrations/0058_encrypt_passwords.py
@@ -15,9 +15,6 @@
from cm.adcm_config import ansible_encrypt_and_format, obj_to_dict
-from cm.logger import log
-
-
def get_prototype_config(proto, PrototypeConfig):
spec = {}
flist = ('default', 'required', 'type', 'limits')
@@ -73,10 +70,8 @@ def encrypt_passwords(apps, schema_editor):
ConfigLog = apps.get_model('cm', 'ConfigLog')
PrototypeConfig = apps.get_model('cm', 'PrototypeConfig')
for model_name in 'Cluster', 'ClusterObject', 'HostProvider', 'Host', 'ADCM':
- log.debug('QQ model %s', model_name)
Model = apps.get_model('cm', model_name)
for obj in Model.objects.filter(config__isnull=False):
- log.debug('QQ model %s, obj %s', model_name, obj)
process_objects(obj, ConfigLog, PrototypeConfig)
diff --git a/python/cm/migrations/0059_auto_20200904_0910.py b/python/cm/migrations/0059_auto_20200904_0910.py
index 90e3360f27..a67867c2ea 100644
--- a/python/cm/migrations/0059_auto_20200904_0910.py
+++ b/python/cm/migrations/0059_auto_20200904_0910.py
@@ -12,8 +12,7 @@
#
# Generated by Django 3.0.5 on 2020-09-04 09:10
-import cm.models
-from django.db import migrations
+from django.db import migrations, models
def fix_default_json_fields_action(apps, schema_editor):
@@ -55,46 +54,46 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name='action',
name='ui_options',
- field=cm.models.JSONField(default={}),
+ field=models.JSONField(default=dict),
),
migrations.AlterField(
model_name='configlog',
name='attr',
- field=cm.models.JSONField(default={}),
+ field=models.JSONField(default=dict),
),
migrations.AlterField(
model_name='configlog',
name='config',
- field=cm.models.JSONField(default={}),
+ field=models.JSONField(default=dict),
),
migrations.AlterField(
model_name='prototypeconfig',
name='limits',
- field=cm.models.JSONField(default={}),
+ field=models.JSONField(default=dict),
),
migrations.AlterField(
model_name='prototypeconfig',
name='ui_options',
- field=cm.models.JSONField(blank=True, default={}),
+ field=models.JSONField(blank=True, default=dict),
),
migrations.AlterField(
model_name='stageaction',
name='ui_options',
- field=cm.models.JSONField(default={}),
+ field=models.JSONField(default=dict),
),
migrations.AlterField(
model_name='stageprototypeconfig',
name='limits',
- field=cm.models.JSONField(default={}),
+ field=models.JSONField(default=dict),
),
migrations.AlterField(
model_name='stageprototypeconfig',
name='ui_options',
- field=cm.models.JSONField(blank=True, default={}),
+ field=models.JSONField(blank=True, default=dict),
),
migrations.AlterField(
model_name='tasklog',
name='attr',
- field=cm.models.JSONField(default={}),
+ field=models.JSONField(default=dict),
),
]
diff --git a/python/cm/migrations/0060_auto_20201201_1122.py b/python/cm/migrations/0060_auto_20201201_1122.py
index 84a88cfd55..2c1b599319 100644
--- a/python/cm/migrations/0060_auto_20201201_1122.py
+++ b/python/cm/migrations/0060_auto_20201201_1122.py
@@ -13,9 +13,10 @@
# Generated by Django 3.1.2 on 2020-12-01 11:22
# pylint: disable=line-too-long
-import cm.models
-from django.db import migrations, models
import django.db.models.deletion
+from django.db import migrations, models
+
+import cm.models
def create_component_prototype(apps, schema_editor):
@@ -68,7 +69,7 @@ class Migration(migrations.Migration):
migrations.AddField(
model_name='prototype',
name='constraint',
- field=cm.models.JSONField(default=[0, '+']),
+ field=models.JSONField(default=cm.models.get_default_constraint),
),
migrations.AddField(
model_name='prototype',
@@ -78,7 +79,7 @@ class Migration(migrations.Migration):
migrations.AddField(
model_name='prototype',
name='requires',
- field=cm.models.JSONField(default=[]),
+ field=models.JSONField(default=list),
),
migrations.AddField(
model_name='servicecomponent',
@@ -88,7 +89,7 @@ class Migration(migrations.Migration):
migrations.AddField(
model_name='servicecomponent',
name='issue',
- field=cm.models.JSONField(default={}),
+ field=models.JSONField(default=dict),
),
migrations.AddField(
model_name='servicecomponent',
@@ -98,7 +99,7 @@ class Migration(migrations.Migration):
migrations.AddField(
model_name='servicecomponent',
name='stack',
- field=cm.models.JSONField(default=[]),
+ field=models.JSONField(default=list),
),
migrations.AddField(
model_name='servicecomponent',
@@ -108,7 +109,7 @@ class Migration(migrations.Migration):
migrations.AddField(
model_name='stageprototype',
name='constraint',
- field=cm.models.JSONField(default=[0, '+']),
+ field=models.JSONField(default=cm.models.get_default_constraint),
),
migrations.AddField(
model_name='stageprototype',
@@ -118,7 +119,7 @@ class Migration(migrations.Migration):
migrations.AddField(
model_name='stageprototype',
name='requires',
- field=cm.models.JSONField(default=[]),
+ field=models.JSONField(default=list),
),
migrations.AlterField(
model_name='prototype',
diff --git a/python/cm/migrations/0062_auto_20201225_0949.py b/python/cm/migrations/0062_auto_20201225_0949.py
index 6ddd710556..2726369b69 100644
--- a/python/cm/migrations/0062_auto_20201225_0949.py
+++ b/python/cm/migrations/0062_auto_20201225_0949.py
@@ -12,8 +12,7 @@
#
# Generated by Django 3.1.2 on 2020-12-25 09:49
-import cm.models
-from django.db import migrations
+from django.db import migrations, models
class Migration(migrations.Migration):
@@ -26,11 +25,11 @@ class Migration(migrations.Migration):
migrations.AddField(
model_name='prototype',
name='bound_to',
- field=cm.models.JSONField(default={}),
+ field=models.JSONField(default=dict),
),
migrations.AddField(
model_name='stageprototype',
name='bound_to',
- field=cm.models.JSONField(default={}),
+ field=models.JSONField(default=dict),
),
]
diff --git a/python/cm/migrations/0064_auto_20210210_1532.py b/python/cm/migrations/0064_auto_20210210_1532.py
new file mode 100644
index 0000000000..bee3cc7212
--- /dev/null
+++ b/python/cm/migrations/0064_auto_20210210_1532.py
@@ -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.
+# Generated by Django 3.1.1 on 2021-02-10 15:32
+# pylint: disable=line-too-long
+
+from django.db import migrations, models
+
+FIELDS = [
+ ('string', 'string'),
+ ('text', 'text'),
+ ('password', 'password'),
+ ('secrettext', 'secrettext'),
+ ('json', 'json'),
+ ('integer', 'integer'),
+ ('float', 'float'),
+ ('option', 'option'),
+ ('variant', 'variant'),
+ ('boolean', 'boolean'),
+ ('file', 'file'),
+ ('list', 'list'),
+ ('map', 'map'),
+ ('structure', 'structure'),
+ ('group', 'group')
+]
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ ('cm', '0063_tasklog_verbose'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='prototypeconfig',
+ name='type',
+ field=models.CharField(
+ choices=FIELDS,
+ max_length=16
+ ),
+ ),
+ migrations.AlterField(
+ model_name='stageprototypeconfig',
+ name='type',
+ field=models.CharField(
+ choices=FIELDS,
+ max_length=16
+ ),
+ ),
+ ]
diff --git a/python/cm/migrations/0065_auto_20210220_0902.py b/python/cm/migrations/0065_auto_20210220_0902.py
new file mode 100644
index 0000000000..0a1a7bb357
--- /dev/null
+++ b/python/cm/migrations/0065_auto_20210220_0902.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.
+# Generated by Django 3.1.1 on 2021-02-20 09:02
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ ('cm', '0064_auto_20210210_1532'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='action',
+ name='host_action',
+ field=models.BooleanField(default=False),
+ ),
+ migrations.AddField(
+ model_name='stageaction',
+ name='host_action',
+ field=models.BooleanField(default=False),
+ ),
+ ]
diff --git a/python/cm/migrations/0066_auto_20210427_0853.py b/python/cm/migrations/0066_auto_20210427_0853.py
new file mode 100644
index 0000000000..bcb7e8d07f
--- /dev/null
+++ b/python/cm/migrations/0066_auto_20210427_0853.py
@@ -0,0 +1,199 @@
+# 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.1.2 on 2021-04-27 08:53
+# pylint: disable=line-too-long
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+def fix_tasklog(apps, schema_editor):
+ TaskLog = apps.get_model('cm', 'TaskLog')
+ Action = apps.get_model('cm', 'Action')
+ for task in TaskLog.objects.all():
+ if task.old_action_id:
+ try:
+ action = Action.objects.get(id=task.old_action_id)
+ task.action = action
+ if task.attr is None:
+ task.attr = {}
+ task.save()
+ except Action.DoesNotExist:
+ pass
+
+
+def fix_joblog(apps, schema_editor):
+ JobLog = apps.get_model('cm', 'JobLog')
+ TaskLog = apps.get_model('cm', 'TaskLog')
+ Action = apps.get_model('cm', 'Action')
+ SubAction = apps.get_model('cm', 'SubAction')
+ for job in JobLog.objects.all():
+ if job.old_action_id:
+ try:
+ action = Action.objects.get(id=job.old_action_id)
+ job.action = action
+ except Action.DoesNotExist:
+ pass
+ if job.old_sub_action_id:
+ try:
+ sub_action = SubAction.objects.get(id=job.old_sub_action_id)
+ job.sub_action = sub_action
+ except SubAction.DoesNotExist:
+ pass
+ try:
+ task = TaskLog.objects.get(id=job.old_task_id)
+ job.task = task
+ except TaskLog.DoesNotExist:
+ pass
+ job.save()
+
+
+def fix_checklog(apps, schema_editor):
+ JobLog = apps.get_model('cm', 'JobLog')
+ CheckLog = apps.get_model('cm', 'CheckLog')
+ for cl in CheckLog.objects.all():
+ if cl.old_job_id:
+ try:
+ job = JobLog.objects.get(id=cl.old_job_id)
+ cl.job = job
+ cl.save()
+ except JobLog.DoesNotExist:
+ pass
+
+
+def fix_grouplog(apps, schema_editor):
+ JobLog = apps.get_model('cm', 'JobLog')
+ GroupCheckLog = apps.get_model('cm', 'GroupCheckLog')
+ for cl in GroupCheckLog.objects.all():
+ if cl.old_job_id:
+ try:
+ job = JobLog.objects.get(id=cl.old_job_id)
+ cl.job = job
+ cl.save()
+ except JobLog.DoesNotExist:
+ pass
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('cm', '0065_auto_20210220_0902'),
+ ]
+
+ operations = [
+ migrations.RenameField(
+ model_name='joblog',
+ old_name='action_id',
+ new_name='old_action_id',
+ ),
+ migrations.AlterField(
+ model_name='joblog',
+ name='old_action_id',
+ field=models.PositiveIntegerField(default=0),
+ ),
+ migrations.RenameField(
+ model_name='joblog',
+ old_name='sub_action_id',
+ new_name='old_sub_action_id',
+ ),
+ migrations.RenameField(
+ model_name='joblog',
+ old_name='task_id',
+ new_name='old_task_id',
+ ),
+ migrations.RenameField(
+ model_name='tasklog',
+ old_name='action_id',
+ new_name='old_action_id',
+ ),
+ migrations.AlterField(
+ model_name='tasklog',
+ name='old_action_id',
+ field=models.PositiveIntegerField(default=0),
+ ),
+ migrations.AddField(
+ model_name='joblog',
+ name='action',
+ field=models.ForeignKey(default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, to='cm.action'),
+ ),
+ migrations.AddField(
+ model_name='joblog',
+ name='sub_action',
+ field=models.ForeignKey(default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, to='cm.subaction'),
+ ),
+ migrations.AddField(
+ model_name='joblog',
+ name='task',
+ field=models.ForeignKey(default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, to='cm.tasklog'),
+ ),
+ migrations.AddField(
+ model_name='tasklog',
+ name='action',
+ field=models.ForeignKey(default=None, null=True, on_delete=django.db.models.deletion.CASCADE, to='cm.action'),
+ ),
+ migrations.RemoveConstraint(
+ model_name='groupchecklog',
+ name='unique_group_job',
+ ),
+ migrations.RenameField(
+ model_name='checklog',
+ old_name='job_id',
+ new_name='old_job_id',
+ ),
+ migrations.RenameField(
+ model_name='groupchecklog',
+ old_name='job_id',
+ new_name='old_job_id',
+ ),
+ migrations.AddField(
+ model_name='checklog',
+ name='job',
+ field=models.ForeignKey(default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, to='cm.joblog'),
+ ),
+ migrations.AddField(
+ model_name='groupchecklog',
+ name='job',
+ field=models.ForeignKey(default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, to='cm.joblog'),
+ ),
+ migrations.AddConstraint(
+ model_name='groupchecklog',
+ constraint=models.UniqueConstraint(fields=('job', 'title'), name='unique_group_job'),
+ ),
+ migrations.RunPython(fix_tasklog),
+ migrations.RunPython(fix_joblog),
+ migrations.RunPython(fix_checklog),
+ migrations.RunPython(fix_grouplog),
+ migrations.RemoveField(
+ model_name='checklog',
+ name='old_job_id',
+ ),
+ migrations.RemoveField(
+ model_name='groupchecklog',
+ name='old_job_id',
+ ),
+ migrations.RemoveField(
+ model_name='joblog',
+ name='old_action_id',
+ ),
+ migrations.RemoveField(
+ model_name='joblog',
+ name='old_sub_action_id',
+ ),
+ migrations.RemoveField(
+ model_name='joblog',
+ name='old_task_id',
+ ),
+ migrations.RemoveField(
+ model_name='tasklog',
+ name='old_action_id',
+ ),
+ ]
diff --git a/python/cm/models.py b/python/cm/models.py
index 9bd657b79c..98d72e2384 100644
--- a/python/cm/models.py
+++ b/python/cm/models.py
@@ -12,9 +12,8 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
-import json
-
from django.contrib.auth.models import User, Group, Permission
+from django.core.exceptions import ObjectDoesNotExist
from django.db import models
from cm.errors import AdcmEx
@@ -36,35 +35,67 @@
)
-class JSONField(models.Field):
- def db_type(self, connection):
- return 'text'
-
- def from_db_value(self, value, expression, connection):
- if value is not None:
- try:
- return json.loads(value)
- except json.JSONDecodeError:
- raise AdcmEx(
- 'JSON_DB_ERROR',
- msg=f"Not correct field format '{expression.field.attname}'") from None
- return value
-
- def get_prep_value(self, value):
- if value is not None:
- return str(json.dumps(value))
- return value
-
- def to_python(self, value):
- if value is not None:
- return json.loads(value)
- return value
+def get_model_by_type(object_type):
+ if object_type == 'adcm':
+ return ADCM
+ if object_type == 'cluster':
+ return Cluster
+ elif object_type == 'provider':
+ return HostProvider
+ elif object_type == 'service':
+ return ClusterObject
+ elif object_type == 'component':
+ return ServiceComponent
+ elif object_type == 'host':
+ return Host
+ else:
+ # This function should return a Model, this is necessary for the correct
+ # construction of the schema.
+ return Cluster
+
+
+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.
+
+ Using ADCMManager can shorten you code significaly. Insted of
+
+ try:
+ cluster = Cluster.objects.get(id=id)
+ except Cluster.DoesNotExist:
+ raise AdcmEx(f'Cluster {id} is not found')
+
+ You can just write
+
+ cluster = Cluster.obj.get(id=id)
+
+ 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,
+ so if you need familiar behavior you can use it as usual.
+ """
+ def get(self, *args, **kwargs):
+ try:
+ return super().get(*args, **kwargs)
+ except ObjectDoesNotExist:
+ if not hasattr(self.model, '__error_code__'):
+ raise AdcmEx('NO_MODEL_ERROR_CODE', f'model: {self.model.__name__}') from None
+ msg = '{} {} does not exist'.format(self.model.__name__, kwargs)
+ raise AdcmEx(self.model.__error_code__, msg) from None
+
+
+class ADCMModel(models.Model):
+ objects = models.Manager()
+ obj = ADCMManager()
- def value_to_string(self, obj):
- return self.value_from_object(obj)
+ class Meta:
+ abstract = True
-class Bundle(models.Model):
+class Bundle(ADCMModel):
name = models.CharField(max_length=160)
version = models.CharField(max_length=80)
version_order = models.PositiveIntegerField(default=0)
@@ -76,22 +107,30 @@ class Bundle(models.Model):
description = models.TextField(blank=True)
date = models.DateTimeField(auto_now=True)
+ __error_code__ = 'BUNDLE_NOT_FOUND'
+
class Meta:
unique_together = (('name', 'version', 'edition'),)
-class Upgrade(models.Model):
+def get_default_from_edition():
+ return ['community']
+
+
+class Upgrade(ADCMModel):
bundle = models.ForeignKey(Bundle, on_delete=models.CASCADE)
name = models.CharField(max_length=160, blank=True)
description = models.TextField(blank=True)
min_version = models.CharField(max_length=80)
max_version = models.CharField(max_length=80)
- from_edition = JSONField(default=['community'])
+ from_edition = models.JSONField(default=get_default_from_edition)
min_strict = models.BooleanField(default=False)
max_strict = models.BooleanField(default=False)
- state_available = JSONField(default=[])
+ state_available = models.JSONField(default=list)
state_on_success = models.CharField(max_length=64, blank=True)
+ __error_code__ = 'UPGRADE_NOT_FOUND'
+
MONITORING_TYPE = (
('active', 'active'),
@@ -99,7 +138,11 @@ class Upgrade(models.Model):
)
-class Prototype(models.Model):
+def get_default_constraint():
+ return [0, '+']
+
+
+class Prototype(ADCMModel):
bundle = models.ForeignKey(Bundle, on_delete=models.CASCADE)
type = models.CharField(max_length=16, choices=PROTO_TYPE)
parent = models.ForeignKey("self", on_delete=models.CASCADE, null=True, default=None)
@@ -110,13 +153,15 @@ class Prototype(models.Model):
version_order = models.PositiveIntegerField(default=0)
required = models.BooleanField(default=False)
shared = models.BooleanField(default=False)
- constraint = JSONField(default=[0, '+'])
- requires = JSONField(default=[])
- bound_to = JSONField(default={})
+ constraint = models.JSONField(default=get_default_constraint)
+ requires = models.JSONField(default=list)
+ bound_to = models.JSONField(default=dict)
adcm_min_version = models.CharField(max_length=80, default=None, null=True)
monitoring = models.CharField(max_length=16, choices=MONITORING_TYPE, default='active')
description = models.TextField(blank=True)
+ __error_code__ = 'PROTOTYPE_NOT_FOUND'
+
def __str__(self):
return str(self.name)
@@ -124,40 +169,55 @@ class Meta:
unique_together = (('bundle', 'type', 'parent', 'name', 'version'),)
-class ObjectConfig(models.Model):
+class ObjectConfig(ADCMModel):
current = models.PositiveIntegerField()
previous = models.PositiveIntegerField()
+ __error_code__ = 'CONFIG_NOT_FOUND'
+
-class ConfigLog(models.Model):
+class ConfigLog(ADCMModel):
obj_ref = models.ForeignKey(ObjectConfig, on_delete=models.CASCADE)
- config = JSONField(default={})
- attr = JSONField(default={})
+ config = models.JSONField(default=dict)
+ attr = models.JSONField(default=dict)
date = models.DateTimeField(auto_now=True)
description = models.TextField(blank=True)
+ __error_code__ = 'CONFIG_NOT_FOUND'
+
-class ADCM(models.Model):
+class ADCM(ADCMModel):
prototype = models.ForeignKey(Prototype, on_delete=models.CASCADE)
name = models.CharField(max_length=16, choices=(('ADCM', 'ADCM'),), unique=True)
config = models.OneToOneField(ObjectConfig, on_delete=models.CASCADE, null=True)
state = models.CharField(max_length=64, default='created')
- stack = JSONField(default=[])
- issue = JSONField(default={})
+ stack = models.JSONField(default=list)
+ issue = models.JSONField(default=dict)
@property
def bundle_id(self):
return self.prototype.bundle_id
+ @property
+ def serialized_issue(self):
+ result = {
+ 'id': self.id,
+ 'name': self.name,
+ 'issue': self.issue,
+ }
+ return result if result['issue'] else {}
+
-class Cluster(models.Model):
+class Cluster(ADCMModel):
prototype = models.ForeignKey(Prototype, on_delete=models.CASCADE)
name = models.CharField(max_length=80, unique=True)
description = models.TextField(blank=True)
config = models.OneToOneField(ObjectConfig, on_delete=models.CASCADE, null=True)
state = models.CharField(max_length=64, default='created')
- stack = JSONField(default=[])
- issue = JSONField(default={})
+ stack = models.JSONField(default=list)
+ issue = models.JSONField(default=dict)
+
+ __error_code__ = 'CLUSTER_NOT_FOUND'
@property
def bundle_id(self):
@@ -172,17 +232,28 @@ def license(self):
return self.prototype.bundle.license
def __str__(self):
- return str(self.name)
+ return f'{self.name} ({self.id})'
+
+ @property
+ def serialized_issue(self):
+ result = {
+ 'id': self.id,
+ 'name': self.name,
+ 'issue': self.issue,
+ }
+ return result if result['issue'] else {}
-class HostProvider(models.Model):
+class HostProvider(ADCMModel):
prototype = models.ForeignKey(Prototype, on_delete=models.CASCADE)
name = models.CharField(max_length=80, unique=True)
description = models.TextField(blank=True)
config = models.OneToOneField(ObjectConfig, on_delete=models.CASCADE, null=True)
state = models.CharField(max_length=64, default='created')
- stack = JSONField(default=[])
- issue = JSONField(default={})
+ stack = models.JSONField(default=list)
+ issue = models.JSONField(default=dict)
+
+ __error_code__ = 'PROVIDER_NOT_FOUND'
@property
def bundle_id(self):
@@ -199,8 +270,17 @@ def license(self):
def __str__(self):
return str(self.name)
+ @property
+ def serialized_issue(self):
+ result = {
+ 'id': self.id,
+ 'name': self.name,
+ 'issue': self.issue,
+ }
+ return result if result['issue'] else {}
+
-class Host(models.Model):
+class Host(ADCMModel):
prototype = models.ForeignKey(Prototype, on_delete=models.CASCADE)
fqdn = models.CharField(max_length=160, unique=True)
description = models.TextField(blank=True)
@@ -208,8 +288,10 @@ class Host(models.Model):
cluster = models.ForeignKey(Cluster, on_delete=models.SET_NULL, null=True, default=None)
config = models.OneToOneField(ObjectConfig, on_delete=models.CASCADE, null=True)
state = models.CharField(max_length=64, default='created')
- stack = JSONField(default=[])
- issue = JSONField(default={})
+ stack = models.JSONField(default=list)
+ issue = models.JSONField(default=dict)
+
+ __error_code__ = 'HOST_NOT_FOUND'
@property
def bundle_id(self):
@@ -222,15 +304,29 @@ def monitoring(self):
def __str__(self):
return "{}".format(self.fqdn)
-
-class ClusterObject(models.Model):
+ @property
+ def serialized_issue(self):
+ result = {
+ 'id': self.id,
+ 'name': self.fqdn,
+ 'issue': self.issue.copy()
+ }
+ provider_issue = self.provider.serialized_issue
+ if provider_issue:
+ result['issue']['provider'] = provider_issue
+ return result if result['issue'] else {}
+
+
+class ClusterObject(ADCMModel):
cluster = models.ForeignKey(Cluster, on_delete=models.CASCADE)
prototype = models.ForeignKey(Prototype, on_delete=models.CASCADE)
service = models.ForeignKey("self", on_delete=models.CASCADE, null=True, default=None)
config = models.OneToOneField(ObjectConfig, on_delete=models.CASCADE, null=True)
state = models.CharField(max_length=64, default='created')
- stack = JSONField(default=[])
- issue = JSONField(default={})
+ stack = models.JSONField(default=list)
+ issue = models.JSONField(default=dict)
+
+ __error_code__ = 'CLUSTER_SERVICE_NOT_FOUND'
@property
def bundle_id(self):
@@ -246,7 +342,7 @@ def name(self):
@property
def display_name(self):
- return self.prototype.display_name
+ return self.prototype.display_name or self.name
@property
def description(self):
@@ -256,18 +352,29 @@ def description(self):
def monitoring(self):
return self.prototype.monitoring
+ @property
+ def serialized_issue(self):
+ result = {
+ 'id': self.id,
+ 'name': self.display_name,
+ 'issue': self.issue,
+ }
+ return result if result['issue'] else {}
+
class Meta:
unique_together = (('cluster', 'prototype'),)
-class ServiceComponent(models.Model):
+class ServiceComponent(ADCMModel):
cluster = models.ForeignKey(Cluster, on_delete=models.CASCADE)
service = models.ForeignKey(ClusterObject, on_delete=models.CASCADE)
prototype = models.ForeignKey(Prototype, on_delete=models.CASCADE, null=True, default=None)
config = models.OneToOneField(ObjectConfig, on_delete=models.CASCADE, null=True)
state = models.CharField(max_length=64, default='created')
- stack = JSONField(default=[])
- issue = JSONField(default={})
+ stack = models.JSONField(default=list)
+ issue = models.JSONField(default=dict)
+
+ __error_code__ = 'COMPONENT_NOT_FOUND'
@property
def name(self):
@@ -275,7 +382,7 @@ def name(self):
@property
def display_name(self):
- return self.prototype.display_name
+ return self.prototype.display_name or self.name
@property
def description(self):
@@ -297,6 +404,15 @@ def bound_to(self):
def monitoring(self):
return self.prototype.monitoring
+ @property
+ def serialized_issue(self):
+ result = {
+ 'id': self.id,
+ 'name': self.display_name,
+ 'issue': self.issue,
+ }
+ return result if result['issue'] else {}
+
class Meta:
unique_together = (('cluster', 'service', 'prototype'),)
@@ -312,12 +428,12 @@ class Meta:
)
-class Action(models.Model):
+class Action(ADCMModel):
prototype = models.ForeignKey(Prototype, on_delete=models.CASCADE)
name = models.CharField(max_length=160)
display_name = models.CharField(max_length=160, blank=True)
description = models.TextField(blank=True)
- ui_options = JSONField(default={})
+ ui_options = models.JSONField(default=dict)
type = models.CharField(max_length=16, choices=ACTION_TYPE)
button = models.CharField(max_length=64, default=None, null=True)
@@ -327,14 +443,29 @@ class Action(models.Model):
state_on_success = models.CharField(max_length=64, blank=True)
state_on_fail = models.CharField(max_length=64, blank=True)
- state_available = JSONField(default=[])
+ state_available = models.JSONField(default=list)
- params = JSONField(default={})
- log_files = JSONField(default=[])
+ params = models.JSONField(default=dict)
+ log_files = models.JSONField(default=list)
- hostcomponentmap = JSONField(default=[])
+ hostcomponentmap = models.JSONField(default=list)
allow_to_terminate = models.BooleanField(default=False)
partial_execution = models.BooleanField(default=False)
+ host_action = models.BooleanField(default=False)
+
+ __error_code__ = 'ACTION_NOT_FOUND'
+
+ @property
+ def prototype_name(self):
+ return self.prototype.name
+
+ @property
+ def prototype_version(self):
+ return self.prototype.version
+
+ @property
+ def prototype_type(self):
+ return self.prototype.type
def __str__(self):
return "{} {}".format(self.prototype, self.name)
@@ -343,17 +474,17 @@ class Meta:
unique_together = (('prototype', 'name'),)
-class SubAction(models.Model):
+class SubAction(ADCMModel):
action = models.ForeignKey(Action, on_delete=models.CASCADE)
name = models.CharField(max_length=160)
display_name = models.CharField(max_length=160, blank=True)
script = models.CharField(max_length=160)
script_type = models.CharField(max_length=16, choices=SCRIPT_TYPE)
state_on_fail = models.CharField(max_length=64, blank=True)
- params = JSONField(default={})
+ params = models.JSONField(default=dict)
-class HostComponent(models.Model):
+class HostComponent(ADCMModel):
cluster = models.ForeignKey(Cluster, on_delete=models.CASCADE)
host = models.ForeignKey(Host, on_delete=models.CASCADE)
service = models.ForeignKey(ClusterObject, on_delete=models.CASCADE)
@@ -368,6 +499,7 @@ class Meta:
('string', 'string'),
('text', 'text'),
('password', 'password'),
+ ('secrettext', 'secrettext'),
('json', 'json'),
('integer', 'integer'),
('float', 'float'),
@@ -382,7 +514,7 @@ class Meta:
)
-class PrototypeConfig(models.Model):
+class PrototypeConfig(ADCMModel):
prototype = models.ForeignKey(Prototype, on_delete=models.CASCADE)
action = models.ForeignKey(Action, on_delete=models.CASCADE, null=True, default=None)
name = models.CharField(max_length=160)
@@ -391,15 +523,15 @@ class PrototypeConfig(models.Model):
type = models.CharField(max_length=16, choices=CONFIG_FIELD_TYPE)
display_name = models.CharField(max_length=160, blank=True)
description = models.TextField(blank=True)
- limits = JSONField(default={})
- ui_options = JSONField(blank=True, default={})
+ limits = models.JSONField(default=dict)
+ ui_options = models.JSONField(blank=True, default=dict)
required = models.BooleanField(default=True)
class Meta:
unique_together = (('prototype', 'action', 'name', 'subname'),)
-class PrototypeExport(models.Model):
+class PrototypeExport(ADCMModel):
prototype = models.ForeignKey(Prototype, on_delete=models.CASCADE)
name = models.CharField(max_length=160)
@@ -407,14 +539,14 @@ class Meta:
unique_together = (('prototype', 'name'),)
-class PrototypeImport(models.Model):
+class PrototypeImport(ADCMModel):
prototype = models.ForeignKey(Prototype, on_delete=models.CASCADE)
name = models.CharField(max_length=160)
min_version = models.CharField(max_length=80)
max_version = models.CharField(max_length=80)
min_strict = models.BooleanField(default=False)
max_strict = models.BooleanField(default=False)
- default = JSONField(null=True, default=None)
+ default = models.JSONField(null=True, default=None)
required = models.BooleanField(default=False)
multibind = models.BooleanField(default=False)
@@ -422,7 +554,7 @@ class Meta:
unique_together = (('prototype', 'name'),)
-class ClusterBind(models.Model):
+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(
@@ -436,6 +568,8 @@ class ClusterBind(models.Model):
default=None
)
+ __error_code__ = 'BIND_NOT_FOUND'
+
class Meta:
unique_together = (('cluster', 'service', 'source_cluster', 'source_service'),)
@@ -448,12 +582,12 @@ class Meta:
)
-class UserProfile(models.Model):
+class UserProfile(ADCMModel):
login = models.CharField(max_length=32, unique=True)
- profile = JSONField(default='')
+ profile = models.JSONField(default=str)
-class Role(models.Model):
+class Role(ADCMModel):
name = models.CharField(max_length=32, unique=True)
description = models.TextField(blank=True)
permissions = models.ManyToManyField(Permission, blank=True)
@@ -461,35 +595,37 @@ class Role(models.Model):
group = models.ManyToManyField(Group, blank=True)
-class JobLog(models.Model):
- task_id = models.PositiveIntegerField(default=0)
- action_id = models.PositiveIntegerField()
- sub_action_id = models.PositiveIntegerField(default=0)
+class TaskLog(ADCMModel):
+ object_id = models.PositiveIntegerField()
+ action = models.ForeignKey(Action, on_delete=models.CASCADE, null=True, default=None)
pid = models.PositiveIntegerField(blank=True, default=0)
- selector = JSONField(default={})
- log_files = JSONField(default=[])
+ selector = models.JSONField(default=dict)
status = models.CharField(max_length=16, choices=JOB_STATUS)
+ config = models.JSONField(null=True, default=None)
+ attr = models.JSONField(default=dict)
+ hostcomponentmap = models.JSONField(null=True, default=None)
+ hosts = models.JSONField(null=True, default=None)
+ verbose = models.BooleanField(default=False)
start_date = models.DateTimeField()
- finish_date = models.DateTimeField(db_index=True)
+ finish_date = models.DateTimeField()
-class TaskLog(models.Model):
- action_id = models.PositiveIntegerField()
- object_id = models.PositiveIntegerField()
+class JobLog(ADCMModel):
+ task = models.ForeignKey(TaskLog, on_delete=models.SET_NULL, null=True, default=None)
+ action = models.ForeignKey(Action, on_delete=models.SET_NULL, null=True, default=None)
+ sub_action = models.ForeignKey(SubAction, on_delete=models.SET_NULL, null=True, default=None)
pid = models.PositiveIntegerField(blank=True, default=0)
- selector = JSONField(default={})
+ selector = models.JSONField(default=dict)
+ log_files = models.JSONField(default=list)
status = models.CharField(max_length=16, choices=JOB_STATUS)
- config = JSONField(null=True, default=None)
- attr = JSONField(default={})
- hostcomponentmap = JSONField(null=True, default=None)
- hosts = JSONField(null=True, default=None)
- verbose = models.BooleanField(default=False)
start_date = models.DateTimeField()
- finish_date = models.DateTimeField()
+ finish_date = models.DateTimeField(db_index=True)
+
+ __error_code__ = 'JOB_NOT_FOUND'
-class GroupCheckLog(models.Model):
- job_id = models.PositiveIntegerField(default=0)
+class GroupCheckLog(ADCMModel):
+ job = models.ForeignKey(JobLog, on_delete=models.SET_NULL, null=True, default=None)
title = models.TextField()
message = models.TextField(blank=True, null=True)
result = models.BooleanField(blank=True, null=True)
@@ -497,13 +633,13 @@ class GroupCheckLog(models.Model):
class Meta:
constraints = [
models.UniqueConstraint(
- fields=['job_id', 'title'], name='unique_group_job')
+ fields=['job', 'title'], name='unique_group_job')
]
-class CheckLog(models.Model):
+class CheckLog(ADCMModel):
group = models.ForeignKey(GroupCheckLog, blank=True, null=True, on_delete=models.CASCADE)
- job_id = models.PositiveIntegerField(default=0)
+ job = models.ForeignKey(JobLog, on_delete=models.SET_NULL, null=True, default=None)
title = models.TextField()
message = models.TextField()
result = models.BooleanField()
@@ -522,7 +658,7 @@ class CheckLog(models.Model):
)
-class LogStorage(models.Model):
+class LogStorage(ADCMModel):
job = models.ForeignKey(JobLog, on_delete=models.CASCADE)
name = models.TextField(default='')
body = models.TextField(blank=True, null=True)
@@ -538,7 +674,7 @@ class Meta:
# Stage: Temporary tables to load bundle
-class StagePrototype(models.Model):
+class StagePrototype(ADCMModel):
type = models.CharField(max_length=16, choices=PROTO_TYPE)
parent = models.ForeignKey("self", on_delete=models.CASCADE, null=True, default=None)
name = models.CharField(max_length=160)
@@ -550,13 +686,15 @@ class StagePrototype(models.Model):
license_hash = models.CharField(max_length=64, default=None, null=True)
required = models.BooleanField(default=False)
shared = models.BooleanField(default=False)
- constraint = JSONField(default=[0, '+'])
- requires = JSONField(default=[])
- bound_to = JSONField(default={})
+ constraint = models.JSONField(default=get_default_constraint)
+ requires = models.JSONField(default=list)
+ bound_to = models.JSONField(default=dict)
adcm_min_version = models.CharField(max_length=80, default=None, null=True)
description = models.TextField(blank=True)
monitoring = models.CharField(max_length=16, choices=MONITORING_TYPE, default='active')
+ __error_code__ = 'PROTOTYPE_NOT_FOUND'
+
def __str__(self):
return str(self.name)
@@ -564,24 +702,24 @@ class Meta:
unique_together = (('type', 'parent', 'name', 'version'),)
-class StageUpgrade(models.Model):
+class StageUpgrade(ADCMModel):
name = models.CharField(max_length=160, blank=True)
description = models.TextField(blank=True)
min_version = models.CharField(max_length=80)
max_version = models.CharField(max_length=80)
min_strict = models.BooleanField(default=False)
max_strict = models.BooleanField(default=False)
- from_edition = JSONField(default=['community'])
- state_available = JSONField(default=[])
+ from_edition = models.JSONField(default=get_default_from_edition)
+ state_available = models.JSONField(default=list)
state_on_success = models.CharField(max_length=64, blank=True)
-class StageAction(models.Model):
+class StageAction(ADCMModel):
prototype = models.ForeignKey(StagePrototype, on_delete=models.CASCADE)
name = models.CharField(max_length=160)
display_name = models.CharField(max_length=160, blank=True)
description = models.TextField(blank=True)
- ui_options = JSONField(default={})
+ ui_options = models.JSONField(default=dict)
type = models.CharField(max_length=16, choices=ACTION_TYPE)
button = models.CharField(max_length=64, default=None, null=True)
@@ -591,14 +729,15 @@ class StageAction(models.Model):
state_on_success = models.CharField(max_length=64, blank=True)
state_on_fail = models.CharField(max_length=64, blank=True)
- state_available = JSONField(default=[])
+ state_available = models.JSONField(default=list)
- params = JSONField(default={})
- log_files = JSONField(default=[])
+ params = models.JSONField(default=dict)
+ log_files = models.JSONField(default=list)
- hostcomponentmap = JSONField(default=[])
+ hostcomponentmap = models.JSONField(default=list)
allow_to_terminate = models.BooleanField(default=False)
partial_execution = models.BooleanField(default=False)
+ host_action = models.BooleanField(default=False)
def __str__(self):
return "{}:{}".format(self.prototype, self.name)
@@ -607,17 +746,17 @@ class Meta:
unique_together = (('prototype', 'name'),)
-class StageSubAction(models.Model):
+class StageSubAction(ADCMModel):
action = models.ForeignKey(StageAction, on_delete=models.CASCADE)
name = models.CharField(max_length=160)
display_name = models.CharField(max_length=160, blank=True)
script = models.CharField(max_length=160)
script_type = models.CharField(max_length=16, choices=SCRIPT_TYPE)
state_on_fail = models.CharField(max_length=64, blank=True)
- params = JSONField(default={})
+ params = models.JSONField(default=dict)
-class StagePrototypeConfig(models.Model):
+class StagePrototypeConfig(ADCMModel):
prototype = models.ForeignKey(StagePrototype, on_delete=models.CASCADE)
action = models.ForeignKey(StageAction, on_delete=models.CASCADE, null=True, default=None)
name = models.CharField(max_length=160)
@@ -626,15 +765,15 @@ class StagePrototypeConfig(models.Model):
type = models.CharField(max_length=16, choices=CONFIG_FIELD_TYPE)
display_name = models.CharField(max_length=160, blank=True)
description = models.TextField(blank=True)
- limits = JSONField(default={})
- ui_options = JSONField(blank=True, default={}) # JSON
+ limits = models.JSONField(default=dict)
+ ui_options = models.JSONField(blank=True, default=dict)
required = models.BooleanField(default=True)
class Meta:
unique_together = (('prototype', 'action', 'name', 'subname'),)
-class StagePrototypeExport(models.Model):
+class StagePrototypeExport(ADCMModel):
prototype = models.ForeignKey(StagePrototype, on_delete=models.CASCADE)
name = models.CharField(max_length=160)
@@ -642,14 +781,14 @@ class Meta:
unique_together = (('prototype', 'name'),)
-class StagePrototypeImport(models.Model):
+class StagePrototypeImport(ADCMModel):
prototype = models.ForeignKey(StagePrototype, on_delete=models.CASCADE)
name = models.CharField(max_length=160)
min_version = models.CharField(max_length=80)
max_version = models.CharField(max_length=80)
min_strict = models.BooleanField(default=False)
max_strict = models.BooleanField(default=False)
- default = JSONField(null=True, default=None)
+ default = models.JSONField(null=True, default=None)
required = models.BooleanField(default=False)
multibind = models.BooleanField(default=False)
@@ -657,5 +796,5 @@ class Meta:
unique_together = (('prototype', 'name'),)
-class DummyData(models.Model):
+class DummyData(ADCMModel):
date = models.DateTimeField(auto_now=True)
diff --git a/python/cm/stack.py b/python/cm/stack.py
index ddbef03dbf..9f693080e4 100644
--- a/python/cm/stack.py
+++ b/python/cm/stack.py
@@ -15,18 +15,20 @@
import json
import yaml
-import toml
+import ruyaml
import hashlib
+import warnings
import yspec.checker
from rest_framework import status
from cm.logger import log
from cm.errors import raise_AdcmEx as err
-from cm.adcm_config import VARIANT_FUNCTIONS
+import cm.config as config
+import cm.checker
+
from cm.adcm_config import proto_ref, check_config_type, type_is_complex, read_bundle_file
from cm.models import StagePrototype, StageAction, StagePrototypeConfig
-from cm.models import ACTION_TYPE, SCRIPT_TYPE, CONFIG_FIELD_TYPE, PROTO_TYPE
from cm.models import StagePrototypeExport, StagePrototypeImport, StageUpgrade, StageSubAction
@@ -47,23 +49,10 @@ def cook_obj_id(conf):
def save_object_definition(path, fname, conf, obj_list, bundle_hash, adcm=False):
- if not isinstance(conf, dict):
- msg = 'Object definition should be a map ({})'
- return err('INVALID_OBJECT_DEFINITION', msg.format(fname))
-
- if 'type' not in conf:
- msg = 'No type in object definition: {}'
- return err('INVALID_OBJECT_DEFINITION', msg.format(fname))
-
def_type = conf['type']
- if def_type not in (proto_type for (proto_type, _) in PROTO_TYPE):
- msg = 'Unknown type "{}" in object definition: {}'
- return err('INVALID_OBJECT_DEFINITION', msg.format(def_type, fname))
-
if def_type == 'adcm' and not adcm:
msg = 'Invalid type "{}" in object definition: {}'
return err('INVALID_OBJECT_DEFINITION', msg.format(def_type, fname))
-
check_object_definition(fname, conf, def_type, obj_list)
obj = save_prototype(path, conf, def_type, bundle_hash)
log.info('Save definition of %s "%s" %s to stage', def_type, conf['name'], conf['version'])
@@ -72,31 +61,9 @@ def save_object_definition(path, fname, conf, obj_list, bundle_hash, adcm=False)
def check_object_definition(fname, conf, def_type, obj_list):
- if 'name' not in conf:
- msg = 'No name in {} definition: {}'
- err('INVALID_OBJECT_DEFINITION', msg.format(def_type, fname))
- if 'version' not in conf:
- msg = 'No version in {} "{}" definition: {}'
- err('INVALID_OBJECT_DEFINITION', msg.format(def_type, conf['name'], fname))
ref = '{} "{}" {}'.format(def_type, conf['name'], conf['version'])
if cook_obj_id(conf) in obj_list:
- msg = 'Duplicate definition of {} (file {})'
- err('INVALID_OBJECT_DEFINITION', msg.format(ref, fname))
- allow = (
- 'type', 'name', 'version', 'display_name', 'description', 'actions', 'components',
- 'config', 'upgrade', 'export', 'import', 'required', 'shared', 'monitoring',
- 'adcm_min_version', 'edition', 'license',
- )
- check_extra_keys(conf, allow, ref)
-
-
-def check_extra_keys(conf, acceptable, ref):
- if not isinstance(conf, dict):
- return
- for key in conf.keys():
- if key not in acceptable:
- msg = 'Not allowed key "{}" in {} definition'
- err('INVALID_OBJECT_DEFINITION', msg.format(key, ref))
+ err('INVALID_OBJECT_DEFINITION', f'Duplicate definition of {ref} (file {fname})')
def get_config_files(path, bundle_hash):
@@ -104,15 +71,9 @@ def get_config_files(path, bundle_hash):
conf_types = [
('config.yaml', 'yaml'),
('config.yml', 'yaml'),
- ('config.toml', 'toml'),
- ('config.json', 'json'),
]
if not os.path.isdir(path):
- return err(
- 'STACK_LOAD_ERROR',
- 'no directory: {}'.format(path),
- status.HTTP_404_NOT_FOUND
- )
+ return err('STACK_LOAD_ERROR', f'no directory: {path}', status.HTTP_404_NOT_FOUND)
for root, _, files in os.walk(path):
for conf_file, conf_type in conf_types:
if conf_file in files:
@@ -121,32 +82,43 @@ def get_config_files(path, bundle_hash):
conf_list.append((path, root + '/' + conf_file, conf_type))
break
if not conf_list:
- msg = 'no config files in stack directory "{}"'
- return err('STACK_LOAD_ERROR', msg.format(path))
+ return err('STACK_LOAD_ERROR', f'no config files in stack directory "{path}"')
return conf_list
+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) as fd:
+ rules = ruyaml.round_trip_load(fd)
+ try:
+ with open(conf_file) as fd:
+ ruyaml.version_info = (0, 15, 0) # switch off duplicate keys error
+ data = ruyaml.round_trip_load(fd, version="1.1")
+ except (ruyaml.parser.ParserError, ruyaml.scanner.ScannerError, NotImplementedError) as e:
+ err('STACK_LOAD_ERROR', f'YAML decode "{conf_file}" error: {e}')
+ except ruyaml.error.ReusedAnchorWarning as e:
+ err('STACK_LOAD_ERROR', f'YAML decode "{conf_file}" error: {e}')
+ except ruyaml.constructor.DuplicateKeyError as e:
+ msg = f'{e.context}\n{e.context_mark}\n{e.problem}\n{e.problem_mark}'
+ err('STACK_LOAD_ERROR', f'Duplicate Keys error: {msg}')
+ try:
+ cm.checker.check(data, rules)
+ return data
+ except cm.checker.FormatError as e:
+ args = ''
+ if e.errors:
+ for ee in e.errors:
+ if 'Input data for' in ee.message:
+ continue
+ args += f'line {ee.line}: {ee}\n'
+ err('INVALID_OBJECT_DEFINITION', f'"{conf_file}" line {e.line} error: {e}', args)
+ return {}
+
+
def read_definition(conf_file, conf_type):
- parsers = {
- 'toml': toml.load,
- 'yaml': yaml.safe_load,
- 'json': json.load
- }
- fn = parsers[conf_type]
if os.path.isfile(conf_file):
- with open(conf_file) as fd:
- try:
- conf = fn(fd)
- except (toml.TomlDecodeError, IndexError) as e:
- err('STACK_LOAD_ERROR', 'TOML decode "{}" error: {}'.format(conf_file, e))
- except yaml.parser.ParserError as e:
- err('STACK_LOAD_ERROR', 'YAML decode "{}" error: {}'.format(conf_file, e))
- except yaml.composer.ComposerError as e:
- err('STACK_LOAD_ERROR', 'YAML decode "{}" error: {}'.format(conf_file, e))
- except yaml.constructor.ConstructorError as e:
- err('STACK_LOAD_ERROR', 'YAML decode "{}" error: {}'.format(conf_file, e))
- except yaml.scanner.ScannerError as e:
- err('STACK_LOAD_ERROR', 'YAML decode "{}" error: {}'.format(conf_file, e))
+ conf = check_adcm_config(conf_file)
log.info('Read config file: "%s"', conf_file)
return conf
log.warning('Can not open config file: "%s"', conf_file)
@@ -156,10 +128,7 @@ def read_definition(conf_file, conf_type):
def get_license_hash(proto, conf, bundle_hash):
if 'license' not in conf:
return None
- if not isinstance(conf['license'], str):
- err('INVALID_OBJECT_DEFINITION', 'license should be a string ({})'.format(proto_ref(proto)))
- msg = 'license file'
- body = read_bundle_file(proto, conf['license'], bundle_hash, msg)
+ body = read_bundle_file(proto, conf['license'], bundle_hash, 'license file')
sha1 = hashlib.sha256()
sha1.update(body.encode('utf-8'))
return sha1.hexdigest()
@@ -190,87 +159,23 @@ def save_prototype(path, conf, def_type, bundle_hash):
return proto
-def check_component_constraint_definition(proto, name, conf):
- if not isinstance(conf, dict):
+def check_component_constraint(proto, name, conf):
+ if not conf:
return
if 'constraint' not in conf:
return
- const = conf['constraint']
- ref = proto_ref(proto)
-
- def check_item(item):
- if isinstance(item, int):
- return
- elif item == '+':
- return
- elif item == 'odd':
- return
- else:
- msg = 'constraint item of component "{}" in {} should be only digit or "+" or "odd"'
- err('INVALID_COMPONENT_DEFINITION', msg.format(name, ref))
-
- if not isinstance(const, list):
- msg = 'constraint of component "{}" in {} should be array'
- err('INVALID_COMPONENT_DEFINITION', msg.format(name, ref))
- if len(const) > 2:
+ if len(conf['constraint']) > 2:
msg = 'constraint of component "{}" in {} should have only 1 or 2 elements'
- err('INVALID_COMPONENT_DEFINITION', msg.format(name, ref))
-
- check_item(const[0])
- if len(const) > 1:
- check_item(const[1])
-
-
-def check_component_requires(proto, name, conf):
- if not isinstance(conf, dict):
- return
- if 'requires' not in conf:
- return
- req = conf['requires']
- ref = proto_ref(proto)
- if not isinstance(req, list):
- msg = 'requires of component "{}" in {} should be array'
- err('INVALID_COMPONENT_DEFINITION', msg.format(name, ref))
- for item in req:
- check_extra_keys(item, ('service', 'component'), f'requires of component "{name}" of {ref}')
-
-
-def check_bound_component(proto, name, conf):
- if not isinstance(conf, dict):
- return
- if 'bound_to' not in conf:
- return
- bind = conf['bound_to']
- ref = proto_ref(proto)
- if not isinstance(bind, dict):
- msg = 'bound_to of component "{}" in {} should be a map'
- err('INVALID_COMPONENT_DEFINITION', msg.format(name, ref))
- check_extra_keys(bind, ('service', 'component'), f'bound_to of component "{name}" of {ref}')
- msg = 'Component "{}" has no mandatory "{}" key in bound_to statment ({})'
- for item in ('service', 'component'):
- if item not in bind:
- err('INVALID_COMPONENT_DEFINITION', msg.format(name, item, ref))
+ err('INVALID_COMPONENT_DEFINITION', msg.format(name, proto_ref(proto)))
def save_components(proto, conf, bundle_hash):
ref = proto_ref(proto)
if not in_dict(conf, 'components'):
return
- if proto.type != 'service':
- log.warning('%s has unexpected "components" key', ref)
- return
- if not isinstance(conf['components'], dict):
- msg = 'Components definition should be a map ({})'
- err('INVALID_COMPONENT_DEFINITION', msg.format(ref))
for comp_name in conf['components']:
cc = conf['components'][comp_name]
- err_msg = 'Component name "{}" of {}'.format(comp_name, ref)
- validate_name(comp_name, err_msg)
- allow = (
- 'display_name', 'description', 'params', 'constraint', 'requires', 'monitoring',
- 'bound_to', 'actions', 'config',
- )
- check_extra_keys(cc, allow, 'component "{}" of {}'.format(comp_name, ref))
+ validate_name(comp_name, f'Component name "{comp_name}" of {ref}')
component = StagePrototype(
type='component',
parent=proto,
@@ -283,9 +188,7 @@ def save_components(proto, conf, bundle_hash):
dict_to_obj(cc, 'display_name', component)
dict_to_obj(cc, 'monitoring', component)
fix_display_name(cc, component)
- check_component_constraint_definition(proto, comp_name, cc)
- check_component_requires(proto, comp_name, cc)
- check_bound_component(proto, comp_name, cc)
+ check_component_constraint(proto, comp_name, cc)
dict_to_obj(cc, 'params', component)
dict_to_obj(cc, 'constraint', component)
dict_to_obj(cc, 'requires', component)
@@ -296,24 +199,12 @@ def save_components(proto, conf, bundle_hash):
def check_upgrade(proto, conf):
- check_key(proto.type, proto.name, '', 'upgrade', 'name', conf)
check_versions(proto, conf, f"upgrade \"{conf['name']}\"")
def check_versions(proto, conf, label):
ref = proto_ref(proto)
msg = '{} has no mandatory \"versions\" key ({})'
- if not conf:
- err('INVALID_VERSION_DEFINITION', msg.format(label, ref))
- if 'versions' not in conf:
- err('INVALID_VERSION_DEFINITION', msg.format(label, ref))
- if not isinstance(conf['versions'], dict):
- err('INVALID_VERSION_DEFINITION', msg.format(label, ref))
- check_extra_keys(
- conf['versions'],
- ('min', 'max', 'min_strict', 'max_strict'),
- '{} versions of {}'.format(label, proto_ref(proto))
- )
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))
@@ -348,38 +239,20 @@ def set_version(obj, conf):
obj.max_strict = True
-def check_upgrade_edition(proto, conf):
- if 'from_edition' not in conf:
- return
- if isinstance(conf['from_edition'], str) and conf['from_edition'] == 'any':
- return
- if not isinstance(conf['from_edition'], list):
- msg = 'from_edition upgrade filed of {} should be array, not string'
- err('INVALID_UPGRADE_DEFINITION', msg.format(proto_ref(proto)))
-
-
def save_upgrade(proto, conf):
- ref = proto_ref(proto)
if not in_dict(conf, 'upgrade'):
return
- if not isinstance(conf['upgrade'], list):
- msg = 'Upgrade definition of {} should be an array'
- err('INVALID_UPGRADE_DEFINITION', msg.format(ref))
for item in conf['upgrade']:
- allow = ('versions', 'from_edition', 'states', 'name', 'description')
- check_extra_keys(item, allow, 'upgrade of {}'.format(ref))
- check_upgrade(proto, item)
+ check_versions(proto, item, f"upgrade \"{conf['name']}\"")
upg = StageUpgrade(name=item['name'])
set_version(upg, item)
dict_to_obj(item, 'description', upg)
if 'states' in item:
- check_upgrade_states(proto, item)
dict_to_obj(item['states'], 'available', upg)
if 'available' in item['states']:
upg.state_available = item['states']['available']
if 'on_success' in item['states']:
upg.state_on_success = item['states']['on_success']
- check_upgrade_edition(proto, item)
if in_dict(item, 'from_edition'):
upg.from_edition = item['from_edition']
upg.save()
@@ -389,22 +262,13 @@ def save_export(proto, conf):
ref = proto_ref(proto)
if not in_dict(conf, 'export'):
return
- if proto.type not in ('cluster', 'service'):
- msg = 'Only cluster or service can have export section ({})'
- err('INVALID_OBJECT_DEFINITION', msg.format(ref))
if isinstance(conf['export'], str):
export = [conf['export']]
elif isinstance(conf['export'], list):
export = conf['export']
- else:
- err('INVALID_OBJECT_DEFINITION', '{} export should be string or array type'.format(ref))
-
msg = '{} does not has "{}" config group'
for key in export:
- try:
- if not StagePrototypeConfig.objects.filter(prototype=proto, name=key):
- err('INVALID_OBJECT_DEFINITION', msg.format(ref, key))
- except StagePrototypeConfig.DoesNotExist:
+ if not StagePrototypeConfig.objects.filter(prototype=proto, name=key):
err('INVALID_OBJECT_DEFINITION', msg.format(ref, key))
se = StagePrototypeExport(prototype=proto, name=key)
se.save()
@@ -422,9 +286,6 @@ def check_default_import(proto, conf):
ref = proto_ref(proto)
if 'default' not in conf:
return
- if not isinstance(conf['default'], list):
- msg = 'Import deafult section should be an array ({})'
- err('INVALID_OBJECT_DEFINITION', msg.format(ref))
groups = get_config_groups(proto)
for key in conf['default']:
if key not in groups:
@@ -436,13 +297,7 @@ def save_import(proto, conf):
ref = proto_ref(proto)
if not in_dict(conf, 'import'):
return
- if proto.type not in ('cluster', 'service'):
- err('INVALID_OBJECT_DEFINITION', 'Only cluster or service can has an import section')
- if not isinstance(conf['import'], dict):
- err('INVALID_OBJECT_DEFINITION', '{} import should be object type'.format(ref))
- allowed_keys = ('versions', 'default', 'required', 'multibind')
for key in conf['import']:
- check_extra_keys(conf['import'][key], allowed_keys, ref + ' import')
check_versions(proto, conf['import'][key], f'import "{key}"')
if 'default' in conf['import'][key] and 'required' in conf['import'][key]:
msg = 'Import can\'t have default and be required in the same time ({})'
@@ -459,48 +314,17 @@ def save_import(proto, conf):
def check_action_hc(proto, conf, name):
if 'hc_acl' not in conf:
return
- ref = proto_ref(proto)
- if not isinstance(conf['hc_acl'], list):
- msg = 'hc_acl of action "{}" in {} should be array'
- err('INVALID_ACTION_DEFINITION', msg.format(name, ref))
- allow = ('service', 'component', 'action')
for idx, item in enumerate(conf['hc_acl']):
- if not isinstance(item, dict):
- msg = 'hc_acl entry of action "{}" in {} should be a map'
- err('INVALID_ACTION_DEFINITION', msg.format(name, ref))
- check_extra_keys(item, allow, 'hc_acl of action "{}" in {}'.format(name, ref))
if 'service' not in item:
if proto.type == 'service':
item['service'] = proto.name
conf['hc_acl'][idx]['service'] = proto.name
- for key in allow:
- if key not in item:
- msg = 'hc_acl of action "{}" in {} doesn\'t has required key "{}"'
- err('INVALID_ACTION_DEFINITION', msg.format(name, ref, key))
- if item['action'] not in ('add', 'remove'):
- msg = 'hc_acl of action "{}" in {} action value "{}" is not "add" or "remove"'
- err('INVALID_ACTION_DEFINITION', msg.format(name, ref, item['action']))
-
-
-def check_sub_action(proto, sub_config, action):
- label = 'sub action of action'
- ref = '{} "{}" in {}'.format(label, action.name, proto_ref(proto))
- check_key(proto.type, proto.name, label, action.name, 'name', sub_config)
- check_key(proto.type, proto.name, label, action.name, 'script', sub_config)
- check_key(proto.type, proto.name, label, action.name, 'script_type', sub_config)
- allow = ('name', 'display_name', 'script', 'script_type', 'on_fail', 'params')
- check_extra_keys(sub_config, allow, ref)
def save_sub_actions(proto, conf, action):
- ref = proto_ref(proto)
if action.type != 'task':
return
- if not isinstance(conf['scripts'], list):
- msg = 'scripts entry of action "{}" in {} should be a list'
- err('INVALID_ACTION_DEFINITION', msg.format(action.name, ref))
for sub in conf['scripts']:
- check_sub_action(proto, sub, action)
sub_action = StageSubAction(
action=action,
script=sub['script'],
@@ -532,15 +356,14 @@ def save_actions(proto, conf, bundle_hash):
dict_to_obj(ac, 'description', action)
dict_to_obj(ac, 'allow_to_terminate', action)
dict_to_obj(ac, 'partial_execution', action)
+ dict_to_obj(ac, 'host_action', action)
dict_to_obj(ac, 'ui_options', action)
dict_to_obj(ac, 'params', action)
dict_to_obj(ac, 'log_files', action)
fix_display_name(ac, action)
-
check_action_hc(proto, ac, action_name)
dict_to_obj(ac, 'hc_acl', action, 'hostcomponentmap')
-
- if check_action_states(proto, action_name, ac):
+ if 'states' in ac:
if 'on_success' in ac['states'] and ac['states']['on_success']:
action.state_on_success = ac['states']['on_success']
if 'on_fail' in ac['states'] and ac['states']['on_fail']:
@@ -551,84 +374,10 @@ def save_actions(proto, conf, bundle_hash):
save_prototype_config(proto, ac, bundle_hash, action)
-def check_action_states(proto, action, ac):
- if 'states' not in ac:
- return False
- ref = 'action "{}" of {}'.format(action, proto_ref(proto))
- check_extra_keys(ac['states'], ('available', 'on_success', 'on_fail'), ref)
- check_key(proto.type, proto, 'states of action', action, 'available', ac['states'])
-
- if 'on_success' in ac['states']:
- if not isinstance(ac['states']['on_success'], str):
- msg = 'states:on_success of {} should be string'
- err('INVALID_ACTION_DEFINITION', msg.format(ref))
-
- if 'on_fail' in ac['states']:
- if not isinstance(ac['states']['on_fail'], str):
- msg = 'states:on_fail of {} should be string'
- err('INVALID_ACTION_DEFINITION', msg.format(ref))
-
- if isinstance(ac['states']['available'], str) and ac['states']['available'] == 'any':
- return True
- if not isinstance(ac['states']['available'], list):
- msg = 'states:available of {} should be array, not string'
- err('INVALID_ACTION_DEFINITION', msg.format(ref))
- return True
-
-
-def check_upgrade_states(proto, ac):
- if 'states' not in ac:
- return False
- ref = 'upgrade states of {}'.format(proto_ref(proto))
- check_extra_keys(ac['states'], ('available', 'on_success'), ref)
-
- if 'on_success' in ac['states']:
- if not isinstance(ac['states']['on_success'], str):
- msg = 'states:on_success of {} should be string'
- err('INVALID_ACTION_DEFINITION', msg.format(ref))
-
- if 'available' not in ac['states']:
- return True
- if isinstance(ac['states']['available'], str) and ac['states']['available'] == 'any':
- return True
- if not isinstance(ac['states']['available'], list):
- msg = 'states:available of upgrade in {} "{}" should be array, not string'
- err('INVALID_UPGRADE_DEFINITION', msg.format(proto.type, proto.name))
- return True
-
-
def check_action(proto, action, act_config):
- ref = 'action "{}" in {} "{}" {}'.format(action, proto.type, proto.name, proto.version)
err_msg = 'Action name "{}" of {} "{}" {}'.format(action, proto.type, proto.name, proto.version)
validate_name(action, err_msg)
- check_key(proto.type, proto.name, 'action', action, 'type', act_config)
- action_type = act_config['type']
- if action_type == 'job':
- check_key(proto.type, proto.name, 'action', action, 'script', act_config)
- check_key(proto.type, proto.name, 'action', action, 'script_type', act_config)
- elif action_type == 'task':
- check_key(proto.type, proto.name, 'action', action, 'scripts', act_config)
- else:
- err('WRONG_ACTION_TYPE', '{} has unknown type "{}"'.format(ref, action_type))
-
- if 'button' in act_config:
- if not isinstance(act_config['button'], str):
- err('INVALID_ACTION_DEFINITION', 'button of {} should be string'.format(ref))
-
- if (action_type, action_type) not in ACTION_TYPE:
- err('WRONG_ACTION_TYPE', '{} has unknown type "{}"'.format(ref, action_type))
- if 'script_type' in act_config:
- script_type = act_config['script_type']
- if (script_type, script_type) not in SCRIPT_TYPE:
- err('WRONG_ACTION_TYPE', '{} has unknown script_type "{}"'.format(ref, script_type))
- allow = (
- 'type', 'script', 'script_type', 'scripts', 'states', 'params', 'config',
- 'log_files', 'hc_acl', 'button', 'display_name', 'description', 'ui_options',
- 'allow_to_terminate', 'partial_execution'
- )
- check_extra_keys(act_config, allow, ref)
-
def is_group(conf):
if conf['type'] == 'group':
@@ -637,12 +386,6 @@ def is_group(conf):
def get_yspec(proto, ref, bundle_hash, conf, name, subname):
- if 'yspec' not in conf:
- msg = 'Config key "{}/{}" of {} has no mandatory yspec key'
- err('CONFIG_TYPE_ERROR', msg.format(name, subname, ref))
- if not isinstance(conf['yspec'], str):
- msg = 'Config key "{}/{}" of {} yspec field should be string'
- err('CONFIG_TYPE_ERROR', msg.format(name, subname, ref))
msg = 'yspec file of config key "{}/{}":'.format(name, subname)
yspec_body = read_bundle_file(proto, conf['yspec'], bundle_hash, msg)
try:
@@ -657,149 +400,47 @@ def get_yspec(proto, ref, bundle_hash, conf, name, subname):
return schema
-def save_prototype_config(proto, proto_conf, bundle_hash, action=None): # pylint: disable=too-many-locals,too-many-statements,too-many-branches
+def save_prototype_config(proto, proto_conf, bundle_hash, action=None): # pylint: disable=too-many-statements,too-many-locals
if not in_dict(proto_conf, 'config'):
return
conf_dict = proto_conf['config']
ref = proto_ref(proto)
- def check_type(param_type, name, subname):
- if (param_type, param_type) in CONFIG_FIELD_TYPE:
- return 1
- else:
- msg = 'Unknown config type: "{}" for config key "{}/{}" of {}'
- return err('CONFIG_TYPE_ERROR', msg.format(param_type, name, subname, ref))
-
- def check_options(conf, name, subname):
- if not in_dict(conf, 'option'):
- msg = 'Config key "{}/{}" of {} has no mandatory option key'
- err('CONFIG_TYPE_ERROR', msg.format(name, subname, ref))
- if not isinstance(conf['option'], dict):
- msg = 'Config key "{}/{}" of {} option field should be map'
- err('CONFIG_TYPE_ERROR', msg.format(name, subname, ref))
- for (label, value) in conf['option'].items():
- if value is None:
- msg = 'Option "{}" value should not be empty (config key "{}/{}" of {})'
- err('CONFIG_TYPE_ERROR', msg.format(label, name, subname, ref))
- if isinstance(value, list):
- msg = 'Option "{}" value "{}" should be flat (config key "{}/{}" of {})'
- err('CONFIG_TYPE_ERROR', msg.format(label, value, name, subname, ref))
- if isinstance(value, dict):
- msg = 'Option "{}" value "{}" should be flat (config key "{}/{}" of {})'
- err('CONFIG_TYPE_ERROR', msg.format(label, value, name, subname, ref))
- return True
-
- def check_variant_args(conf, name, subname):
- if 'args' not in conf:
- return None
- if not isinstance(conf['args'], dict):
- msg = 'Config key "{}/{}" of {} "source:args" field should be a map'
- err('CONFIG_TYPE_ERROR', msg.format(name, subname, ref))
- allowed_keys = ('service', 'component')
- check_extra_keys(conf['args'], allowed_keys, f'{ref} config key "{name}/{subname}"')
- if 'component' in conf['args'] and 'service' not in conf['args']:
- msg = 'There is no "service" field in source:args config key "{}/{}" of {}'
- err('CONFIG_TYPE_ERROR', msg.format(name, subname, ref))
- return conf['args']
-
- def check_variant(conf, name, subname): # pylint: disable=too-many-branches
- if not in_dict(conf, 'source'):
- msg = 'Config key "{}/{}" of {} has no mandatory "source" key'
- err('CONFIG_TYPE_ERROR', msg.format(name, subname, ref))
- if not isinstance(conf['source'], dict):
- msg = 'Config key "{}/{}" of {} "source" field should be a map'
- err('CONFIG_TYPE_ERROR', msg.format(name, subname, ref))
- if not in_dict(conf['source'], 'type'):
- msg = 'Config key "{}/{}" of {} has no mandatory source:type statment'
- err('CONFIG_TYPE_ERROR', msg.format(name, subname, ref))
- allowed_keys = ('type', 'name', 'value', 'strict', 'args')
- check_extra_keys(conf['source'], allowed_keys, f'{ref} config key "{name}/{subname}"')
+ def check_variant(conf, name, subname):
vtype = conf['source']['type']
- if vtype not in ('inline', 'config', 'builtin'):
- msg = 'Config key "{}/{}" of {} has unknown source type "{}"'
- err('CONFIG_TYPE_ERROR', msg.format(name, subname, ref, vtype))
source = {'type': vtype, 'args': None}
if 'strict' in conf['source']:
- if not isinstance(conf['source']['strict'], bool):
- msg = 'Config key "{}/{}" of {} "source:strict" field should be boolean'
- err('CONFIG_TYPE_ERROR', msg.format(name, subname, ref))
source['strict'] = conf['source']['strict']
else:
source['strict'] = True
if vtype == 'inline':
- if not in_dict(conf['source'], 'value'):
- msg = 'Config key "{}/{}" of {} has no mandatory source:value statment'
- err('CONFIG_TYPE_ERROR', msg.format(name, subname, ref))
source['value'] = conf['source']['value']
- if not isinstance(source['value'], list):
- msg = 'Config key "{}/{}" of {} source value should be an array'
- err('CONFIG_TYPE_ERROR', msg.format(name, subname, ref))
elif vtype in ('config', 'builtin'):
- if not in_dict(conf['source'], 'name'):
- msg = 'Config key "{}/{}" of {} has no mandatory source:name statment'
- err('CONFIG_TYPE_ERROR', msg.format(name, subname, ref))
source['name'] = conf['source']['name']
if vtype == 'builtin':
- if conf['source']['name'] not in VARIANT_FUNCTIONS:
- msg = 'Config key "{}/{}" of {} has unknown builtin function "{}"'
- err(
- 'CONFIG_TYPE_ERROR',
- msg.format(name, subname, ref, conf['source']['name']),
- list(VARIANT_FUNCTIONS.keys())
- )
- source['args'] = check_variant_args(conf['source'], name, subname)
+ if 'args' in conf['source']:
+ source['args'] = conf['source']['args']
return source
- def check_limit(conf_type, value, name, subname, label):
- if conf_type == 'integer':
- if not isinstance(value, int):
- msg = '{} ("{}") should be integer (config key "{}/{}" of {})'
- err('CONFIG_TYPE_ERROR', msg.format(label, value, name, subname, ref))
- if conf_type == 'float':
- if not isinstance(value, (int, float)):
- msg = '{} ("{}") should be float (config key "{}/{}" of {})'
- err('CONFIG_TYPE_ERROR', msg.format(label, value, name, subname, ref))
-
- def check_wr(label, conf, name, subname):
- if label not in conf:
- return False
- if isinstance(conf[label], str) and conf[label] == 'any':
- return True
- if not isinstance(conf[label], list):
- msg = '"{}" should be array, not string (config key "{}/{}" of {})'
- err('INVALID_CONFIG_DEFINITION', msg.format(label, name, subname, ref))
- return True
-
- def process_limits(conf, name, subname): # pylint: disable=too-many-branches
- def valudate_bool(value, label, name):
- if not isinstance(value, bool):
- msg = 'config group "{}" {} field ("{}") is not boolean ({})'
- err('CONFIG_TYPE_ERROR', msg.format(name, label, value, ref))
-
+ def process_limits(conf, name, subname):
opt = {}
if conf['type'] == 'option':
- if check_options(conf, name, subname):
- opt = {'option': conf['option']}
- if conf['type'] == 'variant':
+ opt = {'option': conf['option']}
+ elif conf['type'] == 'variant':
opt['source'] = check_variant(conf, name, subname)
elif conf['type'] == 'integer' or conf['type'] == 'float':
if 'min' in conf:
- check_limit(conf['type'], conf['min'], name, subname, 'min')
opt['min'] = conf['min']
if 'max' in conf:
- check_limit(conf['type'], conf['max'], name, subname, 'max')
opt['max'] = conf['max']
elif conf['type'] == 'structure':
opt['yspec'] = get_yspec(proto, ref, bundle_hash, conf, name, subname)
elif is_group(conf):
if 'activatable' in conf:
- valudate_bool(conf['activatable'], 'activatable', name)
opt['activatable'] = conf['activatable']
+ opt['active'] = False
if 'active' in conf:
- valudate_bool(conf['active'], 'active', name)
opt['active'] = conf['active']
- else:
- opt['active'] = False
if 'read_only' in conf and 'writable' in conf:
key_ref = '(config key "{}/{}" of {})'.format(name, subname, ref)
@@ -808,41 +449,11 @@ def valudate_bool(value, label, name):
for label in ('read_only', 'writable'):
if label in conf:
- if check_wr(label, conf, name, subname):
- opt[label] = conf[label]
+ opt[label] = conf[label]
return opt
- def valudate_boolean(value, name, subname):
- if not isinstance(value, bool):
- msg = 'config key "{}/{}" required parameter ("{}") is not boolean ({})'
- return err('CONFIG_TYPE_ERROR', msg.format(name, subname, value, ref))
- return value
-
- def cook_conf(obj, conf, name, subname): # pylint: disable=too-many-branches
- if not in_dict(conf, 'type'):
- msg = 'No type in config key "{}/{}" of {}'
- err('INVALID_CONFIG_DEFINITION', msg.format(name, subname, ref))
- check_type(conf['type'], name, subname)
- if subname:
- if is_group(conf):
- msg = 'Only group can have type "group" (config key "{}/{}" of {})'
- err('INVALID_CONFIG_DEFINITION', msg.format(name, subname, ref))
- else:
- if 'subs' in conf and conf['type'] != 'group':
- msg = 'Group "{}" shoud have type "group" of {})'
- err('INVALID_CONFIG_DEFINITION', msg.format(name, ref))
- if is_group(conf):
- allow = (
- 'type', 'description', 'display_name', 'required', 'ui_options',
- 'name', 'subs', 'activatable', 'active'
- )
- else:
- allow = (
- 'type', 'description', 'display_name', 'default', 'required', 'name', 'yspec',
- 'option', 'source', 'limits', 'max', 'min', 'read_only', 'writable', 'ui_options'
- )
- check_extra_keys(conf, allow, 'config key "{}/{}" of {}'.format(name, subname, ref))
+ def cook_conf(obj, conf, name, subname):
sc = StagePrototypeConfig(
prototype=obj,
action=action,
@@ -851,33 +462,25 @@ def cook_conf(obj, conf, name, subname): # pylint: disable=too-many-branches
)
dict_to_obj(conf, 'description', sc)
dict_to_obj(conf, 'display_name', sc)
+ dict_to_obj(conf, 'required', sc)
+ dict_to_obj(conf, 'ui_options', sc)
+ conf['limits'] = process_limits(conf, name, subname)
+ dict_to_obj(conf, 'limits', sc)
if 'display_name' not in conf:
if subname:
sc.display_name = subname
else:
sc.display_name = name
- conf['limits'] = process_limits(conf, name, subname)
- dict_to_obj(conf, 'limits', sc)
- if 'ui_options' in conf:
- if not isinstance(conf['ui_options'], dict):
- msg = 'ui_options of config key "{}/{}" of {} should be a map'
- err('INVALID_CONFIG_DEFINITION', msg.format(name, subname, ref))
- dict_to_obj(conf, 'ui_options', sc)
if 'default' in conf:
check_config_type(proto, name, subname, conf, conf['default'], bundle_hash)
if type_is_complex(conf['type']):
dict_json_to_obj(conf, 'default', sc)
else:
dict_to_obj(conf, 'default', sc)
- if 'required' in conf:
- sc.required = valudate_boolean(conf['required'], name, subname)
return sc
if isinstance(conf_dict, dict):
for (name, conf) in conf_dict.items():
- if not isinstance(conf, dict):
- msg = 'Config definition of {}, key "{}" should be a map'
- err('INVALID_CONFIG_DEFINITION', msg.format(ref, name))
if 'type' in conf:
validate_name(name, 'Config key "{}" of {}'.format(name, ref))
sc = cook_conf(proto, conf, name, '')
@@ -896,30 +499,12 @@ def cook_conf(obj, conf, name, subname): # pylint: disable=too-many-branches
sc.save()
elif isinstance(conf_dict, list):
for conf in conf_dict:
- if not isinstance(conf, dict):
- msg = 'Config definition of {} items should be a map'
- err('INVALID_CONFIG_DEFINITION', msg.format(ref))
- if 'name' not in conf:
- msg = 'Config definition of {} should have a name required parameter'
- err('INVALID_CONFIG_DEFINITION', msg.format(ref))
name = conf['name']
validate_name(name, 'Config key "{}" of {}'.format(name, ref))
sc = cook_conf(proto, conf, name, '')
sc.save()
if is_group(conf):
- if 'subs' not in conf:
- msg = 'Config definition of {}, group "{}" shoud have "subs" required section)'
- err('INVALID_CONFIG_DEFINITION', msg.format(ref, name))
- if not isinstance(conf['subs'], list):
- msg = 'Config definition of {}, group "{}" subs section should be an array'
- err('INVALID_CONFIG_DEFINITION', msg.format(ref, name))
for subconf in conf['subs']:
- if not isinstance(subconf, dict):
- msg = 'Config definition of {} sub items of item "{}" should be a map'
- err('INVALID_CONFIG_DEFINITION', msg.format(ref, name))
- if 'name' not in subconf:
- msg = 'Config definition of {}, group "{}" subs items should have a name'
- err('INVALID_CONFIG_DEFINITION', msg.format(ref, name))
subname = subconf['name']
err_msg = 'Config key "{}/{}" of {}'.format(name, subname, ref)
validate_name(name, err_msg)
@@ -927,21 +512,6 @@ def cook_conf(obj, conf, name, subname): # pylint: disable=too-many-branches
sc = cook_conf(proto, subconf, name, subname)
sc.subname = subname
sc.save()
- else:
- msg = 'Config definition of {} should be a map or an array'
- err('INVALID_CONFIG_DEFINITION', msg.format(ref))
-
-
-def check_key(context, context_name, param, param_name, key, conf):
- msg = '{} "{}" in {} "{}" has no mandatory "{}" key'.format(
- param, param_name, context, context_name, key
- )
- if not conf:
- err('DEFINITION_KEY_ERROR', msg)
- if key not in conf:
- err('DEFINITION_KEY_ERROR', msg)
- if not conf[key]:
- err('DEFINITION_KEY_ERROR', msg)
def validate_name(value, name):
@@ -952,9 +522,8 @@ def validate_name(value, name):
' dots (.), dashes (-), and underscores (_) are allowed.'
if p.fullmatch(value) is None:
err("WRONG_NAME", msg1.format(name))
- msg2 = "{} is too long. Max length is {}"
if len(value) > MAX_NAME_LENGTH:
- raise err("LONG_NAME", msg2.format(name, MAX_NAME_LENGTH))
+ raise err("LONG_NAME", f'{name} is too long. Max length is {MAX_NAME_LENGTH}')
return value
diff --git a/python/cm/status_api.py b/python/cm/status_api.py
index c5b2d114a5..813d434f0e 100644
--- a/python/cm/status_api.py
+++ b/python/cm/status_api.py
@@ -117,7 +117,7 @@ def set_task_status(task_id, status):
def set_obj_state(obj_type, obj_id, state):
if obj_type == 'adcm':
return None
- if obj_type not in ('cluster', 'service', 'host', 'provider'):
+ if obj_type not in ('cluster', 'service', 'host', 'provider', 'component'):
log.error('Unknown object type: "%s"', obj_type)
return None
return post_event('change_state', obj_type, obj_id, 'state', state)
diff --git a/python/cm/tests_api.py b/python/cm/tests_api.py
index 1e0d4cb25a..85bf4e6180 100644
--- a/python/cm/tests_api.py
+++ b/python/cm/tests_api.py
@@ -90,9 +90,9 @@ def test_set_object_state(self):
self.cluster.prototype.type, self.cluster.id, 'created')
@patch('cm.status_api.load_service_map')
- @patch('cm.issue.save_issue')
+ @patch('cm.issue.update_hierarchy_issues')
@patch('cm.status_api.post_event')
- def test_save_hc(self, mock_post_event, mock_save_issue, mock_load_service_map):
+ def test_save_hc(self, mock_post_event, mock_update_issues, mock_load_service_map):
cluster_object = models.ClusterObject.objects.create(
prototype=self.prototype, cluster=self.cluster)
host = models.Host.objects.create(prototype=self.prototype, cluster=self.cluster)
@@ -111,5 +111,5 @@ def test_save_hc(self, mock_post_event, mock_save_issue, mock_load_service_map):
self.assertListEqual(hc_list, [models.HostComponent.objects.get(id=2)])
mock_post_event.assert_called_once_with(
'change_hostcomponentmap', 'cluster', self.cluster.id)
- mock_save_issue.assert_called_once_with(self.cluster)
+ mock_update_issues.assert_called_once_with(self.cluster)
mock_load_service_map.assert_called_once()
diff --git a/python/cm/tests_hc.py b/python/cm/tests_hc.py
index 13aade98cc..08482739e5 100644
--- a/python/cm/tests_hc.py
+++ b/python/cm/tests_hc.py
@@ -66,7 +66,7 @@ def test_action_hc_simple(self): # pylint: disable=too-many-locals
(hc_list, _) = cm.job.check_hostcomponentmap(cluster, action, hc)
self.assertNotEqual(hc_list, None)
except AdcmEx as e:
- self.assertEqual(e.code, 'SERVICE_NOT_FOUND')
+ self.assertEqual(e.code, 'CLUSTER_SERVICE_NOT_FOUND')
try:
action = Action(name="run", hostcomponentmap="qwe")
diff --git a/python/cm/tests_inventory.py b/python/cm/tests_inventory.py
index 5ea427c210..a40cd279ba 100644
--- a/python/cm/tests_inventory.py
+++ b/python/cm/tests_inventory.py
@@ -292,6 +292,6 @@ def test_prepare_job_inventory(self, mock_open, mock_dump):
prototype.type = prototype_type
prototype.save()
- cm.inventory.prepare_job_inventory(selector, job.id, [])
+ cm.inventory.prepare_job_inventory(selector, job.id, action, [])
mock_dump.assert_called_once_with(inv, fd, indent=3)
mock_dump.reset_mock()
diff --git a/python/cm/tests_issue.py b/python/cm/tests_issue.py
index a7f65823f9..cc1028b8c2 100644
--- a/python/cm/tests_issue.py
+++ b/python/cm/tests_issue.py
@@ -94,7 +94,7 @@ def test_issue_cluster_required_import(self):
_, _, cluster2 = self.cook_cluster('Not_Monitoring', 'Cluster2')
ClusterBind.objects.create(cluster=cluster1, source_cluster=cluster2)
- self.assertEqual(cm.issue.check_issue(cluster1), {'required_import': False})
+ self.assertEqual(cm.issue.check_for_issue(cluster1), {'required_import': False})
def test_issue_cluster_imported(self):
_, proto1, cluster1 = self.cook_cluster('Hadoop', 'Cluster1')
@@ -103,7 +103,7 @@ def test_issue_cluster_imported(self):
_, _, cluster2 = self.cook_cluster('Monitoring', 'Cluster2')
ClusterBind.objects.create(cluster=cluster1, source_cluster=cluster2)
- self.assertEqual(cm.issue.check_issue(cluster1), {})
+ self.assertEqual(cm.issue.check_for_issue(cluster1), {})
def test_issue_service_required_import(self):
b1, _, cluster1 = self.cook_cluster('Hadoop', 'Cluster1')
@@ -114,7 +114,7 @@ def test_issue_service_required_import(self):
_, _, cluster2 = self.cook_cluster('Non_Monitoring', 'Cluster2')
ClusterBind.objects.create(cluster=cluster1, service=service, source_cluster=cluster2)
- self.assertEqual(cm.issue.check_issue(service), {'required_import': False})
+ self.assertEqual(cm.issue.check_for_issue(service), {'required_import': False})
def test_issue_service_imported(self):
b1, _, cluster1 = self.cook_cluster('Hadoop', 'Cluster1')
@@ -125,4 +125,4 @@ def test_issue_service_imported(self):
_, _, cluster2 = self.cook_cluster('Monitoring', 'Cluster2')
ClusterBind.objects.create(cluster=cluster1, service=service, source_cluster=cluster2)
- self.assertEqual(cm.issue.check_issue(service), {})
+ self.assertEqual(cm.issue.check_for_issue(service), {})
diff --git a/python/cm/tests_job.py b/python/cm/tests_job.py
index f754f86d7f..8858952a1c 100644
--- a/python/cm/tests_job.py
+++ b/python/cm/tests_job.py
@@ -9,16 +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.
+
+# pylint: disable=protected-access
+
import os
-from unittest.mock import patch, Mock, call
+from unittest.mock import patch, Mock
from django.test import TestCase
from django.utils import timezone
import cm.config as config
import cm.job as job_module
+import cm.lock as lock_module
from cm import models
from cm.logger import log
+from cm.unit_tests import utils
class TestJob(TestCase):
@@ -41,7 +46,7 @@ def test_set_job_status(self):
pid = 10
event = Mock()
- job_module. set_job_status(job.id, status, event, pid)
+ job_module.set_job_status(job.id, status, event, pid)
job = models.JobLog.objects.get(id=job.id)
self.assertEqual(job.status, status)
@@ -51,8 +56,11 @@ def test_set_job_status(self):
def test_set_task_status(self):
event = Mock()
+ bundle = models.Bundle.objects.create()
+ prototype = models.Prototype.objects.create(bundle=bundle)
+ action = models.Action.objects.create(prototype=prototype)
task = models.TaskLog.objects.create(
- action_id=1, object_id=1,
+ action=action, object_id=1,
start_date=timezone.now(), finish_date=timezone.now())
job_module.set_task_status(task, config.Job.RUNNING, event)
@@ -135,7 +143,7 @@ def test_set_action_state(self, mock_push_obj):
adcm = models.ADCM.objects.create(prototype=prototype)
action = models.Action.objects.create(prototype=prototype)
task = models.TaskLog.objects.create(
- action_id=action.id, object_id=1, start_date=timezone.now(),
+ action=action, object_id=1, start_date=timezone.now(),
finish_date=timezone.now())
data = [
@@ -165,55 +173,26 @@ def test_unlock_obj(self, mock_set_object_state):
for obj, check_assert in data:
with self.subTest(obj=obj):
- job_module.unlock_obj(obj, event)
+ lock_module._unlock_obj(obj, event)
check_assert()
mock_set_object_state.reset_mock()
- @patch('cm.job.unlock_obj')
+ @patch('cm.lock._unlock_obj')
def test_unlock_objects(self, mock_unlock_obj):
- bundle = models.Bundle.objects.create()
- prototype = models.Prototype.objects.create(bundle=bundle)
- cluster = models.Cluster.objects.create(prototype=prototype)
- cluster_object = models.ClusterObject.objects.create(prototype=prototype, cluster=cluster)
- host = models.Host.objects.create(prototype=prototype, cluster=cluster)
- host_provider = models.HostProvider.objects.create(prototype=prototype)
- adcm = models.ADCM.objects.create(prototype=prototype)
-
- data = [cluster_object, host, host_provider, adcm, cluster]
+ bundle = utils.gen_bundle()
+ cluster = utils.gen_cluster(bundle=bundle)
+ service = utils.gen_service(cluster, bundle=bundle)
+ component = utils.gen_component(service, bundle=bundle)
+ host_provider = utils.gen_provider(bundle=bundle)
+ host = utils.gen_host(provider=host_provider, cluster=cluster, bundle=bundle)
+ utils.gen_host_component(component, host)
event = Mock()
- for obj in data:
+ for obj in [cluster, service, component, host, host_provider]:
with self.subTest(obj=obj):
-
- job_module.unlock_objects(obj, event)
-
- if isinstance(obj, models.ClusterObject):
- mock_unlock_obj.assert_has_calls([
- call(obj, event),
- call(cluster, event),
- call(host, event)
- ])
- if isinstance(obj, models.Host):
- mock_unlock_obj.assert_has_calls([
- call(obj, event),
- call(obj.cluster, event),
- call(cluster_object, event),
- ])
- if isinstance(obj, models.HostProvider):
- mock_unlock_obj.assert_has_calls([
- call(obj, event)
- ])
- if isinstance(obj, models.ADCM):
- mock_unlock_obj.assert_has_calls([
- call(obj, event)
- ])
- if isinstance(obj, models.Cluster):
- mock_unlock_obj.assert_has_calls([
- call(obj, event),
- call(cluster_object, event),
- call(host, event),
- ])
+ lock_module.unlock_objects(obj, event)
+ self.assertEqual(mock_unlock_obj.call_count, 5)
mock_unlock_obj.reset_mock()
@patch('cm.job.api.save_hc')
@@ -319,7 +298,7 @@ def test_prepare_job(self, mock_prepare_job_inventory, mock_prepare_job_config,
job_module.prepare_job(action, None, {'cluster': 1}, job.id, cluster, '', {}, None, False)
- mock_prepare_job_inventory.assert_called_once_with({'cluster': 1}, job.id, {}, None)
+ mock_prepare_job_inventory.assert_called_once_with({'cluster': 1}, job.id, action, {}, None)
mock_prepare_job_config.assert_called_once_with(action, None, {'cluster': 1},
job.id, cluster, '', False)
mock_prepare_ansible_config.assert_called_once_with(job.id, action, None)
diff --git a/python/cm/tests_runner.py b/python/cm/tests_runner.py
index 4afc792b5b..e44dc998f8 100644
--- a/python/cm/tests_runner.py
+++ b/python/cm/tests_runner.py
@@ -20,7 +20,7 @@
import job_runner
import task_runner
from cm.logger import log
-from cm.models import TaskLog, JobLog
+from cm.models import TaskLog, JobLog, Bundle, Prototype, Action
class PreparationData:
@@ -33,9 +33,12 @@ def __init__(self, number_tasks, number_jobs):
self.to_prepare()
def to_prepare(self):
+ bundle = Bundle.objects.create()
+ prototype = Prototype.objects.create(bundle=bundle, type='cluster')
+ action = Action.objects.create(prototype=prototype, name='do')
for task_id in range(1, self.number_tasks + 1):
task_log_data = {
- 'action_id': task_id,
+ 'action': action,
'object_id': task_id,
'pid': task_id,
'selector': {'cluster': task_id},
@@ -50,7 +53,7 @@ def to_prepare(self):
for jn in range(1, self.number_jobs + 1):
job_log_data = {
'task_id': task_id,
- 'action_id': task_id,
+ 'action': action,
'pid': jn + 1,
'selector': {'cluster': task_id},
'status': 'success',
diff --git a/python/cm/tests_variant.py b/python/cm/tests_variant.py
new file mode 100644
index 0000000000..5aab9b4432
--- /dev/null
+++ b/python/cm/tests_variant.py
@@ -0,0 +1,421 @@
+# 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.test import TestCase
+
+from cm.api import add_host, add_host_to_cluster, add_host_provider
+from cm.variant import get_variant, variant_host, var_host_solver, VARIANT_HOST_FUNC
+from cm.errors import AdcmEx
+from cm.models import Cluster, ClusterObject, ServiceComponent, HostComponent
+from cm.models import Bundle, Prototype
+
+
+def cook_cluster():
+ b = Bundle.objects.create(name="ADH", version="1.0")
+ proto = Prototype.objects.create(type="cluster", name="ADH", bundle=b)
+ return Cluster.objects.create(prototype=proto, name='Blue Tiber')
+
+
+def cook_provider():
+ b = Bundle.objects.create(name="SSH", version="1.0")
+ pp = Prototype.objects.create(type="provider", bundle=b)
+ provider = add_host_provider(pp, 'SSHone')
+ host_proto = Prototype.objects.create(bundle=b, type='host')
+ return (provider, host_proto)
+
+
+def cook_service(cluster, name='UBER'):
+ proto = Prototype.objects.create(type="service", name=name, bundle=cluster.prototype.bundle)
+ return ClusterObject.objects.create(cluster=cluster, prototype=proto)
+
+
+def cook_component(cluster, service, name):
+ proto = Prototype.objects.create(
+ type="component", name=name, bundle=cluster.prototype.bundle, parent=service.prototype
+ )
+ return ServiceComponent.objects.create(cluster=cluster, service=service, prototype=proto)
+
+
+class TestVariantInline(TestCase):
+ def test_inline(self):
+ limits = {"source": {"type": "inline", "value": [1, 2, 3]}}
+ self.assertEqual(get_variant(None, None, limits), [1, 2, 3])
+
+
+class TestVariantBuiltIn(TestCase):
+ add_hc = HostComponent.objects.create
+
+ def test_host_in_cluster_no_host(self):
+ cls = cook_cluster()
+ limits = {"source": {"type": "builtin", "name": "host_in_cluster"}}
+ self.assertEqual(get_variant(cls, None, limits), [])
+
+ def test_host_in_cluster(self):
+ cls = cook_cluster()
+ provider, hp = cook_provider()
+ h1 = add_host(hp, provider, 'h10')
+ limits = {"source": {"type": "builtin", "name": "host_in_cluster"}}
+ self.assertEqual(get_variant(cls, None, limits), [])
+ add_host_to_cluster(cls, h1)
+ self.assertEqual(get_variant(cls, None, limits), ['h10'])
+
+ def test_host_in_cluster_service(self):
+ cls = cook_cluster()
+ service = cook_service(cls)
+ comp1 = cook_component(cls, service, 'Server')
+ provider, hp = cook_provider()
+ h1 = add_host(hp, provider, 'h10')
+ h2 = add_host(hp, provider, 'h11')
+ limits = {"source": {"type": "builtin", "name": "host_in_cluster"}}
+ self.assertEqual(get_variant(cls, None, limits), [])
+ add_host_to_cluster(cls, h1)
+ add_host_to_cluster(cls, h2)
+ self.assertEqual(get_variant(cls, None, limits), ['h10', 'h11'])
+ limits['source']['args'] = {'service': 'QWE'}
+ self.assertEqual(get_variant(cls, None, limits), [])
+ self.add_hc(cluster=cls, service=service, component=comp1, host=h2)
+ self.assertEqual(get_variant(cls, None, limits), [])
+ limits['source']['args']['service'] = 'UBER'
+ self.assertEqual(get_variant(cls, None, limits), ['h11'])
+
+ def test_host_in_cluster_component(self):
+ cls = cook_cluster()
+ service = cook_service(cls)
+ comp1 = cook_component(cls, service, 'Server')
+ comp2 = cook_component(cls, service, 'Node')
+ provider, hp = cook_provider()
+ h1 = add_host(hp, provider, 'h10')
+ h2 = add_host(hp, provider, 'h11')
+ h3 = add_host(hp, provider, 'h12')
+ limits = {"source": {"type": "builtin", "name": "host_in_cluster"}}
+ add_host_to_cluster(cls, h1)
+ add_host_to_cluster(cls, h2)
+ add_host_to_cluster(cls, h3)
+ self.assertEqual(get_variant(cls, None, limits), ['h10', 'h11', 'h12'])
+ limits['source']['args'] = {'service': 'UBER', 'component': 'QWE'}
+ self.assertEqual(get_variant(cls, None, limits), [])
+ self.add_hc(cluster=cls, service=service, component=comp1, host=h1)
+ self.add_hc(cluster=cls, service=service, component=comp2, host=h2)
+ self.assertEqual(get_variant(cls, None, limits), [])
+ limits['source']['args']['component'] = 'Node'
+ self.assertEqual(get_variant(cls, None, limits), ['h11'])
+
+
+class TestVariantHost(TestCase):
+ add_hc = HostComponent.objects.create
+
+ def test_solver(self):
+ cls = cook_cluster()
+ with self.assertRaises(AdcmEx) as e:
+ self.assertEqual(variant_host(cls, {'any': 'dict'}), {'any': 'dict'})
+ self.assertEqual(e.exception.msg, 'no "predicate" key in variant host function arguments')
+
+ with self.assertRaises(AdcmEx) as e:
+ self.assertEqual(variant_host(cls, {}), {})
+ self.assertEqual(e.exception.msg, 'no "predicate" key in variant host function arguments')
+ with self.assertRaises(AdcmEx) as e:
+ self.assertEqual(variant_host(cls, []), [])
+ self.assertEqual(e.exception.msg, 'arguments of variant host function should be a map')
+ with self.assertRaises(AdcmEx) as e:
+ variant_host(cls, 42)
+ self.assertEqual(e.exception.msg, 'arguments of variant host function should be a map')
+ with self.assertRaises(AdcmEx) as e:
+ variant_host(cls, 'qwe')
+ self.assertEqual(e.exception.msg, 'arguments of variant host function should be a map')
+ with self.assertRaises(AdcmEx) as e:
+ variant_host(cls, {'predicate': 'qwe'})
+ self.assertEqual(e.exception.msg, 'no "qwe" in list of host functions')
+ with self.assertRaises(AdcmEx) as e:
+ variant_host(cls, {'predicate': 'inline_list'})
+ self.assertEqual(e.exception.msg, 'no "args" key in solver args')
+ with self.assertRaises(AdcmEx) as e:
+ var_host_solver(cls, VARIANT_HOST_FUNC, [{"qwe": 1}])
+ self.assertEqual(e.exception.msg, 'no "predicate" key in solver args')
+ with self.assertRaises(AdcmEx) as e:
+ var_host_solver(cls, VARIANT_HOST_FUNC, [{"predicate": 'qwe'}])
+ self.assertEqual(e.exception.msg, 'no "args" key in solver args')
+ with self.assertRaises(AdcmEx) as e:
+ var_host_solver(cls, VARIANT_HOST_FUNC, [{"predicate": 'qwe', 'args': {}}])
+ self.assertEqual(e.exception.msg, 'no "qwe" in list of host functions')
+
+ args = {'predicate': 'inline_list', 'args': {'list': [1, 2, 3]}}
+ self.assertEqual(variant_host(cls, args), [1, 2, 3])
+ args = {'predicate': 'and', 'args': [
+ {'predicate': 'inline_list', 'args': {'list': [1, 2, 3]}},
+ {'predicate': 'inline_list', 'args': {'list': [2, 3, 4]}},
+ ]}
+ self.assertEqual(variant_host(cls, args), [2, 3])
+ args = {'predicate': 'or', 'args': [
+ {'predicate': 'inline_list', 'args': {'list': [1, 2, 3]}},
+ {'predicate': 'inline_list', 'args': {'list': [2, 3, 4]}},
+ ]}
+ self.assertEqual(variant_host(cls, args), [1, 2, 3, 4])
+ args = {'predicate': 'or', 'args': [
+ {'predicate': 'inline_list', 'args': {'list': [1, 2, 3]}},
+ ]}
+ self.assertEqual(variant_host(cls, args), [1, 2, 3])
+
+ def test_no_host_in_cluster(self):
+ cls = cook_cluster()
+ hosts = variant_host(cls, {'predicate': 'in_cluster', 'args': None})
+ self.assertEqual(hosts, [])
+ hosts = variant_host(cls, {'predicate': 'in_cluster', 'args': []})
+ self.assertEqual(hosts, [])
+ hosts = variant_host(cls, {'predicate': 'in_cluster', 'args': {}})
+ self.assertEqual(hosts, [])
+
+ def test_host_in_cluster(self):
+ cls = cook_cluster()
+ provider, hp = cook_provider()
+ h1 = add_host(hp, provider, 'h10')
+ add_host_to_cluster(cls, h1)
+ hosts = variant_host(cls, {'predicate': 'in_cluster', 'args': []})
+ self.assertEqual(hosts, ['h10'])
+
+ def test_host_in_service(self):
+ cls = cook_cluster()
+ service = cook_service(cls)
+ comp = cook_component(cls, service, 'Server')
+ provider, hp = cook_provider()
+ h1 = add_host(hp, provider, 'h10')
+ add_host_to_cluster(cls, h1)
+ self.add_hc(cluster=cls, service=service, component=comp, host=h1)
+ with self.assertRaises(AdcmEx) as e:
+ variant_host(cls, {'predicate': 'in_service', 'args': {}})
+ self.assertEqual(e.exception.msg, 'no "service" argument for predicate "in_service"')
+ with self.assertRaises(AdcmEx) as e:
+ variant_host(cls, {'predicate': 'in_service', 'args': {'service': 'qwe'}})
+ self.assertTrue('ClusterObject {' in e.exception.msg)
+ self.assertTrue('} does not exist' in e.exception.msg)
+
+ args = {'predicate': 'in_service', 'args': {'service': 'UBER'}}
+ hosts = variant_host(cls, args)
+ self.assertEqual(hosts, ['h10'])
+
+ def test_host_not_in_service(self):
+ cls = cook_cluster()
+ service = cook_service(cls)
+ comp = cook_component(cls, service, 'Server')
+ service2 = cook_service(cls, 'Gett')
+ comp2 = cook_component(cls, service2, 'Server')
+ provider, hp = cook_provider()
+ h1 = add_host(hp, provider, 'h10')
+ h2 = add_host(hp, provider, 'h11')
+ h3 = add_host(hp, provider, 'h12')
+ add_host(hp, provider, 'h13')
+ add_host_to_cluster(cls, h1)
+ add_host_to_cluster(cls, h2)
+ add_host_to_cluster(cls, h3)
+ self.add_hc(cluster=cls, service=service, component=comp, host=h1)
+ self.add_hc(cluster=cls, service=service2, component=comp2, host=h3)
+ with self.assertRaises(AdcmEx) as e:
+ variant_host(cls, {'predicate': 'not_in_service', 'args': {}})
+ self.assertEqual(e.exception.msg, 'no "service" argument for predicate "not_in_service"')
+ with self.assertRaises(AdcmEx) as e:
+ variant_host(cls, {'predicate': 'not_in_service', 'args': {'service': 'qwe'}})
+ self.assertTrue('ClusterObject {' in e.exception.msg)
+ self.assertTrue('} does not exist' in e.exception.msg)
+
+ args = {'predicate': 'not_in_service', 'args': {'service': 'UBER'}}
+ hosts = variant_host(cls, args)
+ self.assertEqual(hosts, ['h11', 'h12'])
+ args = {'predicate': 'not_in_service', 'args': {'service': 'Gett'}}
+ hosts = variant_host(cls, args)
+ self.assertEqual(hosts, ['h10', 'h11'])
+
+ def test_host_in_component(self):
+ cls = cook_cluster()
+ service = cook_service(cls)
+ comp1 = cook_component(cls, service, 'Server')
+ comp2 = cook_component(cls, service, 'Node')
+ provider, hp = cook_provider()
+ h1 = add_host(hp, provider, 'h10')
+ h2 = add_host(hp, provider, 'h11')
+ add_host_to_cluster(cls, h1)
+ add_host_to_cluster(cls, h2)
+ self.add_hc(cluster=cls, service=service, component=comp1, host=h1)
+ self.add_hc(cluster=cls, service=service, component=comp2, host=h2)
+ with self.assertRaises(AdcmEx) as e:
+ variant_host(cls, {'predicate': 'in_component'})
+ self.assertEqual(e.exception.msg, 'no "args" key in solver args')
+ with self.assertRaises(AdcmEx) as e:
+ variant_host(cls, {'predicate': 'in_component', 'args': 123})
+ self.assertEqual(e.exception.msg, 'arguments of solver should be a list or a map')
+ with self.assertRaises(AdcmEx) as e:
+ variant_host(cls, {'predicate': 'in_component', 'args': []})
+ self.assertEqual(e.exception.msg, 'no "service" argument for predicate "in_component"')
+ with self.assertRaises(AdcmEx) as e:
+ variant_host(cls, {'predicate': 'in_component', 'args': {'service': 'qwe'}})
+ self.assertTrue('ClusterObject {' in e.exception.msg)
+ self.assertTrue('} does not exist' in e.exception.msg)
+ with self.assertRaises(AdcmEx) as e:
+ args = {'predicate': 'in_component', 'args': {'service': 'UBER', 'component': 'asd'}}
+ variant_host(cls, args)
+ self.assertTrue('ServiceComponent {' in e.exception.msg)
+ self.assertTrue('} does not exist' in e.exception.msg)
+
+ args = {'predicate': 'in_component', 'args': {'service': 'UBER', 'component': 'Node'}}
+ hosts = variant_host(cls, args)
+ self.assertEqual(hosts, ['h11'])
+
+ def test_host_not_in_component(self):
+ cls = cook_cluster()
+ service = cook_service(cls)
+ comp1 = cook_component(cls, service, 'Server')
+ comp2 = cook_component(cls, service, 'Node')
+ service2 = cook_service(cls, 'Gett')
+ comp3 = cook_component(cls, service2, 'Server')
+ provider, hp = cook_provider()
+ h1 = add_host(hp, provider, 'h10')
+ h2 = add_host(hp, provider, 'h11')
+ h3 = add_host(hp, provider, 'h12')
+ h4 = add_host(hp, provider, 'h13')
+ add_host(hp, provider, 'h14')
+ add_host_to_cluster(cls, h1)
+ add_host_to_cluster(cls, h2)
+ add_host_to_cluster(cls, h3)
+ add_host_to_cluster(cls, h4)
+ self.add_hc(cluster=cls, service=service, component=comp1, host=h1)
+ self.add_hc(cluster=cls, service=service, component=comp2, host=h2)
+ self.add_hc(cluster=cls, service=service2, component=comp3, host=h3)
+ with self.assertRaises(AdcmEx) as e:
+ variant_host(cls, {'predicate': 'not_in_component', 'args': []})
+ self.assertEqual(e.exception.msg, 'no "service" argument for predicate "not_in_component"')
+ with self.assertRaises(AdcmEx) as e:
+ variant_host(cls, {'predicate': 'not_in_component', 'args': {'service': 'qwe'}})
+ self.assertTrue('ClusterObject {' in e.exception.msg)
+ self.assertTrue('} does not exist' in e.exception.msg)
+ with self.assertRaises(AdcmEx) as e:
+ args = {
+ 'predicate': 'not_in_component', 'args': {'service': 'UBER', 'component': 'asd'}
+ }
+ variant_host(cls, args)
+ self.assertTrue('ServiceComponent {' in e.exception.msg)
+ self.assertTrue('} does not exist' in e.exception.msg)
+
+ args = {'predicate': 'not_in_component', 'args': {'service': 'UBER', 'component': 'Node'}}
+ self.assertEqual(variant_host(cls, args), ['h10', 'h12', 'h13'])
+ args = {'predicate': 'not_in_component', 'args': {'service': 'UBER', 'component': 'Server'}}
+ self.assertEqual(variant_host(cls, args), ['h11', 'h12', 'h13'])
+ args = {'predicate': 'not_in_component', 'args': {'service': 'Gett', 'component': 'Server'}}
+ self.assertEqual(variant_host(cls, args), ['h10', 'h11', 'h13'])
+
+ def test_host_and(self):
+ cls = cook_cluster()
+ service = cook_service(cls)
+ comp1 = cook_component(cls, service, 'Server')
+ comp2 = cook_component(cls, service, 'Node')
+ provider, hp = cook_provider()
+ h1 = add_host(hp, provider, 'h10')
+ h2 = add_host(hp, provider, 'h11')
+ h3 = add_host(hp, provider, 'h12')
+ add_host_to_cluster(cls, h1)
+ add_host_to_cluster(cls, h2)
+ add_host_to_cluster(cls, h3)
+ self.add_hc(cluster=cls, service=service, component=comp1, host=h1)
+ self.add_hc(cluster=cls, service=service, component=comp2, host=h2)
+ self.add_hc(cluster=cls, service=service, component=comp2, host=h3)
+ with self.assertRaises(AdcmEx) as e:
+ variant_host(cls, {'predicate': 'and', 'args': 123})
+ self.assertEqual(e.exception.msg, 'arguments of solver should be a list or a map')
+ with self.assertRaises(AdcmEx) as e:
+ variant_host(cls, {'predicate': 'and', 'args': [123]})
+ self.assertEqual(e.exception.msg, 'predicte item should be a map')
+ with self.assertRaises(AdcmEx) as e:
+ args = {'predicate': 'and', 'args': [{'predicate': 'qwe', 'args': 123}]}
+ variant_host(cls, args)
+ self.assertEqual(e.exception.msg, 'no "qwe" in list of host functions')
+
+ self.assertEqual(variant_host(cls, {'predicate': 'and', 'args': []}), [])
+ args = {'predicate': 'and', 'args': [
+ {'predicate': 'in_service', 'args': {'service': 'UBER'}},
+ {'predicate': 'in_component', 'args': {'service': 'UBER', 'component': 'Node'}},
+ ]}
+ hosts = variant_host(cls, args)
+ self.assertEqual(hosts, ['h11', 'h12'])
+
+ def test_host_or(self):
+ cls = cook_cluster()
+ service = cook_service(cls)
+ comp1 = cook_component(cls, service, 'Server')
+ comp2 = cook_component(cls, service, 'Node')
+ comp3 = cook_component(cls, service, 'Secondary')
+ provider, hp = cook_provider()
+ h1 = add_host(hp, provider, 'h10')
+ h2 = add_host(hp, provider, 'h11')
+ h3 = add_host(hp, provider, 'h12')
+ add_host_to_cluster(cls, h1)
+ add_host_to_cluster(cls, h2)
+ add_host_to_cluster(cls, h3)
+ self.add_hc(cluster=cls, service=service, component=comp1, host=h1)
+ self.add_hc(cluster=cls, service=service, component=comp2, host=h2)
+ self.add_hc(cluster=cls, service=service, component=comp3, host=h3)
+ with self.assertRaises(AdcmEx) as e:
+ variant_host(cls, {'predicate': 'or', 'args': 123})
+ self.assertEqual(e.exception.msg, 'arguments of solver should be a list or a map')
+ with self.assertRaises(AdcmEx) as e:
+ variant_host(cls, {'predicate': 'or', 'args': [123]})
+ self.assertEqual(e.exception.msg, 'predicte item should be a map')
+ with self.assertRaises(AdcmEx) as e:
+ variant_host(cls, {'predicate': 'or', 'args': [{"qwe": 123}]})
+ self.assertEqual(e.exception.msg, 'no "predicate" key in solver args')
+ with self.assertRaises(AdcmEx) as e:
+ variant_host(cls, {'predicate': 'or', 'args': [{'predicate': 'qwe'}]})
+ self.assertEqual(e.exception.msg, 'no "args" key in solver args')
+ with self.assertRaises(AdcmEx) as e:
+ args = {'predicate': 'or', 'args': [{'predicate': 'qwe', 'args': 123}]}
+ variant_host(cls, args)
+ self.assertEqual(e.exception.msg, 'no "qwe" in list of host functions')
+
+ self.assertEqual(variant_host(cls, {'predicate': 'or', 'args': []}), [])
+ args = {
+ 'predicate': 'or', 'args': [
+ {'predicate': 'in_component', 'args': {
+ 'service': 'UBER',
+ 'component': 'Server',
+ }},
+ {'predicate': 'in_component', 'args': {
+ 'service': 'UBER',
+ 'component': 'Secondary',
+ }},
+ ]}
+ hosts = variant_host(cls, args)
+ self.assertEqual(hosts, ['h10', 'h12'])
+
+ def test_host_in_hc(self):
+ cls = cook_cluster()
+ self.assertEqual(variant_host(cls, {'predicate': 'in_hc', 'args': None}), [])
+ service = cook_service(cls)
+ comp1 = cook_component(cls, service, 'Server')
+ provider, hp = cook_provider()
+ h1 = add_host(hp, provider, 'h10')
+ h2 = add_host(hp, provider, 'h11')
+ add_host_to_cluster(cls, h1)
+ add_host_to_cluster(cls, h2)
+ self.assertEqual(variant_host(cls, {'predicate': 'in_hc', 'args': None}), [])
+ self.add_hc(cluster=cls, service=service, component=comp1, host=h2)
+ self.assertEqual(variant_host(cls, {'predicate': 'in_hc', 'args': None}), ['h11'])
+
+ def test_host_not_in_hc(self):
+ cls = cook_cluster()
+ self.assertEqual(variant_host(cls, {'predicate': 'not_in_hc', 'args': None}), [])
+ service = cook_service(cls)
+ comp1 = cook_component(cls, service, 'Server')
+ provider, hp = cook_provider()
+ h1 = add_host(hp, provider, 'h10')
+ h2 = add_host(hp, provider, 'h11')
+ add_host_to_cluster(cls, h1)
+ add_host_to_cluster(cls, h2)
+ hosts = variant_host(cls, {'predicate': 'not_in_hc', 'args': None})
+ self.assertEqual(hosts, ['h10', 'h11'])
+ self.add_hc(cluster=cls, service=service, component=comp1, host=h2)
+ self.assertEqual(variant_host(cls, {'predicate': 'not_in_hc', 'args': None}), ['h10'])
diff --git a/python/cm/unit_tests/__init__.py b/python/cm/unit_tests/__init__.py
new file mode 100644
index 0000000000..824dd6c8fe
--- /dev/null
+++ b/python/cm/unit_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/cm/unit_tests/test_hierarchy.py b/python/cm/unit_tests/test_hierarchy.py
new file mode 100644
index 0000000000..901eaf5b4d
--- /dev/null
+++ b/python/cm/unit_tests/test_hierarchy.py
@@ -0,0 +1,475 @@
+# 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 time
+from unittest import skip
+from django.test import TestCase
+
+from cm import hierarchy
+from cm.unit_tests import utils
+
+
+def generate_hierarchy(): # pylint: disable=too-many-locals,too-many-statements
+ """
+ Generates two hierarchies:
+ cluster_1 - service_11 - component_111 - host_11 - provider_1
+ - host_12 - provider_1
+ - component_112 - host_11 - provider_1
+ - host_12 - provider_1
+ - service_12 - component_121 - host_11 - provider_1
+ - host_12 - provider_1
+ - host_31 - provider_3
+ - component_122 - host_11 - provider_1
+ - host_12 - provider_1
+ - host_31 - provider_3
+ cluster_2 - service_21 - component_211 - host_21 - provider_2
+ - host_22 - provider_2
+ - component_212 - host_21 - provider_2
+ - host_22 - provider_2
+ - service_22 - component_221 - host_21 - provider_2
+ - host_22 - provider_2
+ - host_32 - provider_3
+ - component_222 - host_21 - provider_2
+ - host_22 - provider_2
+ - host_32 - provider_3
+ """
+ cluster_bundle = utils.gen_bundle()
+
+ cluster_pt = utils.gen_prototype(cluster_bundle, 'cluster')
+ service_pt_1 = utils.gen_prototype(cluster_bundle, 'service')
+ service_pt_2 = utils.gen_prototype(cluster_bundle, 'service')
+ component_pt_11 = utils.gen_prototype(cluster_bundle, 'component')
+ component_pt_12 = utils.gen_prototype(cluster_bundle, 'component')
+ component_pt_21 = utils.gen_prototype(cluster_bundle, 'component')
+ component_pt_22 = utils.gen_prototype(cluster_bundle, 'component')
+
+ cluster_1 = utils.gen_cluster(prototype=cluster_pt)
+ service_11 = utils.gen_service(cluster_1, prototype=service_pt_1)
+ service_12 = utils.gen_service(cluster_1, prototype=service_pt_2)
+ component_111 = utils.gen_component(service_11, prototype=component_pt_11)
+ component_112 = utils.gen_component(service_11, prototype=component_pt_12)
+ component_121 = utils.gen_component(service_12, prototype=component_pt_21)
+ component_122 = utils.gen_component(service_12, prototype=component_pt_22)
+
+ cluster_2 = utils.gen_cluster(prototype=cluster_pt)
+ service_21 = utils.gen_service(cluster_2, prototype=service_pt_1)
+ service_22 = utils.gen_service(cluster_2, prototype=service_pt_2)
+ component_211 = utils.gen_component(service_21, prototype=component_pt_11)
+ component_212 = utils.gen_component(service_21, prototype=component_pt_12)
+ component_221 = utils.gen_component(service_22, prototype=component_pt_21)
+ component_222 = utils.gen_component(service_22, prototype=component_pt_22)
+
+ provider_bundle = utils.gen_bundle()
+
+ provider_pt = utils.gen_prototype(provider_bundle, 'provider')
+ host_pt = utils.gen_prototype(provider_bundle, 'host')
+
+ provider_1 = utils.gen_provider(prototype=provider_pt)
+ host_11 = utils.gen_host(provider_1, prototype=host_pt)
+ host_12 = utils.gen_host(provider_1, prototype=host_pt)
+
+ provider_2 = utils.gen_provider(prototype=provider_pt)
+ host_21 = utils.gen_host(provider_2, prototype=host_pt)
+ host_22 = utils.gen_host(provider_2, prototype=host_pt)
+
+ provider_3 = utils.gen_provider(prototype=provider_pt)
+ host_31 = utils.gen_host(provider_3, prototype=host_pt)
+ host_32 = utils.gen_host(provider_3, prototype=host_pt)
+
+ utils.gen_host_component(component_111, host_11)
+ utils.gen_host_component(component_112, host_11)
+ utils.gen_host_component(component_121, host_11)
+ utils.gen_host_component(component_122, host_11)
+
+ utils.gen_host_component(component_111, host_12)
+ utils.gen_host_component(component_112, host_12)
+ utils.gen_host_component(component_121, host_12)
+ utils.gen_host_component(component_122, host_12)
+
+ utils.gen_host_component(component_121, host_31)
+ utils.gen_host_component(component_122, host_31)
+
+ utils.gen_host_component(component_211, host_21)
+ utils.gen_host_component(component_212, host_21)
+ utils.gen_host_component(component_221, host_21)
+ utils.gen_host_component(component_222, host_21)
+
+ utils.gen_host_component(component_211, host_22)
+ utils.gen_host_component(component_212, host_22)
+ utils.gen_host_component(component_221, host_22)
+ utils.gen_host_component(component_222, host_22)
+
+ utils.gen_host_component(component_221, host_32)
+ utils.gen_host_component(component_222, host_32)
+
+ return dict(
+ cluster_1=cluster_1,
+ service_11=service_11,
+ service_12=service_12,
+ component_111=component_111,
+ component_112=component_112,
+ component_121=component_121,
+ component_122=component_122,
+
+ cluster_2=cluster_2,
+ service_21=service_21,
+ service_22=service_22,
+ component_211=component_211,
+ component_212=component_212,
+ component_221=component_221,
+ component_222=component_222,
+
+ provider_1=provider_1,
+ host_11=host_11,
+ host_12=host_12,
+
+ provider_2=provider_2,
+ host_21=host_21,
+ host_22=host_22,
+
+ provider_3=provider_3,
+ host_31=host_31,
+ host_32=host_32,
+ )
+
+
+class HierarchyTest(TestCase):
+ @skip('run as needed to check if performance remains the same')
+ def test_build_tree_performance(self):
+ """
+ Un-skip it for manual performance testing after changes to cm/hierarchy.py
+ average ~0.025 seconds per single build
+ """
+ hierarchy_objects = generate_hierarchy()
+
+ start = time.time()
+ counter = 0
+ for obj in hierarchy_objects.values():
+ hierarchy.Tree(obj)
+ counter += 1
+ duration = time.time() - start
+
+ print(f'\n\n Average tree build is {duration / counter} seconds')
+
+ def test_get_node(self):
+ """Test function `hierarchy.Tree.get_node()` AND if tree was built correctly"""
+ hierarchy_objects = generate_hierarchy()
+ tree = hierarchy.Tree(hierarchy_objects['cluster_1'])
+ expected = (
+ 'cluster_1',
+ 'service_11',
+ 'service_12',
+ 'component_111',
+ 'component_112',
+ 'component_121',
+ 'component_122',
+ 'host_11',
+ 'host_12',
+ 'host_31',
+ 'provider_1',
+ 'provider_3',
+ )
+ for name in expected:
+ assert tree.get_node(hierarchy_objects[name])
+
+ not_expected = (
+ 'cluster_2',
+ 'service_21',
+ 'service_22',
+ 'component_211',
+ 'component_212',
+ 'component_221',
+ 'component_221',
+ 'host_21',
+ 'host_22',
+ 'host_32',
+ 'provider_2',
+ )
+ for name in not_expected:
+ with self.assertRaises(hierarchy.HierarchyError):
+ tree.get_node(hierarchy_objects[name])
+
+ def test_get_directly_affected(self):
+ """Test `hierarchy.Tree.get_directly_affected()` function"""
+ hierarchy_objects = generate_hierarchy()
+ tree = hierarchy.Tree(hierarchy_objects['cluster_1'])
+
+ expected = {
+ 'cluster_1': (
+ 'cluster_1',
+ 'service_11',
+ 'service_12',
+ 'component_111',
+ 'component_112',
+ 'component_121',
+ 'component_122',
+ 'host_11',
+ 'host_12',
+ 'host_31',
+ 'provider_1',
+ 'provider_3',
+ ),
+ 'service_11': (
+ 'cluster_1',
+ 'service_11',
+ 'component_111',
+ 'component_112',
+ 'host_11',
+ 'host_12',
+ 'provider_1',
+ ),
+ 'service_12': (
+ 'cluster_1',
+ 'service_12',
+ 'component_121',
+ 'component_122',
+ 'host_11',
+ 'host_12',
+ 'host_31',
+ 'provider_1',
+ 'provider_3',
+ ),
+ 'component_111': (
+ 'cluster_1',
+ 'service_11',
+ 'component_111',
+ 'host_11',
+ 'host_12',
+ 'provider_1',
+ ),
+ 'component_112': (
+ 'cluster_1',
+ 'service_11',
+ 'component_112',
+ 'host_11',
+ 'host_12',
+ 'provider_1',
+ ),
+ 'component_121': (
+ 'cluster_1',
+ 'service_12',
+ 'component_121',
+ 'host_11',
+ 'host_12',
+ 'host_31',
+ 'provider_1',
+ 'provider_3',
+ ),
+ 'component_122': (
+ 'cluster_1',
+ 'service_12',
+ 'component_122',
+ 'host_11',
+ 'host_12',
+ 'host_31',
+ 'provider_1',
+ 'provider_3',
+ ),
+ 'host_11': (
+ 'cluster_1',
+ 'service_11',
+ 'service_12',
+ 'component_111',
+ 'component_112',
+ 'component_121',
+ 'component_122',
+ 'host_11',
+ 'provider_1',
+ ),
+ 'host_12': (
+ 'cluster_1',
+ 'service_11',
+ 'service_12',
+ 'component_111',
+ 'component_112',
+ 'component_121',
+ 'component_122',
+ 'host_12',
+ 'provider_1',
+ ),
+ 'host_31': (
+ 'cluster_1',
+ 'service_12',
+ 'component_121',
+ 'component_122',
+ 'host_31',
+ 'provider_3',
+ ),
+ }
+
+ 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
+ }
+ got_affected = set(tree.get_directly_affected(target_node))
+ self.assertSetEqual(expected_affected, got_affected)
+
+ def test_get_all_affected(self):
+ """Test `hierarchy.Tree.get_all_affected()` function"""
+ hierarchy_objects = generate_hierarchy()
+ tree = hierarchy.Tree(hierarchy_objects['cluster_1'])
+
+ expected = {
+ 'cluster_1': (
+ 'cluster_1',
+ 'service_11',
+ 'service_12',
+ 'component_111',
+ 'component_112',
+ 'component_121',
+ 'component_122',
+ 'host_11',
+ 'host_12',
+ 'host_31',
+ 'provider_1',
+ 'provider_3',
+ ),
+ 'service_11': (
+ 'cluster_1',
+ 'service_11',
+ 'service_12',
+ 'component_111',
+ 'component_112',
+ 'component_121',
+ 'component_122',
+ 'host_11',
+ 'host_12',
+ 'provider_1',
+ ),
+ 'service_12': (
+ 'cluster_1',
+ 'service_11',
+ 'service_12',
+ 'component_111',
+ 'component_112',
+ 'component_121',
+ 'component_122',
+ 'host_11',
+ 'host_12',
+ 'host_31',
+ 'provider_1',
+ 'provider_3',
+ ),
+ 'component_111': (
+ 'cluster_1',
+ 'service_11',
+ 'service_12',
+ 'component_111',
+ 'component_112',
+ 'component_121',
+ 'component_122',
+ 'host_11',
+ 'host_12',
+ 'provider_1',
+ ),
+ 'component_112': (
+ 'cluster_1',
+ 'service_11',
+ 'service_12',
+ 'component_111',
+ 'component_112',
+ 'component_121',
+ 'component_122',
+ 'host_11',
+ 'host_12',
+ # 'host_31',
+ 'provider_1',
+ # 'provider_3',
+ ),
+ 'component_121': (
+ 'cluster_1',
+ 'service_11',
+ 'service_12',
+ 'component_111',
+ 'component_112',
+ 'component_121',
+ 'component_122',
+ 'host_11',
+ 'host_12',
+ 'host_31',
+ 'provider_1',
+ 'provider_3',
+ ),
+ 'component_122': (
+ 'cluster_1',
+ 'service_11',
+ 'service_12',
+ 'component_111',
+ 'component_112',
+ 'component_121',
+ 'component_122',
+ 'host_11',
+ 'host_12',
+ 'host_31',
+ 'provider_1',
+ 'provider_3',
+ ),
+ 'host_11': (
+ 'cluster_1',
+ 'service_11',
+ 'service_12',
+ 'component_111',
+ 'component_112',
+ 'component_121',
+ 'component_122',
+ 'host_11',
+ 'provider_1',
+ ),
+ 'host_12': (
+ 'cluster_1',
+ 'service_11',
+ 'service_12',
+ 'component_111',
+ 'component_112',
+ 'component_121',
+ 'component_122',
+ 'host_12',
+ 'provider_1',
+ ),
+ 'host_31': (
+ 'cluster_1',
+ 'service_12',
+ 'component_121',
+ 'component_122',
+ 'host_31',
+ 'provider_3',
+ ),
+ 'provider_1': (
+ 'cluster_1',
+ 'service_11',
+ 'service_12',
+ 'component_111',
+ 'component_112',
+ 'component_121',
+ 'component_122',
+ 'host_11',
+ 'host_12',
+ 'provider_1',
+ ),
+ 'provider_3': (
+ 'cluster_1',
+ 'service_12',
+ 'component_121',
+ 'component_122',
+ 'host_31',
+ 'provider_3',
+ ),
+ }
+
+ 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
+ }
+ got_affected = set(tree.get_all_affected(target_node))
+ self.assertSetEqual(expected_affected, got_affected)
diff --git a/python/cm/unit_tests/test_issue.py b/python/cm/unit_tests/test_issue.py
new file mode 100644
index 0000000000..97bca31cda
--- /dev/null
+++ b/python/cm/unit_tests/test_issue.py
@@ -0,0 +1,145 @@
+# 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.test import TestCase
+
+from cm.unit_tests import utils
+from cm import issue
+from cm.hierarchy import Tree
+
+
+def generate_hierarchy(): # pylint: disable=too-many-locals,too-many-statements
+ """
+ Generates hierarchy:
+ cluster - service - component - host - provider
+ """
+ utils.gen_adcm()
+ cluster_bundle = utils.gen_bundle()
+ provider_bundle = utils.gen_bundle()
+ cluster_pt = utils.gen_prototype(cluster_bundle, 'cluster')
+ cluster = utils.gen_cluster(prototype=cluster_pt)
+ service_pt = utils.gen_prototype(cluster_bundle, 'service')
+ service = utils.gen_service(cluster, prototype=service_pt)
+ component_pt = utils.gen_prototype(cluster_bundle, 'component')
+ component = utils.gen_component(service, prototype=component_pt)
+ provider_pt = utils.gen_prototype(provider_bundle, 'provider')
+ provider = utils.gen_provider(prototype=provider_pt)
+ host_pt = utils.gen_prototype(provider_bundle, 'host')
+ host = utils.gen_host(provider, prototype=host_pt)
+ utils.gen_host_component(component, host)
+ return dict(
+ cluster=cluster,
+ service=service,
+ component=component,
+ provider=provider,
+ host=host,
+ )
+
+
+class AggregateIssuesTest(TestCase):
+ """
+ Tests for `cm.issue.aggregate_issues()`
+ BEWARE of different object instances in `self.tree` and `self.hierarchy`, use `refresh_from_db`
+ """
+ def setUp(self) -> None:
+ self.hierarchy = generate_hierarchy()
+ self.tree = Tree(self.hierarchy['cluster'])
+
+ def test_no_issues(self):
+ """Objects with no issues has no aggregated issues as well"""
+ for obj in self.hierarchy.values():
+ self.assertFalse(issue.aggregate_issues(obj, self.tree))
+
+ def test_provider_issue(self):
+ """Test provider's special case - it does not aggregate issues from other objects"""
+ data = {'config': False}
+ for node in self.tree.get_all_affected(self.tree.built_from):
+ node.value.issue = data
+ node.value.save()
+
+ provider = self.hierarchy['provider']
+ provider.refresh_from_db()
+ self.assertDictEqual(data, issue.aggregate_issues(provider, self.tree))
+
+ def test_own_issue(self):
+ """Test if own issue is not doubled as nested"""
+ data = {'config': False}
+ for node in self.tree.get_all_affected(self.tree.built_from):
+ node.value.issue = data
+ node.value.save()
+
+ for name, obj in self.hierarchy.items():
+ obj.refresh_from_db()
+ agg_issue = issue.aggregate_issues(obj, self.tree)
+ self.assertNotIn(name, agg_issue)
+ self.assertIn('config', agg_issue)
+
+ def test_linked_issues(self):
+ """Test if aggregated issue has issues from all linked objects"""
+ data = {'config': False}
+ for node in self.tree.get_all_affected(self.tree.built_from):
+ node.value.issue = data
+ node.value.save()
+
+ keys = {'config', 'cluster', 'service', 'component', 'provider', 'host'}
+ for name, obj in self.hierarchy.items():
+ if name == 'provider':
+ continue
+
+ obj.refresh_from_db()
+ agg_issue = issue.aggregate_issues(obj, self.tree)
+ expected_keys = keys.difference({name})
+ got_keys = set(agg_issue.keys())
+ self.assertSetEqual(expected_keys, got_keys)
+
+
+class IssueReporterTest(TestCase):
+ @patch('cm.status_api.post_event')
+ def test_update__no_issue_changes(self, mock_evt_post):
+ hierarchy = generate_hierarchy()
+ issue.update_hierarchy_issues(hierarchy['cluster'])
+ mock_evt_post.assert_not_called()
+
+ @patch('cm.status_api.post_event')
+ @patch('cm.issue.check_cluster_issue')
+ def test_update__raise_issue(self, mock_check, mock_evt_post):
+ mock_check.return_value = {'config': False}
+ hierarchy = generate_hierarchy()
+
+ issue.update_hierarchy_issues(hierarchy['cluster'])
+
+ affected = ('cluster', 'service', 'component', 'host')
+ self.assertEqual(len(affected), mock_evt_post.call_count)
+ expected_calls = {('raise_issue', n) for n in affected}
+ got_calls = set()
+ for call in mock_evt_post.mock_calls:
+ got_calls.add((call.args[0], call.args[1]))
+ self.assertSetEqual(expected_calls, got_calls)
+
+ @patch('cm.status_api.post_event')
+ @patch('cm.issue.check_cluster_issue')
+ def test_update__clear_issue(self, mock_check, mock_evt_post):
+ mock_check.return_value = {}
+ hierarchy = generate_hierarchy()
+ hierarchy['cluster'].issue = {'config': False}
+ hierarchy['cluster'].save()
+
+ issue.update_hierarchy_issues(hierarchy['cluster'])
+
+ affected = ('cluster', 'service', 'component', 'host')
+ self.assertEqual(len(affected), mock_evt_post.call_count)
+ expected_calls = {('clear_issue', n) for n in affected}
+ got_calls = set()
+ for call in mock_evt_post.mock_calls:
+ got_calls.add((call.args[0], call.args[1]))
+ self.assertSetEqual(expected_calls, got_calls)
diff --git a/python/cm/unit_tests/utils.py b/python/cm/unit_tests/utils.py
new file mode 100644
index 0000000000..1f707bd0e9
--- /dev/null
+++ b/python/cm/unit_tests/utils.py
@@ -0,0 +1,122 @@
+# 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 uuid import uuid4
+
+import cm.models as models
+
+
+def _gen_name(prefix: str, name='name'):
+ """Generate unique name"""
+ return {name: prefix + uuid4().hex}
+
+
+def gen_bundle(name=''):
+ """Generate some bundle"""
+ return models.Bundle.objects.create(
+ **_gen_name(name),
+ version='1.0.0'
+ )
+
+
+def gen_prototype(bundle: models.Bundle, proto_type):
+ """Generate prototype of specified type from bundle"""
+ return models.Prototype.objects.create(
+ type=proto_type,
+ name=bundle.name,
+ version=bundle.version,
+ bundle=bundle,
+ )
+
+
+def gen_adcm():
+ """Generate or return existing the only ADCM object"""
+ try:
+ return models.ADCM.objects.get(name='ADCM')
+ except models.ObjectDoesNotExist:
+ bundle = gen_bundle()
+ prototype = gen_prototype(bundle, 'adcm')
+ return models.ADCM.objects.create(name='ADCM', prototype=prototype)
+
+
+def gen_cluster(name='', bundle=None, prototype=None):
+ """Generate cluster from specified prototype"""
+ if not prototype:
+ bundle = bundle or gen_bundle()
+ prototype = gen_prototype(bundle, 'cluster')
+ return models.Cluster.objects.create(
+ **_gen_name(name),
+ prototype=prototype,
+ )
+
+
+def gen_service(cluster, bundle=None, prototype=None):
+ """Generate service of specified cluster and prototype"""
+ if not prototype:
+ bundle = bundle or gen_bundle()
+ prototype = gen_prototype(bundle, 'service')
+ return models.ClusterObject.objects.create(
+ cluster=cluster,
+ prototype=prototype,
+ )
+
+
+def gen_component(service, bundle=None, prototype=None):
+ """Generate service component for specified service and prototype"""
+ if not prototype:
+ bundle = bundle or gen_bundle()
+ prototype = gen_prototype(bundle, 'component')
+ return models.ServiceComponent.objects.create(
+ cluster=service.cluster,
+ service=service,
+ prototype=prototype,
+ )
+
+
+def gen_provider(name='', bundle=None, prototype=None):
+ """Generate host provider for specified prototype"""
+ if not prototype:
+ bundle = bundle or gen_bundle()
+ prototype = gen_prototype(bundle, 'provider')
+ return models.HostProvider.objects.create(
+ **_gen_name(name),
+ prototype=prototype,
+ )
+
+
+def gen_host(provider, cluster=None, fqdn='', bundle=None, prototype=None):
+ """Generate host for specified cluster, provider, and prototype"""
+ if not prototype:
+ bundle = bundle or gen_bundle()
+ prototype = gen_prototype(bundle, 'host')
+ return models.Host.objects.create(
+ **_gen_name(fqdn, 'fqdn'),
+ cluster=cluster,
+ provider=provider,
+ prototype=prototype,
+ )
+
+
+def gen_host_component(component, host):
+ """Generate host-component for specified host and component"""
+ cluster = component.service.cluster
+ if not host.cluster:
+ host.cluster = cluster
+ host.save()
+ elif host.cluster != cluster:
+ raise models.AdcmEx('Integrity error')
+ return models.HostComponent.objects.create(
+ host=host,
+ cluster=cluster,
+ service=component.service,
+ component=component,
+ )
diff --git a/python/cm/upgrade.py b/python/cm/upgrade.py
index 0632127e09..cffc8621ab 100644
--- a/python/cm/upgrade.py
+++ b/python/cm/upgrade.py
@@ -184,7 +184,7 @@ def get_import(cbind): # pylint: disable=redefined-outer-name
def check_upgrade(obj, upgrade):
- issue = cm.issue.get_issue(obj)
+ issue = cm.issue.aggregate_issues(obj)
if not cm.issue.issue_to_bool(issue):
return False, '{} has issue: {}'.format(obj_ref(obj), issue)
@@ -293,7 +293,7 @@ def do_upgrade(obj, upgrade):
for p in Prototype.objects.filter(bundle=upgrade.bundle, type='host'):
for host in Host.objects.filter(provider=obj, prototype__name=p.name):
switch_service(host, p)
- cm.issue.save_issue(obj)
+ cm.issue.update_hierarchy_issues(obj)
log.info('upgrade %s OK to version %s', obj_ref(obj), obj.prototype.version)
cm.status_api.post_event(
diff --git a/python/cm/variant.py b/python/cm/variant.py
new file mode 100644
index 0000000000..bbb7d19ed1
--- /dev/null
+++ b/python/cm/variant.py
@@ -0,0 +1,311 @@
+# 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 cm.logger import log
+from cm.errors import AdcmEx
+from cm.errors import raise_AdcmEx as err
+from cm.models import Prototype, ClusterObject, ServiceComponent, HostComponent, Host
+
+
+def get_cluster(obj):
+ if obj.prototype.type == 'service':
+ cluster = obj.cluster
+ elif obj.prototype.type == 'host':
+ cluster = obj.cluster
+ elif obj.prototype.type == 'cluster':
+ cluster = obj
+ else:
+ return None
+ return cluster
+
+
+def variant_service_in_cluster(obj, args=None):
+ out = []
+ cluster = get_cluster(obj)
+ if cluster is None:
+ return out
+
+ for co in ClusterObject.objects.filter(cluster=cluster).order_by('prototype__name'):
+ out.append(co.prototype.name)
+ return out
+
+
+def variant_service_to_add(obj, args=None):
+ out = []
+ cluster = get_cluster(obj)
+ if cluster is None:
+ return out
+
+ for proto in Prototype.objects \
+ .filter(bundle=cluster.prototype.bundle, type='service') \
+ .exclude(id__in=ClusterObject.objects.filter(cluster=cluster).values('prototype')) \
+ .order_by('name'):
+ out.append(proto.name)
+ return out
+
+
+def var_host_and(cluster, args):
+ if not isinstance(args, list):
+ err('CONFIG_VARIANT_ERROR', 'arguments of "and" predicate should be a list')
+ if not args:
+ return []
+ return sorted(list(set.intersection(*[set(a) for a in args])))
+
+
+def var_host_or(cluster, args):
+ if not isinstance(args, list):
+ err('CONFIG_VARIANT_ERROR', 'arguments of "or" predicate should be a list')
+ if not args:
+ return []
+ return sorted(list(set.union(*[set(a) for a in args])))
+
+
+def var_host_get_service(cluster, args, func):
+ if 'service' not in args:
+ err('CONFIG_VARIANT_ERROR', f'no "service" argument for predicate "{func}"')
+ return ClusterObject.obj.get(cluster=cluster, prototype__name=args['service'])
+
+
+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']
+ )
+
+
+def var_host_in_service(cluster, args):
+ out = []
+ service = var_host_get_service(cluster, args, 'in_service')
+ for hc in HostComponent.objects \
+ .filter(cluster=cluster, service=service) \
+ .order_by('host__fqdn'):
+ out.append(hc.host.fqdn)
+ return out
+
+
+def var_host_not_in_service(cluster, args):
+ out = []
+ service = var_host_get_service(cluster, args, 'not_in_service')
+ for host in Host.objects.filter(cluster=cluster).order_by('fqdn'):
+ if HostComponent.objects.filter(cluster=cluster, service=service, host=host):
+ continue
+ out.append(host.fqdn)
+ return out
+
+
+def var_host_in_cluster(cluster, args):
+ out = []
+ for host in Host.objects.filter(cluster=cluster).order_by('fqdn'):
+ out.append(host.fqdn)
+ return out
+
+
+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'):
+ out.append(hc.host.fqdn)
+ return out
+
+
+def var_host_not_in_component(cluster, args):
+ out = []
+ service = var_host_get_service(cluster, args, 'not_in_component')
+ comp = var_host_get_component(cluster, args, service, 'not_in_component')
+ for host in Host.objects.filter(cluster=cluster).order_by('fqdn'):
+ if HostComponent.objects.filter(cluster=cluster, component=comp, host=host):
+ continue
+ out.append(host.fqdn)
+ return out
+
+
+def var_host_in_hc(cluster, args):
+ out = []
+ for hc in HostComponent.objects.filter(cluster=cluster).order_by('host__fqdn'):
+ out.append(hc.host.fqdn)
+ return out
+
+
+def var_host_not_in_hc(cluster, args):
+ out = []
+ for host in Host.objects.filter(cluster=cluster).order_by('fqdn'):
+ if HostComponent.objects.filter(cluster=cluster, host=host):
+ continue
+ out.append(host.fqdn)
+ return out
+
+
+def var_host_inline_list(cluster, args):
+ return args['list']
+
+
+VARIANT_HOST_FUNC = {
+ 'and': var_host_and,
+ 'or': var_host_or,
+ 'in_cluster': var_host_in_cluster,
+ 'in_service': var_host_in_service,
+ 'not_in_service': var_host_not_in_service,
+ 'in_component': var_host_in_component,
+ 'not_in_component': var_host_not_in_component,
+ 'in_hc': var_host_in_hc,
+ 'not_in_hc': var_host_not_in_hc,
+ 'inline_list': var_host_inline_list, # just for logic functions (and, or) test purpose
+}
+
+
+def var_host_solver(cluster, func_map, args):
+ def check_key(key, args):
+ if not isinstance(args, dict):
+ err('CONFIG_VARIANT_ERROR', 'predicte item should be a map')
+ if key not in args:
+ err('CONFIG_VARIANT_ERROR', f'no "{key}" key in solver args')
+
+ # log.debug('solver args: %s', args)
+ if args is None:
+ return None
+ if isinstance(args, dict):
+ if 'predicate' not in args:
+ # log.debug('solver res1: %s', args)
+ return args
+ else:
+ predicate = args['predicate']
+ if predicate not in func_map:
+ err('CONFIG_VARIANT_ERROR', f'no "{predicate}" in list of host functions')
+ check_key('args', args)
+ res = func_map[predicate](cluster, var_host_solver(cluster, func_map, args['args']))
+ # log.debug('solver res2: %s', res)
+ return res
+
+ res = []
+ if not isinstance(args, list):
+ err('CONFIG_VARIANT_ERROR', 'arguments of solver should be a list or a map')
+ for item in args:
+ check_key('predicate', item)
+ check_key('args', item)
+ predicate = item['predicate']
+ if predicate not in func_map:
+ err('CONFIG_VARIANT_ERROR', f'no "{predicate}" in list of host functions')
+ res.append(func_map[predicate](cluster, var_host_solver(cluster, func_map, item['args'])))
+
+ # log.debug('solver res3: %s', res)
+ return res
+
+
+def variant_host(obj, args=None):
+ cluster = get_cluster(obj)
+ if not cluster:
+ return []
+ if not isinstance(args, dict):
+ err('CONFIG_VARIANT_ERROR', 'arguments of variant host function should be a map')
+ if 'predicate' not in args:
+ err('CONFIG_VARIANT_ERROR', 'no "predicate" key in variant host function arguments')
+ res = var_host_solver(cluster, VARIANT_HOST_FUNC, args)
+ return res
+
+
+def variant_host_in_cluster(obj, args=None):
+ out = []
+ cluster = get_cluster(obj)
+ if cluster is None:
+ return out
+
+ if args and 'service' in args:
+ try:
+ service = ClusterObject.objects.get(cluster=cluster, prototype__name=args['service'])
+ except ClusterObject.DoesNotExist:
+ return []
+ if 'component' in args:
+ try:
+ 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'):
+ out.append(hc.host.fqdn)
+ return out
+ else:
+ for hc in HostComponent.objects \
+ .filter(cluster=cluster, service=service) \
+ .order_by('host__fqdn'):
+ out.append(hc.host.fqdn)
+ return out
+
+ for host in Host.objects.filter(cluster=cluster).order_by('fqdn'):
+ out.append(host.fqdn)
+ return out
+
+
+def variant_host_not_in_clusters(obj, args=None):
+ out = []
+ for host in Host.objects.filter(cluster=None).order_by('fqdn'):
+ out.append(host.fqdn)
+ return out
+
+
+VARIANT_FUNCTIONS = {
+ 'host': variant_host,
+ 'host_in_cluster': variant_host_in_cluster,
+ 'host_not_in_clusters': variant_host_not_in_clusters,
+ 'service_in_cluster': variant_service_in_cluster,
+ 'service_to_add': variant_service_to_add,
+}
+
+
+def get_builtin_variant(obj, func_name, args):
+ if func_name not in VARIANT_FUNCTIONS:
+ log.warning('unknown variant builtin function: %s', func_name)
+ return None
+ try:
+ return VARIANT_FUNCTIONS[func_name](obj, args)
+ except AdcmEx as e:
+ if e.code == 'CONFIG_VARIANT_ERROR':
+ return []
+ raise e
+
+
+def get_variant(obj, conf, limits):
+ value = None
+ source = limits['source']
+ if source['type'] == 'config':
+ skey = source['name'].split('/')
+ if len(skey) == 1:
+ value = conf[skey[0]]
+ else:
+ value = conf[skey[0]][skey[1]]
+ elif source['type'] == 'builtin':
+ value = get_builtin_variant(obj, source['name'], source.get('args', None))
+ elif source['type'] == 'inline':
+ value = source['value']
+ return value
+
+
+def process_variant(obj, spec, conf):
+ def set_variant(spec):
+ limits = spec['limits']
+ limits['source']['value'] = get_variant(obj, conf, limits)
+ return limits
+
+ for key in spec:
+ if 'type' in spec[key]:
+ if spec[key]['type'] == 'variant':
+ spec[key]['limits'] = set_variant(spec[key])
+ else:
+ for subkey in spec[key]:
+ if spec[key][subkey]['type'] == 'variant':
+ spec[key][subkey]['limits'] = set_variant(spec[key][subkey])
diff --git a/python/init_db.py b/python/init_db.py
index b19385f6d6..973c9b5bc4 100755
--- a/python/init_db.py
+++ b/python/init_db.py
@@ -22,7 +22,8 @@
from cm.models import UserProfile, DummyData, CheckLog, GroupCheckLog
from cm.bundle import load_adcm
from cm.config import SECRETS_FILE
-from cm.job import unlock_all
+from cm.job import abort_all
+from cm.lock import unlock_all
from cm.status_api import Event
@@ -71,6 +72,7 @@ def init():
UserProfile.objects.create(login='admin')
create_status_user()
event = Event()
+ abort_all(event)
unlock_all(event)
clear_temp_tables()
event.send_state()
diff --git a/python/job_runner.py b/python/job_runner.py
index b434f72b81..2147319359 100755
--- a/python/job_runner.py
+++ b/python/job_runner.py
@@ -11,18 +11,20 @@
# See the License for the specific language governing permissions and
# limitations under the License.
-import os
-import sys
+# pylint: disable=unused-import
+
import json
+import os
import subprocess
+import sys
-import adcm.init_django # pylint: disable=unused-import
-
-from cm.logger import log
+import adcm.init_django # DO NOT DELETE !!!
import cm.config as config
import cm.job
-from cm.status_api import Event
+import cm.lock
+from cm.logger import log
from cm.models import LogStorage
+from cm.status_api import Event
def open_file(root, tag, job_id):
diff --git a/python/pytest.ini b/python/pytest.ini
new file mode 100644
index 0000000000..7603a27a52
--- /dev/null
+++ b/python/pytest.ini
@@ -0,0 +1,2 @@
+[pytest]
+DJANGO_SETTINGS_MODULE = adcm.settings
diff --git a/python/task_runner.py b/python/task_runner.py
index f139d9f526..659650b457 100755
--- a/python/task_runner.py
+++ b/python/task_runner.py
@@ -23,7 +23,7 @@
from django.core.exceptions import ObjectDoesNotExist
from django.utils import timezone
-import adcm.init_django
+import adcm.init_django # DO NOT DELETE !!!
import cm.config as config
import cm.job
from cm.logger import log
diff --git a/requirements-test.txt b/requirements-test.txt
index c3222574c5..1b264156aa 100644
--- a/requirements-test.txt
+++ b/requirements-test.txt
@@ -1,10 +1,7 @@
--extra-index-url https://ci.arenadata.io/artifactory/api/pypi/python-packages/simple
-adcm-client
+adcm-client>=2021.3.18.13
adcm_pytest_plugin>=3.0.0
-# Docker IP binding is changed in https://github.com/arenadata/adcm_pytest_plugin/pull/50
--r ./requirements.txt
allure-pytest
-#arenadata_test_suite
delayed_assert
docker
jsonschema
@@ -15,5 +12,4 @@ pytest-rerunfailures
pytest-timeout
pyyaml
requests
-retrying==1.3.3
selenium
diff --git a/requirements.txt b/requirements.txt
index 5c6910762a..d1fcca69c5 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,24 +1 @@
-# ansible==2.8.3
-git+https://github.com/arenadata/ansible.git@v2.8.8-p4
-coreapi
-django
-django-cors-headers
-django-filter
-django-rest-swagger
-djangorestframework
-django-background-tasks
-social-auth-app-django
-git+git://github.com/arenadata/django-generate-secret-key.git
-jinja2
-markdown
-mitogen
-pyyaml
-toml
-uwsgi
-version_utils
-yspec
-# Custom bundle libs
-apache-libcloud
-jmespath
-lxml
-pycrypto
+-r assemble/base/requirements-base.txt
diff --git a/spec/conf.py b/spec/conf.py
index c76bac2b00..042f6702b0 100644
--- a/spec/conf.py
+++ b/spec/conf.py
@@ -74,6 +74,7 @@
"sphinx.ext.intersphinx",
"sphinx.ext.mathjax",
"sphinx.ext.githubpages",
+ "sphinx.ext.extlinks",
"sphinx_markdown_tables",
# "sphinxpapyrus.docxbuilder",
]
@@ -341,6 +342,10 @@
# Example configuration for intersphinx: refer to the Python standard library.
intersphinx_mapping = {"https://docs.python.org/": None}
+extlinks = {
+ 'issue': ('https://arenadata.atlassian.net/browse/%s', '')
+}
+
def setup(app):
app.add_stylesheet("css/theme_custom.css")
diff --git a/spec/index.rst b/spec/index.rst
index e72f460aa3..0424cb7ce3 100644
--- a/spec/index.rst
+++ b/spec/index.rst
@@ -1,9 +1,9 @@
-Main Page
-#########
-
-
+ADCM Specs
+##########
.. toctree::
:maxdepth: 3
:caption: Contents:
+ usecase/index.rst
+ system/index.rst
diff --git a/spec/system/components.rst b/spec/system/components.rst
new file mode 100644
index 0000000000..603cc3b05b
--- /dev/null
+++ b/spec/system/components.rst
@@ -0,0 +1,34 @@
+ADCM Componets
+##############
+
+UI
+===
+
+The frontend part of ADCM. Based on Angular Framework and meterial design.
+
+Job Executor
+============
+
+Job executor of ADCM is quite simple. It just a script to prepare data for Ansible and some script to handle ansible-playbook command.
+
+Ansible
+=======
+
+This part of ADCM responses for all operations on hosts. Playbooks are delivered of bundles.
+
+Currently we are using Ansible 2.8.x version.
+
+WSGI Backend
+============
+
+This is a part of backend repsponsible for most operations. It based on Django ORM framework and Django REST Framework (DRF).
+
+Resident Backend
+================
+
+Resident Backend part, also known as Status Server (SS) is part of ADCM based on Golang platform. It responsible for event fiding from Django to UI over websocket protocol and for handling of statuses.
+
+Nginx
+=====
+
+It is a standart Nginx web server who route request to Resident Backend and WSGI Backend and serve static files.
diff --git a/spec/system/index.rst b/spec/system/index.rst
new file mode 100644
index 0000000000..706fc35e24
--- /dev/null
+++ b/spec/system/index.rst
@@ -0,0 +1,8 @@
+Software Decomposition
+######################
+
+.. toctree::
+ :maxdepth: 2
+ :caption: Contents:
+
+ components.rst
diff --git a/spec/usecase/action/index.rst b/spec/usecase/action/index.rst
new file mode 100644
index 0000000000..55cff63307
--- /dev/null
+++ b/spec/usecase/action/index.rst
@@ -0,0 +1,10 @@
+Actions
+#######
+
+
+
+.. toctree::
+ :maxdepth: 1
+ :caption: Contents:
+
+ onhost.rst
diff --git a/spec/usecase/action/onhost.rst b/spec/usecase/action/onhost.rst
new file mode 100644
index 0000000000..79c3c548d0
--- /dev/null
+++ b/spec/usecase/action/onhost.rst
@@ -0,0 +1,76 @@
+.. _action_onhost:
+
+Actions on Cluster's Host
+#########################
+
+.. toctree::
+ :maxdepth: 0
+ :caption: Contents:
+ :hidden:
+
+ onhost/cluster_on_host.rst
+ onhost/service_on_host.rst
+ onhost/component_on_host.rst
+ onhost/target.rst
+ onhost/on_host_list.rst
+
+This spec is part of changes introduced in story :issue:`ADCM-1620`.
+
+Main idea of this case is execution of some actions defined in cluster bundle on one particular host.
+
+
+“Used” Use Cases
+----------------
+
+List of child cases which is a direct detalisation of this one:
+
+* :ref:`action_onhost_cluster`
+* :ref:`action_onhost_service`
+* :ref:`action_onhost_component`
+
+Additional cases which is explisity define one paticular subfeature:
+
+* :ref:`action_onhost_api_host_only`
+* :ref:`action_onhost_target`
+
+
+Actors
+------
+
+* :term:`End User`
+* :term:`Bundle Developer`
+
+User Value
+----------
+
+This functionality allow :term:`End User` to make operation with :term:`Product` on one particular host. For example:
+
+* Start component
+* Stop component
+* Check application
+
+Pre-Conditions
+--------------
+
+* :term:`End User` has ADCM with a :term:`Product` installed on some cluster
+
+Post-Conditions
+---------------
+
+* :term:`End User` was able to run some action provided by :term:`Bundle Developer` on one host included in cluster
+
+
+Flow of Events
+--------------
+
+#. :term:`Bundle Developer` adds an action to :term:`Product` with special mark parameter "host_action: true"
+#. :term:`End User` goes to installed Cluster "Hosts" page
+#. :term:`End User` see actions available for a host
+#. :term:`End User` choose action provided by :term:`Bundle Developer`
+#. Action executes:
+
+ #. ADCM creates inventory with right context execution context (cluster/service/component)
+ #. ADCM adds "target" group to inventory with the host choosed by :term:`End User`
+
+.. note:: Take a note, that ADCM doesn't restrict :term:`Bundle Developer` with operation on one the host chossed by :term:`End User` only.
+ ADCM just merely pass the ask to playbook over special group in inventory. It is :term:`Bundle Developer` responsibility to care about locality.
diff --git a/spec/usecase/action/onhost/cluster_on_host.rst b/spec/usecase/action/onhost/cluster_on_host.rst
new file mode 100644
index 0000000000..ef44df5bc4
--- /dev/null
+++ b/spec/usecase/action/onhost/cluster_on_host.rst
@@ -0,0 +1,81 @@
+.. _action_onhost_cluster:
+
+Cluster's Actions on Cluster's Host
+###################################
+
+This spec is part of changes introduced in story :issue:`ADCM-1622`.
+
+Main idea of this case is execution of component action on one particular host.
+
+“Used” Use Cases
+----------------
+
+This case is a detalisation of :ref:`action_onhost`.
+
+Actors
+------
+
+* :term:`End User`
+* :term:`Bundle Developer`
+
+User Value
+----------
+
+This functionality allow :term:`End User` to make operation with cluster on one particular host. For example:
+
+* Start everything on one host
+* Stop everything on one host
+
+Pre-Conditions
+--------------
+
+* :term:`End User` has ADCM with a :term:`Product` installed on some cluster
+
+Post-Conditions
+---------------
+
+* :term:`End User` was able to run some action provided by :term:`Bundle Developer` on one host included in cluster
+
+
+Flow of Events
+--------------
+
+1. :term:`Bundle Developer` adds action to a cluster like follows
+
+.. code-block:: yaml
+
+ - type: cluster
+ name: My Supper Cluster
+ version: "1.0"
+ actions:
+ restart:
+ display_name: "Restart Application"
+ type: job
+ script_type: ansible
+ script: restart.yaml
+ host_action: true
+ states:
+ available: somestate
+ - type: service
+ name: My Supper Service
+ version: "1.0"
+ components:
+ mycomponent:
+ constraint: [0,+]
+ mycomponent2:
+ constraint: [0,+]
+
+2. :term:`End User` installs cluster from this :term:`Bundle`
+3. :term:`End User` adds hosts
+4. :term:`End User` sees the action "Restart Application" on the host
+5. :term:`End User` runs the action
+
+Exceptions
+~~~~~~~~~~
+
+4. Cluster "My Supper Cluster" is not in state "somestate"
+
+ a. :term:`End User` sees no action "Restart Application"
+ b. The End
+
+.. warning:: We need to be sure, there is no troubles with mixing states. It should react on cluster state only.
diff --git a/spec/usecase/action/onhost/component_on_host.rst b/spec/usecase/action/onhost/component_on_host.rst
new file mode 100644
index 0000000000..c5f525fff4
--- /dev/null
+++ b/spec/usecase/action/onhost/component_on_host.rst
@@ -0,0 +1,83 @@
+.. _action_onhost_component:
+
+Component's Actions on Cluster's Host
+#####################################
+
+This spec is part of changes introduced in story :issue:`ADCM-1508`.
+
+Main idea of this case is execution of component action on one particular host.
+
+“Used” Use Cases
+----------------
+
+This case is a detalisation of :ref:`action_onhost`.
+
+Actors
+------
+
+* :term:`End User`
+* :term:`Bundle Developer`
+
+User Value
+----------
+
+This functionality allow :term:`End User` to make operation with service on one particular host. For example:
+
+* Start service on one host
+* Stop service on one host
+
+Pre-Conditions
+--------------
+
+* :term:`End User` has ADCM with a :term:`Product` installed on some cluster
+
+Post-Conditions
+---------------
+
+* :term:`End User` was able to run some action provided by :term:`Bundle Developer` on one host which has a component on it
+
+
+Flow of Events
+--------------
+
+1. :term:`Bundle Developer` adds action to a component like follows
+
+.. code-block:: yaml
+
+ - type: service
+ name: My Supper Service
+ version: "1.0"
+ components:
+ mycomponent:
+ constraint: [0,+]
+ actions:
+ restart:
+ display_name: "Restart mycomponent"
+ type: job
+ script_type: ansible
+ script: restart.yaml
+ host_action: true
+ states:
+ available: somestate
+
+2. :term:`End User` installs cluster from this :term:`Bundle`
+3. :term:`End User` adds service
+4. :term:`End User` adds hosts
+5. :term:`End User` places "mycomponnet" on a host
+6. :term:`End User` sees the action "Restart mycomponent" on the host
+7. :term:`End User` runs the action
+
+Exceptions
+~~~~~~~~~~
+
+5. :term:`End User` chooses a host without mycomponent installed on it
+
+ a. :term:`End User` sees no action "Restart mycomonent"
+ b. The End
+
+6. Component "mycomponent" is not in state "somestate"
+
+ a. :term:`End User` sees no action "Restart mycomonent"
+ b. The End
+
+.. warning:: We need to be sure, there is no troubles with mixing states. It should react on component state only.
diff --git a/spec/usecase/action/onhost/on_host_list.rst b/spec/usecase/action/onhost/on_host_list.rst
new file mode 100644
index 0000000000..ccb14f6436
--- /dev/null
+++ b/spec/usecase/action/onhost/on_host_list.rst
@@ -0,0 +1,44 @@
+.. _action_onhost_api_host_only:
+
+API Listing Details
+###################
+
+For any :term:`On Host Action` no matter which of type (cluster/service/component) action is not shown in cluster/service/component's action list but shown host's action list.
+
+There is no proper way to get information of host from API call if it was done on cluster/service/component endpoint. And there is no story about running :term:`On Host Action` as a regular action.
+
+“Used” Use Cases
+----------------
+
+The information provided in this case is in the cases below, but this case detalises this explicity to be sure it will be tested.
+
+* :ref:`action_onhost_cluster`
+* :ref:`action_onhost_service`
+* :ref:`action_onhost_component`
+* :ref:`action_onhost_target`
+
+User Value
+----------
+
+No user value at all. It is a stab use case which prevent us from error.
+
+Actors
+------
+
+* :term:`End User`
+* :term:`Bundle Developer`
+
+Pre-Condition
+-------------
+
+* :term:`End User` has ADCM with a :term:`Product` installed on some cluster
+
+Flow of Events
+--------------
+
+#. :term:`Bundle Developer` adds an action to :term:`Product` with special mark parameter "host_action: true" on cluster/service/component
+#. :term:`End User` goes to installed Cluster "Hosts" page
+#. :term:`End User` see the actions marked by :term:`Bundle Developer` as an :term:`On Host Action`
+#. :term:`End User` goes to action list of cluster/service/component
+#. :term:`End User` see no actions marked by :term:`Bundle Developer` as an :term:`On Host Action`
+
diff --git a/spec/usecase/action/onhost/service_on_host.rst b/spec/usecase/action/onhost/service_on_host.rst
new file mode 100644
index 0000000000..7fa1873efc
--- /dev/null
+++ b/spec/usecase/action/onhost/service_on_host.rst
@@ -0,0 +1,87 @@
+.. _action_onhost_service:
+
+Service's Actions on Cluster's Host
+###################################
+
+This spec is part of changes introduced in story :issue:`ADCM-1621`.
+
+Main idea of this case is execution of service action on one particular host.
+
+“Used” Use Cases
+----------------
+
+This case is a detalisation of :ref:`action_onhost`.
+
+
+Actors
+------
+
+* :term:`End User`
+* :term:`Bundle Developer`
+
+User Value
+----------
+
+This functionality allow :term:`End User` to make operation with service on one particular host. For example:
+
+* Start service on one host
+* Stop service on one host
+
+Pre-Conditions
+--------------
+
+* :term:`End User` has ADCM with a :term:`Product` installed on some cluster
+
+Post-Conditions
+---------------
+
+* :term:`End User` was able to run some action provided by :term:`Bundle Developer` on one host included in service
+
+
+
+Flow of Events
+--------------
+
+1. :term:`Bundle Developer` adds action to a service like follows
+
+.. code-block:: yaml
+
+ - type: service
+ name: My Supper Service
+ version: "1.0"
+ actions:
+ restart:
+ display_name: "Restart service"
+ type: job
+ script_type: ansible
+ script: restart.yaml
+ host_action: true
+ states:
+ available: somestate
+ components:
+ mycomponent:
+ constraint: [0,+]
+ mycomponent2:
+ constraint: [0,+]
+
+2. :term:`End User` installs cluster from this :term:`Bundle`
+3. :term:`End User` adds service
+4. :term:`End User` adds hosts
+5. :term:`End User` places "mycomponnet" or "mycomponent2" or both of them on a host
+6. :term:`End User` sees the action "Restart service" on the host
+7. :term:`End User` runs the action
+
+Exceptions
+~~~~~~~~~~
+
+5. :term:`End User` chooses a host without "mycomponent" or "mycomponent2" installed on it.
+
+ a. :term:`End User` sees no action "Restart service"
+ b. The End
+
+6. Service "My Supper Service" is not in state "somestate"
+
+ a. :term:`End User` sees no action "Restart service"
+ b. The End
+
+.. warning:: We need to be sure, there is no troubles with mixing states. It should react on service state only.
diff --git a/spec/usecase/action/onhost/target.rst b/spec/usecase/action/onhost/target.rst
new file mode 100644
index 0000000000..de9f7da11b
--- /dev/null
+++ b/spec/usecase/action/onhost/target.rst
@@ -0,0 +1,49 @@
+.. _action_onhost_target:
+
+Target group in inventory
+#########################
+
+This spec is part of changes introduced in story :issue:`ADCM-1623`.
+
+We need special group in inventory to provide information for :term:`Bundle Developer` about which of host has been choosed by :term:`End User`
+
+
+“Used” Use Cases
+----------------
+
+This case is a detalisation of :ref:`action_onhost`.
+
+Actors
+------
+
+* :term:`End User`
+* :term:`Bundle Developer`
+
+User Value
+----------
+
+This functionality allow to be sure information passed by :term:`End User` is available for :term:`Bundle Developer`.
+
+Pre-Conditions
+--------------
+
+* :term:`End User` has ADCM with a :term:`Product` installed on some cluster.
+
+Post-Conditions
+---------------
+
+* there a new group "target" in inventory file with host choosed by :term:`End User`
+
+
+Flow of Events
+--------------
+
+1. :term:`Bundle Developer` adds action to some object in :term:`Product` bundle acording to any of the cases:
+ * :ref:`action_onhost_cluster`
+ * :ref:`action_onhost_service`
+ * :ref:`action_onhost_component`
+2. :term:`End User` run "on host" action according to any of the cases
+ * :ref:`action_onhost_cluster`
+ * :ref:`action_onhost_service`
+ * :ref:`action_onhost_component`
+3. Action executes with right inventory
diff --git a/spec/usecase/glossary.rst b/spec/usecase/glossary.rst
new file mode 100644
index 0000000000..8c2e6e92a7
--- /dev/null
+++ b/spec/usecase/glossary.rst
@@ -0,0 +1,33 @@
+Glossary
+########
+
+Persons
+=======
+
+.. glossary::
+
+
+ End User
+ A persone who is using a Product and control it over ADCM. Typically it is an system adminstrator or DBA.
+
+ Bundle Developer
+ A persone who is develops a Product by creating a Bundle. In most cases this persone is on Arenadata side and a part Product Development team.
+
+Objects
+=======
+
+.. glossary::
+
+ Product
+ A bunch of software provided by Arenadata to a customer in form of :term:`Bundle` and inteded to be used as distributed cluster software.
+
+ Bundle
+ A pack of special format created by :term:`Bundle Developer` and distributed to an :term:`End User`
+
+Terms
+=====
+
+.. glossary::
+
+ On Host Action
+ Is an action provided in :term:`Product` :term:`Bundle`, but show and executed on Host, included to the Cluster. See :ref:`action_onhost` for more information
diff --git a/spec/usecase/index.rst b/spec/usecase/index.rst
new file mode 100644
index 0000000000..3c8fbe2f5e
--- /dev/null
+++ b/spec/usecase/index.rst
@@ -0,0 +1,9 @@
+Use Cases
+#########
+
+.. toctree::
+ :maxdepth: 2
+ :caption: Contents:
+
+ action/index.rst
+ glossary.rst
diff --git a/tests/base/data/download/adh.1.5.tar b/tests/base/data/download/adh.1.5.tar
index 0205557225..68a308f33a 100644
Binary files a/tests/base/data/download/adh.1.5.tar and b/tests/base/data/download/adh.1.5.tar differ
diff --git a/tests/base/data/download/ssh.1.0.tar b/tests/base/data/download/ssh.1.0.tar
index 1ec23e7e14..37d090ba0b 100644
Binary files a/tests/base/data/download/ssh.1.0.tar and b/tests/base/data/download/ssh.1.0.tar differ
diff --git a/tests/base/settings.py b/tests/base/settings.py
index 3624738911..be17054a8e 100644
--- a/tests/base/settings.py
+++ b/tests/base/settings.py
@@ -10,6 +10,9 @@
# See the License for the specific language governing permissions and
# limitations under the License.
+import os
+from os.path import dirname
+
from adcm.settings import * # pylint: disable=wildcard-import,unused-wildcard-import,import-error
DEBUG = True
diff --git a/tests/base/test_api.py b/tests/base/test_api.py
index c5ec1fc39e..32263ba8be 100755
--- a/tests/base/test_api.py
+++ b/tests/base/test_api.py
@@ -13,6 +13,7 @@
import os
import json
+import string
import time
import unittest
import requests
@@ -169,7 +170,7 @@ def test_schema(self):
r1 = self.api_get('/schema/')
self.assertEqual(r1.status_code, 200)
- def test_cluster(self):
+ def test_cluster(self): # pylint: disable=too-many-statements
cluster = 'test_cluster'
r1 = self.api_post('/stack/load/', {'bundle_file': self.adh_bundle})
self.assertEqual(r1.status_code, 200)
@@ -179,14 +180,34 @@ def test_cluster(self):
self.assertEqual(r1.status_code, 400)
self.assertEqual(r1.json()['name'], ['This field is required.'])
+ r1 = self.api_post('/cluster/', {'name': ''})
+ self.assertEqual(r1.status_code, 400)
+ self.assertEqual(r1.json()['name'], ['This field may not be blank.'])
+
r1 = self.api_post('/cluster/', {'name': cluster})
self.assertEqual(r1.status_code, 400)
self.assertEqual(r1.json()['prototype_id'], ['This field is required.'])
+ r1 = self.api_post('/cluster/', {'name': cluster, 'prototype_id': ''})
+ self.assertEqual(r1.status_code, 400)
+ self.assertEqual(r1.json()['prototype_id'], ['A valid integer is required.'])
+
+ r1 = self.api_post('/cluster/', {'name': cluster, 'prototype_id': 'some-string'})
+ self.assertEqual(r1.status_code, 400)
+ self.assertEqual(r1.json()['prototype_id'], ['A valid integer is required.'])
+
r1 = self.api_post('/cluster/', {'name': cluster, 'prototype_id': 100500})
self.assertEqual(r1.status_code, 404)
self.assertEqual(r1.json()['code'], 'PROTOTYPE_NOT_FOUND')
+ r1 = self.api_post('/cluster/', {
+ 'name': cluster,
+ 'prototype_id': proto_id,
+ 'description': ''
+ })
+ self.assertEqual(r1.status_code, 400)
+ self.assertEqual(r1.json()['description'], ['This field may not be blank.'])
+
r1 = self.api_post('/cluster/', {'name': cluster, 'prototype_id': proto_id})
self.assertEqual(r1.status_code, 201)
cluster_id = r1.json()['id']
@@ -195,15 +216,14 @@ def test_cluster(self):
self.assertEqual(r2.status_code, 200)
self.assertEqual(r2.json()['name'], cluster)
+ r1 = self.api_post('/cluster/', {'name': cluster, 'prototype_id': proto_id})
+ self.assertEqual(r1.status_code, 409)
+ self.assertEqual(r1.json()['code'], 'CLUSTER_CONFLICT')
+
r1 = self.api_put('/cluster/' + str(cluster_id) + '/', {})
self.assertEqual(r1.status_code, 405)
self.assertEqual(r1.json()['detail'], 'Method "PUT" not allowed.')
- cluster2 = 'тестовый кластер'
- r1 = self.api_patch('/cluster/' + str(cluster_id) + '/', {'name': cluster2})
- self.assertEqual(r1.status_code, 200)
- self.assertEqual(r1.json()['name'], cluster2)
-
r1 = self.api_delete('/cluster/' + str(cluster_id) + '/')
self.assertEqual(r1.status_code, 204)
@@ -211,10 +231,54 @@ def test_cluster(self):
self.assertEqual(r1.status_code, 404)
self.assertEqual(r1.json()['code'], 'CLUSTER_NOT_FOUND')
+ r1 = self.api_delete('/cluster/' + str(cluster_id) + '/')
+ self.assertEqual(r1.status_code, 404)
+ self.assertEqual(r1.json()['code'], 'CLUSTER_NOT_FOUND')
+
+ r1 = self.api_delete('/stack/bundle/' + str(bundle_id) + '/')
+ self.assertEqual(r1.status_code, 204)
+
+ def test_cluster_patching(self):
+ name = 'test_cluster'
+ r1 = self.api_post('/stack/load/', {'bundle_file': self.adh_bundle})
+ self.assertEqual(r1.status_code, 200)
+ bundle_id, proto_id = self.get_cluster_proto_id()
+
+ r1 = self.api_post('/cluster/', {'name': name, 'prototype_id': proto_id})
+ self.assertEqual(r1.status_code, 201)
+ cluster_id = r1.json()['id']
+
+ patched_name = 'patched_cluster'
+ r1 = self.api_patch('/cluster/' + str(cluster_id) + '/', {'name': patched_name})
+ self.assertEqual(r1.status_code, 200)
+ self.assertEqual(r1.json()['name'], patched_name)
+
+ description = 'cluster_description'
+ r1 = self.api_patch('/cluster/' + str(cluster_id) + '/', {
+ 'name': patched_name,
+ 'description': description
+ })
+ self.assertEqual(r1.status_code, 200)
+ self.assertEqual(r1.json()['description'], description)
+
+ r1 = self.api_post('/cluster/', {'name': name, 'prototype_id': proto_id})
+ self.assertEqual(r1.status_code, 201)
+ second_cluster_id = r1.json()['id']
+
+ r1 = self.api_patch('/cluster/' + str(second_cluster_id) + '/', {'name': patched_name})
+ self.assertEqual(r1.status_code, 409)
+ self.assertEqual(r1.json()['code'], 'CLUSTER_CONFLICT')
+
+ r1 = self.api_delete('/cluster/' + str(cluster_id) + '/')
+ self.assertEqual(r1.status_code, 204)
+
+ r1 = self.api_delete('/cluster/' + str(second_cluster_id) + '/')
+ self.assertEqual(r1.status_code, 204)
+
r1 = self.api_delete('/stack/bundle/' + str(bundle_id) + '/')
self.assertEqual(r1.status_code, 204)
- def test_host(self):
+ def test_host(self): # pylint: disable=too-many-statements
host = 'test.server.net'
r1 = self.api_post('/stack/load/', {'bundle_file': self.ssh_bundle})
@@ -234,8 +298,38 @@ def test_host(self):
self.assertEqual(r1.status_code, 201)
provider_id = r1.json()['id']
+ r1 = self.api_post('/host/', {'fqdn': host, 'prototype_id': 42, 'provider_id': provider_id})
+ self.assertEqual(r1.status_code, 404)
+ self.assertEqual(r1.json()['code'], 'PROTOTYPE_NOT_FOUND')
+
+ r1 = self.api_post('/host/', {'fqdn': host, 'provider_id': provider_id})
+ self.assertEqual(r1.status_code, 400)
+ self.assertEqual(r1.json()['prototype_id'], ['This field is required.'])
+
+ r1 = self.api_post('/host/', {'fqdn': host, 'prototype_id': host_proto})
+ self.assertEqual(r1.status_code, 400)
+ self.assertEqual(r1.json()['provider_id'], ['This field is required.'])
+
r1 = self.api_post('/host/', {
- 'fqdn': host, 'prototype_id': host_proto, 'provider_id': provider_id
+ 'fqdn': 'x' + 'deadbeef' * 32, # 257 chars
+ 'prototype_id': host_proto,
+ 'provider_id': provider_id
+ })
+ self.assertEqual(r1.status_code, 400)
+ self.assertEqual(r1.json()['desc'], 'Host name is too long. Max length is 256')
+
+ r1 = self.api_post('/host/', {
+ 'fqdn': 'x' + string.punctuation,
+ 'prototype_id': host_proto,
+ 'provider_id': provider_id
+ })
+ self.assertEqual(r1.status_code, 400)
+ self.assertEqual(r1.json()['code'], 'WRONG_NAME')
+
+ r1 = self.api_post('/host/', {
+ 'fqdn': host,
+ 'prototype_id': host_proto,
+ 'provider_id': provider_id
})
self.assertEqual(r1.status_code, 201)
host_id = r1.json()['id']
@@ -256,6 +350,15 @@ def test_host(self):
r1 = self.api_delete('/host/' + str(host_id) + '/')
self.assertEqual(r1.status_code, 204)
+
+ r1 = self.api_get('/host/' + str(host_id) + '/')
+ self.assertEqual(r1.status_code, 404)
+ self.assertEqual(r1.json()['code'], 'HOST_NOT_FOUND')
+
+ r1 = self.api_delete('/host/' + str(host_id) + '/')
+ self.assertEqual(r1.status_code, 404)
+ self.assertEqual(r1.json()['code'], 'HOST_NOT_FOUND')
+
r1 = self.api_delete('/stack/bundle/' + str(ssh_bundle_id) + '/')
self.assertEqual(r1.status_code, 204)
@@ -290,6 +393,10 @@ def test_cluster_host(self):
self.assertEqual(r1.status_code, 409)
self.assertEqual(r1.json()['code'], 'FOREIGN_HOST')
+ r1 = self.api_post('/cluster/' + str(cluster_id) + '/host/', {'host_id': host_id})
+ self.assertEqual(r1.status_code, 409)
+ self.assertEqual(r1.json()['code'], 'HOST_CONFLICT')
+
r1 = self.api_delete('/cluster/' + str(cluster_id) + '/host/' + str(host_id) + '/')
self.assertEqual(r1.status_code, 204)
@@ -329,6 +436,51 @@ def test_service(self):
r1 = self.api_delete('/stack/bundle/' + str(bundle_id) + '/')
self.assertEqual(r1.status_code, 204)
+ def test_cluster_service(self):
+ r1 = self.api_post('/stack/load/', {'bundle_file': self.adh_bundle})
+ self.assertEqual(r1.status_code, 200)
+
+ service_proto_id = self.get_service_proto_id()
+ bundle_id, cluster_proto_id = self.get_cluster_proto_id()
+
+ cluster = 'test_cluster'
+ r1 = self.api_post('/cluster/', {'name': cluster, 'prototype_id': cluster_proto_id})
+ self.assertEqual(r1.status_code, 201)
+ cluster_id = r1.json()['id']
+
+ r1 = self.api_post('/cluster/' + str(cluster_id) + '/service/', {
+ 'prototype_id': 'some-string',
+ })
+ self.assertEqual(r1.status_code, 400)
+ self.assertEqual(r1.json()['prototype_id'], ['A valid integer is required.'])
+
+ r1 = self.api_post('/cluster/' + str(cluster_id) + '/service/', {
+ 'prototype_id': - service_proto_id,
+ })
+ self.assertEqual(r1.status_code, 404)
+ self.assertEqual(r1.json()['code'], 'PROTOTYPE_NOT_FOUND')
+
+ r1 = self.api_post('/cluster/' + str(cluster_id) + '/service/', {
+ 'prototype_id': service_proto_id,
+ })
+ self.assertEqual(r1.status_code, 201)
+ service_id = r1.json()['id']
+
+ r1 = self.api_post('/cluster/' + str(cluster_id) + '/service/', {
+ 'prototype_id': service_proto_id,
+ })
+ self.assertEqual(r1.status_code, 409)
+ self.assertEqual(r1.json()['code'], 'SERVICE_CONFLICT')
+
+ r1 = self.api_delete('/cluster/' + str(cluster_id) + '/service/' + str(service_id) + '/')
+ self.assertEqual(r1.status_code, 204)
+
+ r1 = self.api_delete('/cluster/' + str(cluster_id) + '/')
+ self.assertEqual(r1.status_code, 204)
+
+ r1 = self.api_delete('/stack/bundle/' + str(bundle_id) + '/')
+ self.assertEqual(r1.status_code, 204)
+
def test_hostcomponent(self): # pylint: disable=too-many-statements,too-many-locals
r1 = self.api_post('/stack/load/', {'bundle_file': self.adh_bundle})
self.assertEqual(r1.status_code, 200)
@@ -418,7 +570,7 @@ def test_hostcomponent(self): # pylint: disable=too-many-statements,too-many-l
{'service_id': service_id, 'host_id': host_id, 'component_id': comp_id}
]})
self.assertEqual(r1.status_code, 404)
- self.assertEqual(r1.json()['code'], "SERVICE_NOT_FOUND")
+ self.assertEqual(r1.json()['code'], "CLUSTER_SERVICE_NOT_FOUND")
r1 = self.api_post(
'/cluster/' + str(cluster_id2) + '/service/', {'prototype_id': service_proto_id}
@@ -449,8 +601,12 @@ def test_task(self):
r1 = self.api_post('/stack/load/', {'bundle_file': self.ssh_bundle})
self.assertEqual(r1.status_code, 200)
+ ssh_bundle_id, provider_id, host_id = self.create_host(self.host)
+ config = {'config': {'entry': 'some value'}}
+ r1 = self.api_post(f'/provider/{provider_id}/config/history/', config)
+ self.assertEqual(r1.status_code, 201)
+
adh_bundle_id, cluster_proto = self.get_cluster_proto_id()
- ssh_bundle_id, _, host_id = self.create_host(self.host)
service_id = self.get_service_proto_id()
action_id = self.get_action_id(service_id, 'start')
r1 = self.api_post('/cluster/', {'name': self.cluster, 'prototype_id': cluster_proto})
@@ -521,7 +677,7 @@ def test_task(self):
r1 = self.api_delete('/stack/bundle/' + str(adh_bundle_id) + '/')
r1 = self.api_delete('/stack/bundle/' + str(ssh_bundle_id) + '/')
- def test_config(self):
+ def test_config(self): # pylint: disable=too-many-statements
r1 = self.api_post('/stack/load/', {'bundle_file': self.adh_bundle})
self.assertEqual(r1.status_code, 200)
adh_bundle_id, proto_id = self.get_cluster_proto_id()
@@ -556,6 +712,11 @@ def test_config(self):
r1 = self.api_post(zurl + 'config/history/', {'config': 'qwe'})
self.assertEqual(r1.status_code, 400)
self.assertEqual(r1.json()['code'], 'JSON_ERROR')
+ self.assertEqual(r1.json()['desc'], "config should not be just one string")
+
+ r1 = self.api_post(zurl + 'config/history/', {'config': 42})
+ self.assertEqual(r1.status_code, 400)
+ self.assertEqual(r1.json()['desc'], "config should not be just one int or float")
c1['zoo.cfg']['autopurge.purgeInterval'] = 42
c1['zoo.cfg']['port'] = 80
diff --git a/tests/conftest.py b/tests/conftest.py
index 2007118b32..0ba4ae28b3 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -10,6 +10,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
# pylint: disable=W0621
+import tarfile
from typing import Optional
import allure
@@ -188,3 +189,19 @@ def login_to_adcm(app_fs, adcm_credentials):
app_fs.driver.get(app_fs.adcm.url)
login = LoginPage(app_fs.driver)
login.login(**adcm_credentials)
+
+
+def _pack_bundle(stack_dir, archive_dir):
+ archive_name = os.path.join(archive_dir, os.path.basename(stack_dir) + ".tar")
+ with tarfile.open(archive_name, "w") as tar:
+ for sub in os.listdir(stack_dir):
+ tar.add(os.path.join(stack_dir, sub), arcname=sub)
+ return archive_name
+
+
+@pytest.fixture()
+def bundle_archive(request, tmp_path):
+ """
+ Prepare tar file from dir without using bundle packer
+ """
+ return _pack_bundle(request.param, tmp_path)
diff --git a/tests/functional/test_actions.py b/tests/functional/test_actions.py
new file mode 100644
index 0000000000..3eddec0ae0
--- /dev/null
+++ b/tests/functional/test_actions.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.
+
+import allure
+import pytest
+from adcm_client.objects import ADCMClient
+from adcm_pytest_plugin import utils
+
+from tests.ui_tests.test_actions_page import check_verbosity
+
+
+@pytest.mark.parametrize(
+ "verbose_state", [True, False], ids=["verbose_state_true", "verbose_state_false"]
+)
+def test_check_verbose_option_of_action_run(sdk_client_fs: ADCMClient, verbose_state):
+ """Test action run with verbose switch"""
+ bundle_dir = utils.get_data_dir(__file__, "verbose_state")
+ bundle = sdk_client_fs.upload_from_fs(bundle_dir)
+ cluster = bundle.cluster_create(utils.random_string())
+ task = cluster.action(name="dummy_action").run(verbose=verbose_state)
+ with allure.step(f"Check if verbosity is {verbose_state}"):
+ task.wait()
+ log = task.job().log()
+ check_verbosity(log, verbose_state)
diff --git a/tests/functional/test_actions_data/verbose_state/config.yaml b/tests/functional/test_actions_data/verbose_state/config.yaml
new file mode 100644
index 0000000000..1f478f4cd5
--- /dev/null
+++ b/tests/functional/test_actions_data/verbose_state/config.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.
+
+- type: cluster
+ name: Dummy cluster
+ version: 1.5
+ actions:
+ dummy_action:
+ type: job
+ script: dummy_action.yaml
+ script_type: ansible
+ states:
+ available: any
diff --git a/tests/functional/test_locked_objects_data/locked_when_action_running/ansible/pause.yaml b/tests/functional/test_actions_data/verbose_state/dummy_action.yaml
similarity index 83%
rename from tests/functional/test_locked_objects_data/locked_when_action_running/ansible/pause.yaml
rename to tests/functional/test_actions_data/verbose_state/dummy_action.yaml
index 334e710286..1f17534f7e 100644
--- a/tests/functional/test_locked_objects_data/locked_when_action_running/ansible/pause.yaml
+++ b/tests/functional/test_actions_data/verbose_state/dummy_action.yaml
@@ -10,13 +10,12 @@
# See the License for the specific language governing permissions and
# limitations under the License.
---
-- name: Do nothing playbook
- hosts: all
+- name: Dummy action
+ hosts: all
connection: local
gather_facts: no
tasks:
- - pause:
- seconds: 500
- - debug:
- msg: "Unstucked now"
+ - name: Dummy?
+ debug:
+ msg: "Some message"
diff --git a/tests/functional/test_actions_on_host.py b/tests/functional/test_actions_on_host.py
new file mode 100644
index 0000000000..c74611e8c5
--- /dev/null
+++ b/tests/functional/test_actions_on_host.py
@@ -0,0 +1,282 @@
+# 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=redefined-outer-name
+from typing import Union
+
+import allure
+import pytest
+from adcm_client.base import ObjectNotFound
+from adcm_client.objects import Cluster, Provider, Host, Service, Component
+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
+from adcm_pytest_plugin.utils import get_data_dir
+
+ACTION_ON_HOST = "action_on_host"
+ACTION_ON_HOST_MULTIJOB = "action_on_host_multijob"
+ACTION_ON_HOST_STATE_REQUIRED = "action_on_host_state_installed"
+FIRST_SERVICE = "Dummy service"
+SECOND_SERVICE = "Second service"
+FIRST_COMPONENT = "first"
+SECOND_COMPONENT = "second"
+SWITCH_SERVICE_STATE = "switch_service_state"
+SWITCH_CLUSTER_STATE = "switch_cluster_state"
+SWITCH_HOST_STATE = "switch_host_state"
+SWITCH_COMPONENT_STATE = "switch_component_state"
+
+
+@allure.title("Create cluster")
+@pytest.fixture()
+def cluster(sdk_client_fs) -> Cluster:
+ bundle = sdk_client_fs.upload_from_fs(get_data_dir(__file__, "cluster"))
+ return bundle.cluster_prototype().cluster_create(name="Cluster")
+
+
+@allure.title("Create a cluster with service")
+@pytest.fixture()
+def cluster_with_service(sdk_client_fs) -> Cluster:
+ bundle = sdk_client_fs.upload_from_fs(get_data_dir(__file__, "cluster_with_service"))
+ cluster = bundle.cluster_prototype().cluster_create(name="Cluster with services")
+ return cluster
+
+
+@allure.title("Create a cluster with service and components")
+@pytest.fixture()
+def cluster_with_components(sdk_client_fs) -> Cluster:
+ bundle = sdk_client_fs.upload_from_fs(get_data_dir(__file__, "cluster_with_components"))
+ cluster = bundle.cluster_prototype().cluster_create(name="Cluster with components")
+ return cluster
+
+
+@allure.title("Create a cluster with target group action")
+@pytest.fixture()
+def cluster_with_target_group_action(sdk_client_fs) -> Cluster:
+ bundle = sdk_client_fs.upload_from_fs(get_data_dir(__file__, "cluster_target_group"))
+ cluster = bundle.cluster_prototype().cluster_create(name="Target group test")
+ return cluster
+
+
+@allure.title("Create provider")
+@pytest.fixture()
+def provider(sdk_client_fs) -> Provider:
+ bundle = sdk_client_fs.upload_from_fs(get_data_dir(__file__, "provider"))
+ return bundle.provider_prototype().provider_create("Some provider")
+
+
+class TestClusterActionsOnHost:
+
+ @pytest.mark.parametrize("action_name", [ACTION_ON_HOST, ACTION_ON_HOST_MULTIJOB])
+ def test_availability(self, cluster: Cluster, provider: Provider, action_name):
+ """
+ Test that cluster host action is available on cluster host and is absent on cluster
+ """
+ host1 = provider.host_create("host_in_cluster")
+ host2 = provider.host_create("host_not_in_cluster")
+ cluster.host_add(host1)
+ action_in_object_is_present(action_name, host1)
+ action_in_object_is_absent(action_name, host2)
+ action_in_object_is_absent(action_name, cluster)
+ run_host_action_and_assert_result(host1, action_name, status="success")
+
+ def test_availability_at_state(self, cluster: Cluster, provider: Provider):
+ """
+ Test that cluster host action is available on specify cluster state
+ """
+ host = provider.host_create("host_in_cluster")
+ cluster.host_add(host)
+ action_in_object_is_absent(ACTION_ON_HOST_STATE_REQUIRED, host)
+ run_cluster_action_and_assert_result(cluster, SWITCH_CLUSTER_STATE)
+ action_in_object_is_present(ACTION_ON_HOST_STATE_REQUIRED, host)
+ run_host_action_and_assert_result(host, ACTION_ON_HOST_STATE_REQUIRED)
+
+ def test_availability_at_host_state(self, cluster: Cluster, provider: Provider):
+ """
+ Test that cluster host action isn't available on specify host state
+ """
+ host = provider.host_create("host_in_cluster")
+ cluster.host_add(host)
+ action_in_object_is_absent(ACTION_ON_HOST_STATE_REQUIRED, host)
+ run_host_action_and_assert_result(host, SWITCH_HOST_STATE)
+ action_in_object_is_absent(ACTION_ON_HOST_STATE_REQUIRED, host)
+ run_cluster_action_and_assert_result(cluster, SWITCH_CLUSTER_STATE)
+ action_in_object_is_present(ACTION_ON_HOST_STATE_REQUIRED, host)
+ run_host_action_and_assert_result(host, ACTION_ON_HOST_STATE_REQUIRED)
+
+
+class TestServiceActionOnHost:
+
+ @pytest.mark.parametrize("action_name", [ACTION_ON_HOST, ACTION_ON_HOST_MULTIJOB])
+ def test_availability(self, cluster_with_service: Cluster, provider: Provider, action_name):
+ """
+ Test that service host action is available on a service host and is absent on cluster
+ """
+ service = cluster_with_service.service_add(name=FIRST_SERVICE)
+ second_service = cluster_with_service.service_add(name=SECOND_SERVICE)
+ host_with_two_components = provider.host_create("host_with_two_components")
+ host_with_one_component = provider.host_create("host_with_one_component")
+ host_without_component = provider.host_create("host_without_component")
+ host_with_different_services = provider.host_create("host_with_different_services")
+ host_outside_cluster = provider.host_create("host_outside_cluster")
+ for host in [host_with_two_components, host_with_one_component,
+ host_without_component, host_with_different_services]:
+ cluster_with_service.host_add(host)
+ cluster_with_service.hostcomponent_set(
+ (host_with_two_components, service.component(name=FIRST_COMPONENT)),
+ (host_with_two_components, service.component(name=SECOND_COMPONENT)),
+ (host_with_one_component, service.component(name=FIRST_COMPONENT)),
+ (host_with_different_services, service.component(name=SECOND_COMPONENT)),
+ (host_with_different_services, second_service.component(name=FIRST_COMPONENT)),
+ )
+
+ action_in_object_is_present(action_name, host_with_one_component)
+ action_in_object_is_present(action_name, host_with_two_components)
+ action_in_object_is_present(action_name, host_with_different_services)
+ action_in_object_is_absent(action_name, host_without_component)
+ action_in_object_is_absent(action_name, host_outside_cluster)
+ action_in_object_is_absent(action_name, cluster_with_service)
+ action_in_object_is_absent(action_name, service)
+ run_host_action_and_assert_result(host_with_one_component, action_name)
+ run_host_action_and_assert_result(host_with_two_components, action_name)
+ run_host_action_and_assert_result(host_with_different_services, action_name)
+
+ def test_availability_at_state(self, cluster_with_service: Cluster, provider: Provider):
+ """
+ Test that service host action is available on specify service state
+ """
+ service = cluster_with_service.service_add(name=FIRST_SERVICE)
+ host = provider.host_create("host_in_cluster")
+ cluster_with_service.host_add(host)
+ cluster_with_service.hostcomponent_set((host, service.component(name=FIRST_COMPONENT)))
+
+ action_in_object_is_absent(ACTION_ON_HOST_STATE_REQUIRED, host)
+ run_cluster_action_and_assert_result(cluster_with_service, SWITCH_CLUSTER_STATE)
+ action_in_object_is_absent(ACTION_ON_HOST_STATE_REQUIRED, host)
+ run_service_action_and_assert_result(service, SWITCH_SERVICE_STATE)
+ action_in_object_is_present(ACTION_ON_HOST_STATE_REQUIRED, host)
+ run_host_action_and_assert_result(host, ACTION_ON_HOST_STATE_REQUIRED)
+
+ def test_availability_at_host_state(self, cluster_with_service: Cluster, provider: Provider):
+ """
+ Test that service host action isn't available on specify host state
+ """
+ service = cluster_with_service.service_add(name=FIRST_SERVICE)
+ host = provider.host_create("host_in_cluster")
+ cluster_with_service.host_add(host)
+ cluster_with_service.hostcomponent_set((host, service.component(name=FIRST_COMPONENT)))
+
+ action_in_object_is_absent(ACTION_ON_HOST_STATE_REQUIRED, host)
+ run_host_action_and_assert_result(host, SWITCH_HOST_STATE)
+ action_in_object_is_absent(ACTION_ON_HOST_STATE_REQUIRED, host)
+ run_service_action_and_assert_result(service, SWITCH_SERVICE_STATE)
+ action_in_object_is_present(ACTION_ON_HOST_STATE_REQUIRED, host)
+ run_host_action_and_assert_result(host, ACTION_ON_HOST_STATE_REQUIRED)
+
+
+class TestComponentActionOnHost:
+
+ @pytest.mark.parametrize("action_name", [ACTION_ON_HOST, ACTION_ON_HOST_MULTIJOB])
+ def test_availability(self, cluster_with_components: Cluster, provider: Provider, action_name):
+ """
+ Test that component host action is available on a component host
+ """
+ service = cluster_with_components.service_add(name=FIRST_SERVICE)
+ component_with_action = service.component(name=FIRST_COMPONENT)
+ component_without_action = service.component(name=SECOND_COMPONENT)
+
+ host_single_component = provider.host_create("host_with_single_component")
+ host_two_components = provider.host_create("host_with_two_components")
+ host_component_without_action = provider.host_create("host_component_without_action")
+ host_without_components = provider.host_create("host_without_components")
+ host_outside_cluster = provider.host_create("host_outside_cluster")
+ for host in [host_single_component, host_two_components,
+ host_component_without_action, host_without_components]:
+ cluster_with_components.host_add(host)
+ cluster_with_components.hostcomponent_set(
+ (host_single_component, component_with_action),
+ (host_two_components, component_with_action),
+ (host_two_components, component_without_action),
+ (host_component_without_action, component_without_action),
+ )
+ action_in_object_is_present(action_name, host_single_component)
+ action_in_object_is_present(action_name, host_two_components)
+ action_in_object_is_absent(action_name, host_component_without_action)
+ action_in_object_is_absent(action_name, host_without_components)
+ action_in_object_is_absent(action_name, host_outside_cluster)
+ action_in_object_is_absent(action_name, cluster_with_components)
+ action_in_object_is_absent(action_name, service)
+ action_in_object_is_absent(action_name, component_with_action)
+ action_in_object_is_absent(action_name, component_without_action)
+ run_host_action_and_assert_result(host_single_component, action_name)
+ run_host_action_and_assert_result(host_two_components, action_name)
+
+ @allure.issue(
+ name="Component state change BUG", url="https://arenadata.atlassian.net/browse/ADCM-1656"
+ )
+ def test_availability_at_state(self, cluster_with_components: Cluster, provider: Provider):
+ """
+ Test that component host action is available on specify service state
+ """
+ service = cluster_with_components.service_add(name=FIRST_SERVICE)
+ component = service.component(name=FIRST_COMPONENT)
+ adjacent_component = service.component(name=SECOND_COMPONENT)
+ host = provider.host_create("host_in_cluster")
+ cluster_with_components.host_add(host)
+ cluster_with_components.hostcomponent_set((host, component))
+
+ action_in_object_is_absent(ACTION_ON_HOST_STATE_REQUIRED, host)
+ run_cluster_action_and_assert_result(cluster_with_components, SWITCH_CLUSTER_STATE)
+ action_in_object_is_absent(ACTION_ON_HOST_STATE_REQUIRED, host)
+ run_service_action_and_assert_result(service, SWITCH_SERVICE_STATE)
+ action_in_object_is_absent(ACTION_ON_HOST_STATE_REQUIRED, host)
+ run_host_action_and_assert_result(host, SWITCH_HOST_STATE)
+ action_in_object_is_absent(ACTION_ON_HOST_STATE_REQUIRED, host)
+ run_component_action_and_assert_result(adjacent_component, SWITCH_COMPONENT_STATE)
+ action_in_object_is_absent(ACTION_ON_HOST_STATE_REQUIRED, host)
+ run_component_action_and_assert_result(component, SWITCH_COMPONENT_STATE)
+ action_in_object_is_present(ACTION_ON_HOST_STATE_REQUIRED, host)
+ run_host_action_and_assert_result(host, ACTION_ON_HOST_STATE_REQUIRED)
+
+
+def test_target_group_in_inventory(cluster_with_target_group_action: Cluster, provider: Provider,
+ sdk_client_fs):
+ """
+ Test that target group action has inventory_hostname info
+ """
+ hostname = "host_in_cluster"
+ host = provider.host_create(hostname)
+ cluster_with_target_group_action.host_add(host)
+ action_in_object_is_present(ACTION_ON_HOST, host)
+ run_host_action_and_assert_result(host, ACTION_ON_HOST)
+ with allure.step("Assert that hostname in job log is present"):
+ assert f"We are on host: {hostname}" in sdk_client_fs.job().log(type="stdout").content, \
+ "No hostname info in the job log"
+
+
+ObjTypes = Union[Cluster, Host, Service, Component]
+
+
+def action_in_object_is_present(action: str, obj: ObjTypes):
+ with allure.step(f"Assert that action {action} is present in {_get_object_represent(obj)}"):
+ try:
+ obj.action(name=action)
+ except ObjectNotFound as err:
+ raise AssertionError(f"Action {action} not found in object {obj}") from err
+
+
+def action_in_object_is_absent(action: str, obj: ObjTypes):
+ with allure.step(f"Assert that action {action} is absent in {_get_object_represent(obj)}"):
+ with pytest.raises(ObjectNotFound):
+ obj.action(name=action)
+
+
+def _get_object_represent(obj: ObjTypes) -> str:
+ return f"host {obj.fqdn}" if isinstance(obj, Host) else f"cluster {obj.name}"
diff --git a/tests/functional/test_actions_on_host_data/cluster/config.yaml b/tests/functional/test_actions_on_host_data/cluster/config.yaml
new file mode 100644
index 0000000000..067045295e
--- /dev/null
+++ b/tests/functional/test_actions_on_host_data/cluster/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: Dummy cluster
+ version: 1.5
+ actions:
+ action_on_host:
+ type: job
+ script: dummy_action.yaml
+ script_type: ansible
+ host_action: true
+ states:
+ available:
+ - created
+
+ action_on_host_state_installed:
+ type: job
+ script: dummy_action.yaml
+ script_type: ansible
+ host_action: true
+ states:
+ available:
+ - installed
+
+ action_on_host_multijob:
+ type: task
+ scripts:
+ - name: part1
+ script_type: ansible
+ script: dummy_action.yaml
+ - name: part2
+ script_type: ansible
+ script: dummy_action.yaml
+ host_action: true
+ states:
+ available:
+ - created
+
+ switch_cluster_state:
+ type: job
+ script: dummy_action.yaml
+ script_type: ansible
+ states:
+ available:
+ - created
+ on_success: installed
diff --git a/tests/functional/test_actions_on_host_data/cluster/dummy_action.yaml b/tests/functional/test_actions_on_host_data/cluster/dummy_action.yaml
new file mode 100644
index 0000000000..1f17534f7e
--- /dev/null
+++ b/tests/functional/test_actions_on_host_data/cluster/dummy_action.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/test_actions_on_host_data/cluster_target_group/config.yaml b/tests/functional/test_actions_on_host_data/cluster_target_group/config.yaml
new file mode 100644
index 0000000000..5f6cb1cc3b
--- /dev/null
+++ b/tests/functional/test_actions_on_host_data/cluster_target_group/config.yaml
@@ -0,0 +1,24 @@
+# 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: Dummy cluster
+ version: 1.5
+ actions:
+ action_on_host:
+ type: job
+ script: echo_hostname.yaml
+ script_type: ansible
+ host_action: true
+ states:
+ available:
+ - created
diff --git a/tests/functional/test_actions_on_host_data/cluster_target_group/echo_hostname.yaml b/tests/functional/test_actions_on_host_data/cluster_target_group/echo_hostname.yaml
new file mode 100644
index 0000000000..82c24325cc
--- /dev/null
+++ b/tests/functional/test_actions_on_host_data/cluster_target_group/echo_hostname.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: Echo hostname
+ hosts: target
+ connection: local
+ gather_facts: false
+
+ tasks:
+ - name: Echo inventory hotname
+ debug:
+ msg: "We are on host: {{ inventory_hostname }}"
diff --git a/tests/functional/test_actions_on_host_data/cluster_with_components/config.yaml b/tests/functional/test_actions_on_host_data/cluster_with_components/config.yaml
new file mode 100644
index 0000000000..87b3464518
--- /dev/null
+++ b/tests/functional/test_actions_on_host_data/cluster_with_components/config.yaml
@@ -0,0 +1,91 @@
+# 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: Dummy cluster
+ version: 1.5
+ actions:
+ switch_cluster_state:
+ type: job
+ script: dummy_action.yaml
+ script_type: ansible
+ states:
+ available:
+ - created
+ on_success: installed
+
+- type: service
+ name: Dummy service
+ version: 1.5
+ components:
+ first:
+ actions:
+ switch_component_state:
+ type: job
+ script: dummy_action.yaml
+ script_type: ansible
+ states:
+ available:
+ - created
+ on_success: installed
+
+ action_on_host:
+ type: job
+ script: dummy_action.yaml
+ script_type: ansible
+ host_action: true
+ states:
+ available:
+ - created
+
+ action_on_host_state_installed:
+ type: job
+ script: dummy_action.yaml
+ script_type: ansible
+ host_action: true
+ states:
+ available:
+ - installed
+
+ action_on_host_multijob:
+ type: task
+ scripts:
+ - name: part1
+ script_type: ansible
+ script: dummy_action.yaml
+ - name: part2
+ script_type: ansible
+ script: dummy_action.yaml
+ host_action: true
+ states:
+ available:
+ - created
+
+ second:
+ actions:
+ switch_component_state:
+ type: job
+ script: dummy_action.yaml
+ script_type: ansible
+ states:
+ available:
+ - created
+ on_success: installed
+ actions:
+ switch_service_state:
+ type: job
+ script: dummy_action.yaml
+ script_type: ansible
+ states:
+ available:
+ - created
+ on_success: installed
diff --git a/tests/functional/test_actions_on_host_data/cluster_with_components/dummy_action.yaml b/tests/functional/test_actions_on_host_data/cluster_with_components/dummy_action.yaml
new file mode 100644
index 0000000000..1f17534f7e
--- /dev/null
+++ b/tests/functional/test_actions_on_host_data/cluster_with_components/dummy_action.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/test_actions_on_host_data/cluster_with_service/config.yaml b/tests/functional/test_actions_on_host_data/cluster_with_service/config.yaml
new file mode 100644
index 0000000000..b84e24681c
--- /dev/null
+++ b/tests/functional/test_actions_on_host_data/cluster_with_service/config.yaml
@@ -0,0 +1,78 @@
+# 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: Dummy cluster
+ version: 1.5
+ actions:
+ switch_cluster_state:
+ type: job
+ script: dummy_action.yaml
+ script_type: ansible
+ states:
+ available:
+ - created
+ on_success: installed
+
+- type: service
+ name: Dummy service
+ version: 1.5
+ components:
+ first:
+ second:
+ actions:
+ action_on_host:
+ type: job
+ script: dummy_action.yaml
+ script_type: ansible
+ host_action: true
+ states:
+ available:
+ - created
+
+ action_on_host_state_installed:
+ type: job
+ script: dummy_action.yaml
+ script_type: ansible
+ host_action: true
+ states:
+ available:
+ - installed
+
+ action_on_host_multijob:
+ type: task
+ scripts:
+ - name: part1
+ script_type: ansible
+ script: dummy_action.yaml
+ - name: part2
+ script_type: ansible
+ script: dummy_action.yaml
+ host_action: true
+ states:
+ available:
+ - created
+
+ switch_service_state:
+ type: job
+ script: dummy_action.yaml
+ script_type: ansible
+ states:
+ available:
+ - created
+ on_success: installed
+
+- type: service
+ name: Second service
+ version: 1.5
+ components:
+ first:
diff --git a/tests/functional/test_actions_on_host_data/cluster_with_service/dummy_action.yaml b/tests/functional/test_actions_on_host_data/cluster_with_service/dummy_action.yaml
new file mode 100644
index 0000000000..1f17534f7e
--- /dev/null
+++ b/tests/functional/test_actions_on_host_data/cluster_with_service/dummy_action.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/test_actions_on_host_data/provider/config.yaml b/tests/functional/test_actions_on_host_data/provider/config.yaml
new file mode 100644
index 0000000000..e77884cf87
--- /dev/null
+++ b/tests/functional/test_actions_on_host_data/provider/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: provider
+ name: provider_name
+ version: 1.4
+ description: "That is description"
+
+- type: host
+ name: host_name
+ version: 1.0
+ description: "That is description"
+ actions:
+ switch_host_state:
+ type: job
+ script_type: ansible
+ script: dummy_action.yaml
+ states:
+ available:
+ - created
+ on_success: installed
diff --git a/tests/functional/test_actions_on_host_data/provider/dummy_action.yaml b/tests/functional/test_actions_on_host_data/provider/dummy_action.yaml
new file mode 100644
index 0000000000..1f17534f7e
--- /dev/null
+++ b/tests/functional/test_actions_on_host_data/provider/dummy_action.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/test_adcm_check_plugin.py b/tests/functional/test_adcm_check_plugin.py
index 2774842a2e..f6373af062 100644
--- a/tests/functional/test_adcm_check_plugin.py
+++ b/tests/functional/test_adcm_check_plugin.py
@@ -14,16 +14,22 @@
import pytest
from adcm_client.objects import ADCMClient
from adcm_pytest_plugin import utils
-from tests.ui_tests.test_actions_page import check_verbosity
-from tests.library import steps
+from adcm_pytest_plugin.steps.asserts import assert_action_result
+
from tests.library.consts import States, MessageStates
-NO_FIELD = ['no_title', 'no_result', 'no_msg',
- 'only_success', 'only_fail', 'bad_result']
-ALL_FIELDS_DATA = [("all_fields", "Group success",
- "Task success", True, True),
- ("all_fields_fail", "Group fail",
- "Task fail", False, False)]
+NO_FIELD = [
+ "no_title",
+ "no_result",
+ "no_msg",
+ "only_success",
+ "only_fail",
+ "bad_result",
+]
+ALL_FIELDS_DATA = [
+ ("all_fields", "Group success", "Task success", True, True),
+ ("all_fields_fail", "Group fail", "Task fail", False, False),
+]
@pytest.mark.parametrize("missed_field", NO_FIELD)
@@ -33,74 +39,76 @@ def test_field_validation(sdk_client_fs: ADCMClient, missed_field):
only success message field, only fail message field.
Expected result: job failed.
"""
- params = {
- 'action': 'adcm_check',
- 'expected_state': States.failed,
- 'logs_amount': 2
- }
+ params = {"action": "adcm_check", "expected_state": States.failed, "logs_amount": 2}
bundle_dir = utils.get_data_dir(__file__, missed_field)
bundle = sdk_client_fs.upload_from_fs(bundle_dir)
cluster = bundle.cluster_create(utils.random_string())
- task = cluster.action_run(name=params['action'])
+ task = cluster.action(name=params["action"]).run()
task.wait()
- steps.check_action_state(action_name=params['action'],
- state_current=task.status,
- state_expected=params['expected_state'])
+ assert_action_result(
+ result=task.status, status=params["expected_state"], name=params["action"]
+ )
with allure.step(f'Check if logs count is equal {params["logs_amount"]}'):
job = task.job()
logs = job.log_list()
current_len = len(logs)
- assert current_len == params['logs_amount'], \
- f'Logs count not equal {params["logs_amount"]}, ' \
- f'current log count {current_len}'
+ assert current_len == params["logs_amount"], (
+ f'Logs count not equal {params["logs_amount"]}, '
+ f"current log count {current_len}"
+ )
-@pytest.mark.parametrize(('name', 'group_msg', 'task_msg', 'group_result', 'task_result'),
- ALL_FIELDS_DATA)
-def test_all_fields(sdk_client_fs: ADCMClient, name, group_msg,
- task_msg, group_result, task_result):
+@pytest.mark.parametrize(
+ ("name", "group_msg", "task_msg", "group_result", "task_result"), ALL_FIELDS_DATA
+)
+def test_all_fields(
+ sdk_client_fs: ADCMClient, name, group_msg, task_msg, group_result, task_result
+):
"""Check that we can run jobs with all fields for
adcm_check task and check all fields after action
execution.
"""
params = {
- 'action': 'adcm_check',
- 'expected_state': States.success,
- 'expected_title': 'Name of group check.',
- 'content_title': 'Check',
+ "action": "adcm_check",
+ "expected_state": States.success,
+ "expected_title": "Name of group check.",
+ "content_title": "Check",
}
bundle = sdk_client_fs.upload_from_fs(utils.get_data_dir(__file__, name))
cluster = bundle.cluster_create(utils.random_string())
- task = cluster.action_run(name=params['action'])
+ task = cluster.action(name=params["action"]).run()
task.wait()
job = task.job()
- steps.check_action_state(action_name=params['action'],
- state_current=job.status,
- state_expected=params['expected_state'])
- with allure.step('Check all fields after action execution'):
+ assert_action_result(
+ result=job.status, status=params["expected_state"], name=params["action"]
+ )
+ 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]
- assert content['message'] == group_msg,\
- f'Expected message {group_msg}. ' \
- f'Current message {content["message"]}'
- assert content['result'] is group_result
- assert content['title'] == params['expected_title'],\
- f'Expected title {params["expected_title"]}. ' \
+ assert content["message"] == group_msg, (
+ f"Expected message {group_msg}. " f'Current message {content["message"]}'
+ )
+ assert content["result"] is group_result
+ assert content["title"] == params["expected_title"], (
+ f'Expected title {params["expected_title"]}. '
f'Current title {content["title"]}'
- content_title = content['content'][0]['title']
- assert content_title == params['content_title'], \
- f'Expected title {params["content_title"]}. ' \
- f'Current title {content_title}'
- content_message = content['content'][0]['message']
- assert content_message == task_msg, \
- f'Expected message {task_msg}. ' \
- f'Current message {content_message}'
- assert content['content'][0]['result'] is task_result
+ )
+ content_title = content["content"][0]["title"]
+ assert content_title == params["content_title"], (
+ f'Expected title {params["content_title"]}. '
+ f"Current title {content_title}"
+ )
+ content_message = content["content"][0]["message"]
+ assert content_message == task_msg, (
+ f"Expected message {task_msg}. " f"Current message {content_message}"
+ )
+ assert content["content"][0]["result"] is task_result
-@pytest.mark.parametrize("name", ['with_success', 'with_fail',
- 'with_success_msg_on_fail',
- 'with_fail_msg_on_fail'])
+@pytest.mark.parametrize(
+ "name",
+ ["with_success", "with_fail", "with_success_msg_on_fail", "with_fail_msg_on_fail"],
+)
def test_message_with_other_field(sdk_client_fs: ADCMClient, name):
"""Check that we can create action with
specific (success or fail) message and message.
@@ -109,25 +117,25 @@ def test_message_with_other_field(sdk_client_fs: ADCMClient, name):
will be in success or fail attribute depends on config.
"""
params = {
- 'action': 'adcm_check',
- 'expected_state': States.success,
+ "action": "adcm_check",
+ "expected_state": States.success,
}
bundle_dir = utils.get_data_dir(__file__, name)
bundle = sdk_client_fs.upload_from_fs(bundle_dir)
cluster = bundle.cluster_create(utils.random_string())
- task = cluster.action_run(name=params['action'])
+ task = cluster.action(name=params["action"]).run()
task.wait()
job = task.job()
- logs = job.log_list()
- log = job.log(job_id=job.id, log_id=logs[2].id)
- steps.check_action_state(action_name=params['action'],
- state_current=job.status,
- state_expected=params['expected_state'])
- with allure.step(f'Check if content message is {name}'):
+ 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)
content = log.content[0]
- assert content['message'] == name,\
- f'Expected content message {name}. ' \
- f'Current {content["message"]}'
+ assert content["message"] == name, (
+ f"Expected content message {name}. " f'Current {content["message"]}'
+ )
def test_success_and_fail_msg_on_success(sdk_client_fs: ADCMClient):
@@ -135,29 +143,28 @@ def test_success_and_fail_msg_on_success(sdk_client_fs: ADCMClient):
success and fail message will be in their own fields.
"""
params = {
- 'action': 'adcm_check',
- 'expected_state': States.success,
- 'expected_message': MessageStates.success_msg,
+ "action": "adcm_check",
+ "expected_state": States.success,
+ "expected_message": MessageStates.success_msg,
}
- bundle_dir = utils.get_data_dir(__file__, 'success_and_fail_msg')
+ bundle_dir = utils.get_data_dir(__file__, "success_and_fail_msg")
bundle = sdk_client_fs.upload_from_fs(bundle_dir)
cluster = bundle.cluster_create(utils.random_string())
- task = cluster.action_run(name=params['action'])
+ task = cluster.action(name=params["action"]).run()
task.wait()
job = task.job()
- steps.check_action_state(action_name=params['action'],
- state_current=job.status,
- state_expected=params['expected_state'])
- with allure.step('Check if success and fail message are '
- 'in their own fields.'):
+ 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)
content = log.content[0]
- assert content['result'],\
- f'Result is {content["result"]} expected True'
- assert content['message'] == params['expected_message'],\
- f'Expected message: {params["expected_message"]}. ' \
+ assert content["result"], f'Result is {content["result"]} expected True'
+ assert content["message"] == params["expected_message"], (
+ f'Expected message: {params["expected_message"]}. '
f'Current message {content["message"]}'
+ )
def test_success_and_fail_msg_on_fail(sdk_client_fs: ADCMClient):
@@ -165,226 +172,221 @@ def test_success_and_fail_msg_on_fail(sdk_client_fs: ADCMClient):
success and fail message will be in their own fields.
"""
params = {
- 'action': 'adcm_check',
- 'expected_state': States.success,
- 'expected_message': MessageStates.fail_msg,
+ "action": "adcm_check",
+ "expected_state": States.success,
+ "expected_message": MessageStates.fail_msg,
}
- bundle_dir = utils.get_data_dir(__file__, 'success_and_fail_msg_on_fail')
+ bundle_dir = utils.get_data_dir(__file__, "success_and_fail_msg_on_fail")
bundle = sdk_client_fs.upload_from_fs(bundle_dir)
cluster = bundle.cluster_create(utils.random_string())
- task = cluster.action_run(name=params['action'])
+ task = cluster.action(name=params["action"]).run()
task.wait()
job = task.job()
- steps.check_action_state(action_name=params['action'],
- state_current=job.status,
- state_expected=params['expected_state'])
- with allure.step('Check if success and fail message are '
- 'in their own fields'):
+ 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)
content = log.content[0]
- assert not content['result'],\
- f'Result is {content["result"]} expected True'
- assert content['message'] == params['expected_message'],\
- f'Expected message: {params["expected_message"]}. ' \
+ assert not content["result"], f'Result is {content["result"]} expected True'
+ assert content["message"] == params["expected_message"], (
+ f'Expected message: {params["expected_message"]}. '
f'Current message {content["message"]}'
+ )
def test_multiple_tasks(sdk_client_fs: ADCMClient):
- """Check adcm_check with multiple tasks with different parameters.
- """
+ """Check adcm_check with multiple tasks with different parameters."""
params = {
- 'action': 'check_sample',
- 'logs_amount': 3,
+ "action": "check_sample",
+ "logs_amount": 3,
}
- expected_result = [('"This is message. Params: msg. result=yes"',
- 'Check log 1', True),
- ("This is fail message. "
- "Params: success_msg, fail_msg. result=no",
- "Check log 2", False),
- ("This is success message. "
- "Params: success_msg, fail_msg. result=yes",
- "Check log 3", True),
- ]
- bundle_dir = utils.get_data_dir(__file__, 'multiple_tasks')
+ expected_result = [
+ ('"This is message. Params: msg. result=yes"', "Check log 1", True),
+ (
+ "This is fail message. Params: success_msg, fail_msg. result=no",
+ "Check log 2",
+ False,
+ ),
+ (
+ "This is success message. Params: success_msg, fail_msg. result=yes",
+ "Check log 3",
+ True,
+ ),
+ ]
+ bundle_dir = utils.get_data_dir(__file__, "multiple_tasks")
bundle = sdk_client_fs.upload_from_fs(bundle_dir)
cluster = bundle.cluster_create(utils.random_string())
- action = cluster.action_run(name=params['action'])
+ action = cluster.action(name=params["action"]).run()
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)
- assert len(log.content) == params['logs_amount'], log.content
+ assert len(log.content) == params["logs_amount"], log.content
with allure.step("Check log's messages, titles and results."):
for result in expected_result:
log_entry = log.content[expected_result.index(result)]
- assert result[0] == log_entry['message'], \
- f"Expected message {result[0]}. " \
+ assert result[0] == log_entry["message"], (
+ f"Expected message {result[0]}. "
f"Actual message {log_entry['message']}"
- assert result[1] == log_entry['title'], \
- f"Expected title {result[1]}. " \
- f"Actual title {log_entry['title']}"
- assert result[2] is log_entry['result'],\
- f"Expected result {result[2]}. " \
- f"Actual result {log_entry['result']}"
+ )
+ assert result[1] == log_entry["title"], (
+ f"Expected title {result[1]}. " f"Actual title {log_entry['title']}"
+ )
+ assert result[2] is log_entry["result"], (
+ f"Expected result {result[2]}. " f"Actual result {log_entry['result']}"
+ )
def test_multiple_group_tasks(sdk_client_fs: ADCMClient):
- """Check that we have correct field values for group tasks
- """
- expected_result_groups = [("This is fail message",
- "Group 1", False),
- ("", "Group 2", True)]
- group1_expected = [("Check log 1",
- "This is message. Params: group_title,"
- " group_success_msg, group_fail_msg, msg. result=yes",
- True),
- ("Check log 2",
- "This is message. Params: group_title,"
- " group_success_msg, group_fail_msg, msg. result=no",
- False)]
- group2_expected = [("Check log 3",
- "This is success message."
- " Params: group_title, success_msg, "
- "fail_msg. result=yes",
- True)]
- bundle_dir = utils.get_data_dir(__file__, 'multiple_tasks_groups')
+ """Check that we have correct field values for group tasks"""
+ expected_result_groups = [
+ ("This is fail message", "Group 1", False),
+ ("", "Group 2", True),
+ ]
+ group1_expected = [
+ (
+ "Check log 1",
+ "This is message. Params: group_title,"
+ " group_success_msg, group_fail_msg, msg. result=yes",
+ True,
+ ),
+ (
+ "Check log 2",
+ "This is message. Params: group_title,"
+ " group_success_msg, group_fail_msg, msg. result=no",
+ False,
+ ),
+ ]
+ group2_expected = [
+ (
+ "Check log 3",
+ "This is success message."
+ " Params: group_title, success_msg, "
+ "fail_msg. result=yes",
+ True,
+ )
+ ]
+ bundle_dir = utils.get_data_dir(__file__, "multiple_tasks_groups")
bundle = sdk_client_fs.upload_from_fs(bundle_dir)
cluster = bundle.cluster_create(utils.random_string())
- action = cluster.action_run(name='check_sample')
+ action = cluster.action(name="check_sample").run()
action.wait()
- with allure.step('Check log content amount'):
+ 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)
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
+ assert len(log.content[0]["content"]) == 2, log.content[0].content
+ assert len(log.content[1]["content"]) == 1, log.content[1].content
with allure.step("Check log's messages, titles and results."):
for result in expected_result_groups:
log_entry = log.content[expected_result_groups.index(result)]
- assert result[0] == log_entry['message'],\
- f"Expected message {result[0]}. " \
+ assert result[0] == log_entry["message"], (
+ f"Expected message {result[0]}. "
f"Actual message {log_entry['message']}"
- assert result[1] == log_entry['title'],\
- f"Expected title {result[1]}. " \
- f"Actual title {log_entry['title']}"
- assert result[2] is log_entry['result'],\
- f"Expected result {result[2]}. " \
- f"Actual result {log_entry['result']}"
- with allure.step('Check group content'):
- group1 = log.content[0]['content']
+ )
+ assert result[1] == log_entry["title"], (
+ f"Expected title {result[1]}. " f"Actual title {log_entry['title']}"
+ )
+ assert result[2] is log_entry["result"], (
+ f"Expected result {result[2]}. " f"Actual result {log_entry['result']}"
+ )
+ with allure.step("Check group content"):
+ group1 = log.content[0]["content"]
group2 = log.content[1]
for result in group1_expected:
log_entry = group1[group1_expected.index(result)]
- assert result[0] == log_entry['title'],\
- f"Expected title {result[0]}. " \
- f"Actual message {log_entry['title']}"
- assert result[1] == log_entry['message'],\
- f"Expected message {result[1]}. " \
+ assert result[0] == log_entry["title"], (
+ f"Expected title {result[0]}. " f"Actual message {log_entry['title']}"
+ )
+ assert result[1] == log_entry["message"], (
+ f"Expected message {result[1]}. "
f"Actual message {log_entry['message']}"
- assert result[2] is log_entry['result'],\
- f"Expected result {result[2]}. " \
- f"Actual result {log_entry['result']}"
+ )
+ assert result[2] is log_entry["result"], (
+ f"Expected result {result[2]}. " f"Actual result {log_entry['result']}"
+ )
for result in group2_expected:
- log_entry = group2['content'][group2_expected.index(result)]
- assert result[0] == log_entry['title'],\
- f"Expected title {result[0]}. " \
- f"Actual message {log_entry['title']}"
- assert result[1] == log_entry['message'],\
- f"Expected message {result[1]}. " \
+ log_entry = group2["content"][group2_expected.index(result)]
+ assert result[0] == log_entry["title"], (
+ f"Expected title {result[0]}. " f"Actual message {log_entry['title']}"
+ )
+ assert result[1] == log_entry["message"], (
+ f"Expected message {result[1]}. "
f"Actual message {log_entry['message']}"
- assert result[2] is log_entry['result'],\
- f"Expected result {result[2]}. " \
- f"Actual result {log_entry['result']}"
+ )
+ assert result[2] is log_entry["result"], (
+ f"Expected result {result[2]}. " f"Actual result {log_entry['result']}"
+ )
def test_multiple_group_tasks_without_group_title(sdk_client_fs: ADCMClient):
- """Check group task without title.
- """
+ """Check group task without title."""
params = {
- 'action': 'check_sample',
- 'logs_amount': 2,
+ "action": "check_sample",
+ "logs_amount": 2,
}
- bundle_dir = utils.get_data_dir(__file__,
- 'group_tasks_without_group_title')
+ bundle_dir = utils.get_data_dir(__file__, "group_tasks_without_group_title")
bundle = sdk_client_fs.upload_from_fs(bundle_dir)
cluster = bundle.cluster_create(utils.random_string())
- action = cluster.action_run(name=params['action'])
+ action = cluster.action(name=params["action"]).run()
action.wait()
- with allure.step(f'Check log content amount is '
- f'equal {params["logs_amount"]}'):
+ with allure.step(f"Check log content amount is " f'equal {params["logs_amount"]}'):
job = action.job()
logs = job.log_list()
log = job.log(job_id=job.id, log_id=logs[2].id)
- assert len(log.content) == params['logs_amount'], log.content
- with allure.step('Check title and result in log content'):
+ 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:
- assert log_entry['title'] == 'Check log 1',\
- f"Expected title 'Check log 1'. " \
- f"Current title {log_entry['title']}"
- assert log_entry['result'], "Result is False, Expected True"
+ assert log_entry["title"] == "Check log 1", (
+ f"Expected title 'Check log 1'. " f"Current title {log_entry['title']}"
+ )
+ assert log_entry["result"], "Result is False, Expected True"
def test_multiple_tasks_action_with_log_files_check(sdk_client_fs: ADCMClient):
- """Check that log_files parameter don't affect action
- """
+ """Check that log_files parameter don't affect action"""
params = {
- 'action': 'check_sample',
- 'expected_state': States.success,
+ "action": "check_sample",
+ "expected_state": States.success,
}
- bundle_dir = utils.get_data_dir(__file__, 'log_files_check')
+
+ bundle_dir = utils.get_data_dir(__file__, "log_files_check")
bundle = sdk_client_fs.upload_from_fs(bundle_dir)
cluster = bundle.cluster_create(utils.random_string())
- task = cluster.action_run(name=params['action'])
+ task = cluster.action(name=params["action"]).run()
task.wait()
job = task.job()
- steps.check_action_state(action_name=params['action'],
- state_current=job.status,
- state_expected=params['expected_state'])
- with allure.step('Check if result is True'):
+ 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)
content = log.content[0]
- assert content['result'],\
- f'Result is {content["result"]}, Expected True'
+ assert content["result"], f'Result is {content["result"]}, Expected True'
def test_result_no(sdk_client_fs: ADCMClient):
- """Check config with result no
- """
+ """Check config with result no"""
params = {
- 'action': 'adcm_check',
- 'expected_state': States.success,
+ "action": "adcm_check",
+ "expected_state": States.success,
}
bundle_dir = utils.get_data_dir(__file__, "result_no")
bundle = sdk_client_fs.upload_from_fs(bundle_dir)
cluster = bundle.cluster_create(utils.random_string())
- task = cluster.action_run(name=params['action'])
+ task = cluster.action(name=params["action"]).run()
task.wait()
job = task.job()
- steps.check_action_state(action_name=params['action'],
- state_current=job.status,
- state_expected=params['expected_state'])
- with allure.step('Check if result is False'):
+ 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)
content = log.content[0]
- assert not content['result'], \
- f'Result is {content["result"]}, Expected False'
-
-
-@pytest.mark.parametrize(
- "verbose_state", [True, False], ids=["verbose_state_true", "verbose_state_false"]
-)
-def test_check_verbose_option_of_action_run(sdk_client_fs: ADCMClient, verbose_state):
- bundle_dir = utils.get_data_dir(__file__, "all_fields")
- bundle = sdk_client_fs.upload_from_fs(bundle_dir)
- cluster = bundle.cluster_create(utils.random_string())
- task = cluster.action(name="adcm_check").run(verbose=verbose_state)
- with allure.step(f'Check if verbosity is {verbose_state}'):
- task.wait()
- job = task.job()
- log = job.log(job_id=job.id, log_id=job.log_list()[0].id)
- check_verbosity(log, verbose_state)
+ assert not content["result"], f'Result is {content["result"]}, Expected False'
diff --git a/tests/functional/test_backend_filtering.py b/tests/functional/test_backend_filtering.py
index 97f78006fb..dd33514a4e 100644
--- a/tests/functional/test_backend_filtering.py
+++ b/tests/functional/test_backend_filtering.py
@@ -10,6 +10,8 @@
# See the License for the specific language governing permissions and
# limitations under the License.
# pylint: disable=W0611, W0621, W0404, W0212, C1801
+from typing import List
+
import allure
import pytest
from adcm_client.base import ResponseTooLong
@@ -233,11 +235,11 @@ def get_params(link):
HostList,
id="Host"),
pytest.param(
- lazy_fixture('host_with_jobs'),
+ lazy_fixture('hosts_with_jobs'),
TaskList,
id="Task"),
pytest.param(
- lazy_fixture('host_with_jobs'),
+ lazy_fixture('hosts_with_jobs'),
JobList,
id="Job"),
],
@@ -370,35 +372,35 @@ def test_paging_fail(sdk_client, TestedClass):
lazy_fixture('one_host_fqdn_attr'),
id="Host Prototype Id Filter"),
pytest.param(
- lazy_fixture('host_with_jobs'),
+ lazy_fixture('hosts_with_jobs'),
Task,
TaskList,
lazy_fixture('task_action_id_attr'),
lazy_fixture('task_action_id_attr'),
id="Task Action Id Filter"),
pytest.param(
- lazy_fixture('host_with_jobs'),
+ lazy_fixture('hosts_with_jobs'),
Task,
TaskList,
lazy_fixture('task_status_attr'),
lazy_fixture('task_status_attr'),
id="Task Status Filter"),
pytest.param(
- lazy_fixture('host_with_jobs'),
+ lazy_fixture('hosts_with_jobs'),
Job,
JobList,
lazy_fixture('task_status_attr'),
lazy_fixture('task_status_attr'),
id="Job Action Id Filter"),
pytest.param(
- lazy_fixture('host_with_jobs'),
+ lazy_fixture('hosts_with_jobs'),
Job,
JobList,
lazy_fixture('task_status_attr'),
lazy_fixture('task_status_attr'),
id="Job Status Filter"),
pytest.param(
- lazy_fixture('host_with_jobs'),
+ lazy_fixture('hosts_with_jobs'),
Job,
JobList,
lazy_fixture('job_task_id_attr'),
@@ -457,16 +459,26 @@ def host_ok_action(host_with_actions: Host):
@pytest.fixture()
-def host_fail_action(host_with_actions: Host):
- return host_with_actions.action(name="fail50")
+def hosts_with_actions(host_with_actions: Host, provider_with_actions: Provider):
+ hosts = [host_with_actions]
+ for i in range(9):
+ hosts.append(provider_with_actions.host_create(fqdn=f'host.with.actions.{i}'))
+ return hosts
@pytest.fixture()
-def host_with_jobs(host_with_actions: Host, host_fail_action, host_ok_action):
- for _ in range(51):
- host_fail_action.run().wait()
+def hosts_with_jobs(hosts_with_actions: List, host_ok_action: Action):
+ """
+ Run multiple actions on hosts. Return first host.
+ """
+ for _ in range(6):
+ actions = []
+ for host in hosts_with_actions:
+ actions.append(host.action(name="fail50").run())
+ for action in actions:
+ action.wait()
host_ok_action.run().try_wait()
- return host_with_actions
+ return hosts_with_actions[0]
@pytest.fixture()
diff --git a/tests/functional/test_backend_filtering_data/cluster_bundles/1/config.yaml b/tests/functional/test_backend_filtering_data/cluster_bundles/1/config.yaml
index 5c3d967db5..a9216671e3 100644
--- a/tests/functional/test_backend_filtering_data/cluster_bundles/1/config.yaml
+++ b/tests/functional/test_backend_filtering_data/cluster_bundles/1/config.yaml
@@ -1,5 +1,5 @@
---
- type: cluster
- name: 1
+ name: "1"
version: ver1
diff --git a/tests/functional/test_backend_filtering_data/cluster_bundles/10/config.yaml b/tests/functional/test_backend_filtering_data/cluster_bundles/10/config.yaml
index 29293577bb..9fe193e66a 100644
--- a/tests/functional/test_backend_filtering_data/cluster_bundles/10/config.yaml
+++ b/tests/functional/test_backend_filtering_data/cluster_bundles/10/config.yaml
@@ -1,5 +1,5 @@
---
- type: cluster
- name: 10
+ name: "10"
version: ver10
diff --git a/tests/functional/test_backend_filtering_data/cluster_bundles/11/config.yaml b/tests/functional/test_backend_filtering_data/cluster_bundles/11/config.yaml
index 0c8bf8e8e6..8ed2dd44a8 100644
--- a/tests/functional/test_backend_filtering_data/cluster_bundles/11/config.yaml
+++ b/tests/functional/test_backend_filtering_data/cluster_bundles/11/config.yaml
@@ -1,5 +1,5 @@
---
- type: cluster
- name: 11
+ name: "11"
version: ver11
diff --git a/tests/functional/test_backend_filtering_data/cluster_bundles/12/config.yaml b/tests/functional/test_backend_filtering_data/cluster_bundles/12/config.yaml
index defa2cab23..1375042625 100644
--- a/tests/functional/test_backend_filtering_data/cluster_bundles/12/config.yaml
+++ b/tests/functional/test_backend_filtering_data/cluster_bundles/12/config.yaml
@@ -1,5 +1,5 @@
---
- type: cluster
- name: 12
+ name: "12"
version: ver12
diff --git a/tests/functional/test_backend_filtering_data/cluster_bundles/13/config.yaml b/tests/functional/test_backend_filtering_data/cluster_bundles/13/config.yaml
index 8e2ce9a4ef..5ee5cbd815 100644
--- a/tests/functional/test_backend_filtering_data/cluster_bundles/13/config.yaml
+++ b/tests/functional/test_backend_filtering_data/cluster_bundles/13/config.yaml
@@ -1,5 +1,5 @@
---
- type: cluster
- name: 13
+ name: "13"
version: ver13
diff --git a/tests/functional/test_backend_filtering_data/cluster_bundles/14/config.yaml b/tests/functional/test_backend_filtering_data/cluster_bundles/14/config.yaml
index 12577b22f3..9932b87297 100644
--- a/tests/functional/test_backend_filtering_data/cluster_bundles/14/config.yaml
+++ b/tests/functional/test_backend_filtering_data/cluster_bundles/14/config.yaml
@@ -1,5 +1,5 @@
---
- type: cluster
- name: 14
+ name: "14"
version: ver14
diff --git a/tests/functional/test_backend_filtering_data/cluster_bundles/15/config.yaml b/tests/functional/test_backend_filtering_data/cluster_bundles/15/config.yaml
index 297ff0f714..40d08288eb 100644
--- a/tests/functional/test_backend_filtering_data/cluster_bundles/15/config.yaml
+++ b/tests/functional/test_backend_filtering_data/cluster_bundles/15/config.yaml
@@ -1,5 +1,5 @@
---
- type: cluster
- name: 15
+ name: "15"
version: ver15
diff --git a/tests/functional/test_backend_filtering_data/cluster_bundles/16/config.yaml b/tests/functional/test_backend_filtering_data/cluster_bundles/16/config.yaml
index 4e32ad105f..877d855120 100644
--- a/tests/functional/test_backend_filtering_data/cluster_bundles/16/config.yaml
+++ b/tests/functional/test_backend_filtering_data/cluster_bundles/16/config.yaml
@@ -1,5 +1,5 @@
---
- type: cluster
- name: 16
+ name: "16"
version: ver16
diff --git a/tests/functional/test_backend_filtering_data/cluster_bundles/17/config.yaml b/tests/functional/test_backend_filtering_data/cluster_bundles/17/config.yaml
index 4165b4f439..92ac9d034c 100644
--- a/tests/functional/test_backend_filtering_data/cluster_bundles/17/config.yaml
+++ b/tests/functional/test_backend_filtering_data/cluster_bundles/17/config.yaml
@@ -1,5 +1,5 @@
---
- type: cluster
- name: 17
+ name: "17"
version: ver17
diff --git a/tests/functional/test_backend_filtering_data/cluster_bundles/18/config.yaml b/tests/functional/test_backend_filtering_data/cluster_bundles/18/config.yaml
index ef4d55508e..89349d046d 100644
--- a/tests/functional/test_backend_filtering_data/cluster_bundles/18/config.yaml
+++ b/tests/functional/test_backend_filtering_data/cluster_bundles/18/config.yaml
@@ -1,5 +1,5 @@
---
- type: cluster
- name: 18
+ name: "18"
version: ver18
diff --git a/tests/functional/test_backend_filtering_data/cluster_bundles/19/config.yaml b/tests/functional/test_backend_filtering_data/cluster_bundles/19/config.yaml
index 43f4683fc8..bfd4933a85 100644
--- a/tests/functional/test_backend_filtering_data/cluster_bundles/19/config.yaml
+++ b/tests/functional/test_backend_filtering_data/cluster_bundles/19/config.yaml
@@ -1,5 +1,5 @@
---
- type: cluster
- name: 19
+ name: "19"
version: ver19
diff --git a/tests/functional/test_backend_filtering_data/cluster_bundles/2/config.yaml b/tests/functional/test_backend_filtering_data/cluster_bundles/2/config.yaml
index bf50603c6f..066298bf66 100644
--- a/tests/functional/test_backend_filtering_data/cluster_bundles/2/config.yaml
+++ b/tests/functional/test_backend_filtering_data/cluster_bundles/2/config.yaml
@@ -1,5 +1,5 @@
---
- type: cluster
- name: 2
+ name: "2"
version: ver2
diff --git a/tests/functional/test_backend_filtering_data/cluster_bundles/20/config.yaml b/tests/functional/test_backend_filtering_data/cluster_bundles/20/config.yaml
index 9820fd5679..6245b17d13 100644
--- a/tests/functional/test_backend_filtering_data/cluster_bundles/20/config.yaml
+++ b/tests/functional/test_backend_filtering_data/cluster_bundles/20/config.yaml
@@ -1,5 +1,5 @@
---
- type: cluster
- name: 20
+ name: "20"
version: ver20
diff --git a/tests/functional/test_backend_filtering_data/cluster_bundles/21/config.yaml b/tests/functional/test_backend_filtering_data/cluster_bundles/21/config.yaml
index 779b76221f..910c8e857c 100644
--- a/tests/functional/test_backend_filtering_data/cluster_bundles/21/config.yaml
+++ b/tests/functional/test_backend_filtering_data/cluster_bundles/21/config.yaml
@@ -1,5 +1,5 @@
---
- type: cluster
- name: 21
+ name: "21"
version: ver21
diff --git a/tests/functional/test_backend_filtering_data/cluster_bundles/22/config.yaml b/tests/functional/test_backend_filtering_data/cluster_bundles/22/config.yaml
index d9b576f486..8e46fdc696 100644
--- a/tests/functional/test_backend_filtering_data/cluster_bundles/22/config.yaml
+++ b/tests/functional/test_backend_filtering_data/cluster_bundles/22/config.yaml
@@ -1,5 +1,5 @@
---
- type: cluster
- name: 22
+ name: "22"
version: ver22
diff --git a/tests/functional/test_backend_filtering_data/cluster_bundles/23/config.yaml b/tests/functional/test_backend_filtering_data/cluster_bundles/23/config.yaml
index 9c844d5ca2..c38fd4eb95 100644
--- a/tests/functional/test_backend_filtering_data/cluster_bundles/23/config.yaml
+++ b/tests/functional/test_backend_filtering_data/cluster_bundles/23/config.yaml
@@ -1,5 +1,5 @@
---
- type: cluster
- name: 23
+ name: "23"
version: ver23
diff --git a/tests/functional/test_backend_filtering_data/cluster_bundles/24/config.yaml b/tests/functional/test_backend_filtering_data/cluster_bundles/24/config.yaml
index b6187b9b9c..792ec9e5f1 100644
--- a/tests/functional/test_backend_filtering_data/cluster_bundles/24/config.yaml
+++ b/tests/functional/test_backend_filtering_data/cluster_bundles/24/config.yaml
@@ -1,5 +1,5 @@
---
- type: cluster
- name: 24
+ name: "24"
version: ver24
diff --git a/tests/functional/test_backend_filtering_data/cluster_bundles/25/config.yaml b/tests/functional/test_backend_filtering_data/cluster_bundles/25/config.yaml
index 28cadcecea..705d4c06af 100644
--- a/tests/functional/test_backend_filtering_data/cluster_bundles/25/config.yaml
+++ b/tests/functional/test_backend_filtering_data/cluster_bundles/25/config.yaml
@@ -1,5 +1,5 @@
---
- type: cluster
- name: 25
+ name: "25"
version: ver25
diff --git a/tests/functional/test_backend_filtering_data/cluster_bundles/26/config.yaml b/tests/functional/test_backend_filtering_data/cluster_bundles/26/config.yaml
index 8616812ba2..e4dd14baf8 100644
--- a/tests/functional/test_backend_filtering_data/cluster_bundles/26/config.yaml
+++ b/tests/functional/test_backend_filtering_data/cluster_bundles/26/config.yaml
@@ -1,5 +1,5 @@
---
- type: cluster
- name: 26
+ name: "26"
version: ver26
diff --git a/tests/functional/test_backend_filtering_data/cluster_bundles/27/config.yaml b/tests/functional/test_backend_filtering_data/cluster_bundles/27/config.yaml
index 231146f90f..917b540599 100644
--- a/tests/functional/test_backend_filtering_data/cluster_bundles/27/config.yaml
+++ b/tests/functional/test_backend_filtering_data/cluster_bundles/27/config.yaml
@@ -1,5 +1,5 @@
---
- type: cluster
- name: 27
+ name: "27"
version: ver27
diff --git a/tests/functional/test_backend_filtering_data/cluster_bundles/28/config.yaml b/tests/functional/test_backend_filtering_data/cluster_bundles/28/config.yaml
index 7d9b876b8d..88f944cd54 100644
--- a/tests/functional/test_backend_filtering_data/cluster_bundles/28/config.yaml
+++ b/tests/functional/test_backend_filtering_data/cluster_bundles/28/config.yaml
@@ -1,5 +1,5 @@
---
- type: cluster
- name: 28
+ name: "28"
version: ver28
diff --git a/tests/functional/test_backend_filtering_data/cluster_bundles/29/config.yaml b/tests/functional/test_backend_filtering_data/cluster_bundles/29/config.yaml
index eb8eb1ed90..029943f135 100644
--- a/tests/functional/test_backend_filtering_data/cluster_bundles/29/config.yaml
+++ b/tests/functional/test_backend_filtering_data/cluster_bundles/29/config.yaml
@@ -1,5 +1,5 @@
---
- type: cluster
- name: 29
+ name: "29"
version: ver29
diff --git a/tests/functional/test_backend_filtering_data/cluster_bundles/3/config.yaml b/tests/functional/test_backend_filtering_data/cluster_bundles/3/config.yaml
index e8bacdf26d..fcdb6a134b 100644
--- a/tests/functional/test_backend_filtering_data/cluster_bundles/3/config.yaml
+++ b/tests/functional/test_backend_filtering_data/cluster_bundles/3/config.yaml
@@ -1,5 +1,5 @@
---
- type: cluster
- name: 3
+ name: "3"
version: ver3
diff --git a/tests/functional/test_backend_filtering_data/cluster_bundles/30/config.yaml b/tests/functional/test_backend_filtering_data/cluster_bundles/30/config.yaml
index ca18163231..ccd99d3a19 100644
--- a/tests/functional/test_backend_filtering_data/cluster_bundles/30/config.yaml
+++ b/tests/functional/test_backend_filtering_data/cluster_bundles/30/config.yaml
@@ -1,5 +1,5 @@
---
- type: cluster
- name: 30
+ name: "30"
version: ver30
diff --git a/tests/functional/test_backend_filtering_data/cluster_bundles/31/config.yaml b/tests/functional/test_backend_filtering_data/cluster_bundles/31/config.yaml
index 3e01912516..1e784bd998 100644
--- a/tests/functional/test_backend_filtering_data/cluster_bundles/31/config.yaml
+++ b/tests/functional/test_backend_filtering_data/cluster_bundles/31/config.yaml
@@ -1,5 +1,5 @@
---
- type: cluster
- name: 31
+ name: "31"
version: ver31
diff --git a/tests/functional/test_backend_filtering_data/cluster_bundles/32/config.yaml b/tests/functional/test_backend_filtering_data/cluster_bundles/32/config.yaml
index 65f0b7b82d..e76ad1cd84 100644
--- a/tests/functional/test_backend_filtering_data/cluster_bundles/32/config.yaml
+++ b/tests/functional/test_backend_filtering_data/cluster_bundles/32/config.yaml
@@ -1,5 +1,5 @@
---
- type: cluster
- name: 32
+ name: "32"
version: ver32
diff --git a/tests/functional/test_backend_filtering_data/cluster_bundles/33/config.yaml b/tests/functional/test_backend_filtering_data/cluster_bundles/33/config.yaml
index ad6f7863dd..815f386864 100644
--- a/tests/functional/test_backend_filtering_data/cluster_bundles/33/config.yaml
+++ b/tests/functional/test_backend_filtering_data/cluster_bundles/33/config.yaml
@@ -1,5 +1,5 @@
---
- type: cluster
- name: 33
+ name: "33"
version: ver33
diff --git a/tests/functional/test_backend_filtering_data/cluster_bundles/34/config.yaml b/tests/functional/test_backend_filtering_data/cluster_bundles/34/config.yaml
index 8d392b9439..b1a00fda86 100644
--- a/tests/functional/test_backend_filtering_data/cluster_bundles/34/config.yaml
+++ b/tests/functional/test_backend_filtering_data/cluster_bundles/34/config.yaml
@@ -1,5 +1,5 @@
---
- type: cluster
- name: 34
+ name: "34"
version: ver34
diff --git a/tests/functional/test_backend_filtering_data/cluster_bundles/35/config.yaml b/tests/functional/test_backend_filtering_data/cluster_bundles/35/config.yaml
index 53cf80ef7a..97e0a13d1b 100644
--- a/tests/functional/test_backend_filtering_data/cluster_bundles/35/config.yaml
+++ b/tests/functional/test_backend_filtering_data/cluster_bundles/35/config.yaml
@@ -1,5 +1,5 @@
---
- type: cluster
- name: 35
+ name: "35"
version: ver35
diff --git a/tests/functional/test_backend_filtering_data/cluster_bundles/36/config.yaml b/tests/functional/test_backend_filtering_data/cluster_bundles/36/config.yaml
index 4c682b317c..7d75279bc4 100644
--- a/tests/functional/test_backend_filtering_data/cluster_bundles/36/config.yaml
+++ b/tests/functional/test_backend_filtering_data/cluster_bundles/36/config.yaml
@@ -1,5 +1,5 @@
---
- type: cluster
- name: 36
+ name: "36"
version: ver36
diff --git a/tests/functional/test_backend_filtering_data/cluster_bundles/37/config.yaml b/tests/functional/test_backend_filtering_data/cluster_bundles/37/config.yaml
index 1c46ccf90c..0fece0ec88 100644
--- a/tests/functional/test_backend_filtering_data/cluster_bundles/37/config.yaml
+++ b/tests/functional/test_backend_filtering_data/cluster_bundles/37/config.yaml
@@ -1,5 +1,5 @@
---
- type: cluster
- name: 37
+ name: "37"
version: ver37
diff --git a/tests/functional/test_backend_filtering_data/cluster_bundles/38/config.yaml b/tests/functional/test_backend_filtering_data/cluster_bundles/38/config.yaml
index 5b84a18f3b..0267079e53 100644
--- a/tests/functional/test_backend_filtering_data/cluster_bundles/38/config.yaml
+++ b/tests/functional/test_backend_filtering_data/cluster_bundles/38/config.yaml
@@ -1,5 +1,5 @@
---
- type: cluster
- name: 38
+ name: "38"
version: ver38
diff --git a/tests/functional/test_backend_filtering_data/cluster_bundles/39/config.yaml b/tests/functional/test_backend_filtering_data/cluster_bundles/39/config.yaml
index f0c5c0ea59..e4b9dc026b 100644
--- a/tests/functional/test_backend_filtering_data/cluster_bundles/39/config.yaml
+++ b/tests/functional/test_backend_filtering_data/cluster_bundles/39/config.yaml
@@ -1,5 +1,5 @@
---
- type: cluster
- name: 39
+ name: "39"
version: ver39
diff --git a/tests/functional/test_backend_filtering_data/cluster_bundles/4/config.yaml b/tests/functional/test_backend_filtering_data/cluster_bundles/4/config.yaml
index dc477d5b45..3b03904c9a 100644
--- a/tests/functional/test_backend_filtering_data/cluster_bundles/4/config.yaml
+++ b/tests/functional/test_backend_filtering_data/cluster_bundles/4/config.yaml
@@ -1,5 +1,5 @@
---
- type: cluster
- name: 4
+ name: "4"
version: ver4
diff --git a/tests/functional/test_backend_filtering_data/cluster_bundles/40/config.yaml b/tests/functional/test_backend_filtering_data/cluster_bundles/40/config.yaml
index 99e9c911fe..b666f8ab2d 100644
--- a/tests/functional/test_backend_filtering_data/cluster_bundles/40/config.yaml
+++ b/tests/functional/test_backend_filtering_data/cluster_bundles/40/config.yaml
@@ -1,5 +1,5 @@
---
- type: cluster
- name: 40
+ name: "40"
version: ver40
diff --git a/tests/functional/test_backend_filtering_data/cluster_bundles/41/config.yaml b/tests/functional/test_backend_filtering_data/cluster_bundles/41/config.yaml
index 06ba9d127a..44e44349d9 100644
--- a/tests/functional/test_backend_filtering_data/cluster_bundles/41/config.yaml
+++ b/tests/functional/test_backend_filtering_data/cluster_bundles/41/config.yaml
@@ -1,5 +1,5 @@
---
- type: cluster
- name: 41
+ name: "41"
version: ver41
diff --git a/tests/functional/test_backend_filtering_data/cluster_bundles/42/config.yaml b/tests/functional/test_backend_filtering_data/cluster_bundles/42/config.yaml
index 48d167577c..0e4f2341de 100644
--- a/tests/functional/test_backend_filtering_data/cluster_bundles/42/config.yaml
+++ b/tests/functional/test_backend_filtering_data/cluster_bundles/42/config.yaml
@@ -1,5 +1,5 @@
---
- type: cluster
- name: 42
+ name: "42"
version: ver42
diff --git a/tests/functional/test_backend_filtering_data/cluster_bundles/43/config.yaml b/tests/functional/test_backend_filtering_data/cluster_bundles/43/config.yaml
index 3fb40d0303..5d173b8c2d 100644
--- a/tests/functional/test_backend_filtering_data/cluster_bundles/43/config.yaml
+++ b/tests/functional/test_backend_filtering_data/cluster_bundles/43/config.yaml
@@ -1,5 +1,5 @@
---
- type: cluster
- name: 43
+ name: "43"
version: ver43
diff --git a/tests/functional/test_backend_filtering_data/cluster_bundles/44/config.yaml b/tests/functional/test_backend_filtering_data/cluster_bundles/44/config.yaml
index c3f3f513d6..b27c0556d6 100644
--- a/tests/functional/test_backend_filtering_data/cluster_bundles/44/config.yaml
+++ b/tests/functional/test_backend_filtering_data/cluster_bundles/44/config.yaml
@@ -1,5 +1,5 @@
---
- type: cluster
- name: 44
+ name: "44"
version: ver44
diff --git a/tests/functional/test_backend_filtering_data/cluster_bundles/45/config.yaml b/tests/functional/test_backend_filtering_data/cluster_bundles/45/config.yaml
index aab898d07d..99930df07a 100644
--- a/tests/functional/test_backend_filtering_data/cluster_bundles/45/config.yaml
+++ b/tests/functional/test_backend_filtering_data/cluster_bundles/45/config.yaml
@@ -1,5 +1,5 @@
---
- type: cluster
- name: 45
+ name: "45"
version: ver45
diff --git a/tests/functional/test_backend_filtering_data/cluster_bundles/46/config.yaml b/tests/functional/test_backend_filtering_data/cluster_bundles/46/config.yaml
index 27e6e4708b..13feb8d20f 100644
--- a/tests/functional/test_backend_filtering_data/cluster_bundles/46/config.yaml
+++ b/tests/functional/test_backend_filtering_data/cluster_bundles/46/config.yaml
@@ -1,5 +1,5 @@
---
- type: cluster
- name: 46
+ name: "46"
version: ver46
diff --git a/tests/functional/test_backend_filtering_data/cluster_bundles/47/config.yaml b/tests/functional/test_backend_filtering_data/cluster_bundles/47/config.yaml
index 34f9999076..55697a3a69 100644
--- a/tests/functional/test_backend_filtering_data/cluster_bundles/47/config.yaml
+++ b/tests/functional/test_backend_filtering_data/cluster_bundles/47/config.yaml
@@ -1,5 +1,5 @@
---
- type: cluster
- name: 47
+ name: "47"
version: ver47
diff --git a/tests/functional/test_backend_filtering_data/cluster_bundles/48/config.yaml b/tests/functional/test_backend_filtering_data/cluster_bundles/48/config.yaml
index 471574dfbd..2d91b4b171 100644
--- a/tests/functional/test_backend_filtering_data/cluster_bundles/48/config.yaml
+++ b/tests/functional/test_backend_filtering_data/cluster_bundles/48/config.yaml
@@ -1,5 +1,5 @@
---
- type: cluster
- name: 48
+ name: "48"
version: ver48
diff --git a/tests/functional/test_backend_filtering_data/cluster_bundles/49/config.yaml b/tests/functional/test_backend_filtering_data/cluster_bundles/49/config.yaml
index bab0bf5234..cbceee3d98 100644
--- a/tests/functional/test_backend_filtering_data/cluster_bundles/49/config.yaml
+++ b/tests/functional/test_backend_filtering_data/cluster_bundles/49/config.yaml
@@ -1,5 +1,5 @@
---
- type: cluster
- name: 49
+ name: "49"
version: ver49
diff --git a/tests/functional/test_backend_filtering_data/cluster_bundles/5/config.yaml b/tests/functional/test_backend_filtering_data/cluster_bundles/5/config.yaml
index d2a4ea9d10..aba61f65ec 100644
--- a/tests/functional/test_backend_filtering_data/cluster_bundles/5/config.yaml
+++ b/tests/functional/test_backend_filtering_data/cluster_bundles/5/config.yaml
@@ -1,5 +1,5 @@
---
- type: cluster
- name: 5
+ name: "5"
version: ver5
diff --git a/tests/functional/test_backend_filtering_data/cluster_bundles/50/config.yaml b/tests/functional/test_backend_filtering_data/cluster_bundles/50/config.yaml
index bd43cf4e7f..9e7458f530 100644
--- a/tests/functional/test_backend_filtering_data/cluster_bundles/50/config.yaml
+++ b/tests/functional/test_backend_filtering_data/cluster_bundles/50/config.yaml
@@ -1,5 +1,5 @@
---
- type: cluster
- name: 50
+ name: "50"
version: ver50
diff --git a/tests/functional/test_backend_filtering_data/cluster_bundles/51/config.yaml b/tests/functional/test_backend_filtering_data/cluster_bundles/51/config.yaml
index 737c56e224..ef90b07940 100644
--- a/tests/functional/test_backend_filtering_data/cluster_bundles/51/config.yaml
+++ b/tests/functional/test_backend_filtering_data/cluster_bundles/51/config.yaml
@@ -1,5 +1,5 @@
---
- type: cluster
- name: 51
+ name: "51"
version: ver51
diff --git a/tests/functional/test_backend_filtering_data/cluster_bundles/52/config.yaml b/tests/functional/test_backend_filtering_data/cluster_bundles/52/config.yaml
index d3ed8b872e..438efcefb6 100644
--- a/tests/functional/test_backend_filtering_data/cluster_bundles/52/config.yaml
+++ b/tests/functional/test_backend_filtering_data/cluster_bundles/52/config.yaml
@@ -1,5 +1,5 @@
---
- type: cluster
- name: 52
+ name: "52"
version: ver52
diff --git a/tests/functional/test_backend_filtering_data/cluster_bundles/53/config.yaml b/tests/functional/test_backend_filtering_data/cluster_bundles/53/config.yaml
index 584185cf7b..9e6d9bbbe3 100644
--- a/tests/functional/test_backend_filtering_data/cluster_bundles/53/config.yaml
+++ b/tests/functional/test_backend_filtering_data/cluster_bundles/53/config.yaml
@@ -1,5 +1,5 @@
---
- type: cluster
- name: 53
+ name: "53"
version: ver53
diff --git a/tests/functional/test_backend_filtering_data/cluster_bundles/54/config.yaml b/tests/functional/test_backend_filtering_data/cluster_bundles/54/config.yaml
index 38ea195fe8..dd79e87c1d 100644
--- a/tests/functional/test_backend_filtering_data/cluster_bundles/54/config.yaml
+++ b/tests/functional/test_backend_filtering_data/cluster_bundles/54/config.yaml
@@ -1,5 +1,5 @@
---
- type: cluster
- name: 54
+ name: "54"
version: ver54
diff --git a/tests/functional/test_backend_filtering_data/cluster_bundles/55/config.yaml b/tests/functional/test_backend_filtering_data/cluster_bundles/55/config.yaml
index 9251640d12..54ca8ccebe 100644
--- a/tests/functional/test_backend_filtering_data/cluster_bundles/55/config.yaml
+++ b/tests/functional/test_backend_filtering_data/cluster_bundles/55/config.yaml
@@ -1,5 +1,5 @@
---
- type: cluster
- name: 55
+ name: "55"
version: ver55
diff --git a/tests/functional/test_backend_filtering_data/cluster_bundles/56/config.yaml b/tests/functional/test_backend_filtering_data/cluster_bundles/56/config.yaml
index 6057f41497..491e3773e0 100644
--- a/tests/functional/test_backend_filtering_data/cluster_bundles/56/config.yaml
+++ b/tests/functional/test_backend_filtering_data/cluster_bundles/56/config.yaml
@@ -1,5 +1,5 @@
---
- type: cluster
- name: 56
+ name: "56"
version: ver56
diff --git a/tests/functional/test_backend_filtering_data/cluster_bundles/57/config.yaml b/tests/functional/test_backend_filtering_data/cluster_bundles/57/config.yaml
index fa2eaad0d1..06b30c8de3 100644
--- a/tests/functional/test_backend_filtering_data/cluster_bundles/57/config.yaml
+++ b/tests/functional/test_backend_filtering_data/cluster_bundles/57/config.yaml
@@ -1,5 +1,5 @@
---
- type: cluster
- name: 57
+ name: "57"
version: ver57
diff --git a/tests/functional/test_backend_filtering_data/cluster_bundles/58/config.yaml b/tests/functional/test_backend_filtering_data/cluster_bundles/58/config.yaml
index 9ece78a57f..618eb48df7 100644
--- a/tests/functional/test_backend_filtering_data/cluster_bundles/58/config.yaml
+++ b/tests/functional/test_backend_filtering_data/cluster_bundles/58/config.yaml
@@ -1,5 +1,5 @@
---
- type: cluster
- name: 58
+ name: "58"
version: ver58
diff --git a/tests/functional/test_backend_filtering_data/cluster_bundles/59/config.yaml b/tests/functional/test_backend_filtering_data/cluster_bundles/59/config.yaml
index 6e207f20b5..2b11b803b5 100644
--- a/tests/functional/test_backend_filtering_data/cluster_bundles/59/config.yaml
+++ b/tests/functional/test_backend_filtering_data/cluster_bundles/59/config.yaml
@@ -1,5 +1,5 @@
---
- type: cluster
- name: 59
+ name: "59"
version: ver59
diff --git a/tests/functional/test_backend_filtering_data/cluster_bundles/6/config.yaml b/tests/functional/test_backend_filtering_data/cluster_bundles/6/config.yaml
index 01fe531727..51b4ad1ded 100644
--- a/tests/functional/test_backend_filtering_data/cluster_bundles/6/config.yaml
+++ b/tests/functional/test_backend_filtering_data/cluster_bundles/6/config.yaml
@@ -1,5 +1,5 @@
---
- type: cluster
- name: 6
+ name: "6"
version: ver6
diff --git a/tests/functional/test_backend_filtering_data/cluster_bundles/7/config.yaml b/tests/functional/test_backend_filtering_data/cluster_bundles/7/config.yaml
index 085158b6af..899b773a53 100644
--- a/tests/functional/test_backend_filtering_data/cluster_bundles/7/config.yaml
+++ b/tests/functional/test_backend_filtering_data/cluster_bundles/7/config.yaml
@@ -1,5 +1,5 @@
---
- type: cluster
- name: 7
+ name: "7"
version: ver7
diff --git a/tests/functional/test_backend_filtering_data/cluster_bundles/8/config.yaml b/tests/functional/test_backend_filtering_data/cluster_bundles/8/config.yaml
index 12517ee4d6..dcb0b6ceeb 100644
--- a/tests/functional/test_backend_filtering_data/cluster_bundles/8/config.yaml
+++ b/tests/functional/test_backend_filtering_data/cluster_bundles/8/config.yaml
@@ -1,5 +1,5 @@
---
- type: cluster
- name: 8
+ name: "8"
version: ver8
diff --git a/tests/functional/test_backend_filtering_data/cluster_bundles/9/config.yaml b/tests/functional/test_backend_filtering_data/cluster_bundles/9/config.yaml
index 835b86eb72..65448113bb 100644
--- a/tests/functional/test_backend_filtering_data/cluster_bundles/9/config.yaml
+++ b/tests/functional/test_backend_filtering_data/cluster_bundles/9/config.yaml
@@ -1,5 +1,5 @@
---
- type: cluster
- name: 9
+ name: "9"
version: ver9
diff --git a/tests/functional/test_backend_filtering_data/cluster_bundles/gen.sh b/tests/functional/test_backend_filtering_data/cluster_bundles/gen.sh
index 97d46e42f8..5aa8d7b423 100755
--- a/tests/functional/test_backend_filtering_data/cluster_bundles/gen.sh
+++ b/tests/functional/test_backend_filtering_data/cluster_bundles/gen.sh
@@ -6,7 +6,7 @@ cd "$cur" || exit 1
TMPL="
---
- type: cluster
- name: %s
+ name: \"%s\"
version: ver%s
"
diff --git a/tests/functional/test_bundle_support.py b/tests/functional/test_bundle_support.py
index 80019024c0..d735a6ef5e 100644
--- a/tests/functional/test_bundle_support.py
+++ b/tests/functional/test_bundle_support.py
@@ -9,199 +9,179 @@
# 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 coreapi
import pytest
+from adcm_client.objects import ADCMClient
from adcm_pytest_plugin import utils
-from adcm_pytest_plugin.docker_utils import DockerWrapper
# pylint: disable=E0401, E0611, W0611, W0621
from tests.library import errorcodes as err
-from tests.library import steps
-from tests.library.utils import wait_until, filter_action_by_name
-
-
-@pytest.fixture()
-def adcm(image, request, adcm_credentials):
- repo, tag = image
- dw = DockerWrapper()
- adcm = dw.run_adcm(image=repo, tag=tag, pull=False)
- adcm.api.auth(**adcm_credentials)
- yield adcm
- adcm.stop()
-
-
-@pytest.fixture()
-def client(adcm):
- return adcm.api.objects
-def test_bundle_should_have_any_cluster_definition(client):
- with allure.step('Upload cluster bundle with no definition'):
- bundle = utils.get_data_dir(__file__, "bundle_wo_cluster_definition")
+@pytest.mark.parametrize(
+ "bundle_archive",
+ [
+ pytest.param(
+ utils.get_data_dir(__file__, "bundle_wo_cluster_definition"),
+ id="bundle_wo_cluster_definition",
+ )
+ ],
+ indirect=True,
+)
+def test_bundle_should_have_any_cluster_definition(
+ sdk_client_fs: ADCMClient, bundle_archive
+):
+ with pytest.raises(coreapi.exceptions.ErrorMessage) as e:
+ sdk_client_fs.upload_from_fs(bundle_archive)
+ with allure.step("Check error message"):
+ err.BUNDLE_ERROR.equal(
+ e, "There isn't any cluster or host provider definition in bundle"
+ )
+
+
+def test_bundle_cant_removed_when_some_object_associated_with(
+ sdk_client_fs: ADCMClient,
+):
+ bundle_path = utils.get_data_dir(__file__, "cluster_inventory_tests")
+ bundle = sdk_client_fs.upload_from_fs(bundle_path)
+ bundle.cluster_prototype().cluster_create(name=__file__)
+ with allure.step("Removing bundle"):
with pytest.raises(coreapi.exceptions.ErrorMessage) as e:
- steps.upload_bundle(client, bundle)
- with allure.step('Check error message'):
- err.BUNDLE_ERROR.equal(e, "There isn't any cluster or host provider definition in bundle")
-
-
-def test_bundle_cant_removed_when_some_object_associated_with(client):
- with allure.step('Upload cluster bundle'):
- bundle = utils.get_data_dir(__file__, "cluster_inventory_tests")
- steps.upload_bundle(client, bundle)
- with allure.step('Create cluster'):
- client.cluster.create(prototype_id=client.stack.cluster.list()[0]['id'], name=__file__)
- with pytest.raises(coreapi.exceptions.ErrorMessage) as e:
- client.stack.bundle.delete(bundle_id=client.stack.bundle.list()[0]['id'])
- with allure.step('Check error message'):
+ bundle.delete()
+ with allure.step("Check error message"):
err.BUNDLE_CONFLICT.equal(e, "There is cluster", "of bundle ")
-def test_bundle_can_be_removed_when_no_object_associated_with(client):
- with allure.step('Upload cluster bundle'):
- bundle = utils.get_data_dir(__file__, "cluster_inventory_tests")
- steps.upload_bundle(client, bundle)
- with allure.step('Remove cluster bundle'):
- client.stack.bundle.delete(bundle_id=client.stack.bundle.list()[0]['id'])
- with allure.step('Check cluster bundle is removed'):
- assert not client.stack.bundle.list()
+def test_bundle_can_be_removed_when_no_object_associated_with(
+ sdk_client_fs: ADCMClient,
+):
+ bundle_path = utils.get_data_dir(__file__, "cluster_inventory_tests")
+ bundle = sdk_client_fs.upload_from_fs(bundle_path)
+ with allure.step("Removing bundle"):
+ bundle.delete()
+ with allure.step("Check cluster bundle is removed"):
+ assert not sdk_client_fs.bundle_list()
+
# TODO: Make this test to cover ADCM-202
# def test_default_values_should_according_to_their_datatypes(client):
# bundle = os.path.join(BUNDLES, "")
-empty_bundles_fields = ['empty_success_cluster',
- 'empty_fail_cluster',
- 'empty_success_host',
- 'empty_fail_host'
- ]
+empty_bundles_fields = [
+ "empty_success_cluster",
+ "empty_fail_cluster",
+ "empty_success_host",
+ "empty_fail_host",
+]
@pytest.mark.parametrize("empty_fields", empty_bundles_fields)
-def test_that_check_empty_field_is(empty_fields, client):
- with allure.step('Upload cluster bundle'):
- bundle = utils.get_data_dir(__file__, "empty_states", empty_fields)
- steps.upload_bundle(client, bundle)
- with allure.step('Check cluster bundle'):
- assert client.stack.bundle.list() is not None
+def test_that_check_empty_field_is(empty_fields, sdk_client_fs: ADCMClient):
+ bundle_path = utils.get_data_dir(__file__, "empty_states", empty_fields)
+ sdk_client_fs.upload_from_fs(bundle_path)
+ with allure.step("Check cluster bundle"):
+ assert sdk_client_fs.bundle_list() is not None
cluster_fields = [
- ('empty_success_cluster', 'failed'),
- ('empty_fail_cluster', 'installed'),
+ ("empty_success_cluster", "failed"),
+ ("empty_fail_cluster", "installed"),
]
-@pytest.mark.parametrize(('cluster_bundle', 'state'), cluster_fields)
-def test_check_cluster_state_after_run_action_when_empty(cluster_bundle, state, client):
- with allure.step(f'Upload cluster bundle: {cluster_bundle}'):
- bundle = utils.get_data_dir(__file__, "empty_states", cluster_bundle)
- steps.upload_bundle(client, bundle)
- with allure.step('Create cluster'):
- cluster = client.cluster.create(prototype_id=client.stack.cluster.list()[0]['id'],
- name=utils.random_string())
- with allure.step('Run cluster'):
- action = client.cluster.action.run.create(
- action_id=filter_action_by_name(
- client.cluster.action.list(cluster_id=cluster['id']), 'install')[0]['id'],
- cluster_id=cluster['id'])
- wait_until(client, action)
- with allure.step(f'Check if cluster state is {state}'):
- assert client.cluster.read(cluster_id=cluster['id'])['state'] == state
+@pytest.mark.parametrize(("cluster_bundle", "state"), cluster_fields)
+def test_check_cluster_state_after_run_action_when_empty(
+ cluster_bundle, state, sdk_client_fs: ADCMClient
+):
+ bundle_path = utils.get_data_dir(__file__, "empty_states", cluster_bundle)
+ bundle = sdk_client_fs.upload_from_fs(bundle_path)
+ cluster = bundle.cluster_prototype().cluster_create(name=utils.random_string())
+ cluster.action(name="install").run().wait()
+ with allure.step(f"Check if cluster is in state {state}"):
+ cluster.reread()
+ assert cluster.state == state
host_fields = [
- ('empty_success_host', 'failed'),
- ('empty_fail_host', 'initiated'),
+ ("empty_success_host", "failed"),
+ ("empty_fail_host", "initiated"),
]
-@pytest.mark.parametrize(('host_bundle', 'state'), host_fields)
-def test_check_host_state_after_run_action_when_empty(host_bundle, state, client):
- with allure.step(f'Upload cluster bundle: {host_bundle}'):
- bundle = utils.get_data_dir(__file__, "empty_states", host_bundle)
- steps.upload_bundle(client, bundle)
- with allure.step('Create provider and host'):
- provider = client.provider.create(prototype_id=client.stack.provider.list()[0]['id'],
- name=utils.random_string())
- host = client.host.create(prototype_id=client.stack.host.list()[0]['id'],
- provider_id=provider['id'],
- fqdn=utils.random_string())
- with allure.step('Run host'):
- action = client.host.action.run.create(
- action_id=filter_action_by_name(
- client.host.action.list(host_id=host['id']), 'init')[0]['id'],
- host_id=host['id'])
- wait_until(client, action)
- with allure.step(f'Check if host state is {state}'):
- assert client.host.read(host_id=host['id'])['state'] == state
-
-
-def test_loading_provider_bundle_must_be_pass(client):
- with allure.step('Upload cluster bundle'):
- bundle = utils.get_data_dir(__file__, "hostprovider_loading_pass")
- steps.upload_bundle(client, bundle)
- with allure.step('Check that hostprovider loading pass'):
- host_provider = client.stack.provider.list()
- assert host_provider is not None
-
-
-def test_run_parametrized_action_must_be_runned(client):
- with allure.step('Upload cluster bundle'):
- bundle = utils.get_data_dir(__file__, "run_parametrized_action")
- steps.upload_bundle(client, bundle)
- with allure.step('Create cluster'):
- cluster = client.cluster.create(prototype_id=client.stack.cluster.list()[0]['id'],
- name=utils.random_string())
- with allure.step('Run cluster'):
- action = client.cluster.action.run.create(
- action_id=filter_action_by_name(
- client.cluster.action.list(cluster_id=cluster['id']),
- 'install')[0]['id'],
- cluster_id=cluster['id'], config={"param": "test test test test test"})
- wait_until(client, action)
- with allure.step('Check if state is success'):
- assert client.job.read(job_id=client.job.list()[0]['id'])['status'] == 'success'
+@pytest.mark.parametrize(("host_bundle", "state"), host_fields)
+def test_check_host_state_after_run_action_when_empty(
+ host_bundle, state, sdk_client_fs: ADCMClient
+):
+ bundle_path = utils.get_data_dir(__file__, "empty_states", host_bundle)
+ bundle = sdk_client_fs.upload_from_fs(bundle_path)
+ provider = bundle.provider_prototype().provider_create(name=utils.random_string())
+ host = provider.host_create(fqdn=utils.random_string())
+ host.action(name="init").run().wait()
+ with allure.step(f"Check if host is in state {state}"):
+ host.reread()
+ assert host.state == state
-state_cases = [
- ('cluster', 'on_success', 'was_dict'),
- ('cluster', 'on_success', 'was_list'),
- ('cluster', 'on_success', 'was_sequence'),
- ('cluster', 'on_fail', 'was_dict'),
- ('cluster', 'on_fail', 'was_list'),
- ('cluster', 'on_fail', 'was_sequence'),
- ('provider', 'on_success', 'was_dict'),
- ('provider', 'on_success', 'was_list'),
- ('provider', 'on_success', 'was_sequence'),
- ('provider', 'on_fail', 'was_dict'),
- ('provider', 'on_fail', 'was_list'),
- ('provider', 'on_fail', 'was_sequence'),
- ('host', 'on_success', 'was_dict'),
- ('host', 'on_success', 'was_list'),
- ('host', 'on_success', 'was_sequence'),
- ('host', 'on_fail', 'was_dict'),
- ('host', 'on_fail', 'was_list'),
- ('host', 'on_fail', 'was_sequence'),
-]
+def test_loading_provider_bundle_must_be_pass(sdk_client_fs: ADCMClient):
+ bundle_path = utils.get_data_dir(__file__, "hostprovider_loading_pass")
+ bundle = sdk_client_fs.upload_from_fs(bundle_path)
+ with allure.step("Check that hostprovider loading pass"):
+ assert bundle.provider_prototype() is not None
-@pytest.mark.parametrize(('entity', 'state', 'case'), state_cases)
-def test_load_should_fail_when(client, entity, state, case):
- with allure.step(f'Upload {entity} bundle with {case}'):
- bundle = utils.get_data_dir(__file__, 'states', entity, state, case)
- with pytest.raises(coreapi.exceptions.ErrorMessage) as e:
- steps.upload_bundle(client, bundle)
- with allure.step(f'Check if state is {state}'):
- err.INVALID_ACTION_DEFINITION.equal(e, state, entity, 'should be string')
+def test_run_parametrized_action_must_be_runned(sdk_client_fs: ADCMClient):
+ bundle_path = utils.get_data_dir(__file__, "run_parametrized_action")
+ bundle = sdk_client_fs.upload_from_fs(bundle_path)
+ cluster = bundle.cluster_prototype().cluster_create(name=utils.random_string())
+ task = cluster.action(name="install").run(
+ config={"param": "test test test test test"}
+ )
+ task.try_wait()
+ with allure.step("Check if state is success"):
+ assert task.job().status == "success"
+
+
+state_cases = [
+ ("cluster", "on_success", "was_dict"),
+ ("cluster", "on_success", "was_list"),
+ ("cluster", "on_success", "was_sequence"),
+ ("cluster", "on_fail", "was_dict"),
+ ("cluster", "on_fail", "was_list"),
+ ("cluster", "on_fail", "was_sequence"),
+ ("provider", "on_success", "was_dict"),
+ ("provider", "on_success", "was_list"),
+ ("provider", "on_success", "was_sequence"),
+ ("provider", "on_fail", "was_dict"),
+ ("provider", "on_fail", "was_list"),
+ ("provider", "on_fail", "was_sequence"),
+ ("host", "on_success", "was_dict"),
+ ("host", "on_success", "was_list"),
+ ("host", "on_success", "was_sequence"),
+ ("host", "on_fail", "was_dict"),
+ ("host", "on_fail", "was_list"),
+ ("host", "on_fail", "was_sequence"),
+]
-@allure.link('https://jira.arenadata.io/browse/ADCM-580')
-def test_provider_bundle_shouldnt_load_when_has_export_section(client):
- with allure.step('Upload cluster bundle'):
- bundle = utils.get_data_dir(__file__, 'hostprovider_with_export')
+@pytest.mark.parametrize(("entity", "state", "case"), state_cases)
+def test_load_should_fail_when(sdk_client_fs: ADCMClient, entity, state, case):
+ with allure.step(f"Upload {entity} bundle with {case}"):
+ bundle_path = utils.get_data_dir(__file__, "states", entity, state, case)
with pytest.raises(coreapi.exceptions.ErrorMessage) as e:
- steps.upload_bundle(client, bundle)
- with allure.step('Check error'):
- err.INVALID_OBJECT_DEFINITION.equal(e, 'Only cluster or service can have export section')
+ sdk_client_fs.upload_from_fs(bundle_path)
+ with allure.step(f"Check if state is {state}"):
+ err.INVALID_OBJECT_DEFINITION.equal(e, state, "should be a ")
+
+
+@allure.link("https://jira.arenadata.io/browse/ADCM-580")
+def test_provider_bundle_shouldnt_load_when_has_export_section(
+ sdk_client_fs: ADCMClient,
+):
+ bundle_path = utils.get_data_dir(__file__, "hostprovider_with_export")
+ with pytest.raises(coreapi.exceptions.ErrorMessage) as e:
+ sdk_client_fs.upload_from_fs(bundle_path)
+ with allure.step("Check error"):
+ err.INVALID_OBJECT_DEFINITION.equal(e, 'Map key "export" is not allowed here')
diff --git a/tests/functional/test_bundle_support_data/bundle_wo_cluster_definition/config.yaml b/tests/functional/test_bundle_support_data/bundle_wo_cluster_definition/config.yaml
index d1bbb3eeeb..66f6b42388 100644
--- a/tests/functional/test_bundle_support_data/bundle_wo_cluster_definition/config.yaml
+++ b/tests/functional/test_bundle_support_data/bundle_wo_cluster_definition/config.yaml
@@ -24,7 +24,7 @@
on_fail: cluster_install_fail
type: job
script: stack/extcode/cook.py
- script_type: task_generator
+ script_type: ansible
components:
ZOOKEEPER_CLIENT:
config:
diff --git a/tests/functional/test_bundle_support_data/empty_states/empty_fail_cluster/config.yml b/tests/functional/test_bundle_support_data/empty_states/empty_fail_cluster/config.yml
index 967b8f7d70..c7f776bb29 100644
--- a/tests/functional/test_bundle_support_data/empty_states/empty_fail_cluster/config.yml
+++ b/tests/functional/test_bundle_support_data/empty_states/empty_fail_cluster/config.yml
@@ -1,53 +1,52 @@
--
- type: cluster
- name: Empty states
- version: 0.02
- actions:
- install:
- type: job
- script: ansible/install.yaml
- script_type: ansible
- states:
- available:
- - created
- on_success: installed
- params:
- qwe: 42
- config:
- required:
- type: integer
- required: true
- default: 10
- str-key:
- default: value
- type: string
- required: false
+- type: cluster
+ name: Empty states
+ version: 0.02
+ actions:
+ install:
+ type: job
+ script: ansible/install.yaml
+ script_type: ansible
+ states:
+ available:
+ - created
+ on_success: installed
+ params:
+ qwe: 42
+ config:
+ required:
+ type: integer
+ required: true
+ default: 10
+ str-key:
+ default: value
+ type: string
+ required: false
- int_key:
- type: integer
- required: false
- default: 150
+ int_key:
+ type: integer
+ required: false
+ default: 150
- float_key:
- type: float
- required: false
- default: 34.7
+ float_key:
+ type: float
+ required: false
+ default: 34.7
- bool:
- type: boolean
- required : false
- default: false
- option:
- type: option
- option:
- http: 80
- https: 443
- ftp: 21
- required: FALSE
- password:
- default: qwerty
- type: password
- required: false
- input_file:
- type: file
- required: false
+ bool:
+ type: boolean
+ required: false
+ default: false
+ option:
+ type: option
+ option:
+ http: 80
+ https: 443
+ ftp: 21
+ required: FALSE
+ password:
+ default: qwerty
+ type: password
+ required: false
+ input_file:
+ type: file
+ required: false
diff --git a/tests/functional/test_bundle_support_data/empty_states/empty_fail_host/config.yaml b/tests/functional/test_bundle_support_data/empty_states/empty_fail_host/config.yaml
index 7b650ff2d5..0483db28ec 100644
--- a/tests/functional/test_bundle_support_data/empty_states/empty_fail_host/config.yaml
+++ b/tests/functional/test_bundle_support_data/empty_states/empty_fail_host/config.yaml
@@ -10,36 +10,33 @@
# See the License for the specific language governing permissions and
# limitations under the License.
---
-- type: provider
- name: Prov_upd_1
- version: &version 0.6
- config:
--
+- type: provider
+ name: Prov_upd_1
+ version: &version 0.6
+- type: host
+ name: sample host
+ version: 1.0
- type: host
- name: sample host
- version: 1.0
-
- actions:
- init:
- type: job
- log_files: [remote]
- script: ansible/init.yaml
- script_type: ansible
- states:
- available:
- - created
- on_success: initiated
- params:
- qwe: 42
- config:
- required:
- type: integer
- required: yes
- default: 40
- display_name: required integer field
- str-key:
- default: value
- type: string
- required: false
- display_name: non-required string
+ actions:
+ init:
+ type: job
+ log_files: [ remote ]
+ script: ansible/init.yaml
+ script_type: ansible
+ states:
+ available:
+ - created
+ on_success: initiated
+ params:
+ qwe: 42
+ config:
+ required:
+ type: integer
+ required: yes
+ default: 40
+ display_name: required integer field
+ str-key:
+ default: value
+ type: string
+ required: false
+ display_name: non-required string
diff --git a/tests/functional/test_bundle_support_data/empty_states/empty_success_cluster/config.yml b/tests/functional/test_bundle_support_data/empty_states/empty_success_cluster/config.yml
index 5100575e6e..1bc98382e0 100644
--- a/tests/functional/test_bundle_support_data/empty_states/empty_success_cluster/config.yml
+++ b/tests/functional/test_bundle_support_data/empty_states/empty_success_cluster/config.yml
@@ -1,53 +1,52 @@
--
- type: cluster
- name: Empty states
- version: 0.02
- actions:
- install:
- type: job
- script: ansible/install.yaml
- script_type: ansible
- states:
- available:
- - created
- on_fail: failed
- params:
- qwe: 42
- config:
- required:
- type: integer
- required: true
- default: 10
- str-key:
- default: value
- type: string
- required: false
+- type: cluster
+ name: Empty states
+ version: 0.02
+ actions:
+ install:
+ type: job
+ script: ansible/install.yaml
+ script_type: ansible
+ states:
+ available:
+ - created
+ on_fail: failed
+ params:
+ qwe: 42
+ config:
+ required:
+ type: integer
+ required: true
+ default: 10
+ str-key:
+ default: value
+ type: string
+ required: false
- int_key:
- type: integer
- required: false
- default: 150
+ int_key:
+ type: integer
+ required: false
+ default: 150
- float_key:
- type: float
- required: false
- default: 34.7
+ float_key:
+ type: float
+ required: false
+ default: 34.7
- bool:
- type: boolean
- required : false
- default: false
- option:
- type: option
- option:
- http: 80
- https: 443
- ftp: 21
- required: FALSE
- password:
- default: qwerty
- type: password
- required: false
- input_file:
- type: file
- required: false
+ bool:
+ type: boolean
+ required: false
+ default: false
+ option:
+ type: option
+ option:
+ http: 80
+ https: 443
+ ftp: 21
+ required: FALSE
+ password:
+ default: qwerty
+ type: password
+ required: false
+ input_file:
+ type: file
+ required: false
diff --git a/tests/functional/test_bundle_support_data/empty_states/empty_success_host/config.yml b/tests/functional/test_bundle_support_data/empty_states/empty_success_host/config.yml
index 05f2d44f47..75e879cd46 100644
--- a/tests/functional/test_bundle_support_data/empty_states/empty_success_host/config.yml
+++ b/tests/functional/test_bundle_support_data/empty_states/empty_success_host/config.yml
@@ -1,34 +1,31 @@
---
-- type: provider
- name: Prov_upd_1
- version: &version 0.6
- config:
--
+- type: provider
+ name: Prov_upd_1
+ version: &version 0.6
+- type: host
+ name: sample host
+ version: 1.0
- type: host
- name: sample host
- version: 1.0
-
- actions:
- init:
- type: job
- log_files: [remote]
- script: ansible/init.yaml
- script_type: ansible
- states:
- available:
- - created
- on_fail: failed
- params:
- qwe: 42
- config:
- required:
- type: integer
- required: yes
- default: 40
- display_name: required integer field
- str-key:
- default: value
- type: string
- required: false
- display_name: non-required string
+ actions:
+ init:
+ type: job
+ log_files: [ remote ]
+ script: ansible/init.yaml
+ script_type: ansible
+ states:
+ available:
+ - created
+ on_fail: failed
+ params:
+ qwe: 42
+ config:
+ required:
+ type: integer
+ required: yes
+ default: 40
+ display_name: required integer field
+ str-key:
+ default: value
+ type: string
+ required: false
+ display_name: non-required string
diff --git a/tests/functional/test_bundle_support_data/states/cluster/on_success/was_dict/config.yaml b/tests/functional/test_bundle_support_data/states/cluster/on_success/was_dict/config.yaml
index 6ca181649a..2db3d78952 100644
--- a/tests/functional/test_bundle_support_data/states/cluster/on_success/was_dict/config.yaml
+++ b/tests/functional/test_bundle_support_data/states/cluster/on_success/was_dict/config.yaml
@@ -10,28 +10,27 @@
# See the License for the specific language governing permissions and
# limitations under the License.
---
--
- type: cluster
- name: stringify_states
- version: 1.0
- actions:
- install:
- type: job
- script: ansible/install.yaml
- script_type: ansible
- states:
- available:
- - created
- on_success: {failed}
- params:
- qwe: 42
- upgrade:
- config:
- group1:
- boooooooooool:
- type: boolean
- required: false
- str-key:
- default: value
- type: string
- required: false
+- type: cluster
+ name: stringify_states
+ version: 1.0
+ actions:
+ install:
+ type: job
+ script: ansible/install.yaml
+ script_type: ansible
+ states:
+ available:
+ - created
+ on_success: { failed }
+ params:
+ qwe: 42
+ upgrade:
+ config:
+ group1:
+ boooooooooool:
+ type: boolean
+ required: false
+ str-key:
+ default: value
+ type: string
+ required: false
diff --git a/tests/functional/test_bundle_support_data/states/cluster/on_success/was_list/config.yaml b/tests/functional/test_bundle_support_data/states/cluster/on_success/was_list/config.yaml
index 0639ce76e2..3fdf0a5733 100644
--- a/tests/functional/test_bundle_support_data/states/cluster/on_success/was_list/config.yaml
+++ b/tests/functional/test_bundle_support_data/states/cluster/on_success/was_list/config.yaml
@@ -10,26 +10,25 @@
# See the License for the specific language governing permissions and
# limitations under the License.
---
--
- type: cluster
- name: stringify_states
- version: 1.0
- actions:
- install:
- type: job
- script: ansible/install.yaml
- script_type: ansible
- states:
- available:
- - created
- on_success: [failed]
- upgrade:
- config:
- group1:
- boooooooooool:
- type: boolean
- required: false
- str-key:
- default: value
- type: string
- required: false
+- type: cluster
+ name: stringify_states
+ version: 1.0
+ actions:
+ install:
+ type: job
+ script: ansible/install.yaml
+ script_type: ansible
+ states:
+ available:
+ - created
+ on_success: [ failed ]
+ upgrade:
+ config:
+ group1:
+ boooooooooool:
+ type: boolean
+ required: false
+ str-key:
+ default: value
+ type: string
+ required: false
diff --git a/tests/functional/test_bundle_support_data/states/cluster/on_success/was_sequence/config.yaml b/tests/functional/test_bundle_support_data/states/cluster/on_success/was_sequence/config.yaml
index 682072428b..994b37306b 100644
--- a/tests/functional/test_bundle_support_data/states/cluster/on_success/was_sequence/config.yaml
+++ b/tests/functional/test_bundle_support_data/states/cluster/on_success/was_sequence/config.yaml
@@ -10,27 +10,26 @@
# See the License for the specific language governing permissions and
# limitations under the License.
---
--
- type: cluster
- name: stringify_states
- version: 1.0
- actions:
- install:
- type: job
- script: ansible/install.yaml
- script_type: ansible
- states:
- available:
- - created
- on_success:
- - item1
- - item2
- config:
- group1:
- boooooooooool:
- type: boolean
- required: false
- str-key:
- default: value
- type: string
- required: false
+- type: cluster
+ name: stringify_states
+ version: 1.0
+ actions:
+ install:
+ type: job
+ script: ansible/install.yaml
+ script_type: ansible
+ states:
+ available:
+ - created
+ on_success:
+ - item1
+ - item2
+ config:
+ group1:
+ boooooooooool:
+ type: boolean
+ required: false
+ str-key:
+ default: value
+ type: string
+ required: false
diff --git a/tests/functional/test_bundle_upgrades.py b/tests/functional/test_bundle_upgrades.py
index 9c4d7bba7c..4c9b6a9aad 100644
--- a/tests/functional/test_bundle_upgrades.py
+++ b/tests/functional/test_bundle_upgrades.py
@@ -72,7 +72,7 @@ def test_that_check_nonexistent_cluster_upgrade(sdk_client_fs: ADCMClient, clust
with pytest.raises(coreapi.exceptions.ErrorMessage) as e:
upgr.do(upgrade_id=5555, cluster_id=cluster.id)
with allure.step('Check if upgrade is not found'):
- UPGRADE_NOT_FOUND.equal(e, 'upgrade is not found')
+ UPGRADE_NOT_FOUND.equal(e, 'Upgrade', 'does not exist')
def test_that_check_nonexistent_hostprovider_upgrade(sdk_client_fs: ADCMClient, host_bundles):
@@ -84,7 +84,7 @@ def test_that_check_nonexistent_hostprovider_upgrade(sdk_client_fs: ADCMClient,
with pytest.raises(coreapi.exceptions.ErrorMessage) as e:
upgr.do(upgrade_id=5555, provider_id=hostprovider.id)
with allure.step('Check if upgrade is not found'):
- UPGRADE_NOT_FOUND.equal(e, 'upgrade is not found')
+ UPGRADE_NOT_FOUND.equal(e, 'Upgrade', 'does not exist')
def test_a_hostprovider_bundle_upgrade_will_ends_successfully(sdk_client_fs: ADCMClient,
diff --git a/tests/functional/test_bundle_upgrades_data/cluster_bundle_before_upgrade/config.yaml b/tests/functional/test_bundle_upgrades_data/cluster_bundle_before_upgrade/config.yaml
index 1391b1242a..dba23f9e55 100644
--- a/tests/functional/test_bundle_upgrades_data/cluster_bundle_before_upgrade/config.yaml
+++ b/tests/functional/test_bundle_upgrades_data/cluster_bundle_before_upgrade/config.yaml
@@ -9,76 +9,74 @@
# 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: ADH
- version: '1.5'
- actions:
- config:
- required:
- type: integer
- required: true
- default: 10
- str-key:
- default: value
- type: string
- required: false
+- type: cluster
+ name: ADH
+ version: '1.5'
+ config:
+ required:
+ type: integer
+ required: true
+ default: 10
+ str-key:
+ default: value
+ type: string
+ required: false
- int_key:
- type: integer
- required: false
- default: 150
+ int_key:
+ type: integer
+ required: false
+ default: 150
- float_key:
- type: float
- required: false
- default: 34.7
+ float_key:
+ type: float
+ required: false
+ default: 34.7
- bool:
- type: boolean
- required : false
- default: false
- option:
- type: option
- option:
- http: 80
- https: 443
- ftp: 21
- required: FALSE
- password:
- default: qwerty
- type: password
- required: false
- json:
- type: json
- required: false
- default:
- {
- "foo": "bar"
- }
- sysctl_params:
- default: >-
- [
- [ "kernel.sysrq", "1" ],
- [ "kernel.core_uses_pid", "1" ],
- [ "kernel.shmmni", "4096" ],
- [ "kernel.sem", "250 912000 100 8192" ],
- [ "kernel.core_uses_pid", "1" ],
- [ "kernel.msgmnb", "65536" ],
- [ "kernel.msgmax", "65536" ],
- [ "kernel.msgmni", "2048" ],
- [ "net.ipv4.tcp_syncookies", "1" ],
- [ "net.ipv4.ip_forward", "0" ],
- [ "net.ipv4.conf.default.accept_source_route", "0" ],
- [ "net.ipv4.tcp_tw_recycle", "1" ],
- [ "net.ipv4.tcp_max_syn_backlog", "4096" ],
- [ "net.ipv4.conf.all.arp_filter", "1" ],
- [ "net.ipv4.ip_local_port_range", "40000 59000" ],
- [ "net.core.netdev_max_backlog", "10000" ],
- [ "net.core.rmem_max", "2097152" ],
- [ "net.core.wmem_max", "2097152" ],
- [ "vm.overcommit_memory", "2" ],
- [ "vm.overcommit_ratio", "93" ],
- [ "kernel.core_pipe_limit", "0" ]
- ]
- type: json
+ bool:
+ type: boolean
+ required: false
+ default: false
+ option:
+ type: option
+ option:
+ http: 80
+ https: 443
+ ftp: 21
+ required: FALSE
+ password:
+ default: qwerty
+ type: password
+ required: false
+ json:
+ type: json
+ required: false
+ default:
+ {
+ "foo": "bar"
+ }
+ sysctl_params:
+ default: >-
+ [
+ [ "kernel.sysrq", "1" ],
+ [ "kernel.core_uses_pid", "1" ],
+ [ "kernel.shmmni", "4096" ],
+ [ "kernel.sem", "250 912000 100 8192" ],
+ [ "kernel.core_uses_pid", "1" ],
+ [ "kernel.msgmnb", "65536" ],
+ [ "kernel.msgmax", "65536" ],
+ [ "kernel.msgmni", "2048" ],
+ [ "net.ipv4.tcp_syncookies", "1" ],
+ [ "net.ipv4.ip_forward", "0" ],
+ [ "net.ipv4.conf.default.accept_source_route", "0" ],
+ [ "net.ipv4.tcp_tw_recycle", "1" ],
+ [ "net.ipv4.tcp_max_syn_backlog", "4096" ],
+ [ "net.ipv4.conf.all.arp_filter", "1" ],
+ [ "net.ipv4.ip_local_port_range", "40000 59000" ],
+ [ "net.core.netdev_max_backlog", "10000" ],
+ [ "net.core.rmem_max", "2097152" ],
+ [ "net.core.wmem_max", "2097152" ],
+ [ "vm.overcommit_memory", "2" ],
+ [ "vm.overcommit_ratio", "93" ],
+ [ "kernel.core_pipe_limit", "0" ]
+ ]
+ type: json
diff --git a/tests/functional/test_bundle_upgrades_data/cluster_without_old_config/old/config.yaml b/tests/functional/test_bundle_upgrades_data/cluster_without_old_config/old/config.yaml
index 436dd6c4ef..204fe73fb2 100644
--- a/tests/functional/test_bundle_upgrades_data/cluster_without_old_config/old/config.yaml
+++ b/tests/functional/test_bundle_upgrades_data/cluster_without_old_config/old/config.yaml
@@ -11,5 +11,5 @@
# limitations under the License.
- type: cluster
name: restgrid
- version: &version 1-noconfig
+ version: &version 1-noconfig
description: Rest to Ignite interface
diff --git a/tests/functional/test_bundle_upgrades_data/hostprovider_bundle_before_upgrade/hostprovider/config.yaml b/tests/functional/test_bundle_upgrades_data/hostprovider_bundle_before_upgrade/hostprovider/config.yaml
index 07ff65c127..ace035613d 100644
--- a/tests/functional/test_bundle_upgrades_data/hostprovider_bundle_before_upgrade/hostprovider/config.yaml
+++ b/tests/functional/test_bundle_upgrades_data/hostprovider_bundle_before_upgrade/hostprovider/config.yaml
@@ -10,82 +10,80 @@
# See the License for the specific language governing permissions and
# limitations under the License.
--
+- type: provider
+ name: sample hostprovider
+ version: '1.0'
- type: provider
- name: sample hostprovider
- version: '1.0'
-
- config:
- required:
- type: integer
- required: yes
- default: 40
- str-key:
- default: value
- type: string
- required: false
+ config:
+ required:
+ type: integer
+ required: yes
+ default: 40
+ str-key:
+ default: value
+ type: string
+ required: false
- int_key:
- type: integer
- required: NO
- default: 3
+ int_key:
+ type: integer
+ required: NO
+ default: 3
- fkey:
- type: float
- required: false
- default: 1.5
+ fkey:
+ type: float
+ required: false
+ default: 1.5
- bool:
- type: boolean
- required : no
- default: false
+ bool:
+ type: boolean
+ required: no
+ default: false
+ option:
+ type: option
+ option:
+ http: 8080
+ https: 4043
+ ftp: my.host
+ required: FALSE
+ sub:
+ sub1:
+ type: option
option:
- type: option
- option:
- http: 8080
- https: 4043
- ftp: my.host
- required: FALSE
- sub:
- sub1:
- type: option
- option:
- a: 1
- s: 2
- d: 3
- required: no
- password:
- type: password
- required: false
- default: q1w2e3r4t5y6
- json:
- type: json
- required: false
- default: {"foo": "bar"}
- host-file:
- type: file
- required: false
- host-text-area:
- type: text
- required: false
- default: lorem ipsum
- read-only-string:
- type: string
- default: default value
- required: false
- read_only: any
- writable-when-created:
- type: integer
- default: 999
- required: no
- writable: [created]
- read-only-when_installed:
- type: float
- default: 33.6
- required: false
- read_only: [installed]
-
+ a: 1
+ s: 2
+ d: 3
+ required: no
+ password:
+ type: password
+ required: false
+ default: q1w2e3r4t5y6
+ json:
+ type: json
+ required: false
+ default: { "foo": "bar" }
+ host-file:
+ type: file
+ required: false
+ host-text-area:
+ type: text
+ required: false
+ default: lorem ipsum
+ read-only-string:
+ type: string
+ default: default value
+ required: false
+ read_only: any
+ writable-when-created:
+ type: integer
+ default: 999
+ required: no
+ writable: [ created ]
+ read-only-when_installed:
+ type: float
+ default: 33.6
+ required: false
+ read_only: [ installed ]
+
- type: host
name: vHost
version: 00.09
diff --git a/tests/functional/test_bundle_upgrades_data/strict_and_non_strict_upgrade/max_cluster/config.yaml b/tests/functional/test_bundle_upgrades_data/strict_and_non_strict_upgrade/max_cluster/config.yaml
index a1d62cd36a..3dd972ae2d 100644
--- a/tests/functional/test_bundle_upgrades_data/strict_and_non_strict_upgrade/max_cluster/config.yaml
+++ b/tests/functional/test_bundle_upgrades_data/strict_and_non_strict_upgrade/max_cluster/config.yaml
@@ -14,34 +14,31 @@
name: empty_config
version: &version 1.2
- config:
components:
EMPTY_COMPONENT1:
- constraint: [1,+]
+ constraint: [ 1,+ ]
EMPTY_COMPONENT2:
- constraint: [0,+]
--
- type: cluster
- name: strictly
- version: '1.1.0'
- upgrade:
- -
- versions:
- max: 1.5
- max_strict: 5
- description: this upgrade add new config parameters into the cluster
- name: upgrade me
- states:
- available: [created, installed]
- on_success: upgradated
- config:
- template:
- required: false
- description: "Choose a host template to creation in virtual cloud"
- display_name: 'Choose a template'
- type: option
- option:
- template1: cl_02_16
- template2: cl_04_16
- template3: cl_08_32
- template4: cl_08_64
+ constraint: [ 0,+ ]
+- type: cluster
+ name: strictly
+ version: '1.1.0'
+ upgrade:
+ - versions:
+ max: 1.5
+ max_strict: 5
+ description: this upgrade add new config parameters into the cluster
+ name: upgrade me
+ states:
+ available: [ created, installed ]
+ on_success: upgradated
+ config:
+ template:
+ required: false
+ description: "Choose a host template to creation in virtual cloud"
+ display_name: 'Choose a template'
+ type: option
+ option:
+ template1: cl_02_16
+ template2: cl_04_16
+ template3: cl_08_32
+ template4: cl_08_64
diff --git a/tests/functional/test_bundle_upgrades_data/strict_and_non_strict_upgrade/max_hostprovider/config.yaml b/tests/functional/test_bundle_upgrades_data/strict_and_non_strict_upgrade/max_hostprovider/config.yaml
index 5a5b5b0403..caafbc683e 100644
--- a/tests/functional/test_bundle_upgrades_data/strict_and_non_strict_upgrade/max_hostprovider/config.yaml
+++ b/tests/functional/test_bundle_upgrades_data/strict_and_non_strict_upgrade/max_hostprovider/config.yaml
@@ -13,38 +13,22 @@
- type: provider
name: SampleProvider
description: "A sample provider for testing interaction between an old hosts bundles and new functionality with delegation a part of logic to host provider"
- version: &provider_version 0.01
-
config:
- credentials:
- ansible_host:
- display_name: "Hostname"
- type: string
- required: false
- read_only: any
- description: "Name of the host to connect to"
--
- type: host
- name: ssh_new
- version: '2.1'
- upgrade:
- -
- versions:
- min: 1.0
- max_strict: 2.0
- max: 2.1
- description: Use this upgrade and wish U happy new bundle version
- name: To version 2(ssh_new)
- states:
- available: [created, initiated]
- on_success: upgradable
-
- config:
- ssh-key:
- type: string
- default: 1A2B
- int-key:
- type: integer
- default: 50
- required: false
- display_name: super integer key
+ credentials:
+ ansible_host:
+ display_name: "Hostname"
+ type: string
+ required: false
+ read_only: any
+ description: "Name of the host to connect to"
+ version: '2.1'
+ upgrade:
+ - versions:
+ min: 1.0
+ max_strict: 2.0
+ max: 2.1
+ description: Use this upgrade and wish U happy new bundle version
+ name: To version 2(ssh_new)
+ states:
+ available: [ created, initiated ]
+ on_success: upgradable
diff --git a/tests/functional/test_bundle_upgrades_data/strict_and_non_strict_upgrade/min_cluster/config.yaml b/tests/functional/test_bundle_upgrades_data/strict_and_non_strict_upgrade/min_cluster/config.yaml
index 20ee59e978..19532bcd0a 100644
--- a/tests/functional/test_bundle_upgrades_data/strict_and_non_strict_upgrade/min_cluster/config.yaml
+++ b/tests/functional/test_bundle_upgrades_data/strict_and_non_strict_upgrade/min_cluster/config.yaml
@@ -14,34 +14,31 @@
name: empty_config
version: &version 1.2
- config:
components:
EMPTY_COMPONENT1:
- constraint: [1,+]
+ constraint: [ 1,+ ]
EMPTY_COMPONENT2:
- constraint: [0,+]
--
- type: cluster
- name: strictly
- version: '1.1.0'
- upgrade:
- -
- versions:
- min: 1.1
- min_strict: 1
- description: this upgrade add new config parameters into the cluster
- name: upgrade me
- states:
- available: [created, installed]
- on_success: upgradated
- config:
- template:
- required: false
- description: "Choose a host template to creation in virtual cloud"
- display_name: 'Choose a template'
- type: option
- option:
- template1: cl_02_16
- template2: cl_04_16
- template3: cl_08_32
- template4: cl_08_64
+ constraint: [ 0,+ ]
+- type: cluster
+ name: strictly
+ version: '1.1.0'
+ upgrade:
+ - versions:
+ min: 1.1
+ min_strict: 1
+ description: this upgrade add new config parameters into the cluster
+ name: upgrade me
+ states:
+ available: [ created, installed ]
+ on_success: upgradated
+ config:
+ template:
+ required: false
+ description: "Choose a host template to creation in virtual cloud"
+ display_name: 'Choose a template'
+ type: option
+ option:
+ template1: cl_02_16
+ template2: cl_04_16
+ template3: cl_08_32
+ template4: cl_08_64
diff --git a/tests/functional/test_bundle_upgrades_data/strict_and_non_strict_upgrade/min_hostprovider/config.yaml b/tests/functional/test_bundle_upgrades_data/strict_and_non_strict_upgrade/min_hostprovider/config.yaml
index e26fc6babd..eb28fb5d86 100644
--- a/tests/functional/test_bundle_upgrades_data/strict_and_non_strict_upgrade/min_hostprovider/config.yaml
+++ b/tests/functional/test_bundle_upgrades_data/strict_and_non_strict_upgrade/min_hostprovider/config.yaml
@@ -13,37 +13,22 @@
- type: provider
name: SampleProvider
description: "A sample provider for testing interaction between an old hosts bundles and new functionality with delegation a part of logic to host provider"
- version: &provider_version 0.01
config:
- credentials:
- ansible_host:
- display_name: "Hostname"
- type: string
- required: false
- read_only: any
- description: "Name of the host to connect to"
--
- type: host
- name: ssh_new
- version: '2.1'
- upgrade:
- -
- versions:
- min: 1.0
- min_strict: 1.0
- max: 2.0
- description: Use this upgrade and wish U happy new bundle version
- name: To version 2(ssh_new)
- states:
- available: [created, initiated]
- on_success: upgradable
-
- config:
- ssh-key:
- type: string
- default: 1A2B
- int-key:
- type: integer
- default: 50
- required: false
- display_name: super integer key
+ credentials:
+ ansible_host:
+ display_name: "Hostname"
+ type: string
+ required: false
+ read_only: any
+ description: "Name of the host to connect to"
+ version: '2.1'
+ upgrade:
+ - versions:
+ min: 1.0
+ min_strict: 1.0
+ max: 2.0
+ description: Use this upgrade and wish U happy new bundle version
+ name: To version 2(ssh_new)
+ states:
+ available: [ created, initiated ]
+ on_success: upgradable
diff --git a/tests/functional/test_bundle_upgrades_data/upgradable_cluster_bundle/config.yaml b/tests/functional/test_bundle_upgrades_data/upgradable_cluster_bundle/config.yaml
index 50b048f81c..f2f858b628 100644
--- a/tests/functional/test_bundle_upgrades_data/upgradable_cluster_bundle/config.yaml
+++ b/tests/functional/test_bundle_upgrades_data/upgradable_cluster_bundle/config.yaml
@@ -9,94 +9,91 @@
# 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: ADH
- version: '1.6'
- upgrade:
- -
- versions:
- min: 0.4
- max: 1.5
- name: upgrade to 1.6
- description: New cool upgrade
- states:
- available: any
- on_success: upgradable
- -
- versions:
- min: 1.0
- max: 1.5
- description: Super new upgrade
- name: upgrade 2
- states:
- available: [created, installed]
- on_success: upgradated
- config:
- required:
- type: integer
- required: true
- default: 15
- str-key:
- default: value2
- type: string
- required: false
+- type: cluster
+ name: ADH
+ version: '1.6'
+ upgrade:
+ - versions:
+ min: 0.4
+ max: 1.5
+ name: upgrade to 1.6
+ description: New cool upgrade
+ states:
+ available: any
+ on_success: upgradable
+ - versions:
+ min: 1.0
+ max: 1.5
+ description: Super new upgrade
+ name: upgrade 2
+ states:
+ available: [ created, installed ]
+ on_success: upgradated
+ config:
+ required:
+ type: integer
+ required: true
+ default: 15
+ str-key:
+ default: value2
+ type: string
+ required: false
- int_key:
- type: integer
- required: false
- default: 15000
+ int_key:
+ type: integer
+ required: false
+ default: 15000
- float_key:
- type: float
- required: false
- default: 34.7
+ float_key:
+ type: float
+ required: false
+ default: 34.7
- bool:
- type: boolean
- required : false
- default: false
- option:
- type: option
- option:
- http: 80
- https: 443
- ftp: 21
- required: FALSE
- password:
- default: qwerty
- type: password
- required: false
- json:
- type: json
- required: false
- default:
- {
- "foo": "bar"
- }
- sysctl_params:
- default: >-
- [
- [ "kernel.sysrq", "1" ],
- [ "kernel.core_uses_pid", "1" ],
- [ "kernel.shmmni", "4096" ],
- [ "kernel.sem", "250 912000 100 8192" ],
- [ "kernel.core_uses_pid", "1" ],
- [ "kernel.msgmnb", "65536" ],
- [ "kernel.msgmax", "65536" ],
- [ "kernel.msgmni", "2048" ],
- [ "net.ipv4.tcp_syncookies", "1" ],
- [ "net.ipv4.ip_forward", "0" ],
- [ "net.ipv4.conf.default.accept_source_route", "0" ],
- [ "net.ipv4.tcp_tw_recycle", "1" ],
- [ "net.ipv4.tcp_max_syn_backlog", "4096" ],
- [ "net.ipv4.conf.all.arp_filter", "1" ],
- [ "net.ipv4.ip_local_port_range", "40000 59000" ],
- [ "net.core.netdev_max_backlog", "10000" ],
- [ "net.core.rmem_max", "2097152" ],
- [ "net.core.wmem_max", "2097152" ],
- [ "vm.overcommit_memory", "2" ],
- [ "vm.overcommit_ratio", "93" ],
- [ "kernel.core_pipe_limit", "0" ]
- ]
- type: json
+ bool:
+ type: boolean
+ required: false
+ default: false
+ option:
+ type: option
+ option:
+ http: 80
+ https: 443
+ ftp: 21
+ required: FALSE
+ password:
+ default: qwerty
+ type: password
+ required: false
+ json:
+ type: json
+ required: false
+ default:
+ {
+ "foo": "bar"
+ }
+ sysctl_params:
+ default: >-
+ [
+ [ "kernel.sysrq", "1" ],
+ [ "kernel.core_uses_pid", "1" ],
+ [ "kernel.shmmni", "4096" ],
+ [ "kernel.sem", "250 912000 100 8192" ],
+ [ "kernel.core_uses_pid", "1" ],
+ [ "kernel.msgmnb", "65536" ],
+ [ "kernel.msgmax", "65536" ],
+ [ "kernel.msgmni", "2048" ],
+ [ "net.ipv4.tcp_syncookies", "1" ],
+ [ "net.ipv4.ip_forward", "0" ],
+ [ "net.ipv4.conf.default.accept_source_route", "0" ],
+ [ "net.ipv4.tcp_tw_recycle", "1" ],
+ [ "net.ipv4.tcp_max_syn_backlog", "4096" ],
+ [ "net.ipv4.conf.all.arp_filter", "1" ],
+ [ "net.ipv4.ip_local_port_range", "40000 59000" ],
+ [ "net.core.netdev_max_backlog", "10000" ],
+ [ "net.core.rmem_max", "2097152" ],
+ [ "net.core.wmem_max", "2097152" ],
+ [ "vm.overcommit_memory", "2" ],
+ [ "vm.overcommit_ratio", "93" ],
+ [ "kernel.core_pipe_limit", "0" ]
+ ]
+ type: json
diff --git a/tests/functional/test_bundle_upgrades_data/upgradable_hostprovider_bundle/hostprovider/config.yaml b/tests/functional/test_bundle_upgrades_data/upgradable_hostprovider_bundle/hostprovider/config.yaml
index f85baee0e9..44b389bbad 100644
--- a/tests/functional/test_bundle_upgrades_data/upgradable_hostprovider_bundle/hostprovider/config.yaml
+++ b/tests/functional/test_bundle_upgrades_data/upgradable_hostprovider_bundle/hostprovider/config.yaml
@@ -10,103 +10,99 @@
# See the License for the specific language governing permissions and
# limitations under the License.
--
-
- type: provider
- name: sample hostprovider
- version: '2.0'
- upgrade:
- -
- versions:
- min: 0.4
- max: 1.9
- description: New cool upgrade
- name: upgrade to 2.0
- states:
- available: any
- on_success: upgradable
- -
- versions:
- min: 1.0
- max: 1.9
- description: Super new upgrade
- name: upgrade 2
- states:
- available: [started]
- on_success: ver2.4
- config:
- required:
- type: integer
- required: yes
- default: 400
- max: 500
- min: 200
- str-key:
- default: value
- type: string
- required: false
+- type: provider
+ name: sample hostprovider
+ version: '2.0'
+ upgrade:
+ - versions:
+ min: 0.4
+ max: 1.9
+ description: New cool upgrade
+ name: upgrade to 2.0
+ states:
+ available: any
+ on_success: upgradable
+ - versions:
+ min: 1.0
+ max: 1.9
+ description: Super new upgrade
+ name: upgrade 2
+ states:
+ available: [ started ]
+ on_success: ver2.4
+ config:
+ required:
+ type: integer
+ required: yes
+ default: 400
+ max: 500
+ min: 200
+ str-key:
+ default: value
+ type: string
+ required: false
- int_key:
- type: integer
- required: NO
- default: 60
+ int_key:
+ type: integer
+ required: NO
+ default: 60
- fkey:
- type: float
- required: false
- default: 1.5
+ fkey:
+ type: float
+ required: false
+ default: 1.5
- bool:
- type: boolean
- required : no
- default: false
+ bool:
+ type: boolean
+ required: no
+ default: false
+ option:
+ type: option
+ option:
+ http: 8080
+ https: 4043
+ ftp: my.host
+ required: FALSE
+ sub:
+ sub1:
+ type: option
option:
- type: option
- option:
- http: 8080
- https: 4043
- ftp: my.host
- required: FALSE
- sub:
- sub1:
- type: option
- option:
- a: 1
- s: 2
- d: 3
- required: no
- password:
- type: password
- required: false
- default: q1w2e3r4t5y6
- json:
- type: json
- required: false
- default: {"foo": "bar"}
- host-file:
- type: file
- required: false
- host-text-area:
- type: text
- required: false
- default: lorem ipsum
- read-only-string:
- type: string
- display_name: just read only string for display it name
- default: default value
- required: false
- read_only: any
- writable-when-created:
- type: integer
- default: 999
- required: no
- writable: [created]
- read-only-when_installed:
- type: float
- default: 33.6
- required: false
- read_only: [installed]
-
+ a: 1
+ s: 2
+ d: 3
+ required: no
+ password:
+ type: password
+ required: false
+ default: q1w2e3r4t5y6
+ json:
+ type: json
+ required: false
+ default: { "foo": "bar" }
+ host-file:
+ type: file
+ required: false
+ host-text-area:
+ type: text
+ required: false
+ default: lorem ipsum
+ read-only-string:
+ type: string
+ display_name: just read only string for display it name
+ default: default value
+ required: false
+ read_only: any
+ writable-when-created:
+ type: integer
+ default: 999
+ required: no
+ writable: [ created ]
+ read-only-when_installed:
+ type: float
+ default: 33.6
+ required: false
+ read_only: [ installed ]
+
- type: host
name: vHost
version: 00.09
diff --git a/tests/functional/test_cluster_functions.py b/tests/functional/test_cluster_functions.py
index c603da6347..df0b44ca87 100644
--- a/tests/functional/test_cluster_functions.py
+++ b/tests/functional/test_cluster_functions.py
@@ -9,443 +9,186 @@
# 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 random
-import time
+
+# pylint: disable=redefined-outer-name, protected-access
import allure
import coreapi
import pytest
-from adcm_client.objects import ADCMClient
+from adcm_client.objects import ADCMClient, Bundle, Cluster, Provider, Host
from adcm_pytest_plugin.utils import get_data_dir
from adcm_pytest_plugin import utils
-# pylint: disable=E0401, W0601, W0611, W0621, W0212
from tests.library import errorcodes as err
-from tests.library import steps
-from tests.library.utils import get_random_service, get_random_cluster_prototype
-
-
-@pytest.fixture(scope="module")
-def hostprovider(sdk_client_ms: ADCMClient):
- bundle = sdk_client_ms.upload_from_fs(get_data_dir(__file__, 'hostprovider_bundle'))
- return bundle.provider_create(utils.random_string())
-
-
-@pytest.fixture(scope="module")
-def host(sdk_client_ms: ADCMClient, hostprovider):
- return hostprovider.host_create(utils.random_string())
-
-@pytest.fixture(scope="module")
-def cluster(sdk_client_ms: ADCMClient):
- return sdk_client_ms.upload_from_fs(get_data_dir(__file__, 'cluster_bundle'))
+DEFAULT_CLUSTER_BUNDLE_PATH = get_data_dir(__file__, "cluster_simple")
+DEFAULT_PROVIDER_BUNDLE_PATH = get_data_dir(__file__, "hostprovider_bundle")
-@pytest.fixture(scope="module")
-def client(sdk_client_ms: ADCMClient, cluster, hostprovider):
- return sdk_client_ms.adcm()._api.objects
+@pytest.fixture()
+def cluster_bundle(request, sdk_client_fs: ADCMClient) -> Bundle:
+ bundle_path = (
+ request.param
+ if hasattr(request, "param") else
+ DEFAULT_CLUSTER_BUNDLE_PATH
+ )
+ return sdk_client_fs.upload_from_fs(bundle_path)
-@pytest.fixture(scope="module")
-def client_action_bundle(sdk_client_ms: ADCMClient):
- sdk_client_ms.upload_from_fs(get_data_dir(__file__, 'cluster_action_bundle'))
- return sdk_client_ms.adcm()._api.objects
+@pytest.fixture()
+def cluster(cluster_bundle: Bundle) -> Cluster:
+ return cluster_bundle.cluster_create(name=utils.random_string())
-class TestCluster:
-
- def test_create_cluster_wo_description(self, sdk_client_fs: ADCMClient):
- bundle = sdk_client_fs.upload_from_fs(get_data_dir(__file__, 'cluster_simple'))
- actual = bundle.cluster_create(utils.random_string())
- with allure.step('Check cluster list without description'):
- cluster_list = bundle.cluster_list()
- if cluster_list:
- for cluster in cluster_list:
- cluster_id = cluster.id
- proto_id = cluster.prototype_id
- actual.reread()
- assert actual.id == cluster_id
- assert actual.prototype_id == proto_id
- def test_create_cluster_with_description(self, sdk_client_fs: ADCMClient):
- bundle = sdk_client_fs.upload_from_fs(get_data_dir(__file__, 'cluster_simple'))
- actual = bundle.cluster_create(utils.random_string(), "description")
- with allure.step('Check cluster list with description'):
- cluster_list = bundle.cluster_list()
- cluster = None
- cluster_id = None
- proto_id = None
- if cluster_list:
- for cluster in cluster_list:
- cluster_id = cluster.id
- proto_id = cluster.prototype_id
- actual.reread()
- assert actual.id == cluster_id
- assert actual.prototype_id == proto_id
- assert cluster.description == 'description'
+@pytest.fixture()
+def provider_bundle(request, sdk_client_fs: ADCMClient) -> Bundle:
+ bundle_path = (
+ request.param
+ if hasattr(request, "param") else
+ DEFAULT_PROVIDER_BUNDLE_PATH
+ )
+ return sdk_client_fs.upload_from_fs(bundle_path)
- def test_shouldnt_create_duplicate_cluster(self, sdk_client_fs: ADCMClient):
- bundle = sdk_client_fs.upload_from_fs(get_data_dir(__file__, 'cluster_simple'))
- bundle.cluster_create("duplicate")
- with pytest.raises(coreapi.exceptions.ErrorMessage) as e:
- bundle.cluster_create("duplicate")
- with allure.step('Check error: duplicate cluster'):
- err.CLUSTER_CONFLICT.equal(e, 'duplicate cluster')
- def test_shouldnt_create_cluster_wo_proto(self, client):
- with pytest.raises(coreapi.exceptions.ParameterError) as e:
- client.cluster.create(name=utils.random_string())
- with allure.step('Check error about required parameter prototype_id'):
- assert str(e.value) == "{'prototype_id': 'This parameter is required.'}"
- steps.delete_all_data(client)
+@pytest.fixture()
+def provider(provider_bundle: Bundle) -> Provider:
+ return provider_bundle.provider_create(name=utils.random_string())
- def test_shouldnt_create_cluster_w_blank_prototype(self, client):
- with pytest.raises(coreapi.exceptions.ErrorMessage) as e:
- client.cluster.create(prototype_id='', name=utils.random_string())
- with allure.step('Check error about valid integer'):
- assert e.value.error.title == '400 Bad Request'
- assert e.value.error['prototype_id'][0] == 'A valid integer is required.'
- steps.delete_all_data(client)
- def test_shouldnt_create_cluster_when_proto_is_string(self, client):
- with pytest.raises(coreapi.exceptions.ErrorMessage) as e:
- client.cluster.create(prototype_id=utils.random_string(), name=utils.random_string())
- with allure.step('Check error about valid integer'):
- assert e.value.error.title == '400 Bad Request'
- assert e.value.error['prototype_id'][0] == 'A valid integer is required.'
- steps.delete_all_data(client)
+def _check_hosts(actual: Host, expected: Host):
+ for prop in ["fqdn", "host_id", "cluster_id"]:
+ assert getattr(actual, prop) == getattr(expected, prop)
- def test_shouldnt_create_cluster_if_proto_not_find(self, client):
- with pytest.raises(coreapi.exceptions.ErrorMessage) as e:
- client.cluster.create(prototype_id=random.randint(900, 950), name=utils.random_string())
- with allure.step('Check error about prototype doesn\'t exist'):
- err.PROTOTYPE_NOT_FOUND.equal(e, 'prototype doesn\'t exist')
- steps.delete_all_data(client)
- def test_shouldnt_create_cluster_wo_name(self, client):
- prototype = get_random_cluster_prototype(client)
- with pytest.raises(coreapi.exceptions.ErrorMessage) as e:
- client.cluster.create(prototype_id=prototype['id'], name='')
- with allure.step('Check error about blank field'):
- assert e.value.error.title == '400 Bad Request'
- assert e.value.error['name'] == ['This field may not be blank.']
- steps.delete_all_data(client)
-
- def test_shoulndt_create_cluster_when_desc_is_null(self, sdk_client_fs: ADCMClient):
- bundle = sdk_client_fs.upload_from_fs(get_data_dir(__file__, 'cluster_simple'))
- with pytest.raises(coreapi.exceptions.ErrorMessage) as e:
- bundle.cluster_create(name=utils.random_string(), description='')
- with allure.step('Check error about blank field'):
- assert e.value.error.title == '400 Bad Request'
- assert e.value.error['description'] == ["This field may not be blank."]
-
- def test_get_cluster_list(self, sdk_client_fs: ADCMClient):
- bundle = sdk_client_fs.upload_from_fs(get_data_dir(__file__, 'cluster_simple'))
- expectedlist = []
- actuallist = []
+class TestCluster:
+ def test_get_cluster_list(self, cluster_bundle: Bundle):
+ actual, expected = [], []
# Create list of clusters and fill expected list
for name in utils.random_string_list():
- bundle.cluster_create(name)
- expectedlist.append(name)
- for cluster in bundle.cluster_list():
- actuallist.append(cluster.name)
- with allure.step('Check cluster list'):
- assert all(a == b for a, b in zip(actuallist, expectedlist))
-
- def test_get_cluster_info(self, client):
- actual = steps.create_cluster(client)
- expected = steps.read_cluster(client, actual['id'])
- # status is a variable, so it is no good to compare it
- assert 'status' in actual
- assert 'status' in expected
- del actual['status']
- del expected['status']
- with allure.step('Check cluster info'):
- assert actual == expected
- steps.delete_all_data(client)
-
- def test_partial_update_cluster_name(self, client):
- name = utils.random_string()
- cluster = steps.create_cluster(client)
- with allure.step('Trying to update cluster'):
- expected = steps.partial_update_cluster(client, cluster, name)
- with allure.step('Take actual data about cluster'):
- actual = steps.read_cluster(client, cluster['id'])
- with allure.step('Check actual and expected result'):
- assert expected == actual
- steps.delete_all_data(client)
-
- def test_partial_update_cluster_name_and_desc(self, client):
- name = utils.random_string()
- desc = utils.random_string()
- with allure.step('Create cluster'):
- cluster = steps.create_cluster(client)
- with allure.step('Trying to update cluster name and description'):
- expected = steps.partial_update_cluster(client, cluster, name, desc)
- with allure.step('Take actual data about cluster'):
- actual = steps.read_cluster(client, cluster['id'])
- with allure.step('Check actual and expected result'):
- assert expected['name'] == name
- assert expected == actual
- steps.delete_all_data(client)
-
- def test_partial_update_duplicate_cluster_name(self, client): # ADCM-74
- name = utils.random_string()
- cluster_one = steps.create_cluster(client)
- steps.partial_update_cluster(client, cluster_one, name)
- cluster_two = steps.create_cluster(client)
- with pytest.raises(coreapi.exceptions.ErrorMessage) as e:
- steps.partial_update_cluster(client, cluster_two, name)
- with allure.step('Check error that cluster already exists'):
- err.CLUSTER_CONFLICT.equal(e, 'cluster with name', 'already exists')
- steps.delete_all_data(client)
-
- def test_delete_cluster(self, sdk_client_fs: ADCMClient):
- bundle = sdk_client_fs.upload_from_fs(get_data_dir(__file__, 'cluster_simple'))
- expected = None
- cluster = bundle.cluster_create(utils.random_string())
- with allure.step('Delete cluster'):
- actual = cluster.delete()
- with allure.step('Check result is None'):
+ cluster_bundle.cluster_create(name)
+ expected.append(name)
+ for cluster in cluster_bundle.cluster_list():
+ actual.append(cluster.name)
+ with allure.step("Check cluster list"):
assert actual == expected
- def test_should_return_correct_error_when_delete_nonexist(self, sdk_client_fs: ADCMClient):
- bundle = sdk_client_fs.upload_from_fs(get_data_dir(__file__, 'cluster_simple'))
- cluster = bundle.cluster_create(utils.random_string())
- cluster.delete()
- with pytest.raises(coreapi.exceptions.ErrorMessage) as e:
- cluster.delete()
- with allure.step('Check error that cluster doesn\'t exist'):
- err.CLUSTER_NOT_FOUND.equal(e, 'cluster doesn\'t exist')
-
- def test_correct_error_when_user_try_to_read_nonexist_cluster(self, sdk_client_fs: ADCMClient):
- bundle = sdk_client_fs.upload_from_fs(get_data_dir(__file__, 'cluster_simple'))
- cluster = bundle.cluster_create(utils.random_string())
- cluster.delete()
- with allure.step('Try to get unknown cluster'):
- with pytest.raises(coreapi.exceptions.ErrorMessage) as e:
- cluster.reread()
- with allure.step('Check error that cluster doesn\'t exist'):
- err.CLUSTER_NOT_FOUND.equal(e, 'cluster doesn\'t exist')
-
- def test_correct_error_when_user_try_to_get_incorrect_cluster(self, client):
- with allure.step('Try to get unknown cluster'):
- with pytest.raises(coreapi.exceptions.ErrorMessage) as e:
- client.cluster.read(cluster_id=random.randint(500, 999))
- with allure.step('Check error that cluster doesn\'t exist'):
- err.CLUSTER_NOT_FOUND.equal(e, 'cluster doesn\'t exist')
- steps.delete_all_data(client)
-
- def test_try_to_create_cluster_with_unknown_prototype(self, client):
- with allure.step('Try to create cluster with unknown prototype'):
- with pytest.raises(coreapi.exceptions.ErrorMessage) as e:
- client.cluster.create(prototype_id=random.randint(100, 500),
- name=utils.random_string())
- with allure.step('Check error that cluster doesn\'t exist'):
- err.PROTOTYPE_NOT_FOUND.equal(e, 'prototype doesn\'t exist')
- steps.delete_all_data(client)
-
- def test_run_cluster_action(self, sdk_client_fs: ADCMClient):
- bundle = sdk_client_fs.upload_from_fs(get_data_dir(__file__, 'cluster_action_bundle'))
- cluster = bundle.cluster_create(utils.random_string())
+ def test_creating_cluster_with_name_and_desc(self, cluster_bundle: Bundle):
+ name, description = utils.random_string_list(2)
+ cluster = cluster_bundle.cluster_create(name=name, description=description)
+ with allure.step("Check created cluster"):
+ assert cluster.name == name
+ assert cluster.description == description
+
+ @pytest.mark.parametrize(
+ "cluster_bundle",
+ [
+ pytest.param(
+ get_data_dir(__file__, "cluster_action_bundle"),
+ id="cluster_action_bundle",
+ )
+ ],
+ indirect=True,
+ )
+ def test_run_cluster_action(self, cluster: Cluster):
cluster.config_set({"required": 10})
cluster.service_add(name="ZOOKEEPER")
- result = cluster.action_run()
- with allure.step('Check if status is running'):
- assert result.status == 'running'
+ result = cluster.action().run()
+ with allure.step("Check if status is running"):
+ assert result.status == "running"
class TestClusterHost:
-
- def test_adding_host_to_cluster(self, sdk_client_fs: ADCMClient):
- bundle = sdk_client_fs.upload_from_fs(get_data_dir(__file__, 'cluster_simple'))
- bundle_hp = sdk_client_fs.upload_from_fs(get_data_dir(__file__, 'hostprovider_bundle'))
- cluster = bundle.cluster_create(utils.random_string())
- hp = bundle_hp.provider_create(utils.random_string())
- host = hp.host_create(utils.random_string())
- actual = cluster.host_add(host)
- with allure.step('Get cluster host info'):
- expected = cluster.host_list()[0]
- with allure.step('Check mapping'):
- assert actual.fqdn == expected.fqdn
- assert actual.id == expected.id
-
- def test_get_cluster_hosts_list(self, sdk_client_fs: ADCMClient):
- bundle = sdk_client_fs.upload_from_fs(get_data_dir(__file__, 'cluster_simple'))
- bundle_hp = sdk_client_fs.upload_from_fs(get_data_dir(__file__, 'hostprovider_bundle'))
- cluster = bundle.cluster_create(utils.random_string())
- hp = bundle_hp.provider_create(utils.random_string())
- host_list = []
- actual_list = []
- with allure.step('Create host list in cluster'):
+ def test_adding_host_to_cluster(self, cluster: Cluster, provider: Provider):
+ host = provider.host_create(utils.random_string())
+ expected = cluster.host_add(host)
+ with allure.step("Get cluster host info"):
+ host_list = cluster.host_list()
+ assert len(host_list) == 1
+ actual = host_list[0]
+ with allure.step("Check mapping"):
+ _check_hosts(actual, expected)
+
+ def test_get_cluster_hosts_list(self, cluster: Cluster, provider: Provider):
+ actual, expected = [], []
+ with allure.step("Create host list in cluster"):
for fqdn in utils.random_string_list():
- host = hp.host_create(fqdn)
- time.sleep(3)
+ host = provider.host_create(fqdn)
cluster.host_add(host)
- host_list.append(host.id)
+ expected.append(host.id)
for host in cluster.host_list():
- actual_list.append(host.id)
- with allure.step('Check test data'):
- assert host_list == actual_list
+ actual.append(host.id)
+ with allure.step("Check test data"):
+ assert actual == expected
- def test_get_cluster_host_info(self, sdk_client_fs: ADCMClient):
- bundle = sdk_client_fs.upload_from_fs(get_data_dir(__file__, 'cluster_simple'))
- bundle_hp = sdk_client_fs.upload_from_fs(get_data_dir(__file__, 'hostprovider_bundle'))
- cluster = bundle.cluster_create(utils.random_string())
- hp = bundle_hp.provider_create(utils.random_string())
- host = hp.host_create(utils.random_string())
- with allure.step('Create mapping between cluster and host'):
+ def test_get_cluster_host_info(self, cluster: Cluster, provider: Provider):
+ host = provider.host_create(utils.random_string())
+ with allure.step("Create mapping between cluster and host"):
expected = cluster.host_add(host)
- with allure.step('Get cluster host info'):
+ with allure.step("Get cluster host info"):
host.reread()
- actual = host
- with allure.step('Check test results'):
- assert expected.fqdn == actual.fqdn
- assert expected.host_id == actual.host_id
- assert expected.cluster_id == actual.cluster_id
+ with allure.step("Check test results"):
+ _check_hosts(host, expected)
- def test_delete_host_from_cluster(self, sdk_client_fs: ADCMClient):
- bundle = sdk_client_fs.upload_from_fs(get_data_dir(__file__, 'cluster_simple'))
- bundle_hp = sdk_client_fs.upload_from_fs(get_data_dir(__file__, 'hostprovider_bundle'))
- cluster = bundle.cluster_create(utils.random_string())
- hp = bundle_hp.provider_create(utils.random_string())
- host = hp.host_create(utils.random_string())
+ def test_delete_host_from_cluster(self, cluster: Cluster, provider: Provider):
+ host = provider.host_create(utils.random_string())
expected = cluster.host_list()
- with allure.step('Create mapping between cluster and host'):
+ with allure.step("Create mapping between cluster and host"):
cluster.host_add(host)
- with allure.step('Deleting host from cluster'):
+ with allure.step("Deleting host from cluster"):
cluster.host_delete(host)
actual = cluster.host_list()
- with allure.step('Check host removed from cluster'):
+ with allure.step("Check host removed from cluster"):
assert actual == expected
- def test_shouldnt_create_duplicate_host_in_cluster(self, sdk_client_fs: ADCMClient):
- bundle = sdk_client_fs.upload_from_fs(get_data_dir(__file__, 'cluster_simple'))
- bundle_hp = sdk_client_fs.upload_from_fs(get_data_dir(__file__, 'hostprovider_bundle'))
- cluster = bundle.cluster_create(utils.random_string())
- hp = bundle_hp.provider_create(utils.random_string())
- host = hp.host_create(utils.random_string())
- with allure.step('Create mapping between cluster and host'):
- cluster.host_add(host)
- with pytest.raises(coreapi.exceptions.ErrorMessage) as e:
- cluster.host_add(host)
- with allure.step('Check error about duplicate host in cluster'):
- err.HOST_CONFLICT.equal(e, 'duplicate host in cluster')
-
- def test_add_unknown_host_to_cluster(self, client):
- cluster = steps.create_cluster(client)
- with allure.step('Create mapping between cluster and unknown host'):
- with pytest.raises(coreapi.exceptions.ErrorMessage) as e:
- client.cluster.host.create(cluster_id=cluster['id'],
- host_id=random.randint(900, 950))
- with allure.step('Check error host doesn\'t exist'):
- err.HOST_NOT_FOUND.equal(e, 'host doesn\'t exist')
- steps.delete_all_data(client)
-
- def test_add_host_in_two_diff_clusters(self, sdk_client_fs: ADCMClient):
- bundle = sdk_client_fs.upload_from_fs(get_data_dir(__file__, 'cluster_simple'))
- bundle_hp = sdk_client_fs.upload_from_fs(get_data_dir(__file__, 'hostprovider_bundle'))
- cluster_one = bundle.cluster_create("cluster1")
- cluster_two = bundle.cluster_create("cluster2")
- hp = bundle_hp.provider_create(utils.random_string())
- host = hp.host_create("new.host.net")
- cluster_one.host_add(host)
- with pytest.raises(coreapi.exceptions.ErrorMessage) as e:
- cluster_two.host_add(host)
- with allure.step('Check error host belong to cluster'):
- err.FOREIGN_HOST.equal(e, 'Host', 'belong to cluster #' + str(cluster_one.id))
-
- def test_host_along_to_cluster_shouldnt_deleted(self, sdk_client_fs: ADCMClient):
- bundle = sdk_client_fs.upload_from_fs(get_data_dir(__file__, 'cluster_simple'))
- bundle_hp = sdk_client_fs.upload_from_fs(get_data_dir(__file__, 'hostprovider_bundle'))
- cluster = bundle.cluster_create(utils.random_string())
- hp = bundle_hp.provider_create(utils.random_string())
- host = hp.host_create(utils.random_string())
+ def test_host_along_to_cluster_should_not_deleted(
+ self, cluster: Cluster, provider: Provider
+ ):
+ host = provider.host_create(utils.random_string())
cluster.host_add(host)
with pytest.raises(coreapi.exceptions.ErrorMessage) as e:
host.delete()
- with allure.step('Check error host belong to cluster'):
- err.HOST_CONFLICT.equal(e, 'Host', 'belong to cluster')
+ with allure.step("Check error host belong to cluster"):
+ err.HOST_CONFLICT.equal(e, "Host", "belong to cluster")
class TestClusterService:
- def test_cluster_service_create(self, sdk_client_fs: ADCMClient):
- bundle = sdk_client_fs.upload_from_fs(get_data_dir(__file__, 'cluster_simple'))
- cluster = bundle.cluster_create(utils.random_string())
- actual = cluster.service_add(name="ZOOKEEPER")
- expected = cluster.service_list()[0]
- with allure.step('Check expected and actual value'):
+ def test_cluster_service_create(self, cluster: Cluster):
+ expected = cluster.service_add(name="ZOOKEEPER")
+ service_list = cluster.service_list()
+ assert len(service_list) == 1
+ actual = service_list[0]
+ with allure.step("Check expected and actual value"):
assert actual.id == expected.id
assert actual.name == expected.name
- def test_get_cluster_service_list(self, client):
- cluster = steps.create_cluster(client)
+ def test_get_cluster_service_list(
+ self, sdk_client_fs: ADCMClient, cluster: Cluster
+ ):
expected = []
- with allure.step('Create a list of services in the cluster'):
- for service in client.stack.service.list():
- expected.append(client.cluster.service.create(cluster_id=cluster['id'],
- prototype_id=service['id']))
- with allure.step('Get a service list in cluster'):
- actual = client.cluster.service.list(cluster_id=cluster['id'])
- with allure.step('Check expected and actual value'):
- assert expected == actual
- steps.delete_all_data(client)
-
- def test_shouldnt_create_service_w_id_eq_negative_number(self, client):
- cluster = steps.create_cluster(client)
- with allure.step('Try to create service with id as a negative number'):
- with pytest.raises(coreapi.exceptions.ErrorMessage) as e:
- client.cluster.service.create(
- cluster_id=cluster['id'],
- prototype_id=(get_random_service(client)['id'] * -1))
- with allure.step('Check error prototype doesn\'t exist'):
- err.PROTOTYPE_NOT_FOUND.equal(e, 'prototype doesn\'t exist')
- steps.delete_all_data(client)
-
- def test_souldnt_create_service_w_id_as_string(self, client):
- cluster = steps.create_cluster(client)
- with allure.step('Try to create service with id as a negative number'):
- with pytest.raises(coreapi.exceptions.ErrorMessage) as e:
- client.cluster.service.create(cluster_id=cluster['id'],
- prototype_id=utils.random_string())
- with allure.step('Check error about valid integer'):
- assert e.value.error.title == '400 Bad Request'
- assert e.value.error['prototype_id'][0] == 'A valid integer is required.'
- steps.delete_all_data(client)
-
- def test_shouldnt_add_two_identical_service_in_cluster(self, sdk_client_fs: ADCMClient):
- bundle = sdk_client_fs.upload_from_fs(get_data_dir(__file__, 'cluster_simple'))
- cluster = bundle.cluster_create(utils.random_string())
- cluster.service_add(name="ZOOKEEPER")
- with pytest.raises(coreapi.exceptions.ErrorMessage) as e:
- cluster.service_add(name="ZOOKEEPER")
- with allure.step('Check error that service already exists in specified cluster'):
- err.SERVICE_CONFLICT.equal(e, 'service already exists in specified cluster')
-
- @pytest.mark.skip(reason="Task is non production right now")
- def test_that_task_generator_function_with_the_only_one_reqiured_parameter(self, client):
- cluster = steps.create_cluster(client)
- client.cluster.config.history.create(cluster_id=cluster['id'], config={"required": 10})
- with allure.step('Create service in cluster'):
- service = steps.create_random_service(client, cluster['id'])
- action = client.cluster.service.action.list(cluster_id=cluster['id'],
- service_id=service['id'])
- with allure.step('Try to run service action'):
- with pytest.raises(coreapi.exceptions.ErrorMessage) as e:
- client.cluster.service.action.run.create(cluster_id=cluster['id'],
- service_id=service['id'],
- action_id=action[0]['id'])
- with allure.step('Check error about positional arguments'):
- err.TASK_GENERATOR_ERROR.equal(
- e, 'task_generator() takes 1 positional argument but 2 were given')
- steps.delete_all_data(client)
+ with allure.step("Create a list of services in the cluster"):
+ for prototype in sdk_client_fs.service_prototype_list():
+ service = cluster.service_add(prototype_id=prototype.id)
+ expected.append(service._data)
+ with allure.step("Get a service list in cluster"):
+ actual = [x._data for x in cluster.service_list()]
+ with allure.step("Check expected and actual value"):
+ assert actual == expected
- def test_cluster_action_runs_task(self, sdk_client_fs: ADCMClient):
- bundle = sdk_client_fs.upload_from_fs(get_data_dir(__file__, 'cluster_action_bundle'))
- cluster = bundle.cluster_create(utils.random_string())
+ @pytest.mark.parametrize(
+ "cluster_bundle",
+ [
+ pytest.param(
+ get_data_dir(__file__, "cluster_action_bundle"),
+ id="cluster_action_bundle",
+ )
+ ],
+ indirect=True,
+ )
+ def test_cluster_action_runs_task(self, cluster: Cluster):
cluster.config_set({"required": 10})
cluster.service_add(name="ZOOKEEPER")
- task = cluster.action_run(name='check-file-type')
- with allure.step('Check if status is running'):
- assert task.status == 'running'
+ task = cluster.action(name="check-file-type").run()
+ with allure.step("Check if status is running"):
+ assert task.status == "running"
diff --git a/tests/functional/test_cluster_functions_data/cluster_action_bundle/cluster/config.yaml b/tests/functional/test_cluster_functions_data/cluster_action_bundle/cluster/config.yaml
index 525fd5bdcc..88a9454068 100644
--- a/tests/functional/test_cluster_functions_data/cluster_action_bundle/cluster/config.yaml
+++ b/tests/functional/test_cluster_functions_data/cluster_action_bundle/cluster/config.yaml
@@ -10,22 +10,20 @@
# See the License for the specific language governing permissions and
# limitations under the License.
--
+- type: cluster
+ name: ADH16
+ version: 1.6
- type: cluster
- name: ADH16
- version: 1.6
-
- actions:
- check-file-type:
- type: job
- script: cluster/check_data_in_file.yaml
- script_type: ansible
- states:
- available: [created]
- on_success: all_installed
- on_fail: cluster_install_fail
- config:
- required:
- type: integer
- required: true
+ actions:
+ check-file-type:
+ type: job
+ script: cluster/check_data_in_file.yaml
+ script_type: ansible
+ states:
+ available: [ created ]
+ on_success: all_installed
+ on_fail: cluster_install_fail
+ config:
+ required:
+ type: integer
+ required: true
diff --git a/tests/functional/test_cluster_functions_data/cluster_action_bundle/services/ZOOKEEPER/config.yaml b/tests/functional/test_cluster_functions_data/cluster_action_bundle/services/ZOOKEEPER/config.yaml
index 7616feb3d2..76866146b8 100644
--- a/tests/functional/test_cluster_functions_data/cluster_action_bundle/services/ZOOKEEPER/config.yaml
+++ b/tests/functional/test_cluster_functions_data/cluster_action_bundle/services/ZOOKEEPER/config.yaml
@@ -9,33 +9,34 @@
# 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: ZOOKEEPER
-type: service
-description: ZooKeeper
-version: '1.2'
+---
+- name: ZOOKEEPER
+ type: service
+ description: ZooKeeper
+ version: '1.2'
-components:
+ components:
ZOOKEEPER_CLIENT:
- params:
- folder: stacks/ADH/1.0/services/ZOOKEEPER/package
- packages: [zookeeper]
- script: scripts/zookeeper_client.py
+ params:
+ folder: stacks/ADH/1.0/services/ZOOKEEPER/package
+ packages: [ zookeeper ]
+ script: scripts/zookeeper_client.py
ZOOKEEPER_SERVER:
- params:
- folder: stacks/ADH/1.0/services/ZOOKEEPER/package
- packages: [zookeeper]
- script: scripts/zookeeper_server.py
+ params:
+ folder: stacks/ADH/1.0/services/ZOOKEEPER/package
+ packages: [ zookeeper ]
+ script: scripts/zookeeper_server.py
-config:
- ssh-key: {default: TItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAA, type: string, required: false}
- integer-key: {default: 24, max: 48, min: 2, type: integer, required: false}
- float-key: {default: 4.4, max: 50.0, min: 4.0, type: float, required: false}
+ config:
+ ssh-key: { default: TItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAA, type: string, required: false }
+ integer-key: { default: 24, max: 48, min: 2, type: integer, required: false }
+ float-key: { default: 4.4, max: 50.0, min: 4.0, type: float, required: false }
zoo.cfg:
- autopurge.purgeInterval: {default: 24, max: 48, min: 2, type: integer}
- dataDir: {default: /hadoop/zookeeper, type: string}
- port:
- required: false
- default: 80
- option: {http: 80, https: 443}
- type: option
- required-key: {default: value, type: string}
+ autopurge.purgeInterval: { default: 24, max: 48, min: 2, type: integer }
+ dataDir: { default: /hadoop/zookeeper, type: string }
+ port:
+ required: false
+ default: 80
+ option: { http: 80, https: 443 }
+ type: option
+ required-key: { default: value, type: string }
diff --git a/tests/functional/test_cluster_functions_data/cluster_bundle/cluster/config.yaml b/tests/functional/test_cluster_functions_data/cluster_bundle/cluster/config.yaml
index 02a77686fd..3352e16d82 100644
--- a/tests/functional/test_cluster_functions_data/cluster_bundle/cluster/config.yaml
+++ b/tests/functional/test_cluster_functions_data/cluster_bundle/cluster/config.yaml
@@ -10,49 +10,47 @@
# See the License for the specific language governing permissions and
# limitations under the License.
--
-
- type: cluster
- name: ADH
- version: 1.5
-
- config:
- required:
- type: integer
- required: true
- str-key:
- default: value
- type: string
- required: false
-
- int_key:
- type: integer
- required: false
-
- fkey:
- type: float
- required: false
-
- bool:
- type: boolean
- required : false
- option:
- type: option
- option:
- http: 80
- https: 443
- ftp: 21
- required: FALSE
-
- textarea:
- type: text
- required: false
- default: loren ipsum
-
- password_phrase:
- type: password
- required: false
-
- input_file:
- type: file
- required: false
+- type: cluster
+ name: ADH
+ version: 1.5
+
+ config:
+ required:
+ type: integer
+ required: true
+ str-key:
+ default: value
+ type: string
+ required: false
+
+ int_key:
+ type: integer
+ required: false
+
+ fkey:
+ type: float
+ required: false
+
+ bool:
+ type: boolean
+ required: false
+ option:
+ type: option
+ option:
+ http: 80
+ https: 443
+ ftp: 21
+ required: FALSE
+
+ textarea:
+ type: text
+ required: false
+ default: loren ipsum
+
+ password_phrase:
+ type: password
+ required: false
+
+ input_file:
+ type: file
+ required: false
diff --git a/tests/functional/test_cluster_functions_data/cluster_bundle/services/ZOOKEEPER/config.yaml b/tests/functional/test_cluster_functions_data/cluster_bundle/services/ZOOKEEPER/config.yaml
index 7616feb3d2..a024b8b423 100644
--- a/tests/functional/test_cluster_functions_data/cluster_bundle/services/ZOOKEEPER/config.yaml
+++ b/tests/functional/test_cluster_functions_data/cluster_bundle/services/ZOOKEEPER/config.yaml
@@ -9,33 +9,33 @@
# 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: ZOOKEEPER
-type: service
-description: ZooKeeper
-version: '1.2'
+- name: ZOOKEEPER
+ type: service
+ description: ZooKeeper
+ version: '1.2'
-components:
+ components:
ZOOKEEPER_CLIENT:
- params:
- folder: stacks/ADH/1.0/services/ZOOKEEPER/package
- packages: [zookeeper]
- script: scripts/zookeeper_client.py
+ params:
+ folder: stacks/ADH/1.0/services/ZOOKEEPER/package
+ packages: [ zookeeper ]
+ script: scripts/zookeeper_client.py
ZOOKEEPER_SERVER:
- params:
- folder: stacks/ADH/1.0/services/ZOOKEEPER/package
- packages: [zookeeper]
- script: scripts/zookeeper_server.py
+ params:
+ folder: stacks/ADH/1.0/services/ZOOKEEPER/package
+ packages: [ zookeeper ]
+ script: scripts/zookeeper_server.py
-config:
- ssh-key: {default: TItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAA, type: string, required: false}
- integer-key: {default: 24, max: 48, min: 2, type: integer, required: false}
- float-key: {default: 4.4, max: 50.0, min: 4.0, type: float, required: false}
+ config:
+ ssh-key: { default: TItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAA, type: string, required: false }
+ integer-key: { default: 24, max: 48, min: 2, type: integer, required: false }
+ float-key: { default: 4.4, max: 50.0, min: 4.0, type: float, required: false }
zoo.cfg:
- autopurge.purgeInterval: {default: 24, max: 48, min: 2, type: integer}
- dataDir: {default: /hadoop/zookeeper, type: string}
- port:
- required: false
- default: 80
- option: {http: 80, https: 443}
- type: option
- required-key: {default: value, type: string}
+ autopurge.purgeInterval: { default: 24, max: 48, min: 2, type: integer }
+ dataDir: { default: /hadoop/zookeeper, type: string }
+ port:
+ required: false
+ default: 80
+ option: { http: 80, https: 443 }
+ type: option
+ required-key: { default: value, type: string }
diff --git a/tests/functional/test_cluster_functions_data/cluster_simple/cluster/config.yaml b/tests/functional/test_cluster_functions_data/cluster_simple/cluster/config.yaml
index b532e6faea..e1d74a8506 100644
--- a/tests/functional/test_cluster_functions_data/cluster_simple/cluster/config.yaml
+++ b/tests/functional/test_cluster_functions_data/cluster_simple/cluster/config.yaml
@@ -10,9 +10,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
--
-
- type: cluster
- name: ADH
- version: 1.4
+- type: cluster
+ name: ADH
+ version: 1.4
diff --git a/tests/functional/test_cluster_functions_data/cluster_simple/services/ZOOKEEPER/config.yaml b/tests/functional/test_cluster_functions_data/cluster_simple/services/ZOOKEEPER/config.yaml
index 7616feb3d2..a024b8b423 100644
--- a/tests/functional/test_cluster_functions_data/cluster_simple/services/ZOOKEEPER/config.yaml
+++ b/tests/functional/test_cluster_functions_data/cluster_simple/services/ZOOKEEPER/config.yaml
@@ -9,33 +9,33 @@
# 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: ZOOKEEPER
-type: service
-description: ZooKeeper
-version: '1.2'
+- name: ZOOKEEPER
+ type: service
+ description: ZooKeeper
+ version: '1.2'
-components:
+ components:
ZOOKEEPER_CLIENT:
- params:
- folder: stacks/ADH/1.0/services/ZOOKEEPER/package
- packages: [zookeeper]
- script: scripts/zookeeper_client.py
+ params:
+ folder: stacks/ADH/1.0/services/ZOOKEEPER/package
+ packages: [ zookeeper ]
+ script: scripts/zookeeper_client.py
ZOOKEEPER_SERVER:
- params:
- folder: stacks/ADH/1.0/services/ZOOKEEPER/package
- packages: [zookeeper]
- script: scripts/zookeeper_server.py
+ params:
+ folder: stacks/ADH/1.0/services/ZOOKEEPER/package
+ packages: [ zookeeper ]
+ script: scripts/zookeeper_server.py
-config:
- ssh-key: {default: TItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAA, type: string, required: false}
- integer-key: {default: 24, max: 48, min: 2, type: integer, required: false}
- float-key: {default: 4.4, max: 50.0, min: 4.0, type: float, required: false}
+ config:
+ ssh-key: { default: TItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAA, type: string, required: false }
+ integer-key: { default: 24, max: 48, min: 2, type: integer, required: false }
+ float-key: { default: 4.4, max: 50.0, min: 4.0, type: float, required: false }
zoo.cfg:
- autopurge.purgeInterval: {default: 24, max: 48, min: 2, type: integer}
- dataDir: {default: /hadoop/zookeeper, type: string}
- port:
- required: false
- default: 80
- option: {http: 80, https: 443}
- type: option
- required-key: {default: value, type: string}
+ autopurge.purgeInterval: { default: 24, max: 48, min: 2, type: integer }
+ dataDir: { default: /hadoop/zookeeper, type: string }
+ port:
+ required: false
+ default: 80
+ option: { http: 80, https: 443 }
+ type: option
+ required-key: { default: value, type: string }
diff --git a/tests/functional/test_cluster_functions_data/hostprovider_bundle/provider/config.yaml b/tests/functional/test_cluster_functions_data/hostprovider_bundle/provider/config.yaml
index 2e7fb5d911..ecbff9f78f 100644
--- a/tests/functional/test_cluster_functions_data/hostprovider_bundle/provider/config.yaml
+++ b/tests/functional/test_cluster_functions_data/hostprovider_bundle/provider/config.yaml
@@ -19,29 +19,29 @@
version: "1.01"
config:
required:
- type: integer
- required: yes
- default: 40
+ type: integer
+ required: yes
+ default: 40
str-key:
- default: value
- type: string
- required: false
+ default: value
+ type: string
+ required: false
int_key:
- type: integer
- required: NO
- default:
+ type: integer
+ required: NO
+ default: 1
fkey:
- type: float
- required: false
- default: 1
+ type: float
+ required: false
+ default: 1
bool:
- type: boolean
- required : no
- default: false
+ type: boolean
+ required: no
+ default: false
option:
- type: option
- option:
- http: 8080
- https: 4043
- ftp: my.host
- required: FALSE
+ type: option
+ option:
+ http: 8080
+ https: 4043
+ ftp: my.host
+ required: FALSE
diff --git a/tests/functional/test_cluster_service_config_functions.py b/tests/functional/test_cluster_service_config_functions.py
index b18ca14354..07a9d70ddf 100644
--- a/tests/functional/test_cluster_service_config_functions.py
+++ b/tests/functional/test_cluster_service_config_functions.py
@@ -9,582 +9,455 @@
# 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 random
+from typing import Tuple, Any
import allure
import coreapi
import pytest
+from adcm_client.base import BaseAPIObject
+from adcm_client.objects import ADCMClient, Cluster, Service, Bundle, Provider
from adcm_pytest_plugin import utils
-from adcm_pytest_plugin.docker_utils import DockerWrapper
+from adcm_pytest_plugin.steps.actions import run_cluster_action_and_assert_result
from jsonschema import validate
-# pylint: disable=E0401, W0601, W0611, W0621
+# pylint: disable=E0401, W0601, W0611, W0621, W0212
from tests.library import errorcodes as err
-from tests.library import steps
-from tests.library.utils import (
- get_random_service,
- get_random_cluster_service_component,
- get_action_by_name, wait_until
-)
BUNDLES = os.path.join(os.path.dirname(__file__), "../stack/")
SCHEMAS = os.path.join(os.path.dirname(__file__), "schemas/")
-@pytest.fixture(scope="module")
-def adcm(image, request, adcm_credentials):
- repo, tag = image
- dw = DockerWrapper()
- adcm = dw.run_adcm(image=repo, tag=tag, pull=False)
- adcm.api.auth(**adcm_credentials)
- yield adcm
- adcm.stop()
+@pytest.fixture()
+def cluster_bundle(sdk_client_fs: ADCMClient) -> Bundle:
+ return sdk_client_fs.upload_from_fs(BUNDLES + "cluster_bundle")
+
+
+@pytest.fixture()
+def cluster(cluster_bundle: Bundle) -> Cluster:
+ return cluster_bundle.cluster_create(name=utils.random_string())
+
+
+@pytest.fixture()
+def cluster_with_service(cluster: Cluster) -> Tuple[Cluster, Service]:
+ service = cluster.service_add()
+ return cluster, service
+
+
+@pytest.fixture()
+def provider_bundle(sdk_client_fs: ADCMClient) -> Bundle:
+ return sdk_client_fs.upload_from_fs(BUNDLES + "hostprovider_bundle")
+
+
+@pytest.fixture()
+def provider(provider_bundle: Bundle) -> Provider:
+ return provider_bundle.provider_create(name=utils.random_string())
+
+
+def _get_prev_config(obj: BaseAPIObject, full=False):
+ """Copy of config() method"""
+ history_entry = obj._subcall("config", "previous", "list")
+ if full:
+ return history_entry
+ return history_entry["config"]
-@pytest.fixture(scope="module")
-def client(adcm):
- steps.upload_bundle(adcm.api.objects, BUNDLES + "cluster_bundle")
- steps.upload_bundle(adcm.api.objects, BUNDLES + "hostprovider_bundle")
- return adcm.api.objects
+def _get_config_history(obj: BaseAPIObject):
+ return obj._subcall("config", "history", "list")
class TestClusterServiceConfig:
- def test_create_cluster_service_config(self, client):
- cluster = steps.create_cluster(client)
- cfg_json = {"ssh-key": "TItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAA",
- "zoo.cfg": {"autopurge.purgeInterval": 30, "dataDir": "/dev/0", "port": 80},
- "required-key": "value"}
- with allure.step('Create service'):
- cluster_svc = client.cluster.service.create(
- cluster_id=cluster['id'], prototype_id=get_random_service(client)['id'])
- with allure.step('Create config'):
- config = client.cluster.service.config.history.create(cluster_id=cluster['id'],
- service_id=cluster_svc['id'],
- description='simple desc',
- config=cfg_json)
- with allure.step('Check created config'):
- expected = client.cluster.service.config.history.read(cluster_id=cluster['id'],
- service_id=cluster_svc['id'],
- version=config['id'])
- assert config == expected
- steps.delete_all_data(client)
-
- def test_shouldnt_create_service_config_when_config_not_json(self, client):
- cluster = steps.create_cluster(client)
- with allure.step('Create service on the cluster'):
- cluster_svc = client.cluster.service.create(
- cluster_id=cluster['id'], prototype_id=get_random_service(client)['id'])
- with allure.step('Try to create config from non-json string'):
- with pytest.raises(coreapi.exceptions.ErrorMessage) as e:
- client.cluster.service.config.history.create(cluster_id=cluster['id'],
- service_id=cluster_svc['id'],
- config=utils.random_string())
- with allure.step('Check error that config should not be just one string'):
- err.JSON_ERROR.equal(e, 'config should not be just one string')
- steps.delete_all_data(client)
-
- def test_shouldnt_create_service_config_when_config_is_number(self, client): # ADCM-86
- cluster = steps.create_cluster(client)
- with allure.step('Create service on the cluster'):
- cluster_svc = client.cluster.service.create(
- cluster_id=cluster['id'], prototype_id=get_random_service(client)['id'])
- with allure.step('Try to create config from a number'):
- with pytest.raises(coreapi.exceptions.ErrorMessage) as e:
- client.cluster.service.config.history.create(cluster_id=cluster['id'],
- service_id=cluster_svc['id'],
- config=random.randint(0, 9))
- with allure.step('Check error that config should not be just one int or float'):
- err.JSON_ERROR.equal(e, 'should not be just one int or float')
- steps.delete_all_data(client)
-
- def test_shouldnt_create_service_config_when_config_doesnt_have_one_req_sub(self, client):
- cluster = steps.create_cluster(client)
- config_wo_required_sub = {"ssh-key": "TItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAA",
- "zoo.cfg": {"autopurge.purgeInterval": 34},
- "required-key": "110"}
- with allure.step('Create service on the cluster'):
- cluster_svc = client.cluster.service.create(
- cluster_id=cluster['id'], prototype_id=get_random_service(client)['id'])
- with allure.step('Try to create config when config doesn\'t have required'):
- with pytest.raises(coreapi.exceptions.ErrorMessage) as e:
- client.cluster.service.config.history.create(cluster_id=cluster['id'],
- service_id=cluster_svc['id'],
- config=config_wo_required_sub)
- with allure.step('Check error about no required subkey'):
- err.CONFIG_KEY_ERROR.equal(e, 'There is no required subkey')
- steps.delete_all_data(client)
-
- def test_shouldnt_create_service_config_when_config_doesnt_have_one_req_key(self, client):
- cluster = steps.create_cluster(client)
- config_wo_required_key = {"ssh-key": "TItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAA",
- "zoo.cfg": {"autopurge.purgeInterval": 34,
- "dataDir": "/zookeeper", "port": 80}}
- with allure.step('Create service on the cluster'):
- cluster_svc = client.cluster.service.create(
- cluster_id=cluster['id'], prototype_id=get_random_service(client)['id'])
- with allure.step('Try to create config without required key'):
- with pytest.raises(coreapi.exceptions.ErrorMessage) as e:
- client.cluster.service.config.history.create(cluster_id=cluster['id'],
- service_id=cluster_svc['id'],
- config=config_wo_required_key)
- with allure.step('Check error about no required key'):
- err.CONFIG_KEY_ERROR.equal(e, 'There is no required key')
- steps.delete_all_data(client)
-
- def test_shouldnt_create_service_config_when_parameter_is_not_integer(self, client):
- cluster = steps.create_cluster(client)
- config_w_illegal_param = {"ssh-key": "TItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAA",
- "zoo.cfg": {"autopurge.purgeInterval": "blabla",
- "dataDir": "/zookeeper", "port": 80},
- "required-key": "value"}
- with allure.step('Create service on the cluster'):
- cluster_svc = client.cluster.service.create(
- cluster_id=cluster['id'], prototype_id=get_random_service(client)['id'])
- with allure.step('Try to create config when parameter is not integer'):
- with pytest.raises(coreapi.exceptions.ErrorMessage) as e:
- client.cluster.service.config.history.create(cluster_id=cluster['id'],
- service_id=cluster_svc['id'],
- config=config_w_illegal_param)
- with allure.step('Check error that parameter is not integer'):
- err.CONFIG_VALUE_ERROR.equal(e, 'should be integer')
- steps.delete_all_data(client)
-
- def test_shouldnt_create_service_config_when_parameter_is_not_float(self, client):
- cluster = steps.create_cluster(client)
- config_w_illegal_param = {"ssh-key": "TItbmlzHyNTAAIbmzdHAyNTYAAA", "float-key": "blah",
- "zoo.cfg": {"autopurge.purgeInterval": 30,
- "dataDir": "/zookeeper", "port": 80},
- "required-key": "value"}
- with allure.step('Create service on the cluster'):
- cluster_svc = client.cluster.service.create(
- cluster_id=cluster['id'], prototype_id=get_random_service(client)['id'])
- with allure.step('Try to create config when param is not float'):
- with pytest.raises(coreapi.exceptions.ErrorMessage) as e:
- client.cluster.service.config.history.create(cluster_id=cluster['id'],
- service_id=cluster_svc['id'],
- config=config_w_illegal_param)
- with allure.step('Check error that parameter is not float'):
- err.CONFIG_VALUE_ERROR.equal(e, 'should be float')
- steps.delete_all_data(client)
-
- def test_shouldnt_create_service_config_when_parameter_is_not_string(self, client):
- cluster = steps.create_cluster(client)
- config_w_illegal_param = {"ssh-key": "TItbmlzdHAyNTYAAAAIbmlzdHAyNTY", "float-key": 5.7,
- "zoo.cfg": {"autopurge.purgeInterval": 30,
- "dataDir": "/zookeeper", "port": 80},
- "required-key": 500}
- with allure.step('Create service on the cluster'):
- cluster_svc = client.cluster.service.create(
- cluster_id=cluster['id'], prototype_id=get_random_service(client)['id'])
- with allure.step('Try to create config when param is not float'):
- with pytest.raises(coreapi.exceptions.ErrorMessage) as e:
- client.cluster.service.config.history.create(cluster_id=cluster['id'],
- service_id=cluster_svc['id'],
- config=config_w_illegal_param)
- with allure.step('Check error that parameter is not string'):
- err.CONFIG_VALUE_ERROR.equal(e, 'should be string')
- steps.delete_all_data(client)
-
- def test_shouldnt_create_service_config_when_parameter_is_not_in_option_list(self, client):
- cluster = steps.create_cluster(client)
- config_w_illegal_param = {"ssh-key": "TItbmlzdHAyNTYAIbmlzdHAyNTYAAA", "float-key": 4.5,
- "zoo.cfg": {"autopurge.purgeInterval": 30,
- "dataDir": "/zookeeper", "port": 500},
- "required-key": "value"}
- with allure.step('Create service on the cluster'):
- cluster_svc = client.cluster.service.create(
- cluster_id=cluster['id'], prototype_id=get_random_service(client)['id'])
- with allure.step('Try to create config has not option in a list'):
- with pytest.raises(coreapi.exceptions.ErrorMessage) as e:
- client.cluster.service.config.history.create(cluster_id=cluster['id'],
- service_id=cluster_svc['id'],
- config=config_w_illegal_param)
- with allure.step('Check CONFIG_VALUE_ERROR'):
- assert e.value.error.title == '400 Bad Request'
- assert e.value.error['code'] == 'CONFIG_VALUE_ERROR'
- assert ('not in option list' in e.value.error['desc']) is True
- steps.delete_all_data(client)
-
- def test_shouldnt_create_service_config_when_integer_param_bigger_than_boundary(self, client):
- cluster = steps.create_cluster(client)
- config_int_bigger_boundary = {"ssh-key": "TItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAA",
- "zoo.cfg": {"autopurge.purgeInterval": 999,
- "dataDir": "/zookeeper", "port": 80},
- "required-key": "value"}
- with allure.step('Create service on the cluster'):
- cluster_svc = client.cluster.service.create(
- cluster_id=cluster['id'], prototype_id=get_random_service(client)['id'])
- with allure.step('Try to create config when integer bigger than boundary'):
- with pytest.raises(coreapi.exceptions.ErrorMessage) as e:
- client.cluster.service.config.history.create(cluster_id=cluster['id'],
- service_id=cluster_svc['id'],
- config=config_int_bigger_boundary)
- with allure.step('Check error that integer bigger than boundary'):
- err.CONFIG_VALUE_ERROR.equal(e, 'Value', 'should be less than')
- steps.delete_all_data(client)
-
- def test_shouldnt_create_service_config_when_integer_param_less_than_boundary(self, client):
- cluster = steps.create_cluster(client)
- config_int_less_boundary = {"ssh-key": "TItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAA",
- "zoo.cfg": {"autopurge.purgeInterval": 0,
- "dataDir": "/zookeeper", "port": 80},
- "required-key": "value"}
- with allure.step('Create service on the cluster'):
- cluster_svc = client.cluster.service.create(
- cluster_id=cluster['id'], prototype_id=get_random_service(client)['id'])
- with allure.step('Try to create config when integer less than boundary'):
- with pytest.raises(coreapi.exceptions.ErrorMessage) as e:
- client.cluster.service.config.history.create(cluster_id=cluster['id'],
- service_id=cluster_svc['id'],
- config=config_int_less_boundary)
- with allure.step('Check error that integer less than boundary'):
- err.CONFIG_VALUE_ERROR.equal(e, 'Value', 'should be more than')
- steps.delete_all_data(client)
-
- def test_shouldnt_create_service_config_when_float_param_bigger_than_boundary(self, client):
- cluster = steps.create_cluster(client)
- config_float_bigger_boundary = {"ssh-key": "TItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAA",
- "zoo.cfg": {"autopurge.purgeInterval": 24,
- "dataDir": "/zookeeper", "port": 80},
- "float-key": 50.5, "required-key": "value"}
- with allure.step('Create service on the cluster'):
- cluster_svc = client.cluster.service.create(
- cluster_id=cluster['id'], prototype_id=get_random_service(client)['id'])
- with allure.step('Try to create config when float bigger than boundary'):
- with pytest.raises(coreapi.exceptions.ErrorMessage) as e:
- client.cluster.service.config.history.create(cluster_id=cluster['id'],
- service_id=cluster_svc['id'],
- config=config_float_bigger_boundary)
- with allure.step('Check error that float bigger than boundary'):
- err.CONFIG_VALUE_ERROR.equal(e, 'Value', 'should be less than')
- steps.delete_all_data(client)
-
- def test_shouldnt_create_service_config_when_float_param_less_than_boundary(self, client):
- cluster = steps.create_cluster(client)
- config_float_less_boundary = {"ssh-key": "TItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAA",
- "zoo.cfg": {"autopurge.purgeInterval": 24,
- "dataDir": "/zookeeper", "port": 80},
- "float-key": 3.3, "required-key": "value"}
- with allure.step('Create service on the cluster'):
- cluster_svc = client.cluster.service.create(
- cluster_id=cluster['id'], prototype_id=get_random_service(client)['id'])
- with allure.step('Try to create config when float less than boundary'):
- with pytest.raises(coreapi.exceptions.ErrorMessage) as e:
- client.cluster.service.config.history.create(cluster_id=cluster['id'],
- service_id=cluster_svc['id'],
- config=config_float_less_boundary)
- with allure.step('Check error that float less than boundary'):
- err.CONFIG_VALUE_ERROR.equal(e, 'Value', 'should be more than')
- steps.delete_all_data(client)
-
- def test_shouldnt_create_service_config_when_config_doesnt_have_all_req_param(self, client):
- cluster = steps.create_cluster(client)
- config_wo_required_param = {"ssh-key": "TItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAA"}
- with allure.step('Create service on the cluster'):
- cluster_svc = client.cluster.service.create(
- cluster_id=cluster['id'], prototype_id=get_random_service(client)['id'])
- with allure.step('Try to create config when config doesnt have all params'):
- with pytest.raises(coreapi.exceptions.ErrorMessage) as e:
- client.cluster.service.config.history.create(cluster_id=cluster['id'],
- service_id=cluster_svc['id'],
- config=config_wo_required_param)
- with allure.step('Check error about params'):
- err.CONFIG_KEY_ERROR.equal(e)
- steps.delete_all_data(client)
-
- def test_shouldnt_create_service_config_when_config_have_unknown_subkey(self, client):
- cluster = steps.create_cluster(client)
- config_w_unknown_subkey = {"ssh-key": "TItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAA",
- "zoo.cfg": {"autopurge.purgeInterval": 24,
- "dataDir": "/zookeeper", "portium": "http"},
- "required-key": "value"}
- with allure.step('Create service on the cluster'):
- cluster_svc = client.cluster.service.create(
- cluster_id=cluster['id'], prototype_id=get_random_service(client)['id'])
- with allure.step('Try to create config with unknown subkey'):
- with pytest.raises(coreapi.exceptions.ErrorMessage) as e:
- client.cluster.service.config.history.create(cluster_id=cluster['id'],
- service_id=cluster_svc['id'],
- config=config_w_unknown_subkey)
- with allure.step('Check error about unknown subkey'):
- err.CONFIG_KEY_ERROR.equal(e, 'There is unknown subkey')
- steps.delete_all_data(client)
-
- def test_shouldnt_create_service_config_when_config_have_unknown_param(self, client):
- cluster = steps.create_cluster(client)
- config_w_unknown_param = {"name": "foo"}
- with allure.step('Create service on the cluster'):
- cluster_svc = client.cluster.service.create(
- cluster_id=cluster['id'], prototype_id=get_random_service(client)['id'])
- with allure.step('Try to create config with unknown parameter'):
- with pytest.raises(coreapi.exceptions.ErrorMessage) as e:
- client.cluster.service.config.history.create(cluster_id=cluster['id'],
- service_id=cluster_svc['id'],
- config=config_w_unknown_param)
- with allure.step('Check error about unknown key'):
- err.CONFIG_KEY_ERROR.equal(e, 'There is unknown key')
- steps.delete_all_data(client)
-
- def test_shouldnt_create_service_config_when_key_shouldnt_have_any_subkeys(self, client):
- cluster = steps.create_cluster(client)
- config_shouldnt_have_subkeys = {"ssh-key": {"key": "value"},
- "zoo.cfg": {"autopurge.purgeInterval": "24",
- "dataDir": "/zookeeper", "port": "http"}}
- with allure.step('Create service on the cluster'):
- cluster_svc = client.cluster.service.create(
- cluster_id=cluster['id'], prototype_id=get_random_service(client)['id'])
- with allure.step('Try to create config where param shouldn\'t have any subkeys'):
+ def test_create_cluster_service_config(
+ self, cluster_with_service: Tuple[Cluster, Service]
+ ):
+ cfg_json = {
+ "ssh-key": "TItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAA",
+ "zoo.cfg": {"autopurge.purgeInterval": 30, "dataDir": "/dev/0", "port": 80},
+ "required-key": "value",
+ }
+ _, cluster_svc = cluster_with_service
+ with allure.step("Create config"):
+ cluster_svc.config_set(cfg_json)
+ with allure.step("Check created config"):
+ assert cluster_svc.config() == cfg_json
+
+ INVALID_SERVICE_CONFIGS = [
+ pytest.param(
+ {
+ "ssh-key": "TItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAA",
+ "zoo.cfg": {"autopurge.purgeInterval": 34},
+ "required-key": "110",
+ },
+ (err.CONFIG_KEY_ERROR, "There is no required subkey"),
+ id="without_one_required_sub_key",
+ ),
+ pytest.param(
+ {
+ "ssh-key": "TItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAA",
+ "zoo.cfg": {
+ "autopurge.purgeInterval": 34,
+ "dataDir": "/zookeeper",
+ "port": 80,
+ },
+ },
+ (err.CONFIG_KEY_ERROR, "There is no required key"),
+ id="without_one_required_key",
+ ),
+ pytest.param(
+ {
+ "ssh-key": "TItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAA",
+ "zoo.cfg": {
+ "autopurge.purgeInterval": "blabla",
+ "dataDir": "/zookeeper",
+ "port": 80,
+ },
+ "required-key": "value",
+ },
+ (err.CONFIG_VALUE_ERROR, "should be integer"),
+ id="int_value_set_to_string",
+ ),
+ pytest.param(
+ {
+ "ssh-key": "TItbmlzHyNTAAIbmzdHAyNTYAAA",
+ "float-key": "blah",
+ "zoo.cfg": {
+ "autopurge.purgeInterval": 30,
+ "dataDir": "/zookeeper",
+ "port": 80,
+ },
+ "required-key": "value",
+ },
+ (err.CONFIG_VALUE_ERROR, "should be float"),
+ id="float_value_set_to_string",
+ ),
+ pytest.param(
+ {
+ "ssh-key": "TItbmlzdHAyNTYAAAAIbmlzdHAyNTY",
+ "float-key": 5.7,
+ "zoo.cfg": {
+ "autopurge.purgeInterval": 30,
+ "dataDir": "/zookeeper",
+ "port": 80,
+ },
+ "required-key": 500,
+ },
+ (err.CONFIG_VALUE_ERROR, "should be string"),
+ id="string_value_set_to_int",
+ ),
+ pytest.param(
+ {
+ "ssh-key": "TItbmlzdHAyNTYAIbmlzdHAyNTYAAA",
+ "float-key": 4.5,
+ "zoo.cfg": {
+ "autopurge.purgeInterval": 30,
+ "dataDir": "/zookeeper",
+ "port": 500,
+ },
+ "required-key": "value",
+ },
+ (err.CONFIG_VALUE_ERROR, "not in option list"),
+ id="parameter_is_not_in_option_list",
+ ),
+ pytest.param(
+ {
+ "ssh-key": "TItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAA",
+ "zoo.cfg": {
+ "autopurge.purgeInterval": 999,
+ "dataDir": "/zookeeper",
+ "port": 80,
+ },
+ "required-key": "value",
+ },
+ (err.CONFIG_VALUE_ERROR, "should be less than"),
+ id="int_value_greater_than_max",
+ ),
+ pytest.param(
+ {
+ "ssh-key": "TItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAA",
+ "zoo.cfg": {
+ "autopurge.purgeInterval": 0,
+ "dataDir": "/zookeeper",
+ "port": 80,
+ },
+ "required-key": "value",
+ },
+ (err.CONFIG_VALUE_ERROR, "should be more than"),
+ id="int_value_less_than_min",
+ ),
+ pytest.param(
+ {
+ "ssh-key": "TItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAA",
+ "zoo.cfg": {
+ "autopurge.purgeInterval": 24,
+ "dataDir": "/zookeeper",
+ "port": 80,
+ },
+ "float-key": 50.5,
+ "required-key": "value",
+ },
+ (err.CONFIG_VALUE_ERROR, "should be less than"),
+ id="float_value_greater_than_max",
+ ),
+ pytest.param(
+ {
+ "ssh-key": "TItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAA",
+ "zoo.cfg": {
+ "autopurge.purgeInterval": 24,
+ "dataDir": "/zookeeper",
+ "port": 80,
+ },
+ "float-key": 3.3,
+ "required-key": "value",
+ },
+ (err.CONFIG_VALUE_ERROR, "should be more than"),
+ id="float_value_less_than_min",
+ ),
+ pytest.param(
+ {"ssh-key": "TItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAA"},
+ (err.CONFIG_KEY_ERROR, str()),
+ id="without_all_required_params",
+ ),
+ pytest.param(
+ {
+ "ssh-key": "TItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAA",
+ "zoo.cfg": {
+ "autopurge.purgeInterval": 24,
+ "dataDir": "/zookeeper",
+ "portium": "http",
+ },
+ "required-key": "value",
+ },
+ (err.CONFIG_KEY_ERROR, "There is unknown subkey"),
+ id="unknown_sub_key",
+ ),
+ pytest.param(
+ {"name": "foo"},
+ (err.CONFIG_KEY_ERROR, "There is unknown key"),
+ id="unknown_param",
+ ),
+ pytest.param(
+ {
+ "ssh-key": {"key": "value"},
+ "zoo.cfg": {
+ "autopurge.purgeInterval": "24",
+ "dataDir": "/zookeeper",
+ "port": "http",
+ },
+ },
+ (err.CONFIG_KEY_ERROR, "input config should not have any subkeys"),
+ id="unexpected_sub_key",
+ ),
+ pytest.param(
+ {
+ "ssh-key": "as32fKj14fT88",
+ "zoo.cfg": {
+ "autopurge.purgeInterval": 24,
+ "dataDir": "/zookeeper",
+ "port": {"foo": "bar"},
+ },
+ "required-key": "value",
+ },
+ (err.CONFIG_VALUE_ERROR, "should be flat"),
+ id="scalar_value_set_to_dict",
+ ),
+ ]
+
+ @pytest.mark.parametrize(
+ ("service_config", "expected_error"), INVALID_SERVICE_CONFIGS
+ )
+ def test_should_not_create_service_with_invalid_config(
+ self,
+ cluster_with_service: Tuple[Cluster, Service],
+ service_config: Any,
+ expected_error: Tuple[err.ADCMError, str],
+ ):
+ _, cluster_svc = cluster_with_service
+ adcm_error, expected_msg = expected_error
+ with allure.step("Try to set invalid config"):
+ allure.attach(
+ json.dumps(service_config), "config.json", allure.attachment_type.JSON
+ )
with pytest.raises(coreapi.exceptions.ErrorMessage) as e:
- client.cluster.service.config.history.create(cluster_id=cluster['id'],
- service_id=cluster_svc['id'],
- config=config_shouldnt_have_subkeys)
- with allure.step('Check error about unknown subkey'):
- err.CONFIG_KEY_ERROR.equal(e, 'input config should not have any subkeys')
- steps.delete_all_data(client)
-
- def test_shouldnt_create_service_config_when_try_to_put_dictionary_in_flat_key(self, client):
- cluster = steps.create_cluster(client)
- config = {"ssh-key": "as32fKj14fT88",
- "zoo.cfg": {"autopurge.purgeInterval": 24, "dataDir": "/zookeeper",
- "port": {"foo": "bar"}}, "required-key": "value"}
- with allure.step('Create service on the cluster'):
- cluster_svc = client.cluster.service.create(
- cluster_id=cluster['id'], prototype_id=get_random_service(client)['id'])
- with allure.step('Try to create config where in flat param we put a dictionary'):
+ cluster_svc.config_set(service_config)
+ with allure.step("Check error"):
+ adcm_error.equal(e, expected_msg)
+
+ def test_when_delete_host_all_children_cannot_be_deleted(
+ self, cluster_with_service: Tuple[Cluster, Service], provider: Provider
+ ):
+ host = provider.host_create(fqdn=utils.random_string())
+ cluster, service = cluster_with_service
+ cluster.host_add(host)
+ with allure.step("Create hostcomponent"):
+ component = service.component()
+ cluster.hostcomponent_set((host, component))
+ with allure.step(f"Removing host id={host.id}"):
with pytest.raises(coreapi.exceptions.ErrorMessage) as e:
- client.cluster.service.config.history.create(cluster_id=cluster['id'],
- service_id=cluster_svc['id'],
- config=config)
- with allure.step('Check error about flat param'):
- err.CONFIG_VALUE_ERROR.equal(e, 'should be flat')
- steps.delete_all_data(client)
-
- def test_when_delete_host_all_children_cannot_be_deleted(self, client):
- # Should be faild if random service has not components
- cluster = steps.create_cluster(client)
- with allure.step('Create provider'):
- provider = client.provider.create(prototype_id=client.stack.provider.list()[0]['id'],
- name=utils.random_string())
- with allure.step('Create host'):
- host = client.host.create(prototype_id=client.stack.host.list()[0]['id'],
- provider_id=provider['id'],
- fqdn=utils.random_string())
- steps.add_host_to_cluster(client, host, cluster)
- with allure.step('Create random service'):
- service = steps.create_random_service(client, cluster['id'])
- with allure.step('Create random service component'):
- component = get_random_cluster_service_component(client, cluster, service)
- with allure.step('Create hostcomponent'):
- steps.create_hostcomponent_in_cluster(client, cluster, host, service, component)
- with pytest.raises(coreapi.exceptions.ErrorMessage) as e:
- client.cluster.host.delete(host_id=host['id'], cluster_id=cluster['id'])
- with allure.step('Check host conflict'):
+ host.delete()
+ with allure.step("Check host conflict"):
err.HOST_CONFLICT.equal(e)
- def test_should_throws_exception_when_havent_previous_config(self, client):
- cluster = steps.create_cluster(client)
- service = steps.create_random_service(client, cluster['id'])
- with allure.step('Try to get previous version of the service config'):
+ def test_should_throws_exception_when_havent_previous_config(
+ self, cluster_with_service: Tuple[Cluster, Service]
+ ):
+ _, service = cluster_with_service
+ with allure.step("Try to get previous version of the service config"):
with pytest.raises(coreapi.exceptions.ErrorMessage) as e:
- client.cluster.service.config.previous.list(cluster_id=cluster['id'],
- service_id=service['id'])
- with allure.step('Check error that config version doesn\'t exist'):
- err.CONFIG_NOT_FOUND.equal(e, 'config version doesn\'t exist')
- steps.delete_all_data(client)
+ _get_prev_config(service)
+ with allure.step("Check error that config version doesn't exist"):
+ err.CONFIG_NOT_FOUND.equal(e, "ConfigLog", "does not exist")
class TestClusterServiceConfigHistory:
- def test_config_history_url_must_point_to_the_service_config(self, client):
- cluster = steps.create_cluster(client)
- service = steps.create_random_service(client, cluster['id'])
- config_str = {"ssh-key": "eulav", "integer-key": 23, "required-key": "10",
- "float-key": 38.5, "zoo.cfg": {"autopurge.purgeInterval": 40,
- "dataDir": "/opt/data", "port": 80}}
- i = 0
- while i < random.randint(0, 10):
- client.cluster.service.config.history.create(cluster_id=cluster['id'],
- service_id=service['id'],
- description=utils.random_string(),
- config=config_str)
- i += 1
- history = client.cluster.service.config.history.list(cluster_id=cluster['id'],
- service_id=service['id'])
- with allure.step('Check config history'):
- for conf in history:
- assert ('cluster/{0}/service/'.format(cluster['id']) in conf['url']) is True
- steps.delete_all_data(client)
-
- def test_get_config_from_nonexistant_cluster_service(self, client):
- cluster = steps.create_cluster(client)
+ # Do we really need this test?
+ def test_config_history_url_must_point_to_the_service_config(
+ self, cluster_with_service: Tuple[Cluster, Service]
+ ):
+ _, service = cluster_with_service
+ config_str = {
+ "ssh-key": "eulav",
+ "integer-key": 23,
+ "required-key": "10",
+ "float-key": 38.5,
+ "zoo.cfg": {
+ "autopurge.purgeInterval": 40,
+ "dataDir": "/opt/data",
+ "port": 80,
+ },
+ }
+ for _ in range(10):
+ service.config_set(config_str)
+ with allure.step("Check config history"):
+ for conf in _get_config_history(service):
+ # url changed, because request is related to the service
+ assert "/service/{}".format(service.id) in conf["url"]
+
+ def test_get_config_from_nonexistant_cluster_service(
+ self, cluster_with_service: Tuple[Cluster, Service]
+ ):
+ _, service = cluster_with_service
+ with allure.step(f"Removing service id={service.id}"):
+ service.delete()
with pytest.raises(coreapi.exceptions.ErrorMessage) as e:
- client.cluster.service.config.list(cluster_id=cluster['id'],
- service_id=random.randint(100, 500))
- with allure.step('Check error that service doesn\'t exist'):
- err.SERVICE_NOT_FOUND.equal(e, "service doesn\'t exist")
- steps.delete_all_data(client)
+ service.config()
+ with allure.step("Check error that service doesn't exist"):
+ err.CLUSTER_SERVICE_NOT_FOUND.equal(e, "ClusterObject", "does not exist")
class TestClusterConfig:
- def test_config_history_url_must_point_to_the_cluster_config(self, client):
- cluster = steps.create_cluster(client)
+ # Do we really need this test?
+ def test_config_history_url_must_point_to_the_cluster_config(
+ self, cluster: Cluster
+ ):
config_str = {"required": 10, "int_key": 50, "bool": False, "str-key": "eulav"}
- i = 0
- with allure.step('Create config history'):
- while i < random.randint(0, 10):
- client.cluster.config.history.create(cluster_id=cluster['id'],
- description=utils.random_string(),
- config=config_str)
- i += 1
- history = client.cluster.config.history.list(cluster_id=cluster['id'])
- with allure.step('Check config history'):
+ with allure.step("Create config history"):
+ for _ in range(10):
+ cluster.config_set(config_str)
+ history = _get_config_history(cluster)
+ with allure.step("Check config history"):
for conf in history:
- assert ('api/v1/cluster/{0}/config/'.format(cluster['id']) in conf['url']) is True
- steps.delete_all_data(client)
-
- def test_read_default_cluster_config(self, client):
- cluster = steps.create_cluster(client)
- config = client.cluster.config.current.list(cluster_id=cluster['id'])
- if config:
- config_json = utils.ordered_dict_to_dict(config)
- with allure.step('Load schema'):
- schema = json.load(open(SCHEMAS + '/config_item_schema.json'))
- with allure.step('Check schema'):
+ assert "api/v1/cluster/{0}/config/".format(cluster.id) in conf["url"]
+
+ def test_read_default_cluster_config(self, cluster: Cluster):
+ config = cluster.config(full=True)
+ config_json = utils.ordered_dict_to_dict(config)
+ with allure.step("Load schema"):
+ schema = json.load(open(SCHEMAS + "/config_item_schema.json"))
+ with allure.step("Check schema"):
assert validate(config_json, schema) is None
- steps.delete_all_data(client)
-
- def test_create_new_config_version_with_one_req_parameter(self, client):
- cluster = steps.create_cluster(client)
- cfg = {"required": random.randint(0, 9)}
- with allure.step('Create new config'):
- new_config = client.cluster.config.history.create(cluster_id=cluster['id'], config=cfg)
- with allure.step('Create config history'):
- expected = client.cluster.config.history.read(cluster_id=cluster['id'],
- version=new_config['id'])
- with allure.step('Check new config'):
- assert new_config == expected
- steps.delete_all_data(client)
-
- def test_create_new_config_version_with_other_parameters(self, client):
- cluster = steps.create_cluster(client)
+
+ def test_create_new_config_version_with_one_req_parameter(self, cluster: Cluster):
+ cfg = {"required": 42}
+ expected = cluster.config_set(cfg)
+ with allure.step("Check new config"):
+ assert cluster.config() == expected
+
+ def test_create_new_config_version_with_other_parameters(self, cluster: Cluster):
cfg = {"required": 99, "str-key": utils.random_string()}
- with allure.step('Create new config'):
- new_config = client.cluster.config.history.create(cluster_id=cluster['id'], config=cfg)
- with allure.step('Create config history'):
- expected = client.cluster.config.history.read(cluster_id=cluster['id'],
- version=new_config['id'])
- with allure.step('Check new config'):
- assert new_config == expected
- steps.delete_all_data(client)
-
- def test_shouldnt_create_cluster_config_when_config_not_json(self, client):
- cluster = steps.create_cluster(client)
- with allure.step('Try to create the cluster config from non-json string'):
- with pytest.raises(coreapi.exceptions.ErrorMessage) as e:
- client.cluster.config.history.create(cluster_id=cluster['id'],
- config=utils.random_string())
- with allure.step('Check that config should not be just one string'):
- err.JSON_ERROR.equal(e, 'config should not be just one string')
- steps.delete_all_data(client)
-
- def test_shouldnt_create_service_config_when_config_is_number(self, client): # ADCM-86
- cluster = steps.create_cluster(client)
- with allure.step('Try to create config from number'):
- with pytest.raises(coreapi.exceptions.ErrorMessage) as e:
- client.cluster.config.history.create(cluster_id=cluster['id'],
- config=random.randint(0, 9))
- with allure.step('Check that config should not be just one int or float'):
- err.JSON_ERROR.equal(e, 'config should not be just one int or float')
- steps.delete_all_data(client)
-
- def test_shouldnt_create_config_when_config_doesnt_have_required_key(self, client):
- cluster = steps.create_cluster(client)
- config_wo_required_key = {"str-key": "value"}
- with allure.step('Try to create config wo required key'):
- with pytest.raises(coreapi.exceptions.ErrorMessage) as e:
- client.cluster.config.history.create(cluster_id=cluster['id'],
- config=config_wo_required_key)
- with allure.step('Check that no required key'):
- err.CONFIG_KEY_ERROR.equal(e, 'There is no required key')
- steps.delete_all_data(client)
-
- def test_shouldnt_create_config_when_key_is_not_in_option_list(self, client):
- cluster = steps.create_cluster(client)
- config_key_not_in_list = {"option": "bluh", "required": 10}
- with allure.step('Try to create config has not option in a list'):
- with pytest.raises(coreapi.exceptions.ErrorMessage) as e:
- client.cluster.config.history.create(cluster_id=cluster['id'],
- config=config_key_not_in_list)
- with allure.step('Check that not in option list'):
- err.CONFIG_VALUE_ERROR.equal(e, 'Value', 'not in option list')
- steps.delete_all_data(client)
-
- def test_shouldnt_create_config_with_unknown_key(self, client):
- # config has key that not defined in prototype
- cluster = steps.create_cluster(client)
- config = {"new_key": "value"}
- with allure.step('Try to create config with unknown key'):
- with pytest.raises(coreapi.exceptions.ErrorMessage) as e:
- client.cluster.config.history.create(cluster_id=cluster['id'], config=config)
- with allure.step('Check that unknown key'):
- err.CONFIG_KEY_ERROR.equal(e, 'There is unknown key')
- steps.delete_all_data(client)
-
- def test_shouldnt_create_config_when_try_to_put_map_in_option(self, client):
- # we try to put key:value in a parameter with the option datatype
- cluster = steps.create_cluster(client)
- config_with_deep_depth = {"str-key": "{1bbb}", "option": {"http": "string"},
- "sub": {"sub1": "f"}}
- with allure.step('Try to create config with map in flat key'):
+ expected = cluster.config_set(cfg)
+ with allure.step("Check new config"):
+ assert cluster.config() == expected
+
+ INVALID_CLUSTER_CONFIGS = [
+ pytest.param(
+ {"str-key": "value"},
+ (err.CONFIG_KEY_ERROR, "There is no required key"),
+ id="without_required_key",
+ ),
+ pytest.param(
+ {"option": "bluh", "required": 10},
+ (err.CONFIG_VALUE_ERROR, "not in option list"),
+ id="param_not_in_option_list",
+ ),
+ pytest.param(
+ {"new_key": "value"},
+ (err.CONFIG_KEY_ERROR, "There is unknown key"),
+ id="unknown_key",
+ ),
+ pytest.param(
+ {"str-key": "{1bbb}", "option": {"http": "string"}, "sub": {"sub1": "f"}},
+ (err.CONFIG_KEY_ERROR, "input config should not have any subkeys"),
+ id="map_in_option",
+ ),
+ ]
+
+ @pytest.mark.parametrize(
+ ("cluster_config", "expected_error"), INVALID_CLUSTER_CONFIGS
+ )
+ def test_should_not_create_cluster_with_invalid_config(
+ self,
+ cluster: Cluster,
+ cluster_config: Any,
+ expected_error: Tuple[err.ADCMError, str],
+ ):
+ adcm_error, expected_msg = expected_error
+ with allure.step("Try to set invalid config"):
+ allure.attach(
+ json.dumps(cluster_config), "config.json", allure.attachment_type.JSON
+ )
with pytest.raises(coreapi.exceptions.ErrorMessage) as e:
- client.cluster.config.history.create(cluster_id=cluster['id'],
- config=config_with_deep_depth)
- with allure.step('Check that input config should not have any subkeys'):
- err.CONFIG_KEY_ERROR.equal(e, 'input config should not have any subkeys')
- steps.delete_all_data(client)
+ cluster.config_set(cluster_config)
+ with allure.step("Check error"):
+ adcm_error.equal(e, expected_msg)
- def test_get_nonexistant_cluster_config(self, client):
+ def test_get_nonexistant_cluster_config(self, cluster: Cluster):
# we try to get a nonexistant cluster config, test should raise exception
- with allure.step('Get cluster config from non existant cluster'):
+ with allure.step(f"Removing cluster id={cluster.id}"):
+ cluster.delete()
+ with allure.step("Get cluster config from non existent cluster"):
with pytest.raises(coreapi.exceptions.ErrorMessage) as e:
- client.cluster.config.list(cluster_id=random.randint(100, 500))
- with allure.step('Check that cluster doesn\'t exist'):
- err.CLUSTER_NOT_FOUND.equal(e, 'cluster doesn\'t exist')
- steps.delete_all_data(client)
+ cluster.config()
+ with allure.step("Check that cluster doesn't exist"):
+ err.CLUSTER_NOT_FOUND.equal(e, "Cluster", "does not exist")
check_types = [
- ('file', 'input_file'),
- ('text', 'textarea'),
- ('password', 'password_phrase'),
+ ("file", "input_file"),
+ ("text", "textarea"),
+ ("password", "password_phrase"),
]
- @pytest.mark.parametrize(('datatype', 'name'), check_types)
- def test_verify_that_supported_type_is(self, client, datatype, name):
- with allure.step('Create stack'):
- stack = client.stack.cluster.read(prototype_id=client.stack.cluster.list()[0]['id'])
- with allure.step('Check stack config'):
- for item in stack['config']:
- if item['name'] == name:
- assert item['type'] == datatype
- steps.delete_all_data(client)
-
- def test_check_that_file_field_put_correct_data_in_file_inside_docker(self, client):
- cluster = steps.create_cluster(client)
+ @pytest.mark.parametrize(("datatype", "name"), check_types)
+ def test_verify_that_supported_type_is(
+ self, cluster_bundle: Bundle, datatype, name
+ ):
+ with allure.step("Check stack config"):
+ for item in cluster_bundle.cluster_prototype().config:
+ if item["name"] == name:
+ assert item["type"] == datatype
+
+ def test_check_that_file_field_put_correct_data_in_file_inside_docker(
+ self, cluster: Cluster
+ ):
test_data = "lorem ipsum"
- with allure.step('Create config data'):
- config_data = utils.ordered_dict_to_dict(
- client.cluster.config.current.list(cluster_id=cluster['id'])['config'])
- config_data['input_file'] = test_data
- config_data['required'] = random.randint(0, 99)
- with allure.step('Create config history'):
- client.cluster.config.history.create(cluster_id=cluster['id'], config=config_data)
- with allure.step('Check file type'):
- action = client.cluster.action.run.create(
- action_id=get_action_by_name(client, cluster, 'check-file-type')['id'],
- cluster_id=cluster['id']
+ with allure.step("Create config data"):
+ config_data = utils.ordered_dict_to_dict(cluster.config())
+ config_data["input_file"] = test_data
+ config_data["required"] = 42
+ with allure.step("Create config history"):
+ cluster.config_set(config_data)
+ with allure.step("Check file type"):
+ run_cluster_action_and_assert_result(
+ cluster=cluster, action="check-file-type"
)
- wait_until(client, action)
- with allure.step('Check that state is success'):
- expected = client.task.read(task_id=action['id'])
- assert expected['status'] == 'success'
diff --git a/tests/functional/test_config.py b/tests/functional/test_config.py
index 903d4e6b54..d2414e13d4 100644
--- a/tests/functional/test_config.py
+++ b/tests/functional/test_config.py
@@ -60,7 +60,7 @@ def assert_config_value_error(entity, sent_data):
def assert_action_has_issues(entity):
with pytest.raises(ActionHasIssues):
- entity.action_run(name='job').wait()
+ entity.action(name='job').run().wait()
def assert_list_type(*args):
@@ -82,11 +82,11 @@ def assert_list_type(*args):
if sent_value_type == 'null_value' and not is_default:
assert_action_has_issues(entity)
else:
- action_status = entity.action_run(name='job').wait()
+ action_status = entity.action(name='job').run().wait()
assert action_status == 'success'
else:
assert entity.config_set(sent_data) == sent_data
- action_status = entity.action_run(name='job').wait()
+ action_status = entity.action(name='job').run().wait()
assert action_status == 'success'
@@ -108,11 +108,11 @@ def assert_map_type(*args):
if sent_value_type == 'null_value' and not is_default:
assert_action_has_issues(entity)
else:
- action_status = entity.action_run(name='job').wait()
+ action_status = entity.action(name='job').run().wait()
assert action_status == 'success'
else:
assert entity.config_set(sent_data) == sent_data
- action_status = entity.action_run(name='job').wait()
+ action_status = entity.action(name='job').run().wait()
assert action_status == 'success'
@@ -130,7 +130,7 @@ def assert_string_type(*args):
else:
assert entity.config_set(sent_data) == sent_data
- action_status = entity.action_run(name='job').wait()
+ action_status = entity.action(name='job').run().wait()
assert action_status == 'success'
else:
if sent_value_type in ['empty_value', 'null_value']:
@@ -144,12 +144,12 @@ def assert_string_type(*args):
if sent_value_type in ['empty_value', 'null_value']:
assert_action_has_issues(entity)
else:
- action_status = entity.action_run(name='job').wait()
+ action_status = entity.action(name='job').run().wait()
assert action_status == 'success'
else:
assert entity.config_set(sent_data) == sent_data
- action_status = entity.action_run(name='job').wait()
+ action_status = entity.action(name='job').run().wait()
assert action_status == 'success'
@@ -168,7 +168,7 @@ def assert_password_type(*args):
assert entity.config_set(
sent_data)['password'].startswith('$ANSIBLE_VAULT;1.1;AES256')
- action_status = entity.action_run(name='job').wait()
+ action_status = entity.action(name='job').run().wait()
assert action_status == 'success'
else:
if sent_value_type in ['empty_value', 'null_value']:
@@ -183,7 +183,7 @@ def assert_password_type(*args):
if sent_value_type in ['empty_value', 'null_value']:
assert_action_has_issues(entity)
else:
- action_status = entity.action_run(name='job').wait()
+ action_status = entity.action(name='job').run().wait()
assert action_status == 'success'
else:
if sent_value_type == 'correct_value':
@@ -191,7 +191,7 @@ def assert_password_type(*args):
else:
assert entity.config_set(sent_data) == sent_data
- action_status = entity.action_run(name='job').wait()
+ action_status = entity.action(name='job').run().wait()
assert action_status == 'success'
@@ -209,7 +209,7 @@ def assert_text_type(*args):
else:
assert entity.config_set(sent_data) == sent_data
- action_status = entity.action_run(name='job').wait()
+ action_status = entity.action(name='job').run().wait()
assert action_status == 'success'
else:
if sent_value_type in ['empty_value', 'null_value']:
@@ -223,12 +223,12 @@ def assert_text_type(*args):
if sent_value_type in ['empty_value', 'null_value']:
assert_action_has_issues(entity)
else:
- action_status = entity.action_run(name='job').wait()
+ action_status = entity.action(name='job').run().wait()
assert action_status == 'success'
else:
assert entity.config_set(sent_data) == sent_data
- action_status = entity.action_run(name='job').wait()
+ action_status = entity.action(name='job').run().wait()
assert action_status == 'success'
@@ -247,7 +247,7 @@ def assert_file_type(*args):
assert entity.config_set(sent_data) == sent_data
if is_default:
- action_status = entity.action_run(name='job').wait()
+ action_status = entity.action(name='job').run().wait()
assert action_status == 'success'
else:
if is_required and isinstance(entity, Cluster):
@@ -256,7 +256,7 @@ def assert_file_type(*args):
if is_required and sent_value_type in ['empty_value', 'null_value']:
assert_action_has_issues(entity)
else:
- action_status = entity.action_run(name='job').wait()
+ action_status = entity.action(name='job').run().wait()
assert action_status == 'success'
@@ -271,7 +271,7 @@ def assert_structure_type(*args):
if is_required:
if is_default:
assert_config_value_error(entity, sent_data)
- action_status = entity.action_run(name='job').wait()
+ action_status = entity.action(name='job').run().wait()
assert action_status == 'success'
else:
assert_config_value_error(entity, sent_data)
@@ -282,7 +282,7 @@ def assert_structure_type(*args):
assert_action_has_issues(entity)
else:
assert entity.config_set(sent_data) == sent_data
- action_status = entity.action_run(name='job').wait()
+ action_status = entity.action(name='job').run().wait()
assert action_status == 'success'
@@ -304,7 +304,7 @@ def assert_boolean_type(*args):
if sent_value_type == 'null_value':
assert_action_has_issues(entity)
else:
- action_status = entity.action_run(name='job').wait()
+ action_status = entity.action(name='job').run().wait()
assert action_status == 'success'
@@ -326,7 +326,7 @@ def assert_integer_type(*args):
if sent_value_type == 'null_value':
assert_action_has_issues(entity)
else:
- action_status = entity.action_run(name='job').wait()
+ action_status = entity.action(name='job').run().wait()
assert action_status == 'success'
@@ -348,7 +348,7 @@ def assert_float_type(*args):
if sent_value_type == 'null_value':
assert_action_has_issues(entity)
else:
- action_status = entity.action_run(name='job').wait()
+ action_status = entity.action(name='job').run().wait()
assert action_status == 'success'
@@ -370,7 +370,7 @@ def assert_option_type(*args):
if sent_value_type == 'null_value':
assert_action_has_issues(entity)
else:
- action_status = entity.action_run(name='job').wait()
+ action_status = entity.action(name='job').run().wait()
assert action_status == 'success'
diff --git a/tests/functional/test_config_data/not_required/with_default/sent_correct_value/structure/cluster/config.yaml b/tests/functional/test_config_data/not_required/with_default/sent_correct_value/structure/cluster/config.yaml
index 076befa761..a358d56f4a 100644
--- a/tests/functional/test_config_data/not_required/with_default/sent_correct_value/structure/cluster/config.yaml
+++ b/tests/functional/test_config_data/not_required/with_default/sent_correct_value/structure/cluster/config.yaml
@@ -3,17 +3,17 @@
type: cluster
version: '1.0'
config:
- - name: structure
- type: structure
- required: false
- yspec: ./schema.yaml
- default: &id001
- - code: 30
- country: Greece
- - code: 33
- country: France
- - code: 34
- country: Spain
+ - name: structure
+ type: structure
+ required: false
+ yspec: ./schema.yaml
+ default: &id001
+ - code: 30
+ country: Greece
+ - code: 33
+ country: France
+ - code: 34
+ country: Spain
actions:
job:
script: cluster_action.yaml
@@ -21,16 +21,16 @@
type: job
states:
available:
- - created
+ - created
- name: service_structure_not_required_with_default_sent_correct_value
type: service
version: '1.0'
config:
- - name: structure
- type: structure
- required: false
- yspec: ./schema.yaml
- default: *id001
+ - name: structure
+ type: structure
+ required: false
+ yspec: ./schema.yaml
+ default: *id001
actions:
job:
script: service_action.yaml
@@ -38,4 +38,4 @@
type: job
states:
available:
- - created
+ - created
diff --git a/tests/functional/test_custom_log_plugin.py b/tests/functional/test_custom_log_plugin.py
index d19e5bb850..2a65289d28 100644
--- a/tests/functional/test_custom_log_plugin.py
+++ b/tests/functional/test_custom_log_plugin.py
@@ -26,7 +26,7 @@ def test_required_fields(sdk_client_fs: ADCMClient, bundle):
stack_dir = utils.get_data_dir(__file__, "required_fields", "no_{}".format(bundle))
bundle = sdk_client_fs.upload_from_fs(stack_dir)
cluster = bundle.cluster_create(utils.random_string())
- task = cluster.action_run(name='custom_log')
+ task = cluster.action(name='custom_log').run()
task.wait()
with allure.step('Check job state'):
assert task.status == 'failed', "Current job status {}. " \
@@ -45,7 +45,7 @@ def test_different_storage_types_with_format(sdk_client_fs: ADCMClient, bundle):
stack_dir = utils.get_data_dir(__file__, bundle)
bundle = sdk_client_fs.upload_from_fs(stack_dir)
cluster = bundle.cluster_create(utils.random_string())
- task = cluster.action_run(name='custom_log')
+ task = cluster.action(name='custom_log').run()
task.wait()
with allure.step('Check if logs are equal 3, job state and logs'):
job = task.job()
@@ -65,7 +65,7 @@ def test_path_and_content(sdk_client_fs: ADCMClient):
stack_dir = utils.get_data_dir(__file__, "path_and_content")
bundle = sdk_client_fs.upload_from_fs(stack_dir)
cluster = bundle.cluster_create(utils.random_string())
- task = cluster.action_run(name='custom_log')
+ task = cluster.action(name='custom_log').run()
task.wait()
with allure.step('Check logs content and format'):
job = task.job()
@@ -82,7 +82,7 @@ def test_multiple_tasks(sdk_client_fs: ADCMClient, bundle):
stack_dir = utils.get_data_dir(__file__, bundle)
bundle = sdk_client_fs.upload_from_fs(stack_dir)
cluster = bundle.cluster_create(utils.random_string())
- task = cluster.action_run(name='custom_log')
+ task = cluster.action(name='custom_log').run()
task.wait()
with allure.step('Check 4 logs entries'):
job = task.job()
@@ -96,7 +96,7 @@ def test_check_text_file_content(sdk_client_fs: ADCMClient):
stack_dir = utils.get_data_dir(__file__, "txt_path")
bundle = sdk_client_fs.upload_from_fs(stack_dir)
cluster = bundle.cluster_create(utils.random_string())
- task = cluster.action_run(name='custom_log')
+ task = cluster.action(name='custom_log').run()
task.wait()
with allure.step('Check logs content and format'):
job = task.job()
@@ -112,7 +112,7 @@ def test_check_text_content(sdk_client_fs: ADCMClient):
stack_dir = utils.get_data_dir(__file__, "txt_content")
bundle = sdk_client_fs.upload_from_fs(stack_dir)
cluster = bundle.cluster_create(utils.random_string())
- task = cluster.action_run(name='custom_log')
+ task = cluster.action(name='custom_log').run()
task.wait()
with allure.step('Check logs content'):
job = task.job()
@@ -127,7 +127,7 @@ def test_check_json_content(sdk_client_fs: ADCMClient):
stack_dir = utils.get_data_dir(__file__, "json_content")
bundle = sdk_client_fs.upload_from_fs(stack_dir)
cluster = bundle.cluster_create(utils.random_string())
- task = cluster.action_run(name='custom_log')
+ task = cluster.action(name='custom_log').run()
task.wait()
with allure.step('Check logs content'):
job = task.job()
@@ -142,7 +142,7 @@ def test_incorrect_syntax_for_fields(sdk_client_fs: ADCMClient):
stack_dir = utils.get_data_dir(__file__, "syntax_for_fields")
bundle = sdk_client_fs.upload_from_fs(stack_dir)
cluster = bundle.cluster_create(utils.random_string())
- task = cluster.action_run(name='custom_log')
+ task = cluster.action(name='custom_log').run()
task.wait()
with allure.step('Check logs content'):
job = task.job()
diff --git a/tests/functional/test_delete_service_plugin.py b/tests/functional/test_delete_service_plugin.py
index 0552b2a0e4..4f19792c7a 100644
--- a/tests/functional/test_delete_service_plugin.py
+++ b/tests/functional/test_delete_service_plugin.py
@@ -20,7 +20,7 @@ def test_delete_service_plugin(sdk_client_fs: ADCMClient):
bundle = sdk_client_fs.upload_from_fs(utils.get_data_dir(__file__, "cluster"))
cluster = bundle.cluster_create(utils.random_string())
service = cluster.service_add(name="service")
- task = service.action_run(name='remove_service')
+ task = service.action(name='remove_service').run()
task.wait()
with allure.step(f'Check that job state is {task.status}'):
assert task.status == 'success', "Current job status {}. " \
@@ -37,7 +37,7 @@ def test_delete_service_with_import(sdk_client_fs: ADCMClient):
cluster_import = bundle_import.cluster_create("cluster_import")
service = cluster.service_add(name="hadoop")
cluster_import.bind(service)
- task = service.action_run(name='remove_service')
+ task = service.action(name='remove_service').run()
task.wait()
with allure.step(f'Check that job state is {task.status}'):
assert task.status == 'success', "Current job status {}. " \
@@ -56,14 +56,14 @@ def test_delete_service_with_export(sdk_client_fs: ADCMClient):
service = cluster.service_add(name="hadoop")
import_service = cluster_import.service_add(name='hadoop')
import_service.bind(service)
- task = service.action_run(name='remove_service')
+ task = service.action(name='remove_service').run()
task.wait()
with allure.step(f'Check that job state is {task.status}'):
assert task.status == 'success', "Current job status {}. " \
"Expected: success".format(task.status)
assert not cluster.service_list()
assert cluster_import.service_list()
- task = import_service.action_run(name='remove_service')
+ task = import_service.action(name='remove_service').run()
task.wait()
with allure.step(f'Check that job state is {task.status}'):
assert task.status == 'success', "Current job status {}. " \
@@ -87,7 +87,7 @@ def test_delete_service_with_host(sdk_client_fs: ADCMClient):
component = service.component(name="ZOOKEEPER_SERVER")
cluster.hostcomponent_set((host, component))
assert cluster.service_list()
- task = service.action_run(name='remove_service')
+ task = service.action(name='remove_service').run()
task.wait()
with allure.step(f'Check that job state is {task.status}'):
assert task.status == 'success', "Current job status {}. " \
diff --git a/tests/functional/test_events.py b/tests/functional/test_events.py
index 2d0091a4f0..10253c974d 100644
--- a/tests/functional/test_events.py
+++ b/tests/functional/test_events.py
@@ -12,6 +12,7 @@
import json
import os
import re
+from time import gmtime, strftime
import allure
import pytest
@@ -112,8 +113,8 @@ def service(sdk_client_fs, name='zookeeper'):
return cluster(sdk_client_fs).service_add(name=name)
-def cluster_action_run(sdk_client_fs, name):
- return cluster(sdk_client_fs).action_run(name=name)
+def cluster_action_run(sdk_client_fs, name, **kwargs):
+ return cluster(sdk_client_fs).action(name=name).run(**kwargs)
def expected_success_task(obj, job):
@@ -187,7 +188,7 @@ def test_event_when_add_service(sdk_client_fs, ws):
@pytest.mark.parametrize(('case', 'action_name', 'expected'), cluster_actions)
def test_events_when_cluster_action_(case, action_name, expected, ws, cluster_with_svc_and_host):
cluster, _, _ = cluster_with_svc_and_host
- job = cluster.action_run(name=action_name)
+ job = cluster.action(name=action_name).run()
with allure.step('Check job'):
assert_events(
ws,
@@ -198,9 +199,22 @@ def test_events_when_cluster_action_(case, action_name, expected, ws, cluster_wi
@pytest.mark.parametrize(('case', 'action_name', 'expected'), svc_actions)
def test_events_when_service_(case, action_name, expected, ws, cluster_with_svc_and_host):
_, zookeeper, _ = cluster_with_svc_and_host
- job = zookeeper.action_run(name=action_name)
+ job = zookeeper.action(name=action_name).run()
with allure.step('Check job'):
assert_events(
ws,
*expected(zookeeper, job)
)
+
+
+@pytest.mark.parametrize(
+ "verbose_state", [True, False], ids=["verbose_state_true", "verbose_state_false"]
+)
+def test_check_timestamp_in_job_logs(sdk_client_fs: ADCMClient, verbose_state):
+ """Test that timestamps are presented in Job logs for both ordinary and verbose modes."""
+ task = cluster_action_run(sdk_client_fs, name="install", verbose=verbose_state)
+ with allure.step("Check timestamps presence in job logs"):
+ task.wait()
+ log = task.job().log()
+ assert strftime("%A %d %B %Y %H:%M", gmtime()) in log.content, \
+ "There are no timestamps in job logs"
diff --git a/tests/functional/test_events_data/hostprovider/config.yaml b/tests/functional/test_events_data/hostprovider/config.yaml
index 50ba4676ea..ca7df37602 100644
--- a/tests/functional/test_events_data/hostprovider/config.yaml
+++ b/tests/functional/test_events_data/hostprovider/config.yaml
@@ -12,36 +12,36 @@
---
- type: provider
name: sample_provider
- version: &version 0.4
+ version: &version "0.4"
upgrade:
- - name: *version
- versions: { min: 0.1, max_strict: *version }
- description: yopta upgrade
- states: { available: any }
- - name: new_upgrade
- versions:
- min: 0.1
- max_strict: 0.5
- description: without vars
- states:
- available: any
+ - name: *version
+ versions: { min: "0.1", max_strict: *version }
+ description: some upgrade
+ states: { available: any }
+ - name: new_upgrade
+ versions:
+ min: 0.1
+ max_strict: 0.5
+ description: without vars
+ states:
+ available: any
actions:
- init: &init_action
- type: job
- script: ansible/init.yaml
- script_type: ansible
- config:
- init_type:
- display_name: "Choose type to initiate"
- type: option
- option:
- host: host
- hostprovider: provider
- required: true
- states:
- available: [created]
- on_success: initiated
+ init: &init_action
+ type: job
+ script: ansible/init.yaml
+ script_type: ansible
+ config:
+ init_type:
+ display_name: "Choose type to initiate"
+ type: option
+ option:
+ host: host
+ hostprovider: provider
+ required: true
+ states:
+ available: [ created ]
+ on_success: initiated
config:
provider:
ssh_user:
@@ -59,13 +59,13 @@
private_key:
type: text
required: false
- default: null
+ default: "null"
advanced:
ro_when_created:
type: string
default: Change it when successfully installed
required: false
- read_only: [created]
+ read_only: [ created ]
adv_param:
type: float
default: 3.5
@@ -76,13 +76,13 @@
version: 00.09
actions:
- init:
- type: job
- script_type: ansible
- script: ansible/init.yaml
- states:
- available: any
- on_fail: created
+ init:
+ type: job
+ script_type: ansible
+ script: ansible/init.yaml
+ states:
+ available: any
+ on_fail: created
config:
usual:
str_param:
diff --git a/tests/functional/test_full_upgrade_data/cluster/config.yaml b/tests/functional/test_full_upgrade_data/cluster/config.yaml
index b3c21b9c0c..af8256a72f 100644
--- a/tests/functional/test_full_upgrade_data/cluster/config.yaml
+++ b/tests/functional/test_full_upgrade_data/cluster/config.yaml
@@ -9,24 +9,23 @@
# 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: ADH
- version: 1.5
- config:
- required:
- type: integer
- required: true
- default: 15
- str-key:
- default: value
- type: string
- required: false
+- type: cluster
+ name: ADH
+ version: 1.5
+ config:
+ required:
+ type: integer
+ required: true
+ default: 15
+ str-key:
+ default: value
+ type: string
+ required: false
- int_key:
- type: integer
- required: false
- default: 150
+ int_key:
+ type: integer
+ required: false
+ default: 150
- type: service
@@ -47,4 +46,4 @@
master:
display_name: "Master Node"
description: "This node control all data nodes (see below)"
- constraint: [1,2]
+ constraint: [ 1,2 ]
diff --git a/tests/functional/test_full_upgrade_data/hostprovider/config.yaml b/tests/functional/test_full_upgrade_data/hostprovider/config.yaml
index c508d55bf7..4c200f63e2 100644
--- a/tests/functional/test_full_upgrade_data/hostprovider/config.yaml
+++ b/tests/functional/test_full_upgrade_data/hostprovider/config.yaml
@@ -10,48 +10,46 @@
# See the License for the specific language governing permissions and
# limitations under the License.
--
-
- type: provider
- name: sample hostprovider
- version: 1.0
- config:
- required:
- type: integer
- required: yes
- default: 400
- str-key:
- default: value
- type: string
- required: false
+- type: provider
+ name: sample hostprovider
+ version: 1.0
+ config:
+ required:
+ type: integer
+ required: yes
+ default: 400
+ str-key:
+ default: value
+ type: string
+ required: false
- int_key:
- type: integer
- required: NO
- default: 60
+ int_key:
+ type: integer
+ required: NO
+ default: 60
- fkey:
- type: float
- required: false
- default: 1.5
+ fkey:
+ type: float
+ required: false
+ default: 1.5
- bool:
- type: boolean
- required : no
- default: false
+ bool:
+ type: boolean
+ required: no
+ default: false
- type: host
name: vHost
version: '00.09'
config:
- str_param:
- type: string
- required: false
- default: '123'
- description: must be changed to bar()
- int:
- type: integer
- required: false
- default: 2
- description: must be changed to 5
+ str_param:
+ type: string
+ required: false
+ default: '123'
+ description: must be changed to bar()
+ int:
+ type: integer
+ required: false
+ default: 2
+ description: must be changed to 5
diff --git a/tests/functional/test_full_upgrade_data/upgradable_cluster/config.yaml b/tests/functional/test_full_upgrade_data/upgradable_cluster/config.yaml
index 07eb615038..ec9ea82bc1 100644
--- a/tests/functional/test_full_upgrade_data/upgradable_cluster/config.yaml
+++ b/tests/functional/test_full_upgrade_data/upgradable_cluster/config.yaml
@@ -9,41 +9,40 @@
# 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: ADH
- version: 1.6
- upgrade:
- - versions:
- min: 0.4
- max: 1.5
- name: upgrade to 1.6
- description: New cool upgrade
- states:
- available: any
- on_success: upgradable
- - versions:
- min: 1.0
- max: 1.8
- description: Super new upgrade
- name: upgrade 2
- states:
- available: [created, installed, upgradable]
- on_success: upgradated
- config:
- required:
- type: integer
- required: true
- default: 15
- str-key:
- default: value
- type: string
- required: false
+- type: cluster
+ name: ADH
+ version: 1.6
+ upgrade:
+ - versions:
+ min: 0.4
+ max: 1.5
+ name: upgrade to 1.6
+ description: New cool upgrade
+ states:
+ available: any
+ on_success: upgradable
+ - versions:
+ min: 1.0
+ max: 1.8
+ description: Super new upgrade
+ name: upgrade 2
+ states:
+ available: [ created, installed, upgradable ]
+ on_success: upgradated
+ config:
+ required:
+ type: integer
+ required: true
+ default: 15
+ str-key:
+ default: value
+ type: string
+ required: false
- int_key:
- type: integer
- required: false
- default: 150
+ int_key:
+ type: integer
+ required: false
+ default: 150
- type: service
name: zookeeper
diff --git a/tests/functional/test_full_upgrade_data/upgradable_hostprovider/config.yaml b/tests/functional/test_full_upgrade_data/upgradable_hostprovider/config.yaml
index 28dccb41c5..074f020f91 100644
--- a/tests/functional/test_full_upgrade_data/upgradable_hostprovider/config.yaml
+++ b/tests/functional/test_full_upgrade_data/upgradable_hostprovider/config.yaml
@@ -10,69 +10,65 @@
# See the License for the specific language governing permissions and
# limitations under the License.
--
-
- type: provider
- name: sample hostprovider
- version: 2.0
- upgrade:
- -
- versions:
- min: 0.4
- max: 1.9
- description: New cool upgrade
- name: upgrade to 2.0
- states:
- available: any
- on_success: started
- -
- versions:
- min: 1.0
- max: 2.9
- description: Super new upgrade
- name: upgrade 2
- states:
- available: [started]
- on_success: ver2.4
- config:
- required:
- type: integer
- required: yes
- default: 400
- max: 500
- min: 200
- str-key:
- default: value
- type: string
- required: false
+- type: provider
+ name: sample hostprovider
+ version: 2.0
+ upgrade:
+ - versions:
+ min: 0.4
+ max: 1.9
+ description: New cool upgrade
+ name: upgrade to 2.0
+ states:
+ available: any
+ on_success: started
+ - versions:
+ min: 1.0
+ max: 2.9
+ description: Super new upgrade
+ name: upgrade 2
+ states:
+ available: [ started ]
+ on_success: ver2.4
+ config:
+ required:
+ type: integer
+ required: yes
+ default: 400
+ max: 500
+ min: 200
+ str-key:
+ default: value
+ type: string
+ required: false
- int_key:
- type: integer
- required: NO
- default: 60
+ int_key:
+ type: integer
+ required: NO
+ default: 60
- fkey:
- type: float
- required: false
- default: 1.5
+ fkey:
+ type: float
+ required: false
+ default: 1.5
- bool:
- type: boolean
- required : no
- default: false
+ bool:
+ type: boolean
+ required: no
+ default: false
- type: host
name: vHost
version: '00.10'
config:
- str_param:
- type: string
- required: false
- default: '123'
- description: must be changed to bar()
- int:
- type: integer
- required: false
- default: 2
- description: must be changed to 5
+ str_param:
+ type: string
+ required: false
+ default: '123'
+ description: must be changed to bar()
+ int:
+ type: integer
+ required: false
+ default: 2
+ description: must be changed to 5
diff --git a/tests/functional/test_host_functions.py b/tests/functional/test_host_functions.py
index f7020d8b7d..6138897282 100644
--- a/tests/functional/test_host_functions.py
+++ b/tests/functional/test_host_functions.py
@@ -9,349 +9,110 @@
# 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 random
import allure
-import coreapi
+
import pytest
-from adcm_client.objects import ADCMClient
+from adcm_client.objects import ADCMClient, Bundle, Provider, Cluster, Host
from adcm_pytest_plugin.utils import get_data_dir
from adcm_pytest_plugin import utils
from jsonschema import validate
-# pylint: disable=E0401, W0601, W0611, W0621, W0212
-from tests.library import errorcodes as err
-from tests.library import steps
-from tests.library.utils import get_random_service, get_random_host_prototype
+# pylint: disable=W0621,W0212
SCHEMAS = os.path.join(os.path.dirname(__file__), "schemas/")
-host_bad_configs = (({"str-key": "{1bbb}", "required": "158", "option": "my.host",
- "sub": {"sub1": 3}, "credentials": {"sample_string": "test",
- "read_only_initiated": 1}},
- "should be integer"),
- ({"str-key": 61, "required": 158, "fkey": 18.3,
- "option": "my.host", "sub": {"sub1": 3},
- "credentials": {"sample_string": "txt",
- "read_only_initiated": {}}},
- 'should be string'),
- ({"str-key": "{1bbb}", "required": 158, "fkey": 18.3,
- "option": "my.host", "sub": {"sub1": 9}},
- 'not in option list'),
- ({"str-key": "{1bbb}", "required": 158, "option": 8080,
- "sub": {"sub1": {"foo": "bar"}}},
- 'should be flat')
- )
+
+@pytest.fixture()
+def provider_bundle(sdk_client_fs: ADCMClient) -> Bundle:
+ return sdk_client_fs.upload_from_fs(get_data_dir(__file__, "hostprovider_bundle"))
-@pytest.fixture(scope="module")
-def hostprovider(sdk_client_ms: ADCMClient):
- bundle = sdk_client_ms.upload_from_fs(get_data_dir(__file__, 'hostprovider_bundle'))
- return bundle.provider_create(utils.random_string())
+@pytest.fixture()
+def provider(provider_bundle: Bundle) -> Provider:
+ return provider_bundle.provider_create(utils.random_string())
-@pytest.fixture(scope="module")
-def host(sdk_client_ms: ADCMClient, hostprovider):
- return hostprovider.host_create(utils.random_string())
+@pytest.fixture()
+def host(provider: Provider) -> Host:
+ return provider.host_create(utils.random_string())
-@pytest.fixture(scope="module")
-def cluster(sdk_client_ms: ADCMClient):
- return sdk_client_ms.upload_from_fs(get_data_dir(__file__, 'cluster_bundle'))
+@pytest.fixture()
+def cluster_bundle(sdk_client_fs: ADCMClient):
+ return sdk_client_fs.upload_from_fs(get_data_dir(__file__, "cluster_bundle"))
-@pytest.fixture(scope="module")
-def client(sdk_client_ms: ADCMClient, cluster, hostprovider):
- return sdk_client_ms.adcm()._api.objects
+@pytest.fixture()
+def cluster(cluster_bundle: Bundle) -> Cluster:
+ return cluster_bundle.cluster_create(utils.random_string())
class TestHost:
"""
Basic tests for host
"""
- def test_validate_host_prototype(self, client):
- host_prototype = json.loads(json.dumps(client.stack.host.list()[0]))
- schema = json.load(
- open(SCHEMAS + '/stack_list_item_schema.json')
- )
- with allure.step('Match prototype with schema'):
- assert validate(host_prototype, schema) is None
- steps.delete_all_data(client)
- def test_create_host(self, sdk_client_fs: ADCMClient):
- """Check that host have same fqdn and status after reread config
+ @pytest.mark.usefixtures("provider_bundle")
+ def test_validate_host_prototype(self, sdk_client_fs: ADCMClient):
"""
- bundle = sdk_client_fs.upload_from_fs(get_data_dir(__file__, 'hostprovider_bundle'))
- hp = bundle.provider_create(utils.random_string())
- host = hp.host_create(utils.random_string())
- host_status_before = host.status
- host_fqdn_before = host.fqdn
- with allure.step('Reread host'):
- host.reread()
- host_status_after = host.status
- host_fqdn_after = host.fqdn
- with allure.step('Check states and fqdn'):
- assert host_fqdn_before == host_fqdn_after
- assert host_status_before == host_status_after
-
- def test_shouldnt_create_duplicate_host(self, sdk_client_fs: ADCMClient):
- """We have restriction for create duplicated hosts (wuth the same fqdn).
- Scenario:
- 1. Create hostprovider
- 2. Create first host
- 3. Create second host with the same FQDN
- 4. Check that we've got 409 error for second host creation
+ Validate provider object schema
"""
- bundle = sdk_client_fs.upload_from_fs(get_data_dir(__file__, 'hostprovider_simple'))
- hp = bundle.provider_create(utils.random_string())
- hp.host_create("duplicate")
- with pytest.raises(coreapi.exceptions.ErrorMessage) as e:
- hp.host_create('duplicate')
- with allure.step('Check host conflict'):
- err.HOST_CONFLICT.equal(e, 'duplicate host')
-
- def test_shouldnt_create_host_with_unknown_prototype(self, client):
- with allure.step('Create provider'):
- provider_id = client.provider.create(prototype_id=client.stack.provider.list()[0]['id'],
- name=utils.random_string())['id']
- with allure.step('Create host'):
- with pytest.raises(coreapi.exceptions.ErrorMessage) as e:
- client.host.create(prototype_id=random.randint(100, 500),
- provider_id=provider_id,
- fqdn=utils.random_string())
- with allure.step('Check PROTOTYPE_NOT_FOUND error'):
- err.PROTOTYPE_NOT_FOUND.equal(e, 'prototype doesn\'t exist')
-
- def test_shouldnt_create_host_wo_prototype(self, client):
- with allure.step('Create provider'):
- provider = client.provider.create(prototype_id=client.stack.provider.list()[0]['id'],
- name=utils.random_string())
- with allure.step('Try to create host without prototype'):
- with pytest.raises(coreapi.exceptions.ParameterError) as e:
- client.host.create(provider_id=provider['id'], fqdn=utils.random_string())
- with allure.step('Check prototype_id error'):
- assert str(e.value) == "{'prototype_id': 'This parameter is required.'}"
-
- def test_shouldnt_create_host_wo_provider(self, client):
- with allure.step('Create prototype'):
- proto = get_random_host_prototype(client)
- with pytest.raises(coreapi.exceptions.ParameterError) as e:
- client.host.create(prototype_id=proto['id'], fqdn=utils.random_string())
- with allure.step('Check provider_id error'):
- assert str(e.value) == "{'provider_id': 'This parameter is required.'}"
-
- def test_create_host_with_max_length_plus_1(self, sdk_client_fs: ADCMClient):
- """We cannot create host with name more then max length
- """
- bundle = sdk_client_fs.upload_from_fs(get_data_dir(__file__, 'hostprovider_simple'))
- hp = bundle.provider_create(utils.random_string())
- with pytest.raises(coreapi.exceptions.ErrorMessage) as e:
- hp.host_create(utils.random_string(257))
- with allure.step('Check LONG_NAME error'):
- err.LONG_NAME.equal(e, 'Host name is too long. Max length is 256')
+ host_prototype = sdk_client_fs.host_prototype()._data
+ schema = json.load(open(SCHEMAS + "/stack_list_item_schema.json"))
+ with allure.step("Match prototype with schema"):
+ assert validate(host_prototype, schema) is None
- def test_shouldnt_create_host_with_wrong_name(self, sdk_client_fs: ADCMClient):
- """Check that host name cannot contain special characters
+ def test_get_host_list(self, sdk_client_fs: ADCMClient, provider: Provider):
"""
- bundle = sdk_client_fs.upload_from_fs(get_data_dir(__file__, 'hostprovider_simple'))
- hp = bundle.provider_create(utils.random_string())
- with pytest.raises(coreapi.exceptions.ErrorMessage) as e:
- hp.host_create(utils.random_string() + utils.random_special_chars())
- with allure.step('Check WRONG_NAME error'):
- err.WRONG_NAME.equal(e, 'Host name is incorrect. '
- 'Only latin characters, digits, dots (.)')
-
- def test_get_host_list(self, sdk_client_fs: ADCMClient):
- """Create multiple hosts and check that all hosts was created
+ Create multiple hosts and check that all hosts was created
"""
- expected_list = set()
- actual_list = set()
- bundle = sdk_client_fs.upload_from_fs(get_data_dir(__file__, 'hostprovider_simple'))
- hp = bundle.provider_create(utils.random_string())
+ actual, expected = [], []
for fqdn in utils.random_string_list():
- hp.host_create(fqdn)
- expected_list.add(fqdn)
+ provider.host_create(fqdn)
+ expected.append(fqdn)
for host in sdk_client_fs.host_list():
- actual_list.add(host.fqdn)
- with allure.step('Check created hosts with the data from the API'):
- assert actual_list == expected_list
+ actual.append(host.fqdn)
+ with allure.step("Check created hosts with the data from the API"):
+ assert actual == expected
- def test_get_host_info(self, client):
- host = steps.create_host_w_default_provider(client, utils.random_string())
- actual = steps.read_host(client, host['id'])
- with allure.step('Check created host with the data from the API'):
- del actual['status']
- del host['status']
- assert actual == host
-
- def test_delete_host(self, sdk_client_fs: ADCMClient):
- """Check that we can delete host"""
- bundle = sdk_client_fs.upload_from_fs(get_data_dir(__file__, 'hostprovider_simple'))
- hp = bundle.provider_create(utils.random_string())
- host = hp.host_create("deletion_host")
- with allure.step('delete host'):
- deletion_result = host.delete()
- with allure.step('Check that host is deleted'):
- assert deletion_result is None
-
- def test_should_return_correct_error_when_read_deleted(self, sdk_client_fs: ADCMClient):
- """Check that we have 409 error if host not found"""
- bundle = sdk_client_fs.upload_from_fs(get_data_dir(__file__, 'hostprovider_simple'))
- hp = bundle.provider_create(utils.random_string())
- host = hp.host_create(utils.random_string())
- with allure.step('delete host'):
- host.delete()
- with pytest.raises(coreapi.exceptions.ErrorMessage) as e:
- host.reread()
- with allure.step('Check HOST_NOT_FOUND'):
- err.HOST_NOT_FOUND.equal(e)
-
- def test_should_return_correct_error_when_delete_nonexist_host(
- self, sdk_client_fs: ADCMClient):
- """If we try to delete deleted host we've got 409 error.
+ def test_create_hostcomponent(self, sdk_client_fs: ADCMClient, provider: Provider):
"""
- bundle = sdk_client_fs.upload_from_fs(get_data_dir(__file__, 'hostprovider_simple'))
- hp = bundle.provider_create(utils.random_string())
- host = hp.host_create(utils.random_string())
- with allure.step('delete host'):
- host.delete()
- with allure.step('delete host second time'):
- with pytest.raises(coreapi.exceptions.ErrorMessage) as e:
- host.delete()
- with allure.step('Check HOST_NOT_FOUND'):
- err.HOST_NOT_FOUND.equal(e, 'host doesn\'t exist')
-
- # *** Basic tests for hostcomponent ***
- def test_create_hostcomponent(self, sdk_client_fs: ADCMClient):
- """Check that hostcomponent id the same in component list and for service
+ Check that hostcomponent is set
"""
- bundle = sdk_client_fs.upload_from_fs(get_data_dir(
- __file__, 'cluster_service_hostcomponent'))
- bundle_hp = sdk_client_fs.upload_from_fs(get_data_dir(__file__, 'hostprovider_simple'))
+ bundle = sdk_client_fs.upload_from_fs(
+ get_data_dir(__file__, "cluster_service_hostcomponent")
+ )
cluster = bundle.cluster_create(utils.random_string())
- hp = bundle_hp.provider_create(utils.random_string())
- host = hp.host_create(utils.random_string())
+ host = provider.host_create(utils.random_string())
cluster.host_add(host)
service = cluster.service_add(name="ZOOKEEPER")
- component_list = service.component_list()
- component = service.component(name='ZOOKEEPER_CLIENT')
- with allure.step('Check component id and name'):
- assert component.component_id == component_list[0].component_id
- assert component.name == component_list[0].name
-
- def test_get_hostcomponent_list(self, client): # invalid case, random component takes in circle
- cluster = steps.create_cluster(client)
- service = steps.read_service(client, get_random_service(client)['id'])
- cluster_svc = client.cluster.service.create(cluster_id=cluster['id'],
- prototype_id=service['id'])
- components = client.cluster.service.component.list(cluster_id=cluster['id'],
- service_id=cluster_svc['id'])
- # create mapping between cluster and hosts, then create hostcomponent on host
- hostcomponent_list = []
- for fqdn in utils.random_string_list():
- host = steps.create_host_w_default_provider(client, fqdn)
- steps.add_host_to_cluster(client, host, cluster)
- component = random.choice(components)['id']
- hostcomponent_list.append({"host_id": host['id'], "service_id": cluster_svc['id'],
- "component_id": component})
- expected_hostcomponent_list = client.cluster.hostcomponent.create(
- cluster_id=cluster['id'], hc=hostcomponent_list)
- actual_hs_list = client.cluster.hostcomponent.list(cluster_id=cluster['id'])
- with allure.step('Check created data with data from API'):
- assert actual_hs_list == expected_hostcomponent_list
-
-
-class TestHostConfig:
- """Class for test host configuration"""
-
- def test_config_history_url_must_point_to_the_host_config(self, client):
- host = steps.create_host_w_default_provider(client, utils.random_string())
- config = {"str-key": "{1bbb}", "required": 158, "option": 8080, "sub": {"sub1": 2},
- "credentials": {"sample_string": "txt", "read_only_initiated": {}}}
- i = 0
- with allure.step('Create host history'):
- while i < random.randint(0, 10):
- client.host.config.history.create(host_id=host['id'],
- description=utils.random_string(),
- config=config)
- i += 1
- history = client.host.config.history.list(host_id=host['id'])
- with allure.step('Check host history'):
- for conf in history:
- assert ('host/{0}/config/'.format(host['id']) in conf['url']) is True
- steps.delete_all_data(client)
-
- def test_get_default_host_config(self, client):
- # Get a default host config and validate it with json schema
- host = steps.create_host_w_default_provider(client, utils.random_string())
- config_json = {}
- with allure.step('Get default configuration from host'):
- config = client.host.config.current.list(host_id=host['id'])
- if config:
- config_json = json.loads(json.dumps(config))
- schema = json.load(open(SCHEMAS + '/config_item_schema.json'))
- with allure.step('Check config'):
- assert validate(config_json, schema) is None
- steps.delete_all_data(client)
-
- def test_get_config_from_nonexistant_host(self, sdk_client_fs: ADCMClient):
- """Get configuration for non exist host.
+ component = service.component(name="ZOOKEEPER_CLIENT")
+ cluster.hostcomponent_set((host, component))
+ with allure.step("Check host component map"):
+ hc = cluster.hostcomponent()
+ assert len(hc) == 1
+ assert hc[0]["host_id"] == host.id
+ assert hc[0]["component_id"] == component.id
+
+ def test_get_hostcomponent_list(self, cluster: Cluster, provider: Provider):
"""
- bundle_hp = sdk_client_fs.upload_from_fs(get_data_dir(
- __file__, 'hostprovider_simple'))
- hp = bundle_hp.provider_create(utils.random_string())
- with allure.step('Get host config from a non existant host'):
- with pytest.raises(coreapi.exceptions.ErrorMessage) as e:
- hp.host(host_id=random.randint(100, 500))
- with allure.step('Check error host doesn\'t exist'):
- err.HOST_NOT_FOUND.equal(e, 'host doesn\'t exist')
-
- def test_shouldnt_create_host_config_when_config_not_json_string(self, client):
- """Should not create host configuration when config string is not json
+ Check that hostcomponent map retrieved successfully
"""
- host = steps.create_host_w_default_provider(client, utils.random_string())
- config = utils.random_string()
- with allure.step('Try to create the host config from non-json string'):
- with pytest.raises(coreapi.exceptions.ErrorMessage) as e:
- client.host.config.history.create(host_id=host['id'], config=config)
- with allure.step('Check error config should not be just one string'):
- err.JSON_ERROR.equal(e, 'config should not be just one string')
-
- def test_shouldnt_create_host_config_when_config_is_number(self, client):
- """Should not create host configuration when config string is number
- """
- host = steps.create_host_w_default_provider(client, utils.random_string())
- config = random.randint(100, 999)
- with allure.step('Try to create the host configuration with a number'):
- with pytest.raises(coreapi.exceptions.ErrorMessage) as e:
- client.host.config.history.create(host_id=host['id'], config=config)
- with allure.step('Check error should not be just one int or float'):
- err.JSON_ERROR.equal(e, 'should not be just one int or float')
-
- @pytest.mark.parametrize(('config', 'error'), host_bad_configs)
- def test_change_host_config_negative(self, host, config, error):
- """Check that we have error if try to update host config with bad configuration
- :param host: host object
- :param config: dict with bad config
- :param error: expected error
- """
- with allure.step('Try to create config when parameter is not integer'):
- with pytest.raises(coreapi.exceptions.ErrorMessage) as e:
- host.config_set(config)
- with allure.step(f'Check error {error}'):
- err.CONFIG_VALUE_ERROR.equal(e, error)
-
- def test_should_create_host_config_when_parameter_is_integer_and_not_float(
- self, sdk_client_fs: ADCMClient):
- """Create host config for float parameter with integer
- """
- bundle = sdk_client_fs.upload_from_fs(get_data_dir(__file__, 'hostprovider_bundle'))
- hp = bundle.provider_create(utils.random_string())
- host = hp.host_create(utils.random_string())
- config = {"str-key": "{1bbb}", "required": 158, "fkey": 18, "option": "my.host",
- "sub": {"sub1": 3},
- "credentials": {"sample_string": "txt", "read_only_initiated": {}}}
- host.config_set(config)
+ service = cluster.service_add(name="ZOOKEEPER")
+ components = service.component_list()
+ hc_map_temp = []
+ for fqdn in utils.random_string_list():
+ host = provider.host_create(fqdn=fqdn)
+ cluster.host_add(host)
+ component = random.choice(components)
+ hc_map_temp.append((host, component))
+ hc_map_expected = cluster.hostcomponent_set(*hc_map_temp)
+ with allure.step("Check created data with data from API"):
+ hc_map_actual = cluster.hostcomponent()
+ assert hc_map_actual == hc_map_expected
diff --git a/tests/functional/test_host_functions_data/cluster_bundle/services/ZOOKEEPER/config.yaml b/tests/functional/test_host_functions_data/cluster_bundle/services/ZOOKEEPER/config.yaml
index 7616feb3d2..a024b8b423 100644
--- a/tests/functional/test_host_functions_data/cluster_bundle/services/ZOOKEEPER/config.yaml
+++ b/tests/functional/test_host_functions_data/cluster_bundle/services/ZOOKEEPER/config.yaml
@@ -9,33 +9,33 @@
# 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: ZOOKEEPER
-type: service
-description: ZooKeeper
-version: '1.2'
+- name: ZOOKEEPER
+ type: service
+ description: ZooKeeper
+ version: '1.2'
-components:
+ components:
ZOOKEEPER_CLIENT:
- params:
- folder: stacks/ADH/1.0/services/ZOOKEEPER/package
- packages: [zookeeper]
- script: scripts/zookeeper_client.py
+ params:
+ folder: stacks/ADH/1.0/services/ZOOKEEPER/package
+ packages: [ zookeeper ]
+ script: scripts/zookeeper_client.py
ZOOKEEPER_SERVER:
- params:
- folder: stacks/ADH/1.0/services/ZOOKEEPER/package
- packages: [zookeeper]
- script: scripts/zookeeper_server.py
+ params:
+ folder: stacks/ADH/1.0/services/ZOOKEEPER/package
+ packages: [ zookeeper ]
+ script: scripts/zookeeper_server.py
-config:
- ssh-key: {default: TItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAA, type: string, required: false}
- integer-key: {default: 24, max: 48, min: 2, type: integer, required: false}
- float-key: {default: 4.4, max: 50.0, min: 4.0, type: float, required: false}
+ config:
+ ssh-key: { default: TItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAA, type: string, required: false }
+ integer-key: { default: 24, max: 48, min: 2, type: integer, required: false }
+ float-key: { default: 4.4, max: 50.0, min: 4.0, type: float, required: false }
zoo.cfg:
- autopurge.purgeInterval: {default: 24, max: 48, min: 2, type: integer}
- dataDir: {default: /hadoop/zookeeper, type: string}
- port:
- required: false
- default: 80
- option: {http: 80, https: 443}
- type: option
- required-key: {default: value, type: string}
+ autopurge.purgeInterval: { default: 24, max: 48, min: 2, type: integer }
+ dataDir: { default: /hadoop/zookeeper, type: string }
+ port:
+ required: false
+ default: 80
+ option: { http: 80, https: 443 }
+ type: option
+ required-key: { default: value, type: string }
diff --git a/tests/functional/test_host_functions_data/cluster_service_hostcomponent/services/ZOOKEEPER/config.yaml b/tests/functional/test_host_functions_data/cluster_service_hostcomponent/services/ZOOKEEPER/config.yaml
index d3af6e449d..29f03b3e31 100644
--- a/tests/functional/test_host_functions_data/cluster_service_hostcomponent/services/ZOOKEEPER/config.yaml
+++ b/tests/functional/test_host_functions_data/cluster_service_hostcomponent/services/ZOOKEEPER/config.yaml
@@ -9,19 +9,19 @@
# 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: ZOOKEEPER
-type: service
-description: ZooKeeper
-version: '1.2'
+- name: ZOOKEEPER
+ type: service
+ description: ZooKeeper
+ version: '1.2'
-components:
+ components:
ZOOKEEPER_CLIENT:
- params:
- folder: stacks/ADH/1.0/services/ZOOKEEPER/package
- packages: [zookeeper]
- script: scripts/zookeeper_client.py
+ params:
+ folder: stacks/ADH/1.0/services/ZOOKEEPER/package
+ packages: [ zookeeper ]
+ script: scripts/zookeeper_client.py
ZOOKEEPER_SERVER:
- params:
- folder: stacks/ADH/1.0/services/ZOOKEEPER/package
- packages: [zookeeper]
- script: scripts/zookeeper_server.py
+ params:
+ folder: stacks/ADH/1.0/services/ZOOKEEPER/package
+ packages: [ zookeeper ]
+ script: scripts/zookeeper_server.py
diff --git a/tests/functional/test_host_functions_data/cluster_simple/services/ZOOKEEPER/config.yaml b/tests/functional/test_host_functions_data/cluster_simple/services/ZOOKEEPER/config.yaml
index d3af6e449d..29f03b3e31 100644
--- a/tests/functional/test_host_functions_data/cluster_simple/services/ZOOKEEPER/config.yaml
+++ b/tests/functional/test_host_functions_data/cluster_simple/services/ZOOKEEPER/config.yaml
@@ -9,19 +9,19 @@
# 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: ZOOKEEPER
-type: service
-description: ZooKeeper
-version: '1.2'
+- name: ZOOKEEPER
+ type: service
+ description: ZooKeeper
+ version: '1.2'
-components:
+ components:
ZOOKEEPER_CLIENT:
- params:
- folder: stacks/ADH/1.0/services/ZOOKEEPER/package
- packages: [zookeeper]
- script: scripts/zookeeper_client.py
+ params:
+ folder: stacks/ADH/1.0/services/ZOOKEEPER/package
+ packages: [ zookeeper ]
+ script: scripts/zookeeper_client.py
ZOOKEEPER_SERVER:
- params:
- folder: stacks/ADH/1.0/services/ZOOKEEPER/package
- packages: [zookeeper]
- script: scripts/zookeeper_server.py
+ params:
+ folder: stacks/ADH/1.0/services/ZOOKEEPER/package
+ packages: [ zookeeper ]
+ script: scripts/zookeeper_server.py
diff --git a/tests/functional/test_host_functions_data/hostprovider_bundle/provider/config.yaml b/tests/functional/test_host_functions_data/hostprovider_bundle/provider/config.yaml
index a64df7e053..e862a51839 100644
--- a/tests/functional/test_host_functions_data/hostprovider_bundle/provider/config.yaml
+++ b/tests/functional/test_host_functions_data/hostprovider_bundle/provider/config.yaml
@@ -62,7 +62,7 @@
int_key:
type: integer
required: NO
- default:
+ default: 1
fkey:
type: float
required: false
diff --git a/tests/functional/test_hostproviders_functions.py b/tests/functional/test_hostproviders_functions.py
index 7fa247ba0d..747cfed76a 100644
--- a/tests/functional/test_hostproviders_functions.py
+++ b/tests/functional/test_hostproviders_functions.py
@@ -11,106 +11,89 @@
# limitations under the License.
import json
import os
-import random
import allure
import pytest
+from adcm_client.objects import ADCMClient
from adcm_pytest_plugin import utils
-from adcm_pytest_plugin.docker_utils import DockerWrapper
from coreapi import exceptions
from jsonschema import validate
-# pylint: disable=E0401, W0611, W0621
-from tests.library import errorcodes, steps
+# pylint: disable=protected-access
+from tests.library import errorcodes
BUNDLES = os.path.join(os.path.dirname(__file__), "../stack/")
SCHEMAS = os.path.join(os.path.dirname(__file__), "schemas/")
-@pytest.fixture()
-def adcm(image, request, adcm_credentials):
- repo, tag = image
- dw = DockerWrapper()
- adcm = dw.run_adcm(image=repo, tag=tag, pull=False)
- adcm.api.auth(**adcm_credentials)
- yield adcm
- adcm.stop()
+def test_load_host_provider(sdk_client_fs: ADCMClient):
+ sdk_client_fs.upload_from_fs(BUNDLES + "hostprovider_bundle")
+ with allure.step("Check bundle list"):
+ assert len(sdk_client_fs.bundle_list()) == 1
-@pytest.fixture()
-def client(adcm):
- return adcm.api.objects
-
-
-def test_load_host_provider(client):
- steps.upload_bundle(client, BUNDLES + 'hostprovider_bundle')
- with allure.step('Check bundle list'):
- assert client.stack.bundle.list() is not None
-
-
-def test_validate_provider_prototype(client):
- steps.upload_bundle(client, BUNDLES + 'hostprovider_bundle')
- with allure.step('Load provider prototype'):
- provider_prototype = json.loads(json.dumps(client.stack.provider.list()[0]))
+def test_validate_provider_prototype(sdk_client_fs: ADCMClient):
+ bundle = sdk_client_fs.upload_from_fs(BUNDLES + "hostprovider_bundle")
+ with allure.step("Load provider prototype"):
+ provider_prototype = bundle.provider_prototype()._data
schema = json.load(
open(SCHEMAS + '/stack_list_item_schema.json')
)
- with allure.step('Check provider prototype'):
+ with allure.step("Check provider prototype"):
assert validate(provider_prototype, schema) is None
-def test_should_create_provider_wo_description(client):
- steps.upload_bundle(client, BUNDLES + 'hostprovider_bundle')
- with allure.step('Create provider'):
- client.provider.create(prototype_id=client.stack.provider.list()[0]['id'],
- name=utils.random_string())
- with allure.step('Check provider list'):
- assert client.provider.list() is not None
-
-
-def test_should_create_provider_w_description(client):
- steps.upload_bundle(client, BUNDLES + 'hostprovider_bundle')
- with allure.step('Create provider'):
- description = utils.random_string()
- provider = client.provider.create(prototype_id=client.stack.provider.list()[0]['id'],
- name=utils.random_string(),
- description=description)
- with allure.step('Check provider with description'):
- assert provider['description'] == description
-
-
-def test_get_provider_config(client):
- steps.upload_bundle(client, BUNDLES + 'hostprovider_bundle')
- with allure.step('Create provider'):
- provider = client.provider.create(prototype_id=client.stack.provider.list()[0]['id'],
- name=utils.random_string())
- with allure.step('Check provider config'):
- assert client.provider.config.current.list(provider_id=provider['id'])['config'] is not None
-
-
-@allure.link('https://jira.arenadata.io/browse/ADCM-472')
-def test_provider_shouldnt_be_deleted_when_it_has_host(client):
- steps.upload_bundle(client, BUNDLES + 'hostprovider_bundle')
- with allure.step('Create provider'):
- provider = steps.create_hostprovider(client)
- with allure.step('Create host'):
- client.host.create(prototype_id=client.stack.host.list()[0]['id'],
- provider_id=provider['id'],
- fqdn=utils.random_string())
- with allure.step('Delete provider'):
- with pytest.raises(exceptions.ErrorMessage) as e:
- client.provider.delete(provider_id=provider['id'])
- with allure.step('Check error'):
- errorcodes.PROVIDER_CONFLICT.equal(e, 'There is host ', ' of host provider ')
+def test_should_create_provider_wo_description(sdk_client_fs: ADCMClient):
+ bundle = sdk_client_fs.upload_from_fs(BUNDLES + "hostprovider_bundle")
+ bundle.provider_create(name=utils.random_string())
+ with allure.step("Check provider list"):
+ assert len(sdk_client_fs.provider_list()) == 1
-def test_shouldnt_create_host_with_unknown_prototype(client):
- steps.upload_bundle(client, BUNDLES + 'hostprovider_bundle')
- with allure.step('Create host'):
+def test_should_create_provider_w_description(sdk_client_fs: ADCMClient):
+ bundle = sdk_client_fs.upload_from_fs(BUNDLES + "hostprovider_bundle")
+ description = utils.random_string(140)
+ provider = bundle.provider_create(
+ name=utils.random_string(),
+ description=description)
+ with allure.step("Check provider with description"):
+ assert provider.description == description
+
+
+def test_get_provider_config(sdk_client_fs: ADCMClient):
+ bundle = sdk_client_fs.upload_from_fs(BUNDLES + "hostprovider_bundle")
+ provider = bundle.provider_create(
+ name=utils.random_string())
+ with allure.step("Check provider config"):
+ assert provider.config() is not None
+
+
+@allure.link("https://jira.arenadata.io/browse/ADCM-472")
+def test_provider_shouldnt_be_deleted_when_it_has_host(sdk_client_fs: ADCMClient):
+ bundle = sdk_client_fs.upload_from_fs(BUNDLES + "hostprovider_bundle")
+ provider = bundle.provider_create(name=utils.random_string())
+ provider.host_create(fqdn=utils.random_string())
+ with allure.step("Delete provider"):
+ with pytest.raises(exceptions.ErrorMessage) as e:
+ provider.delete()
+ with allure.step("Check error"):
+ errorcodes.PROVIDER_CONFLICT.equal(e, "There is host ", " of host provider ")
+
+
+def test_shouldnt_create_host_with_unknown_prototype(sdk_client_fs):
+ bundle = sdk_client_fs.upload_from_fs(BUNDLES + "hostprovider_bundle")
+ provider = bundle.provider_create(
+ name=utils.random_string()
+ )
+ with allure.step("Delete provider"):
+ provider.delete()
+ with allure.step("Create host"):
with pytest.raises(exceptions.ErrorMessage) as e:
- client.host.create(prototype_id=client.stack.host.list()[0]['id'],
- provider_id=random.randint(100, 500),
- fqdn=utils.random_string())
- with allure.step('Check error provider doesnt exist'):
- errorcodes.PROVIDER_NOT_FOUND.equal(e, "provider doesn't exist")
+ # Using lack of auto refresh of object as workaround.
+ # If adcm_client object behaviour will be changed, test may fall.
+ provider.host_create(
+ fqdn=utils.random_string()
+ )
+ with allure.step("Check error provider doesnt exist"):
+ errorcodes.PROVIDER_NOT_FOUND.equal(e, "HostProvider", "does not exist")
diff --git a/tests/functional/test_inventories.py b/tests/functional/test_inventories.py
index 82b2d8975b..362b812912 100644
--- a/tests/functional/test_inventories.py
+++ b/tests/functional/test_inventories.py
@@ -25,7 +25,7 @@ def test_check_inventories_file(adcm_ms, sdk_client_ms):
cluster_name = random_string()
cluster = cluster_bundle.cluster_prototype().cluster_create(cluster_name)
cluster.service_add(name="zookeeper")
- cluster.action_run(name="install").try_wait()
+ cluster.action(name="install").run().try_wait()
with allure.step('Get inventory file from container'):
text = get_file_from_container(adcm_ms, '/adcm/data/run/1/', 'inventory.json')
inventory = json.loads(text.read().decode('utf8'))
diff --git a/tests/functional/test_locked_objects.py b/tests/functional/test_locked_objects.py
index b5310e4ea4..76b7d32697 100644
--- a/tests/functional/test_locked_objects.py
+++ b/tests/functional/test_locked_objects.py
@@ -9,189 +9,318 @@
# 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
from typing import Union
import allure
import pytest
-from adcm_client.objects import Provider, Cluster, Host, ADCMClient, Service
+from adcm_client.objects import (
+ Provider,
+ Cluster,
+ Host,
+ ADCMClient,
+ Service,
+ Task,
+ Component,
+)
from adcm_pytest_plugin import utils
-
-# pylint: disable=W0611, W0621
+from adcm_pytest_plugin.steps.asserts import assert_state
from adcm_pytest_plugin.utils import random_string
@pytest.fixture()
-def prepared_cluster(sdk_client_fs: ADCMClient) -> Cluster:
+def cluster(sdk_client_fs: ADCMClient) -> Cluster:
uploaded_bundle = sdk_client_fs.upload_from_fs(
- utils.get_data_dir(__file__, "locked_when_action_running")
+ utils.get_data_dir(__file__, "cluster")
)
return uploaded_bundle.cluster_prototype().cluster_create(name=random_string())
@pytest.fixture()
-def hostprovider(sdk_client_fs: ADCMClient) -> Provider:
+def host_provider(sdk_client_fs: ADCMClient) -> Provider:
provider_bundle = sdk_client_fs.upload_from_fs(
- utils.get_data_dir(__file__, "host_bundle_on_any_level")
+ utils.get_data_dir(__file__, "provider")
)
return provider_bundle.provider_prototype().provider_create(random_string())
@pytest.fixture()
-def host(hostprovider: Provider) -> Host:
- return hostprovider.host_create(random_string())
-
-
-def _check_locked_object(obj: Union[Cluster, Service, Provider, Host]):
- """
- Assert that object state is 'locked' and action list is empty
- """
- obj.reread()
- assert obj.state == "locked"
- assert (
- obj.action_list() == []
- ), f"{obj.__class__.__name__} action list not empty. {obj.__class__.__name__} not locked"
-
+def host(host_provider: Provider) -> Host:
+ return host_provider.host_create(random_string())
-def test_cluster_must_be_locked_when_action_running(prepared_cluster: Cluster):
- with allure.step("Run action: lock-cluster for cluster"):
- prepared_cluster.action_run(name="lock-cluster")
- with allure.step("Check if cluster state is 'locked'"):
- prepared_cluster.reread()
- _check_locked_object(prepared_cluster)
-
-
-def test_run_new_action_on_locked_cluster_must_throws_exception(
- prepared_cluster: Cluster,
-):
- with allure.step("Run action: lock-cluster for cluster"):
- prepared_cluster.action_run(name="lock-cluster")
- with allure.step("Check that cluster state is 'locked'"):
- _check_locked_object(prepared_cluster)
-
-
-def test_service_in_cluster_must_be_locked_when_cluster_action_running(
- prepared_cluster: Cluster,
-):
- with allure.step("Add service and run action: lock-cluster for cluster"):
- added_service = prepared_cluster.service_add(name="bookkeeper")
- prepared_cluster.action_run(name="lock-cluster")
- with allure.step("Check if service state is 'locked'"):
- _check_locked_object(added_service)
-
-
-def test_host_in_cluster_must_be_locked_when_cluster_action_running(
- prepared_cluster: Cluster, host: Host
-):
- with allure.step("Add host and run action: lock-cluster for cluster"):
- prepared_cluster.host_add(host)
- prepared_cluster.action_run(name="lock-cluster")
- with allure.step("Check if host state is 'locked'"):
- _check_locked_object(prepared_cluster)
-
-
-def test_host_must_be_locked_when_host_action_running(host):
- with allure.step("Run host action: action-locker"):
- host.action_run(name="action-locker")
- with allure.step("Check if host state is 'locked'"):
- _check_locked_object(host)
+@pytest.fixture()
+def complete_cluster(cluster: Cluster, host: Host):
+ cluster.host_add(host)
+ service = cluster.service_add(name="dummy")
+ cluster.hostcomponent_set((host, service.component(name="dummy")))
+ return cluster
+
+
+class TestClusterLock:
+ def test_lock_unlock(self, cluster, host):
+ """
+ Test that cluster locked when action running and unlocked when action ends
+ """
+ cluster.host_add(host)
+ is_free(cluster)
+ task = _lock_obj(cluster)
+ is_locked(cluster)
+ task.wait()
+ is_free(cluster)
+
+ def test_down_lock(self, complete_cluster, host, sdk_client_fs):
+ """
+ Test that cluster lock also locks:
+ - all cluster services
+ - all service components
+ - all hosts with components
+ """
+ task = _lock_obj(complete_cluster)
+ for service in complete_cluster.service_list():
+ is_locked(service)
+ for component in service.component_list():
+ is_locked(component)
+ for hc in complete_cluster.hostcomponent():
+ is_locked(sdk_client_fs.host(id=hc["host_id"]))
+ task.wait()
+ for service in complete_cluster.service_list():
+ is_free(service)
+ for component in service.component_list():
+ is_free(component)
+ for hc in complete_cluster.hostcomponent():
+ is_free(sdk_client_fs.host(id=hc["host_id"]))
+
+ def test_no_horizontal_lock(self, cluster: Cluster):
+ """
+ Test that no horizontal lock when cluster locked
+ """
+ second_cluster = cluster.prototype().cluster_create(name=random_string())
+ _lock_obj(cluster)
+ is_free(second_cluster)
+
+
+class TestServiceLock:
+ def test_lock_unlock(self, cluster, host):
+ """
+ Test that service locked when action running and unlocked when action ends
+ """
+ cluster.host_add(host)
+ service = cluster.service_add(name="dummy")
+ is_free(service)
+ task = _lock_obj(service)
+ is_locked(service)
+ task.wait()
+ is_free(service)
+
+ def test_up_lock(self, complete_cluster):
+ """
+ Test that service lock also locks parent objects:
+ - Cluster
+ """
+ task = _lock_obj(complete_cluster.service(name="dummy"))
+ is_locked(complete_cluster)
+ task.wait()
+ is_free(complete_cluster)
+
+ def test_down_lock(self, complete_cluster, host):
+ """
+ Test that service lock also locks child objects:
+ - Components
+ - Hosts
+ """
+ service = complete_cluster.service(name="dummy")
+ task = _lock_obj(service)
+ for component in service.component_list():
+ is_locked(component)
+ is_locked(host)
+ task.wait()
+ for component in service.component_list():
+ is_free(component)
+ is_free(host)
+
+ def test_no_horizontal_lock(self, complete_cluster):
+ """
+ Test that no horizontal lock when service locked
+ """
+ second_service = complete_cluster.service_add(name="second")
+ _lock_obj(complete_cluster.service(name="dummy"))
+ is_free(second_service)
+
+
+class TestComponentLock:
+ def test_lock_unlock(self, complete_cluster, host):
+ """
+ Test that component locked when action running and unlocked when action ends
+ """
+ service = complete_cluster.service(name="dummy")
+ component = service.component(name="dummy")
+
+ is_free(component)
+ task = _lock_obj(component)
+ task.wait()
+ is_free(service)
+
+ def test_up_lock(self, complete_cluster):
+ """
+ Test that component lock also locks parent objects:
+ - Service
+ - Cluster
+ """
+ service = complete_cluster.service(name="dummy")
+ task = _lock_obj(service.component(name="dummy"))
+ is_locked(service)
+ is_locked(complete_cluster)
+ task.wait()
+ is_free(service)
+ is_free(complete_cluster)
+
+ def test_down_lock(self, complete_cluster, host):
+ """
+ Test that component lock also locks child objects:
+ - Host
+ """
+ task = _lock_obj(complete_cluster.service(name="dummy").component(name="dummy"))
+ is_locked(host)
+ task.wait()
+ is_free(host)
+
+ def test_no_horizontal_lock(self, complete_cluster):
+ """
+ Test that no horizontal lock when component locked
+ """
+ service = complete_cluster.service(name="dummy")
+ _lock_obj(service.component(name="dummy"))
+ is_free(service.component(name="second"))
+
+
+class TestHostLock:
+ def test_lock_unlock(self, host):
+ """
+ Test that host locked when action running and unlocked when action ends
+ """
+ is_free(host)
+ task = _lock_obj(host)
+ is_locked(host)
+ task.wait()
+ is_free(host)
+
+ def test_up_lock(self, complete_cluster, host_provider, host):
+ """
+ Test that host lock also locks parent objects:
+ - Component
+ - Service
+ - Cluster
+ """
+ service = complete_cluster.service(name="dummy")
+ component = service.component(name="dummy")
+ task = _lock_obj(host)
+ is_locked(component)
+ is_locked(service)
+ is_locked(complete_cluster)
+ task.wait()
+ is_free(component)
+ is_free(service)
+ is_free(complete_cluster)
+
+ def test_no_horizontal_lock(self, host_provider, host):
+ """
+ Test that no horizontal lock when host locked
+ """
+ second_host = host_provider.host_create(fqdn=random_string())
+ _lock_obj(host)
+ is_free(second_host)
+
+
+class TestHostProviderLock:
+ def test_lock_unlock(self, host_provider):
+ """
+ Test that host provider locked when action running and unlocked when action ends
+ """
+ is_free(host_provider)
+ task = _lock_obj(host_provider)
+ is_locked(host_provider)
+ task.wait()
+ is_free(host_provider)
+
+ def test_down_lock(self, host_provider, host):
+ """
+ Test that provider lock also locks child objects:
+ - Host
+ """
+ task = _lock_obj(host_provider)
+ is_locked(host)
+ task.wait()
+ is_free(host)
-def test_cluster_must_be_locked_when_located_host_action_running(
- prepared_cluster: Cluster, host: Host
-):
- with allure.step("Add host and run action: action-locker"):
- prepared_cluster.host_add(host)
- host.action_run(name="action-locker")
- with allure.step("Check if host and cluster states are 'locked'"):
- _check_locked_object(prepared_cluster)
- _check_locked_object(host)
+ def test_no_horizontal_lock(self, host_provider):
+ """
+ Test that no horizontal lock when host locked
+ """
+ second_provider = host_provider.prototype().provider_create(
+ name=random_string()
+ )
+ _lock_obj(host_provider)
+ is_free(second_provider)
-def test_cluster_service_locked_when_located_host_action_running(
- prepared_cluster: Cluster, host: Host
-):
- with allure.step("Add host and service"):
- prepared_cluster.host_add(host)
- added_service = prepared_cluster.service_add(name="bookkeeper")
- with allure.step("Run action: action-locker for host"):
- host.action_run(name="action-locker")
- with allure.step("Check if host, cluster and service states are 'locked'"):
- _check_locked_object(prepared_cluster)
- _check_locked_object(host)
- _check_locked_object(added_service)
-
-
-def test_run_service_action_locked_all_objects_in_cluster(
- prepared_cluster: Cluster, host: Host
-):
- with allure.step("Add host and service"):
- prepared_cluster.host_add(host)
- added_service = prepared_cluster.service_add(name="bookkeeper")
- with allure.step("Run action: service-lock for service"):
- added_service.action_run(name="service-lock")
- with allure.step("Check if host, cluster and service states are 'locked'"):
- _check_locked_object(prepared_cluster)
- _check_locked_object(host)
- _check_locked_object(added_service)
-
-
-def test_cluster_should_be_unlocked_when_ansible_task_killed(prepared_cluster: Cluster):
+def test_cluster_should_be_unlocked_when_ansible_task_killed(cluster: Cluster):
with allure.step("Run cluster action: lock-terminate for cluster"):
- task = prepared_cluster.action_run(name="lock-terminate")
- with allure.step("Check if cluster state is 'locked' and then 'terminate_failed'"):
- _check_locked_object(prepared_cluster)
- task.wait()
- prepared_cluster.reread()
- assert prepared_cluster.state == "terminate_failed"
+ task = cluster.action(name="lock-terminate").run()
+ is_locked(cluster)
+ task.wait()
+ assert_state(cluster, "terminate_failed")
def test_host_should_be_unlocked_when_ansible_task_killed(
- prepared_cluster: Cluster, host: Host
+ complete_cluster: Cluster, host: Host
):
- with allure.step("Add host"):
- prepared_cluster.host_add(host)
with allure.step("Run action: lock-terminate for cluster"):
- task = prepared_cluster.action_run(name="lock-terminate")
+ task = complete_cluster.action(name="lock-terminate").run()
- with allure.step("Check if host state is 'locked' and then is 'created'"):
- _check_locked_object(host)
- task.wait()
- host.reread()
- assert host.state == "created"
+ is_locked(host)
+ task.wait()
+ is_free(host)
-def test_service_should_be_unlocked_when_ansible_task_killed(prepared_cluster: Cluster):
- with allure.step("Add service"):
- added_service = prepared_cluster.service_add(name="bookkeeper")
+def test_service_should_be_unlocked_when_ansible_task_killed(complete_cluster: Cluster):
+ service = complete_cluster.service(name="dummy")
with allure.step("Run action: lock-terminate for cluster"):
- task = prepared_cluster.action_run(name="lock-terminate")
- with allure.step("Check if service state is 'locked' and then is 'created'"):
- _check_locked_object(added_service)
- task.wait()
- added_service.reread()
- assert added_service.state == "created"
+ task = complete_cluster.action(name="lock-terminate").run()
+ is_locked(service)
+ task.wait()
+ is_free(service)
-def test_hostprovider_must_be_unlocked_when_his_task_finished(hostprovider: Provider):
- with allure.step("Run action: action-locker for hostprovider"):
- task = hostprovider.action_run(name="action-locker")
- with allure.step("Check if provider state is 'locked' and then is 'created'"):
- _check_locked_object(hostprovider)
- task.wait()
- hostprovider.reread()
- assert hostprovider.state == "created"
+def _lock_obj(obj) -> Task:
+ """
+ Run action lock on object
+ """
+ with allure.step(f"Lock {obj.__class__.__name__}"):
+ return obj.action(name="lock").run()
-def test_host_and_hostprovider_must_be_unlocked_when_his_task_finished(
- hostprovider: Provider, host: Host
+def is_locked(
+ obj: Union[Cluster, Service, Component, Provider, Host]
):
- with allure.step("Run action: action-locker for hostprovider"):
- task = hostprovider.action_run(name="action-locker")
- with allure.step("Check if provider state is 'locked' and then is 'created'"):
- _check_locked_object(hostprovider)
- _check_locked_object(host)
- task.wait()
- hostprovider.reread()
- host.reread()
- assert hostprovider.state == "created"
- assert host.state == "created"
+ """
+ Assert that object state is 'locked' and action list is empty
+ """
+
+ with allure.step(f"Assert that {obj.__class__.__name__} is locked"):
+ assert_state(obj=obj, state="locked")
+ assert (
+ obj.action_list() == []
+ ), f"{obj.__class__.__name__} action list isn't empty. {obj.__class__.__name__} not locked"
+
+
+def is_free(
+ obj: Union[Cluster, Service, Component, Provider, Host]
+):
+ """
+ Assert that object state is 'created' and action list isn't empty
+ """
+ with allure.step(f"Assert that {obj.__class__.__name__} is free"):
+ assert_state(obj, state="created")
+ assert obj.action_list(), f"{obj.__class__.__name__} action list is empty. " \
+ f"Actions should be available for unlocked objects"
diff --git a/tests/functional/test_locked_objects_data/locked_when_action_running/ansible/svc-lock.yaml b/tests/functional/test_locked_objects_data/cluster/ansible/dummy.yaml
similarity index 85%
rename from tests/functional/test_locked_objects_data/locked_when_action_running/ansible/svc-lock.yaml
rename to tests/functional/test_locked_objects_data/cluster/ansible/dummy.yaml
index 686cf53c3a..9972d27b22 100644
--- a/tests/functional/test_locked_objects_data/locked_when_action_running/ansible/svc-lock.yaml
+++ b/tests/functional/test_locked_objects_data/cluster/ansible/dummy.yaml
@@ -10,13 +10,12 @@
# See the License for the specific language governing permissions and
# limitations under the License.
---
-- name: Waiting for verify an object lock
+- name: Dummy action
hosts: all
connection: local
gather_facts: no
tasks:
- - pause:
- seconds: 500
+ - name: Do nothing
- debug:
- msg: "Unstucked now"
+ msg: "Nothing done"
diff --git a/tests/functional/test_locked_objects_data/locked_when_action_running/ansible/unlock-fail.yaml b/tests/functional/test_locked_objects_data/cluster/ansible/lock-terminate.yaml
similarity index 100%
rename from tests/functional/test_locked_objects_data/locked_when_action_running/ansible/unlock-fail.yaml
rename to tests/functional/test_locked_objects_data/cluster/ansible/lock-terminate.yaml
diff --git a/tests/functional/test_locked_objects_data/host_bundle_on_any_level/hosts/ssh-host/ansible/init.yaml b/tests/functional/test_locked_objects_data/cluster/ansible/lock.yaml
similarity index 83%
rename from tests/functional/test_locked_objects_data/host_bundle_on_any_level/hosts/ssh-host/ansible/init.yaml
rename to tests/functional/test_locked_objects_data/cluster/ansible/lock.yaml
index 6421ce069b..871c361bed 100644
--- a/tests/functional/test_locked_objects_data/host_bundle_on_any_level/hosts/ssh-host/ansible/init.yaml
+++ b/tests/functional/test_locked_objects_data/cluster/ansible/lock.yaml
@@ -10,14 +10,14 @@
# See the License for the specific language governing permissions and
# limitations under the License.
---
-- name: Locker action for host
+- name: Lock action
hosts: all
connection: local
gather_facts: no
tasks:
- - name: emulation locked object for 500 sec
+ - name: emulation locked object for {{ job.config.duration }} sec
pause:
- seconds: 5
+ seconds: "{{ job.config.duration }}"
- debug:
msg: "Unstucked now"
diff --git a/tests/functional/test_locked_objects_data/cluster/config.yaml b/tests/functional/test_locked_objects_data/cluster/config.yaml
new file mode 100644
index 0000000000..37d4616c5a
--- /dev/null
+++ b/tests/functional/test_locked_objects_data/cluster/config.yaml
@@ -0,0 +1,64 @@
+# 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: service
+ name: dummy
+ version: 1.0.10
+
+ components:
+ dummy:
+ actions: &actions
+ lock:
+ type: job
+ script_type: ansible
+ script: ansible/lock.yaml
+ config:
+ duration:
+ default: 5
+ type: integer
+ required: false
+ states:
+ available: any
+ on_success: created
+ on_fail: failed
+ dummy:
+ type: job
+ script: ansible/dummy.yaml
+ script_type: ansible
+ states:
+ available: any
+ second:
+ actions: *actions
+
+ actions: *actions
+
+- type: service
+ name: second
+ version: 1.0.10
+ components:
+ dummy:
+ actions: *actions
+ actions: *actions
+
+- type: cluster
+ name: locking
+ version: 1.0
+ actions:
+ <<: *actions
+ lock-terminate:
+ type: job
+ script: ansible/lock-terminate.yaml
+ script_type: ansible
+ states:
+ available: any
+ on_success: terminated
+ on_fail: terminate_failed
diff --git a/tests/functional/test_locked_objects_data/host_bundle_on_any_level/hosts/ssh-host/config.yaml b/tests/functional/test_locked_objects_data/host_bundle_on_any_level/hosts/ssh-host/config.yaml
deleted file mode 100644
index ca462b1324..0000000000
--- a/tests/functional/test_locked_objects_data/host_bundle_on_any_level/hosts/ssh-host/config.yaml
+++ /dev/null
@@ -1,59 +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.
----
-- type: provider
- name: sample_provider
- version: 0.1.0
-
- actions: &host_actions
- init:
- type: job
- log_files: [remote]
- script: ansible/init.yaml
- script_type: ansible
- states:
- on_success: initiated
- on_fail: failed
- available: any
- action-locker:
- type: job
- log_files: [remote]
- script: hosts/ssh-host/ansible/locker.yaml
- script_type: ansible
- states:
- on_success: created
- on_fail: failed
- available: any
- config:
- ansible_user:
- default: root
- type: string
- required: false
- ansible_ssh_pass:
- type: string
- default: root
- required: false
--
- type: host
- name: simple ssh
- version: .01
-
- actions: *host_actions
- config:
- ansible_user:
- default: root
- type: string
- required: true
- ansible_ssh_pass:
- type: string
- default: root
- required: yes
diff --git a/tests/functional/test_locked_objects_data/locked_when_action_running/config.yaml b/tests/functional/test_locked_objects_data/locked_when_action_running/config.yaml
deleted file mode 100644
index abf1cd8129..0000000000
--- a/tests/functional/test_locked_objects_data/locked_when_action_running/config.yaml
+++ /dev/null
@@ -1,227 +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.
----
-- type: service
- name: bookkeeper
- version: &bookkeeper_version 1.0.10
-
- config:
- client_port:
- type: string
- default: "2181"
- display_name: client_port
- dirs:
- data:
- type: string
- default: "/var/lib/bookkeeper"
- display_name: data dirs for bookkeeper
- svc-file:
- type: file
- required: false
- svc-text-area:
- type: text
- required: false
- default: big dataaaaaaaaaaaaa
- svc-password:
- type: password
- default: qwerty1234
- required: false
- svc-ro-created:
- display_name:
- type: string
- default: bluh
- required: false
- read_only: [created]
- svc-w-installed:
- type: integer
- default: 222
- required: false
- writable: [installed]
- svc-read-only:
- type: float
- default: 2.5
- required: false
- read_only: any
- export:
- dirs
-
- components:
- BOOKKEEPER_SERVER:
- constraint: [0,+]
- BOOKKEEPER_CLIENT:
- constraint: [0,+]
-
- actions:
- install:
- type: job
- script_type: ansible
- script: ansible/nothing.yaml
- params:
- ansible_tags: install
- states:
- available:
- - created
- on_success: installed
- on_fail: created
- expand-bookkeeper:
- type: job
- script_type: ansible
- script: ansible/nothing.yaml
- states:
- available:
- - created
- on_success: created
- on_fail: did_not_expand
- expand-fail:
- type: job
- script_type: ansible
- script: role/fail.yaml
- states:
- available:
- - created
- on_success: created
- on_fail: expand_failed
- config:
- quorum:
- type: integer
- required: false
- read_only: any
- default: 100
- simple_string:
- type: string
- required: false
- default: lorem ipsum
- add-text-field:
- type: text
- required: false
- service-lock:
- type: job
- script_type: ansible
- script: ansible/svc-lock.yaml
- states:
- available: any
- on_success: created
- on_fail: failed
-
- should_be_failed:
- type: job
- script_type: ansible
- script: ansible/failed.yaml
- params:
- ansible_tags: should_be_failed
- states:
- available:
- - created
- - installed
- on_success: failed
- on_fail: created
- config:
- failed-param:
- type: boolean
- default: true
- required: true
-
- components:
- type: job
- script_type: ansible
- script: ansible/nothing.yaml
--
- type: cluster
- name: locking
- version: 1.0
- actions:
- install:
- type: job
- script: ansible/install.yaml
- script_type: ansible
- states:
- available:
- - created
- on_fail: upgradated
- params:
- qwe: 42
- lock-cluster:
- type: job
- script: ansible/pause.yaml
- script_type: ansible
- states:
- available: any
- on_success: created
- on_fail: created
- lock-terminate:
- type: job
- script: ansible/unlock-fail.yaml
- script_type: ansible
- states:
- available: any
- on_success: terminated
- on_fail: terminate_failed
- sleep:
- type: job
- script_type: ansible
- script: ansible/sleep.yaml
- config:
- sleeptime:
- display_name: "Sleep time (sec)"
- type: integer
- required: true
- default: 60
- states: { available: any }
- config:
- group1:
- boooooooooool:
- type: boolean
- required: false
- bluhhh:
- type: integer
- required: false
- group2:
- boooooooooool:
- type: boolean
- required: false
- required:
- type: integer
- required: true
- default: 10
- str-key:
- default: value
- type: string
- required: false
-
- int_key:
- type: integer
- required: false
- default: 150
-
- float_key:
- type: float
- required: false
- default: 34.7
-
- bool:
- type: boolean
- required : false
- default: false
- option:
- type: option
- option:
- http: 80
- https: 443
- ftp: 21
- required: FALSE
- password:
- default: qwerty
- type: password
- required: false
- input_file:
- type: file
- required: false
diff --git a/tests/functional/test_locked_objects_data/locked_when_action_running/ansible/install.yaml b/tests/functional/test_locked_objects_data/provider/ansible/dummy.yaml
similarity index 85%
rename from tests/functional/test_locked_objects_data/locked_when_action_running/ansible/install.yaml
rename to tests/functional/test_locked_objects_data/provider/ansible/dummy.yaml
index 5762725c33..9972d27b22 100644
--- a/tests/functional/test_locked_objects_data/locked_when_action_running/ansible/install.yaml
+++ b/tests/functional/test_locked_objects_data/provider/ansible/dummy.yaml
@@ -10,13 +10,12 @@
# See the License for the specific language governing permissions and
# limitations under the License.
---
-- name: Do nothing playbook
- hosts: all
+- name: Dummy action
+ hosts: all
connection: local
gather_facts: no
tasks:
- - pause:
- seconds: 5
+ - name: Do nothing
- debug:
- msg: "Unstucked now"
+ msg: "Nothing done"
diff --git a/tests/functional/test_locked_objects_data/host_bundle_on_any_level/hosts/ssh-host/ansible/locker.yaml b/tests/functional/test_locked_objects_data/provider/ansible/lock.yaml
similarity index 83%
rename from tests/functional/test_locked_objects_data/host_bundle_on_any_level/hosts/ssh-host/ansible/locker.yaml
rename to tests/functional/test_locked_objects_data/provider/ansible/lock.yaml
index 503624286a..871c361bed 100644
--- a/tests/functional/test_locked_objects_data/host_bundle_on_any_level/hosts/ssh-host/ansible/locker.yaml
+++ b/tests/functional/test_locked_objects_data/provider/ansible/lock.yaml
@@ -10,14 +10,14 @@
# See the License for the specific language governing permissions and
# limitations under the License.
---
-- name: Locker action for host
+- name: Lock action
hosts: all
connection: local
gather_facts: no
tasks:
- - name: emulation locked object for 500 sec
+ - name: emulation locked object for {{ job.config.duration }} sec
pause:
- seconds: 500
+ seconds: "{{ job.config.duration }}"
- debug:
msg: "Unstucked now"
diff --git a/tests/functional/test_locked_objects_data/provider/config.yaml b/tests/functional/test_locked_objects_data/provider/config.yaml
new file mode 100644
index 0000000000..a0fc34a246
--- /dev/null
+++ b/tests/functional/test_locked_objects_data/provider/config.yaml
@@ -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.
+---
+- type: provider
+ name: sample_provider
+ version: 0.1.0
+
+ actions: &host_actions
+ lock:
+ type: job
+ script: ansible/lock.yaml
+ script_type: ansible
+ config:
+ duration:
+ default: 5
+ type: integer
+ required: false
+ states:
+ on_success: created
+ on_fail: failed
+ available: any
+ dummy:
+ type: job
+ script: ansible/dummy.yaml
+ script_type: ansible
+ states:
+ available: any
+
+- type: host
+ name: simple ssh
+ version: .01
+
+ actions: *host_actions
diff --git a/tests/functional/test_multijob.py b/tests/functional/test_multijob.py
index 3cbd14a131..ade2246342 100644
--- a/tests/functional/test_multijob.py
+++ b/tests/functional/test_multijob.py
@@ -25,14 +25,14 @@ def cluster(sdk_client_ms: ADCMClient, request):
def test_cluster_state_after_multijob(sdk_client_ms: ADCMClient, cluster):
with allure.step('Run action: multi'):
- cluster.action_run(name="multi").wait()
+ cluster.action(name="multi").run().wait()
with allure.step('Check cluster state'):
assert sdk_client_ms.cluster(name=cluster.name).state == cluster.name
def test_service_state_after_multijob(sdk_client_ms: ADCMClient, cluster):
with allure.step('Run action: multi'):
- cluster.service(name='multi').action_run(name="multi").wait()
+ cluster.service(name='multi').action(name="multi").run().wait()
with allure.step('Check service state'):
assert cluster.service(name='multi').state == cluster.name
@@ -49,7 +49,7 @@ def test_cluster_service_state_locked(sdk_client_ms: ADCMClient):
assert bundle.cluster(name=bundle.name).service(name='multi').state == 'created'
assert bundle.cluster(name=bundle.name).service(name='stab').state == 'created'
with allure.step('Run cluster action: multi'):
- task = cluster.action_run(name='multi')
+ task = cluster.action(name='multi').run()
with allure.step('Check services states: locked and then created'):
assert bundle.cluster(name=bundle.name).state == 'locked'
assert bundle.cluster(name=bundle.name).service(name='multi').state == 'locked'
@@ -59,7 +59,7 @@ def test_cluster_service_state_locked(sdk_client_ms: ADCMClient):
assert bundle.cluster(name=bundle.name).service(name='multi').state == 'created'
assert bundle.cluster(name=bundle.name).service(name='stab').state == 'created'
with allure.step('Run service action: multi'):
- task = cluster.service(name='multi').action_run(name='multi')
+ task = cluster.service(name='multi').action(name='multi').run()
with allure.step('Check services states: locked and created'):
assert bundle.cluster(name=bundle.name).state == 'locked'
assert bundle.cluster(name=bundle.name).service(name='multi').state == 'locked'
@@ -80,13 +80,13 @@ def provider(sdk_client_ms: ADCMClient, request):
def test_provider_state_after_multijob(sdk_client_ms: ADCMClient, provider):
with allure.step('Run provider action: multi'):
- provider.action_run(name="multi").wait()
+ provider.action(name="multi").run().wait()
with allure.step('Check provider state'):
assert sdk_client_ms.provider(name=provider.name).state == provider.name
def test_host_state_after_multijob(sdk_client_ms: ADCMClient, provider):
with allure.step('Run host action: multi'):
- provider.host(fqdn=provider.name).action_run(name="multi").wait()
+ provider.host(fqdn=provider.name).action(name="multi").run().wait()
with allure.step('Check host state'):
assert provider.host(fqdn=provider.name).state == provider.name
diff --git a/tests/functional/test_nullable_fields.py b/tests/functional/test_nullable_fields.py
index dd627e5297..671bf748d1 100644
--- a/tests/functional/test_nullable_fields.py
+++ b/tests/functional/test_nullable_fields.py
@@ -40,7 +40,7 @@ def read_conf(template_file_name):
@allure.step('Load template file')
def render(template, context):
tmpl = Template(template)
- return yaml.load(tmpl.render(config_type=context))
+ return yaml.safe_load(tmpl.render(config_type=context))
@allure.step('Save template')
diff --git a/tests/functional/test_nullable_fields_data/template.yaml b/tests/functional/test_nullable_fields_data/template.yaml
index b0fe634c09..4c0de90608 100644
--- a/tests/functional/test_nullable_fields_data/template.yaml
+++ b/tests/functional/test_nullable_fields_data/template.yaml
@@ -1,23 +1,21 @@
---
- - type: cluster
- name: {{ config_type }}
- version: &version 0.02
- upgrade:
- - versions:
- min: 0.1
- max_strict: *version
- description: &upgrd 'Upgrade to version'
- name: [*upgrd, *version]
- states:
- available: any
- on_success: upgraded
- actions:
-
- config:
- required:
- type: {{ config_type }}
- required: true
- display_name: required key
- following:
- type: {{ config_type }}
- required: false
+- type: cluster
+ name: {{ config_type }}
+ version: &version 0.02
+ upgrade:
+ - versions:
+ min: 0.1
+ max_strict: *version
+ description: 'Upgrade to version'
+ name: 'Upgrade to version'
+ states:
+ available: any
+ on_success: upgraded
+ config:
+ required:
+ type: {{ config_type }}
+ required: true
+ display_name: required key
+ following:
+ type: {{ config_type }}
+ required: false
diff --git a/tests/functional/test_objects_issues.py b/tests/functional/test_objects_issues.py
index c3606b6c04..7907f7bfa4 100644
--- a/tests/functional/test_objects_issues.py
+++ b/tests/functional/test_objects_issues.py
@@ -9,124 +9,86 @@
# 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 coreapi
import pytest
-from adcm_pytest_plugin import utils
-from adcm_pytest_plugin.docker_utils import DockerWrapper
-
-# pylint: disable=W0611, W0621
-from tests.library import steps
-from tests.library.errorcodes import TASK_ERROR, UPGRADE_ERROR
-from tests.library.utils import get_action_by_name, wait_until
-
-
-@pytest.fixture()
-def adcm(image, request, adcm_credentials):
- repo, tag = image
- dw = DockerWrapper()
- adcm = dw.run_adcm(image=repo, tag=tag, pull=False)
- adcm.api.auth(**adcm_credentials)
- yield adcm
- adcm.stop()
-
-
-@pytest.fixture()
-def client(adcm):
- return adcm.api.objects
-
-
-def test_action_shouldnt_be_run_while_cluster_has_an_issue(client):
- with allure.step('Create default cluster and get id'):
- bundle = utils.get_data_dir(__file__, "cluster")
- steps.upload_bundle(client, bundle)
- cluster_id = steps.create_cluster(client)['id']
- with allure.step(f'Run action with error for cluster {cluster_id}'):
- with pytest.raises(coreapi.exceptions.ErrorMessage) as e:
- client.cluster.action.run.create(
- cluster_id=cluster_id,
- action_id=client.cluster.action.list(cluster_id=cluster_id)[0]['id'])
- with allure.step('Check if cluster action has issues'):
- TASK_ERROR.equal(e, 'action has issues')
-
-def test_action_shouldnt_be_run_while_host_has_an_issue(client):
- with allure.step('Create default host and get id'):
- bundle = utils.get_data_dir(__file__, "host")
- steps.upload_bundle(client, bundle)
- provider_id = steps.create_hostprovider(client)['id']
- host_id = client.host.create(prototype_id=client.stack.host.list()[0]['id'],
- provider_id=provider_id,
- fqdn=utils.random_string())['id']
- with allure.step(f'Run action with error for host {host_id}'):
- with pytest.raises(coreapi.exceptions.ErrorMessage) as e:
- client.host.action.run.create(
- host_id=host_id,
- action_id=client.host.action.list(host_id=host_id)[0]['id'])
- with allure.step('Check if host action has issues'):
- TASK_ERROR.equal(e, 'action has issues')
-
-
-def test_action_shouldnt_be_run_while_hostprovider_has_an_issue(client):
- with allure.step('Create default hostprovider and get id'):
- bundle = utils.get_data_dir(__file__, "provider")
- steps.upload_bundle(client, bundle)
- provider_id = steps.create_hostprovider(client)['id']
- with allure.step(f'Run action with error for provider {provider_id}'):
- with pytest.raises(coreapi.exceptions.ErrorMessage) as e:
- client.provider.action.run.create(
- provider_id=provider_id,
- action_id=client.provider.action.list(provider_id=provider_id)[0]['id'])
- with allure.step('Check if provider action has issues'):
- TASK_ERROR.equal(e, 'action has issues')
-
-
-def test_when_cluster_has_issue_than_upgrade_locked(client):
- with allure.step('Create cluster and upload new one bundle'):
- bundledir = utils.get_data_dir(__file__, "cluster")
- upgrade_bundle = utils.get_data_dir(__file__, "upgrade", "cluster")
- steps.upload_bundle(client, bundledir)
- cluster = steps.create_cluster(client)
- steps.upload_bundle(client, upgrade_bundle)
- with allure.step('Upgrade cluster'):
- upgrade_list = client.cluster.upgrade.list(cluster_id=cluster['id'])
+from adcm_client.base import ActionHasIssues
+from adcm_client.objects import ADCMClient
+from adcm_pytest_plugin import utils
+from tests.library.errorcodes import UPGRADE_ERROR
+
+
+def test_action_should_not_be_run_while_cluster_has_an_issue(sdk_client_fs: ADCMClient):
+ bundle_path = utils.get_data_dir(__file__, "cluster")
+ bundle = sdk_client_fs.upload_from_fs(bundle_path)
+ cluster = bundle.cluster_create(name=utils.random_string())
+ with allure.step(f"Run action with error for cluster {cluster.name}"):
+ with pytest.raises(ActionHasIssues):
+ cluster.action(name="install").run()
+
+
+def test_action_should_not_be_run_while_host_has_an_issue(sdk_client_fs: ADCMClient):
+ bundle_path = utils.get_data_dir(__file__, "host")
+ bundle = sdk_client_fs.upload_from_fs(bundle_path)
+ provider = bundle.provider_create(name=utils.random_string())
+ host = provider.host_create(fqdn=utils.random_string())
+ with allure.step(f"Run action with error for host {host.fqdn}"):
+ with pytest.raises(ActionHasIssues):
+ host.action(name="install").run()
+
+
+def test_action_should_not_be_run_while_hostprovider_has_an_issue(
+ sdk_client_fs: ADCMClient,
+):
+ bundle_path = utils.get_data_dir(__file__, "provider")
+ bundle = sdk_client_fs.upload_from_fs(bundle_path)
+ provider = bundle.provider_create(name=utils.random_string())
+ with allure.step(f"Run action with error for provider {provider.name}"):
+ with pytest.raises(ActionHasIssues):
+ provider.action(name="install").run()
+
+
+def test_when_cluster_has_issue_than_upgrade_locked(sdk_client_fs: ADCMClient):
+ with allure.step("Create cluster and upload new one bundle"):
+ old_bundle_path = utils.get_data_dir(__file__, "cluster")
+ new_bundle_path = utils.get_data_dir(__file__, "upgrade", "cluster")
+ 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("Upgrade cluster"):
with pytest.raises(coreapi.exceptions.ErrorMessage) as e:
- client.cluster.upgrade.do.create(
- cluster_id=cluster['id'],
- upgrade_id=upgrade_list[0]['id'])
- with allure.step('Check if cluster has issues'):
- UPGRADE_ERROR.equal(e, 'cluster ', ' has issue: ')
-
-
-def test_when_hostprovider_has_issue_than_upgrade_locked(client):
- with allure.step('Create hostprovider'):
- bundledir = utils.get_data_dir(__file__, "provider")
- upgrade_bundle = utils.get_data_dir(__file__, "upgrade", "provider")
- steps.upload_bundle(client, bundledir)
- provider_id = steps.create_hostprovider(client)['id']
- steps.upload_bundle(client, upgrade_bundle)
- with allure.step('Upgrade provider'):
+ cluster.upgrade().do()
+ with allure.step("Check if cluster has issues"):
+ UPGRADE_ERROR.equal(e, "cluster ", " has issue: ")
+
+
+def test_when_hostprovider_has_issue_than_upgrade_locked(sdk_client_fs: ADCMClient):
+ with allure.step("Create hostprovider"):
+ old_bundle_path = utils.get_data_dir(__file__, "provider")
+ new_bundle_path = utils.get_data_dir(__file__, "upgrade", "provider")
+ 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("Upgrade provider"):
with pytest.raises(coreapi.exceptions.ErrorMessage) as e:
- client.provider.upgrade.do.create(
- provider_id=provider_id,
- upgrade_id=client.provider.upgrade.list(provider_id=provider_id)[0]['id'])
- with allure.step('Check if upgrade locked'):
+ 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_hasnt_constraint_then_cluster_doesnt_have_issues(client):
- with allure.step('Create cluster (component hasnt constraint)'):
- bundledir = utils.get_data_dir(__file__, "cluster_component_hasnt_constraint")
- steps.upload_bundle(client, bundledir)
- cluster = steps.create_cluster(client)
- with allure.step('Create service'):
- steps.create_random_service(client, cluster['id'])
- with allure.step('Run action: lock cluster'):
- action = get_action_by_name(client, cluster, 'lock-cluster')
- wait_until(
- client,
- task=client.cluster.action.run.create(cluster_id=cluster['id'], action_id=action['id'])
- )
- with allure.step('Check if state is always-locked'):
- assert client.cluster.read(cluster_id=cluster['id'])['state'] == 'always-locked'
+@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,
+):
+ with allure.step("Create cluster (component has no constraint)"):
+ bundle_path = utils.get_data_dir(__file__, "cluster_component_hasnt_constraint")
+ bundle = sdk_client_fs.upload_from_fs(bundle_path)
+ cluster = bundle.cluster_create(name=utils.random_string())
+ cluster.service_add()
+ with allure.step("Run action: lock cluster"):
+ cluster.action(name="lock-cluster").run().try_wait()
+ with allure.step("Check if state is always-locked"):
+ cluster.reread()
+ assert cluster.state == "always-locked"
diff --git a/tests/functional/test_objects_issues_data/cluster_component_hasnt_constraint/config.yaml b/tests/functional/test_objects_issues_data/cluster_component_hasnt_constraint/config.yaml
index 675c10c190..b7a083dc84 100644
--- a/tests/functional/test_objects_issues_data/cluster_component_hasnt_constraint/config.yaml
+++ b/tests/functional/test_objects_issues_data/cluster_component_hasnt_constraint/config.yaml
@@ -12,7 +12,7 @@
---
- type: service
name: bookkeeper
- version: &bookkeeper_version 1.0.10
+ version: 1.0.10
config:
client_port:
@@ -36,7 +36,7 @@
default: qwerty1234
required: false
svc-ro-created:
- display_name:
+ display_name: some display name
type: string
default: bluh
required: false
diff --git a/tests/functional/test_objects_issues_data/host/config.yaml b/tests/functional/test_objects_issues_data/host/config.yaml
index 1ee4130856..0b150a5f00 100644
--- a/tests/functional/test_objects_issues_data/host/config.yaml
+++ b/tests/functional/test_objects_issues_data/host/config.yaml
@@ -12,9 +12,7 @@
---
- type: provider
name: sample hostprovider
- version: 1.0
-
- config:
+ version: &version 1.0
actions:
install:
@@ -26,17 +24,17 @@
on_success: installed
on_fail: created
-- type: host
- name: host_with_issue
- description: host has issues and should not run any action while issues didn't fix
- version: &version 1.0
-
upgrade:
- name: Upgrade to v. *version
versions: { min: 0.5, max_strict: *version }
description: Host bundle upgrade
states: { available: any }
+- type: host
+ name: host_with_issue
+ description: host has issues and should not run any action while issues didn't fix
+ version: 1.0
+
config:
required_param:
type: integer
diff --git a/tests/functional/test_objects_issues_data/provider/config.yaml b/tests/functional/test_objects_issues_data/provider/config.yaml
index 727613d31d..7f38f87bf8 100644
--- a/tests/functional/test_objects_issues_data/provider/config.yaml
+++ b/tests/functional/test_objects_issues_data/provider/config.yaml
@@ -34,8 +34,6 @@
name: host sample
version: 1.0
- config:
-
actions:
install:
type: job
diff --git a/tests/functional/test_objects_issues_data/upgrade/provider/config.yaml b/tests/functional/test_objects_issues_data/upgrade/provider/config.yaml
index d7520598cb..42c4c5d6e7 100644
--- a/tests/functional/test_objects_issues_data/upgrade/provider/config.yaml
+++ b/tests/functional/test_objects_issues_data/upgrade/provider/config.yaml
@@ -40,8 +40,6 @@
name: host sample
version: 1.0
- config:
-
actions:
install:
type: job
diff --git a/tests/functional/test_plugins_config_data/cluster/cluster.yaml b/tests/functional/test_plugins_config_data/cluster/cluster.yaml
index a118332102..1a013bf26c 100644
--- a/tests/functional/test_plugins_config_data/cluster/cluster.yaml
+++ b/tests/functional/test_plugins_config_data/cluster/cluster.yaml
@@ -11,11 +11,11 @@
yyyy
new_string: &new_string "double new"
new_json: &new_json
- - x: "new"
- - y: "z"
+ - "x": "new"
+ - "y": "z"
new_map: &new_map
"one": "two"
- two: "new"
+ "two": "new"
new_list: &new_list
- "one"
- "new"
diff --git a/tests/functional/test_plugins_config_data/cluster/config.yaml b/tests/functional/test_plugins_config_data/cluster/config.yaml
index d79c4526da..f29902a3d9 100644
--- a/tests/functional/test_plugins_config_data/cluster/config.yaml
+++ b/tests/functional/test_plugins_config_data/cluster/config.yaml
@@ -168,15 +168,15 @@
json:
type: json
default:
- - x: "y"
- - y: "z"
+ - "x": "y"
+ - "y": "z"
required: no
map:
type: map
default:
"one": "two"
- two: "three"
+ "two": "three"
required: no
list:
@@ -284,7 +284,6 @@
version: 1
actions:
- <<: *service_actions
- <<: *cluster_actions
+ <<: [*service_actions, *cluster_actions]
config: *config
diff --git a/tests/functional/test_plugins_config_data/cluster/service.yaml b/tests/functional/test_plugins_config_data/cluster/service.yaml
index bc99f92262..3cb1585cf4 100644
--- a/tests/functional/test_plugins_config_data/cluster/service.yaml
+++ b/tests/functional/test_plugins_config_data/cluster/service.yaml
@@ -11,11 +11,11 @@
yyyy
new_string: &new_string "double new"
new_json: &new_json
- - x: "new"
- - y: "z"
+ - "x": "new"
+ - "y": "z"
new_map: &new_map
"one": "two"
- two: "new"
+ "two": "new"
new_list: &new_list
- "one"
- "new"
diff --git a/tests/functional/test_plugins_config_data/cluster/service_name.yaml b/tests/functional/test_plugins_config_data/cluster/service_name.yaml
index c1e085fbff..aecbb1e8d0 100644
--- a/tests/functional/test_plugins_config_data/cluster/service_name.yaml
+++ b/tests/functional/test_plugins_config_data/cluster/service_name.yaml
@@ -11,11 +11,11 @@
yyyy
new_string: &new_string "double new"
new_json: &new_json
- - x: "new"
- - y: "z"
+ - "x": "new"
+ - "y": "z"
new_map: &new_map
"one": "two"
- two: "new"
+ "two": "new"
new_list: &new_list
- "one"
- "new"
diff --git a/tests/functional/test_plugins_config_data/cluster/service_name_First.yaml b/tests/functional/test_plugins_config_data/cluster/service_name_First.yaml
index c1e085fbff..aecbb1e8d0 100644
--- a/tests/functional/test_plugins_config_data/cluster/service_name_First.yaml
+++ b/tests/functional/test_plugins_config_data/cluster/service_name_First.yaml
@@ -11,11 +11,11 @@
yyyy
new_string: &new_string "double new"
new_json: &new_json
- - x: "new"
- - y: "z"
+ - "x": "new"
+ - "y": "z"
new_map: &new_map
"one": "two"
- two: "new"
+ "two": "new"
new_list: &new_list
- "one"
- "new"
diff --git a/tests/functional/test_plugins_config_data/cluster/service_name_Second.yaml b/tests/functional/test_plugins_config_data/cluster/service_name_Second.yaml
index ca9201eaa1..8f3ee000d8 100644
--- a/tests/functional/test_plugins_config_data/cluster/service_name_Second.yaml
+++ b/tests/functional/test_plugins_config_data/cluster/service_name_Second.yaml
@@ -11,11 +11,11 @@
yyyy
new_string: &new_string "double new"
new_json: &new_json
- - x: "new"
- - y: "z"
+ - "x": "new"
+ - "y": "z"
new_map: &new_map
"one": "two"
- two: "new"
+ "two": "new"
new_list: &new_list
- "one"
- "new"
diff --git a/tests/functional/test_plugins_config_data/provider/config.yaml b/tests/functional/test_plugins_config_data/provider/config.yaml
index e6a8842562..7e3448191b 100644
--- a/tests/functional/test_plugins_config_data/provider/config.yaml
+++ b/tests/functional/test_plugins_config_data/provider/config.yaml
@@ -127,8 +127,8 @@
json:
type: json
default:
- - x: "y"
- - y: "z"
+ - "x": "y"
+ - "y": "z"
required: no
map:
diff --git a/tests/functional/test_plugins_config_data/provider/host.yaml b/tests/functional/test_plugins_config_data/provider/host.yaml
index d39dfcfcd7..32cd21df66 100644
--- a/tests/functional/test_plugins_config_data/provider/host.yaml
+++ b/tests/functional/test_plugins_config_data/provider/host.yaml
@@ -12,11 +12,11 @@
yyyy
new_string: &new_string "double new"
new_json: &new_json
- - x: "new"
- - y: "z"
+ - "x": "new"
+ - "y": "z"
new_map: &new_map
"one": "two"
- two: "new"
+ "two": "new"
new_list: &new_list
- "one"
- "new"
diff --git a/tests/functional/test_plugins_config_data/provider/host_from_provider.yaml b/tests/functional/test_plugins_config_data/provider/host_from_provider.yaml
index 6a750ea6df..993f979295 100644
--- a/tests/functional/test_plugins_config_data/provider/host_from_provider.yaml
+++ b/tests/functional/test_plugins_config_data/provider/host_from_provider.yaml
@@ -12,11 +12,11 @@
yyyy
new_string: &new_string "double new"
new_json: &new_json
- - x: "new"
- - y: "z"
+ - "x": "new"
+ - "y": "z"
new_map: &new_map
"one": "two"
- two: "new"
+ "two": "new"
new_list: &new_list
- "one"
- "new"
diff --git a/tests/functional/test_plugins_config_data/provider/provider.yaml b/tests/functional/test_plugins_config_data/provider/provider.yaml
index cd2b3a9076..37abbf7776 100644
--- a/tests/functional/test_plugins_config_data/provider/provider.yaml
+++ b/tests/functional/test_plugins_config_data/provider/provider.yaml
@@ -11,11 +11,11 @@
yyyy
new_string: &new_string "double new"
new_json: &new_json
- - x: "new"
- - y: "z"
+ - "x": "new"
+ - "y": "z"
new_map: &new_map
"one": "two"
- two: "new"
+ "two": "new"
new_list: &new_list
- "one"
- "new"
diff --git a/tests/functional/test_read_only_parameters_data/config.yaml b/tests/functional/test_read_only_parameters_data/config.yaml
index 0536429bef..8dc1451829 100644
--- a/tests/functional/test_read_only_parameters_data/config.yaml
+++ b/tests/functional/test_read_only_parameters_data/config.yaml
@@ -12,8 +12,6 @@
- type: cluster
name: sample
version: '0.001'
- import:
- upgrade:
actions:
install:
diff --git a/tests/functional/test_bound_hc.py b/tests/functional/test_related_hc.py
similarity index 55%
rename from tests/functional/test_bound_hc.py
rename to tests/functional/test_related_hc.py
index 11c317ee1c..df9fb2f6d4 100644
--- a/tests/functional/test_bound_hc.py
+++ b/tests/functional/test_related_hc.py
@@ -9,17 +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.
+
+# pylint: disable=too-many-locals
+
import os
+from contextlib import contextmanager
+from typing import List
import allure
import pytest
import yaml
+from _pytest.mark import ParameterSet
from adcm_client.base import ObjectNotFound
from adcm_client.objects import ADCMClient
from adcm_pytest_plugin.utils import random_string, get_data_dir, get_data_subdirs_as_parameters
from coreapi.exceptions import ErrorMessage
-CASES_PATH = "/cases"
+CASES_PATH = "cases"
+
+
+@contextmanager
+def _does_not_raise():
+ yield
def _get_or_add_service(cluster, service_name):
@@ -32,43 +43,32 @@ def _get_or_add_service(cluster, service_name):
return cluster.service_add(name=service_name)
-class CasesPathsForParametrize:
- def __init__(self, case_paths, ids):
- self.cases_paths = case_paths
- self.ids = ids
-
-
-def get_cases_paths() -> CasesPathsForParametrize:
+def _get_cases_paths(path: str) -> List[ParameterSet]:
"""
Get cases path for parametrize test
"""
- bundles_paths, bundles_ids = get_data_subdirs_as_parameters(__file__, "bundle_configs")
- cases_paths = []
- ids = []
+ bundles_paths, bundles_ids = get_data_subdirs_as_parameters(__file__, "bundle_configs", path)
+ params = []
for i, bundle_path in enumerate(bundles_paths):
- for b in os.listdir(bundle_path + CASES_PATH):
- cases_paths.append("{}/{}".format(bundle_path + CASES_PATH, b))
- ids.append(f"{bundles_ids[i]}_{b.strip('.yaml')}")
- return CasesPathsForParametrize(cases_paths, ids)
-
-
-cases_paths_param = get_cases_paths()
+ for sub_path in ["positive", "negative"]:
+ case_dir = os.path.join(bundle_path, CASES_PATH, sub_path)
+ for b in os.listdir(case_dir):
+ params.append(
+ pytest.param(os.path.join(case_dir, b),
+ id=f"{bundles_ids[i]}_{sub_path}_{b.strip('.yaml')}")
+ )
+ return params
-@allure.link(url="https://arenadata.atlassian.net/browse/ADCM-1535", name="Test cases")
-@pytest.mark.parametrize("case_path", cases_paths_param.cases_paths, ids=cases_paths_param.ids)
-def test_binded_hc(sdk_client_fs: ADCMClient, case_path):
- """
- Tests for binded components on hosts
- https://arenadata.atlassian.net/browse/ADCM-1535
- """
- with allure.step('Upload custom provider bundle and create it in ADCM'):
- provider_bundle = sdk_client_fs.upload_from_fs(get_data_dir(__file__) + "/provider")
+def _test_related_hc(client: ADCMClient, case_path: str):
+ with allure.step("Upload custom provider bundle and create it in ADCM"):
+ provider_bundle = client.upload_from_fs(get_data_dir(__file__) + "/provider")
provider = provider_bundle.provider_prototype().provider_create(random_string())
- with allure.step('Upload custom cluster bundle and create it in ADCM'):
- cluster_bundle = sdk_client_fs.upload_from_fs(case_path.split(CASES_PATH)[0])
+ with allure.step("Upload custom cluster bundle and create it in ADCM"):
+ bundle_path = case_path.split(CASES_PATH)[0]
+ cluster_bundle = client.upload_from_fs(bundle_path)
created_cluster = cluster_bundle.cluster_prototype().cluster_create(random_string())
- with allure.step('Parse case description from YAML file and set host-component map'):
+ with allure.step("Parse case description from YAML file and set host-component map"):
with open(case_path) as file:
case_template = yaml.safe_load(file)
allure.dynamic.description(case_template["description"])
@@ -82,8 +82,28 @@ def test_binded_hc(sdk_client_fs: ADCMClient, case_path):
hostcomponent_list.append(
(added_host, service.component(name=component_name))
)
- if case_template["positive"] is False:
- with pytest.raises(ErrorMessage):
- created_cluster.hostcomponent_set(*hostcomponent_list)
- else:
+ expectation = (
+ _does_not_raise()
+ if case_template["positive"] else
+ pytest.raises(ErrorMessage)
+ )
+ with expectation:
created_cluster.hostcomponent_set(*hostcomponent_list)
+
+
+@allure.link(url="https://arenadata.atlassian.net/browse/ADCM-1535", name="Test cases")
+@pytest.mark.parametrize("case_path", _get_cases_paths("bound_to"))
+def test_bound_hc(sdk_client_fs: ADCMClient, case_path: str):
+ """
+ Tests for bound components on hosts
+ """
+ _test_related_hc(sdk_client_fs, case_path)
+
+
+@allure.link(url="https://arenadata.atlassian.net/browse/ADCM-1633", name="Test cases")
+@pytest.mark.parametrize("case_path", _get_cases_paths("requires"))
+def test_required_hc(sdk_client_fs: ADCMClient, case_path: str):
+ """
+ Tests for required components on hosts
+ """
+ _test_related_hc(sdk_client_fs, case_path)
diff --git a/tests/functional/test_bound_hc_data/bundle_configs/A_X_binded_to_A_Y/cases/case_1.yaml b/tests/functional/test_related_hc_data/bundle_configs/bound_to/A_X_to_A_Y/cases/negative/case_1.yaml
similarity index 100%
rename from tests/functional/test_bound_hc_data/bundle_configs/A_X_binded_to_A_Y/cases/case_1.yaml
rename to tests/functional/test_related_hc_data/bundle_configs/bound_to/A_X_to_A_Y/cases/negative/case_1.yaml
diff --git a/tests/functional/test_bound_hc_data/bundle_configs/A_X_binded_to_A_Y/cases/case_4.yaml b/tests/functional/test_related_hc_data/bundle_configs/bound_to/A_X_to_A_Y/cases/negative/case_4.yaml
similarity index 83%
rename from tests/functional/test_bound_hc_data/bundle_configs/A_X_binded_to_A_Y/cases/case_4.yaml
rename to tests/functional/test_related_hc_data/bundle_configs/bound_to/A_X_to_A_Y/cases/negative/case_4.yaml
index 4f5ce0e50c..700739c63b 100644
--- a/tests/functional/test_bound_hc_data/bundle_configs/A_X_binded_to_A_Y/cases/case_4.yaml
+++ b/tests/functional/test_related_hc_data/bundle_configs/bound_to/A_X_to_A_Y/cases/negative/case_4.yaml
@@ -1,4 +1,4 @@
-description: Attempt to save HC map with A.X and A.Y on first host and A.Y on second host- should fail
+description: Attempt to save HC map with A.X and A.Y on first host and A.Y on second host - should fail
positive: False
hc_map:
first_host:
diff --git a/tests/functional/test_bound_hc_data/bundle_configs/A_X_binded_to_A_Y/cases/case_2.yaml b/tests/functional/test_related_hc_data/bundle_configs/bound_to/A_X_to_A_Y/cases/positive/case_2.yaml
similarity index 100%
rename from tests/functional/test_bound_hc_data/bundle_configs/A_X_binded_to_A_Y/cases/case_2.yaml
rename to tests/functional/test_related_hc_data/bundle_configs/bound_to/A_X_to_A_Y/cases/positive/case_2.yaml
diff --git a/tests/functional/test_bound_hc_data/bundle_configs/A_X_binded_to_A_Y/cases/case_3.yaml b/tests/functional/test_related_hc_data/bundle_configs/bound_to/A_X_to_A_Y/cases/positive/case_3.yaml
similarity index 100%
rename from tests/functional/test_bound_hc_data/bundle_configs/A_X_binded_to_A_Y/cases/case_3.yaml
rename to tests/functional/test_related_hc_data/bundle_configs/bound_to/A_X_to_A_Y/cases/positive/case_3.yaml
diff --git a/tests/functional/test_bound_hc_data/bundle_configs/A_X_binded_to_A_Y/cases/case_5.yaml b/tests/functional/test_related_hc_data/bundle_configs/bound_to/A_X_to_A_Y/cases/positive/case_5.yaml
similarity index 100%
rename from tests/functional/test_bound_hc_data/bundle_configs/A_X_binded_to_A_Y/cases/case_5.yaml
rename to tests/functional/test_related_hc_data/bundle_configs/bound_to/A_X_to_A_Y/cases/positive/case_5.yaml
diff --git a/tests/functional/test_bound_hc_data/bundle_configs/A_X_binded_to_A_Y/config.yaml b/tests/functional/test_related_hc_data/bundle_configs/bound_to/A_X_to_A_Y/config.yaml
similarity index 88%
rename from tests/functional/test_bound_hc_data/bundle_configs/A_X_binded_to_A_Y/config.yaml
rename to tests/functional/test_related_hc_data/bundle_configs/bound_to/A_X_to_A_Y/config.yaml
index 6db4d32654..4e9c07fc6b 100644
--- a/tests/functional/test_bound_hc_data/bundle_configs/A_X_binded_to_A_Y/config.yaml
+++ b/tests/functional/test_related_hc_data/bundle_configs/bound_to/A_X_to_A_Y/config.yaml
@@ -15,12 +15,6 @@
name: some_name
version: 1
- actions:
- first:
- type: job
- script: ansible/init.yaml
- script_type: ansible
-
- type: service
name: service_1
version: 1.0
diff --git a/tests/functional/test_bound_hc_data/bundle_configs/A_X_binded_to_B_Y/cases/case_1.yaml b/tests/functional/test_related_hc_data/bundle_configs/bound_to/A_X_to_B_Y/cases/negative/case_1.yaml
similarity index 100%
rename from tests/functional/test_bound_hc_data/bundle_configs/A_X_binded_to_B_Y/cases/case_1.yaml
rename to tests/functional/test_related_hc_data/bundle_configs/bound_to/A_X_to_B_Y/cases/negative/case_1.yaml
diff --git a/tests/functional/test_bound_hc_data/bundle_configs/A_X_binded_to_B_Y/cases/case_4.yaml b/tests/functional/test_related_hc_data/bundle_configs/bound_to/A_X_to_B_Y/cases/negative/case_4.yaml
similarity index 83%
rename from tests/functional/test_bound_hc_data/bundle_configs/A_X_binded_to_B_Y/cases/case_4.yaml
rename to tests/functional/test_related_hc_data/bundle_configs/bound_to/A_X_to_B_Y/cases/negative/case_4.yaml
index 4defe933ab..0aaab107a6 100644
--- a/tests/functional/test_bound_hc_data/bundle_configs/A_X_binded_to_B_Y/cases/case_4.yaml
+++ b/tests/functional/test_related_hc_data/bundle_configs/bound_to/A_X_to_B_Y/cases/negative/case_4.yaml
@@ -1,4 +1,4 @@
-description: Attempt to save HC map with A.X and B.Y on first host and B.Y on second host- should fail
+description: Attempt to save HC map with A.X and B.Y on first host and B.Y on second host - should fail
positive: False
hc_map:
first_host:
diff --git a/tests/functional/test_bound_hc_data/bundle_configs/A_X_binded_to_B_Y/cases/case_2.yaml b/tests/functional/test_related_hc_data/bundle_configs/bound_to/A_X_to_B_Y/cases/positive/case_2.yaml
similarity index 100%
rename from tests/functional/test_bound_hc_data/bundle_configs/A_X_binded_to_B_Y/cases/case_2.yaml
rename to tests/functional/test_related_hc_data/bundle_configs/bound_to/A_X_to_B_Y/cases/positive/case_2.yaml
diff --git a/tests/functional/test_bound_hc_data/bundle_configs/A_X_binded_to_B_Y/cases/case_3.yaml b/tests/functional/test_related_hc_data/bundle_configs/bound_to/A_X_to_B_Y/cases/positive/case_3.yaml
similarity index 100%
rename from tests/functional/test_bound_hc_data/bundle_configs/A_X_binded_to_B_Y/cases/case_3.yaml
rename to tests/functional/test_related_hc_data/bundle_configs/bound_to/A_X_to_B_Y/cases/positive/case_3.yaml
diff --git a/tests/functional/test_bound_hc_data/bundle_configs/A_X_binded_to_B_Y/cases/case_5.yaml b/tests/functional/test_related_hc_data/bundle_configs/bound_to/A_X_to_B_Y/cases/positive/case_5.yaml
similarity index 100%
rename from tests/functional/test_bound_hc_data/bundle_configs/A_X_binded_to_B_Y/cases/case_5.yaml
rename to tests/functional/test_related_hc_data/bundle_configs/bound_to/A_X_to_B_Y/cases/positive/case_5.yaml
diff --git a/tests/functional/test_bound_hc_data/bundle_configs/A_X_binded_to_B_Y/config.yaml b/tests/functional/test_related_hc_data/bundle_configs/bound_to/A_X_to_B_Y/config.yaml
similarity index 89%
rename from tests/functional/test_bound_hc_data/bundle_configs/A_X_binded_to_B_Y/config.yaml
rename to tests/functional/test_related_hc_data/bundle_configs/bound_to/A_X_to_B_Y/config.yaml
index 188e3d675d..02d7401441 100644
--- a/tests/functional/test_bound_hc_data/bundle_configs/A_X_binded_to_B_Y/config.yaml
+++ b/tests/functional/test_related_hc_data/bundle_configs/bound_to/A_X_to_B_Y/config.yaml
@@ -15,12 +15,6 @@
name: some_name
version: 1
- actions:
- first:
- type: job
- script: ansible/init.yaml
- script_type: ansible
-
- type: service
name: service_1
version: 1.0
diff --git a/tests/functional/test_bound_hc_data/bundle_configs/A_X_binded_to_B_Y_and_B_Y_binded_to_C_Z/cases/case_1.yaml b/tests/functional/test_related_hc_data/bundle_configs/bound_to/A_X_to_B_Y_and_B_Y_to_C_Z/cases/negative/case_1.yaml
similarity index 100%
rename from tests/functional/test_bound_hc_data/bundle_configs/A_X_binded_to_B_Y_and_B_Y_binded_to_C_Z/cases/case_1.yaml
rename to tests/functional/test_related_hc_data/bundle_configs/bound_to/A_X_to_B_Y_and_B_Y_to_C_Z/cases/negative/case_1.yaml
diff --git a/tests/functional/test_bound_hc_data/bundle_configs/A_X_binded_to_B_Y_and_B_Y_binded_to_C_Z/cases/case_2.yaml b/tests/functional/test_related_hc_data/bundle_configs/bound_to/A_X_to_B_Y_and_B_Y_to_C_Z/cases/negative/case_2.yaml
similarity index 100%
rename from tests/functional/test_bound_hc_data/bundle_configs/A_X_binded_to_B_Y_and_B_Y_binded_to_C_Z/cases/case_2.yaml
rename to tests/functional/test_related_hc_data/bundle_configs/bound_to/A_X_to_B_Y_and_B_Y_to_C_Z/cases/negative/case_2.yaml
diff --git a/tests/functional/test_bound_hc_data/bundle_configs/A_X_binded_to_B_Y_and_B_Y_binded_to_C_Z/cases/case_4.yaml b/tests/functional/test_related_hc_data/bundle_configs/bound_to/A_X_to_B_Y_and_B_Y_to_C_Z/cases/negative/case_4.yaml
similarity index 100%
rename from tests/functional/test_bound_hc_data/bundle_configs/A_X_binded_to_B_Y_and_B_Y_binded_to_C_Z/cases/case_4.yaml
rename to tests/functional/test_related_hc_data/bundle_configs/bound_to/A_X_to_B_Y_and_B_Y_to_C_Z/cases/negative/case_4.yaml
diff --git a/tests/functional/test_bound_hc_data/bundle_configs/A_X_binded_to_B_Y_and_B_Y_binded_to_C_Z/cases/case_5.yaml b/tests/functional/test_related_hc_data/bundle_configs/bound_to/A_X_to_B_Y_and_B_Y_to_C_Z/cases/negative/case_5.yaml
similarity index 100%
rename from tests/functional/test_bound_hc_data/bundle_configs/A_X_binded_to_B_Y_and_B_Y_binded_to_C_Z/cases/case_5.yaml
rename to tests/functional/test_related_hc_data/bundle_configs/bound_to/A_X_to_B_Y_and_B_Y_to_C_Z/cases/negative/case_5.yaml
diff --git a/tests/functional/test_bound_hc_data/bundle_configs/A_X_binded_to_B_Y_and_B_Y_binded_to_C_Z/cases/case_3.yaml b/tests/functional/test_related_hc_data/bundle_configs/bound_to/A_X_to_B_Y_and_B_Y_to_C_Z/cases/positive/case_3.yaml
similarity index 100%
rename from tests/functional/test_bound_hc_data/bundle_configs/A_X_binded_to_B_Y_and_B_Y_binded_to_C_Z/cases/case_3.yaml
rename to tests/functional/test_related_hc_data/bundle_configs/bound_to/A_X_to_B_Y_and_B_Y_to_C_Z/cases/positive/case_3.yaml
diff --git a/tests/functional/test_bound_hc_data/bundle_configs/A_X_binded_to_B_Y_and_B_Y_binded_to_C_Z/cases/case_6.yaml b/tests/functional/test_related_hc_data/bundle_configs/bound_to/A_X_to_B_Y_and_B_Y_to_C_Z/cases/positive/case_6.yaml
similarity index 100%
rename from tests/functional/test_bound_hc_data/bundle_configs/A_X_binded_to_B_Y_and_B_Y_binded_to_C_Z/cases/case_6.yaml
rename to tests/functional/test_related_hc_data/bundle_configs/bound_to/A_X_to_B_Y_and_B_Y_to_C_Z/cases/positive/case_6.yaml
diff --git a/tests/functional/test_bound_hc_data/bundle_configs/A_X_binded_to_B_Y_and_B_Y_binded_to_C_Z/cases/case_7.yaml b/tests/functional/test_related_hc_data/bundle_configs/bound_to/A_X_to_B_Y_and_B_Y_to_C_Z/cases/positive/case_7.yaml
similarity index 100%
rename from tests/functional/test_bound_hc_data/bundle_configs/A_X_binded_to_B_Y_and_B_Y_binded_to_C_Z/cases/case_7.yaml
rename to tests/functional/test_related_hc_data/bundle_configs/bound_to/A_X_to_B_Y_and_B_Y_to_C_Z/cases/positive/case_7.yaml
diff --git a/tests/functional/test_bound_hc_data/bundle_configs/A_X_binded_to_B_Y_and_B_Y_binded_to_C_Z/config.yaml b/tests/functional/test_related_hc_data/bundle_configs/bound_to/A_X_to_B_Y_and_B_Y_to_C_Z/config.yaml
similarity index 91%
rename from tests/functional/test_bound_hc_data/bundle_configs/A_X_binded_to_B_Y_and_B_Y_binded_to_C_Z/config.yaml
rename to tests/functional/test_related_hc_data/bundle_configs/bound_to/A_X_to_B_Y_and_B_Y_to_C_Z/config.yaml
index d1c3ffd736..0e14c960fc 100644
--- a/tests/functional/test_bound_hc_data/bundle_configs/A_X_binded_to_B_Y_and_B_Y_binded_to_C_Z/config.yaml
+++ b/tests/functional/test_related_hc_data/bundle_configs/bound_to/A_X_to_B_Y_and_B_Y_to_C_Z/config.yaml
@@ -15,12 +15,6 @@
name: some_name
version: 1
- actions:
- first:
- type: job
- script: ansible/init.yaml
- script_type: ansible
-
- type: service
name: service_1
version: 1.0
diff --git a/tests/functional/test_bound_hc_data/bundle_configs/A_X_binded_to_B_Y_and_B_Y_binded_to_C_Z_and_C_Z_binded_to_A_X/cases/case_1.yaml b/tests/functional/test_related_hc_data/bundle_configs/bound_to/A_X_to_B_Y_and_B_Y_to_C_Z_and_C_Z_to_A_X/cases/negative/case_1.yaml
similarity index 100%
rename from tests/functional/test_bound_hc_data/bundle_configs/A_X_binded_to_B_Y_and_B_Y_binded_to_C_Z_and_C_Z_binded_to_A_X/cases/case_1.yaml
rename to tests/functional/test_related_hc_data/bundle_configs/bound_to/A_X_to_B_Y_and_B_Y_to_C_Z_and_C_Z_to_A_X/cases/negative/case_1.yaml
diff --git a/tests/functional/test_bound_hc_data/bundle_configs/A_X_binded_to_B_Y_and_B_Y_binded_to_C_Z_and_C_Z_binded_to_A_X/cases/case_2.yaml b/tests/functional/test_related_hc_data/bundle_configs/bound_to/A_X_to_B_Y_and_B_Y_to_C_Z_and_C_Z_to_A_X/cases/negative/case_2.yaml
similarity index 100%
rename from tests/functional/test_bound_hc_data/bundle_configs/A_X_binded_to_B_Y_and_B_Y_binded_to_C_Z_and_C_Z_binded_to_A_X/cases/case_2.yaml
rename to tests/functional/test_related_hc_data/bundle_configs/bound_to/A_X_to_B_Y_and_B_Y_to_C_Z_and_C_Z_to_A_X/cases/negative/case_2.yaml
diff --git a/tests/functional/test_bound_hc_data/bundle_configs/A_X_binded_to_B_Y_and_B_Y_binded_to_C_Z_and_C_Z_binded_to_A_X/cases/case_3.yaml b/tests/functional/test_related_hc_data/bundle_configs/bound_to/A_X_to_B_Y_and_B_Y_to_C_Z_and_C_Z_to_A_X/cases/negative/case_3.yaml
similarity index 100%
rename from tests/functional/test_bound_hc_data/bundle_configs/A_X_binded_to_B_Y_and_B_Y_binded_to_C_Z_and_C_Z_binded_to_A_X/cases/case_3.yaml
rename to tests/functional/test_related_hc_data/bundle_configs/bound_to/A_X_to_B_Y_and_B_Y_to_C_Z_and_C_Z_to_A_X/cases/negative/case_3.yaml
diff --git a/tests/functional/test_bound_hc_data/bundle_configs/A_X_binded_to_B_Y_and_B_Y_binded_to_C_Z_and_C_Z_binded_to_A_X/cases/case_4.yaml b/tests/functional/test_related_hc_data/bundle_configs/bound_to/A_X_to_B_Y_and_B_Y_to_C_Z_and_C_Z_to_A_X/cases/negative/case_4.yaml
similarity index 100%
rename from tests/functional/test_bound_hc_data/bundle_configs/A_X_binded_to_B_Y_and_B_Y_binded_to_C_Z_and_C_Z_binded_to_A_X/cases/case_4.yaml
rename to tests/functional/test_related_hc_data/bundle_configs/bound_to/A_X_to_B_Y_and_B_Y_to_C_Z_and_C_Z_to_A_X/cases/negative/case_4.yaml
diff --git a/tests/functional/test_bound_hc_data/bundle_configs/A_X_binded_to_B_Y_and_B_Y_binded_to_C_Z_and_C_Z_binded_to_A_X/cases/case_5.yaml b/tests/functional/test_related_hc_data/bundle_configs/bound_to/A_X_to_B_Y_and_B_Y_to_C_Z_and_C_Z_to_A_X/cases/negative/case_5.yaml
similarity index 100%
rename from tests/functional/test_bound_hc_data/bundle_configs/A_X_binded_to_B_Y_and_B_Y_binded_to_C_Z_and_C_Z_binded_to_A_X/cases/case_5.yaml
rename to tests/functional/test_related_hc_data/bundle_configs/bound_to/A_X_to_B_Y_and_B_Y_to_C_Z_and_C_Z_to_A_X/cases/negative/case_5.yaml
diff --git a/tests/functional/test_bound_hc_data/bundle_configs/A_X_binded_to_B_Y_and_B_Y_binded_to_C_Z_and_C_Z_binded_to_A_X/cases/case_6.yaml b/tests/functional/test_related_hc_data/bundle_configs/bound_to/A_X_to_B_Y_and_B_Y_to_C_Z_and_C_Z_to_A_X/cases/negative/case_6.yaml
similarity index 100%
rename from tests/functional/test_bound_hc_data/bundle_configs/A_X_binded_to_B_Y_and_B_Y_binded_to_C_Z_and_C_Z_binded_to_A_X/cases/case_6.yaml
rename to tests/functional/test_related_hc_data/bundle_configs/bound_to/A_X_to_B_Y_and_B_Y_to_C_Z_and_C_Z_to_A_X/cases/negative/case_6.yaml
diff --git a/tests/functional/test_bound_hc_data/bundle_configs/A_X_binded_to_B_Y_and_C_Z_binded_to_B_Y/cases/case_7.yaml b/tests/functional/test_related_hc_data/bundle_configs/bound_to/A_X_to_B_Y_and_B_Y_to_C_Z_and_C_Z_to_A_X/cases/positive/case_7.yaml
similarity index 100%
rename from tests/functional/test_bound_hc_data/bundle_configs/A_X_binded_to_B_Y_and_C_Z_binded_to_B_Y/cases/case_7.yaml
rename to tests/functional/test_related_hc_data/bundle_configs/bound_to/A_X_to_B_Y_and_B_Y_to_C_Z_and_C_Z_to_A_X/cases/positive/case_7.yaml
diff --git a/tests/functional/test_bound_hc_data/bundle_configs/A_X_binded_to_B_Y_and_B_Y_binded_to_C_Z_and_C_Z_binded_to_A_X/config.yaml b/tests/functional/test_related_hc_data/bundle_configs/bound_to/A_X_to_B_Y_and_B_Y_to_C_Z_and_C_Z_to_A_X/config.yaml
similarity index 91%
rename from tests/functional/test_bound_hc_data/bundle_configs/A_X_binded_to_B_Y_and_B_Y_binded_to_C_Z_and_C_Z_binded_to_A_X/config.yaml
rename to tests/functional/test_related_hc_data/bundle_configs/bound_to/A_X_to_B_Y_and_B_Y_to_C_Z_and_C_Z_to_A_X/config.yaml
index d133c0d744..5aa0d2a913 100644
--- a/tests/functional/test_bound_hc_data/bundle_configs/A_X_binded_to_B_Y_and_B_Y_binded_to_C_Z_and_C_Z_binded_to_A_X/config.yaml
+++ b/tests/functional/test_related_hc_data/bundle_configs/bound_to/A_X_to_B_Y_and_B_Y_to_C_Z_and_C_Z_to_A_X/config.yaml
@@ -15,12 +15,6 @@
name: some_name
version: 1
- actions:
- first:
- type: job
- script: ansible/init.yaml
- script_type: ansible
-
- type: service
name: service_1
version: 1.0
diff --git a/tests/functional/test_bound_hc_data/bundle_configs/A_X_binded_to_B_Y_and_C_Z_binded_to_B_Y/cases/case_1.yaml b/tests/functional/test_related_hc_data/bundle_configs/bound_to/A_X_to_B_Y_and_C_Z_to_B_Y/cases/negative/case_1.yaml
similarity index 100%
rename from tests/functional/test_bound_hc_data/bundle_configs/A_X_binded_to_B_Y_and_C_Z_binded_to_B_Y/cases/case_1.yaml
rename to tests/functional/test_related_hc_data/bundle_configs/bound_to/A_X_to_B_Y_and_C_Z_to_B_Y/cases/negative/case_1.yaml
diff --git a/tests/functional/test_bound_hc_data/bundle_configs/A_X_binded_to_B_Y_and_C_Z_binded_to_B_Y/cases/case_3.yaml b/tests/functional/test_related_hc_data/bundle_configs/bound_to/A_X_to_B_Y_and_C_Z_to_B_Y/cases/negative/case_3.yaml
similarity index 100%
rename from tests/functional/test_bound_hc_data/bundle_configs/A_X_binded_to_B_Y_and_C_Z_binded_to_B_Y/cases/case_3.yaml
rename to tests/functional/test_related_hc_data/bundle_configs/bound_to/A_X_to_B_Y_and_C_Z_to_B_Y/cases/negative/case_3.yaml
diff --git a/tests/functional/test_bound_hc_data/bundle_configs/A_X_binded_to_B_Y_and_C_Z_binded_to_B_Y/cases/case_5.yaml b/tests/functional/test_related_hc_data/bundle_configs/bound_to/A_X_to_B_Y_and_C_Z_to_B_Y/cases/negative/case_5.yaml
similarity index 100%
rename from tests/functional/test_bound_hc_data/bundle_configs/A_X_binded_to_B_Y_and_C_Z_binded_to_B_Y/cases/case_5.yaml
rename to tests/functional/test_related_hc_data/bundle_configs/bound_to/A_X_to_B_Y_and_C_Z_to_B_Y/cases/negative/case_5.yaml
diff --git a/tests/functional/test_bound_hc_data/bundle_configs/A_X_binded_to_B_Y_and_C_Z_binded_to_B_Y/cases/case_2.yaml b/tests/functional/test_related_hc_data/bundle_configs/bound_to/A_X_to_B_Y_and_C_Z_to_B_Y/cases/positive/case_2.yaml
similarity index 100%
rename from tests/functional/test_bound_hc_data/bundle_configs/A_X_binded_to_B_Y_and_C_Z_binded_to_B_Y/cases/case_2.yaml
rename to tests/functional/test_related_hc_data/bundle_configs/bound_to/A_X_to_B_Y_and_C_Z_to_B_Y/cases/positive/case_2.yaml
diff --git a/tests/functional/test_bound_hc_data/bundle_configs/A_X_binded_to_B_Y_and_C_Z_binded_to_B_Y/cases/case_4.yaml b/tests/functional/test_related_hc_data/bundle_configs/bound_to/A_X_to_B_Y_and_C_Z_to_B_Y/cases/positive/case_4.yaml
similarity index 100%
rename from tests/functional/test_bound_hc_data/bundle_configs/A_X_binded_to_B_Y_and_C_Z_binded_to_B_Y/cases/case_4.yaml
rename to tests/functional/test_related_hc_data/bundle_configs/bound_to/A_X_to_B_Y_and_C_Z_to_B_Y/cases/positive/case_4.yaml
diff --git a/tests/functional/test_bound_hc_data/bundle_configs/A_X_binded_to_B_Y_and_C_Z_binded_to_B_Y/cases/case_6.yaml b/tests/functional/test_related_hc_data/bundle_configs/bound_to/A_X_to_B_Y_and_C_Z_to_B_Y/cases/positive/case_6.yaml
similarity index 100%
rename from tests/functional/test_bound_hc_data/bundle_configs/A_X_binded_to_B_Y_and_C_Z_binded_to_B_Y/cases/case_6.yaml
rename to tests/functional/test_related_hc_data/bundle_configs/bound_to/A_X_to_B_Y_and_C_Z_to_B_Y/cases/positive/case_6.yaml
diff --git a/tests/functional/test_bound_hc_data/bundle_configs/A_X_binded_to_B_Y_and_B_Y_binded_to_C_Z_and_C_Z_binded_to_A_X/cases/case_7.yaml b/tests/functional/test_related_hc_data/bundle_configs/bound_to/A_X_to_B_Y_and_C_Z_to_B_Y/cases/positive/case_7.yaml
similarity index 99%
rename from tests/functional/test_bound_hc_data/bundle_configs/A_X_binded_to_B_Y_and_B_Y_binded_to_C_Z_and_C_Z_binded_to_A_X/cases/case_7.yaml
rename to tests/functional/test_related_hc_data/bundle_configs/bound_to/A_X_to_B_Y_and_C_Z_to_B_Y/cases/positive/case_7.yaml
index 788d1801b7..a3e748e0ca 100644
--- a/tests/functional/test_bound_hc_data/bundle_configs/A_X_binded_to_B_Y_and_B_Y_binded_to_C_Z_and_C_Z_binded_to_A_X/cases/case_7.yaml
+++ b/tests/functional/test_related_hc_data/bundle_configs/bound_to/A_X_to_B_Y_and_C_Z_to_B_Y/cases/positive/case_7.yaml
@@ -5,4 +5,3 @@ hc_map:
- service_1.component_1_1
- service_2.component_2_1
- service_3.component_3_1
-
diff --git a/tests/functional/test_bound_hc_data/bundle_configs/A_X_binded_to_B_Y_and_C_Z_binded_to_B_Y/config.yaml b/tests/functional/test_related_hc_data/bundle_configs/bound_to/A_X_to_B_Y_and_C_Z_to_B_Y/config.yaml
similarity index 91%
rename from tests/functional/test_bound_hc_data/bundle_configs/A_X_binded_to_B_Y_and_C_Z_binded_to_B_Y/config.yaml
rename to tests/functional/test_related_hc_data/bundle_configs/bound_to/A_X_to_B_Y_and_C_Z_to_B_Y/config.yaml
index 52aaa9d0ab..a192b587d3 100644
--- a/tests/functional/test_bound_hc_data/bundle_configs/A_X_binded_to_B_Y_and_C_Z_binded_to_B_Y/config.yaml
+++ b/tests/functional/test_related_hc_data/bundle_configs/bound_to/A_X_to_B_Y_and_C_Z_to_B_Y/config.yaml
@@ -15,12 +15,6 @@
name: some_name
version: 1
- actions:
- first:
- type: job
- script: ansible/init.yaml
- script_type: ansible
-
- type: service
name: service_1
version: 1.0
diff --git a/tests/functional/test_related_hc_data/bundle_configs/requires/A_X_to_A_Y_and_A_Y_to_A_X/cases/negative/case_1.yaml b/tests/functional/test_related_hc_data/bundle_configs/requires/A_X_to_A_Y_and_A_Y_to_A_X/cases/negative/case_1.yaml
new file mode 100644
index 0000000000..68e0966cfa
--- /dev/null
+++ b/tests/functional/test_related_hc_data/bundle_configs/requires/A_X_to_A_Y_and_A_Y_to_A_X/cases/negative/case_1.yaml
@@ -0,0 +1,5 @@
+description: Attempt to save HC map with only A.X on 1 host - should fail
+positive: False
+hc_map:
+ first_host:
+ - service_1.component_1_1
diff --git a/tests/functional/test_related_hc_data/bundle_configs/requires/A_X_to_A_Y_and_A_Y_to_A_X/cases/negative/case_2.yaml b/tests/functional/test_related_hc_data/bundle_configs/requires/A_X_to_A_Y_and_A_Y_to_A_X/cases/negative/case_2.yaml
new file mode 100644
index 0000000000..c9dc35f086
--- /dev/null
+++ b/tests/functional/test_related_hc_data/bundle_configs/requires/A_X_to_A_Y_and_A_Y_to_A_X/cases/negative/case_2.yaml
@@ -0,0 +1,5 @@
+description: Attempt to save HC map with only A.Y on 1 host - should fail
+positive: False
+hc_map:
+ first_host:
+ - service_1.component_1_2
diff --git a/tests/functional/test_related_hc_data/bundle_configs/requires/A_X_to_A_Y_and_A_Y_to_A_X/cases/positive/case_3.yaml b/tests/functional/test_related_hc_data/bundle_configs/requires/A_X_to_A_Y_and_A_Y_to_A_X/cases/positive/case_3.yaml
new file mode 100644
index 0000000000..dae15972d6
--- /dev/null
+++ b/tests/functional/test_related_hc_data/bundle_configs/requires/A_X_to_A_Y_and_A_Y_to_A_X/cases/positive/case_3.yaml
@@ -0,0 +1,6 @@
+description: Attempt to save HC map with A.X and A.Y on 1 host - should succeed
+positive: True
+hc_map:
+ first_host:
+ - service_1.component_1_1
+ - service_1.component_1_2
diff --git a/tests/functional/test_related_hc_data/bundle_configs/requires/A_X_to_A_Y_and_A_Y_to_A_X/cases/positive/case_4.yaml b/tests/functional/test_related_hc_data/bundle_configs/requires/A_X_to_A_Y_and_A_Y_to_A_X/cases/positive/case_4.yaml
new file mode 100644
index 0000000000..232dfe4c56
--- /dev/null
+++ b/tests/functional/test_related_hc_data/bundle_configs/requires/A_X_to_A_Y_and_A_Y_to_A_X/cases/positive/case_4.yaml
@@ -0,0 +1,7 @@
+description: Attempt to save HC map with A.X on first host and A.Y on second host - should succeed
+positive: True
+hc_map:
+ first_host:
+ - service_1.component_1_1
+ second_host:
+ - service_1.component_1_2
diff --git a/tests/functional/test_related_hc_data/bundle_configs/requires/A_X_to_A_Y_and_A_Y_to_A_X/config.yaml b/tests/functional/test_related_hc_data/bundle_configs/requires/A_X_to_A_Y_and_A_Y_to_A_X/config.yaml
new file mode 100644
index 0000000000..6aea2cff61
--- /dev/null
+++ b/tests/functional/test_related_hc_data/bundle_configs/requires/A_X_to_A_Y_and_A_Y_to_A_X/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: some_name
+ version: 1
+
+- type: service
+ name: service_1
+ version: 1.0
+ components:
+ component_1_1:
+ requires:
+ - service: service_1
+ component: component_1_2
+
+ component_1_2:
+ requires:
+ - service: service_1
+ component: component_1_1
diff --git a/tests/functional/test_related_hc_data/bundle_configs/requires/A_X_to_A_Y_direct_declaration/cases/negative/case_1.yaml b/tests/functional/test_related_hc_data/bundle_configs/requires/A_X_to_A_Y_direct_declaration/cases/negative/case_1.yaml
new file mode 100644
index 0000000000..68e0966cfa
--- /dev/null
+++ b/tests/functional/test_related_hc_data/bundle_configs/requires/A_X_to_A_Y_direct_declaration/cases/negative/case_1.yaml
@@ -0,0 +1,5 @@
+description: Attempt to save HC map with only A.X on 1 host - should fail
+positive: False
+hc_map:
+ first_host:
+ - service_1.component_1_1
diff --git a/tests/functional/test_related_hc_data/bundle_configs/requires/A_X_to_A_Y_direct_declaration/cases/positive/case_2.yaml b/tests/functional/test_related_hc_data/bundle_configs/requires/A_X_to_A_Y_direct_declaration/cases/positive/case_2.yaml
new file mode 100644
index 0000000000..c8dd48e994
--- /dev/null
+++ b/tests/functional/test_related_hc_data/bundle_configs/requires/A_X_to_A_Y_direct_declaration/cases/positive/case_2.yaml
@@ -0,0 +1,5 @@
+description: Attempt to save HC map with only A.Y on 1 host - should succeed
+positive: True
+hc_map:
+ first_host:
+ - service_1.component_1_2
diff --git a/tests/functional/test_related_hc_data/bundle_configs/requires/A_X_to_A_Y_direct_declaration/cases/positive/case_3.yaml b/tests/functional/test_related_hc_data/bundle_configs/requires/A_X_to_A_Y_direct_declaration/cases/positive/case_3.yaml
new file mode 100644
index 0000000000..dae15972d6
--- /dev/null
+++ b/tests/functional/test_related_hc_data/bundle_configs/requires/A_X_to_A_Y_direct_declaration/cases/positive/case_3.yaml
@@ -0,0 +1,6 @@
+description: Attempt to save HC map with A.X and A.Y on 1 host - should succeed
+positive: True
+hc_map:
+ first_host:
+ - service_1.component_1_1
+ - service_1.component_1_2
diff --git a/tests/functional/test_related_hc_data/bundle_configs/requires/A_X_to_A_Y_direct_declaration/cases/positive/case_4.yaml b/tests/functional/test_related_hc_data/bundle_configs/requires/A_X_to_A_Y_direct_declaration/cases/positive/case_4.yaml
new file mode 100644
index 0000000000..fc897ccb6b
--- /dev/null
+++ b/tests/functional/test_related_hc_data/bundle_configs/requires/A_X_to_A_Y_direct_declaration/cases/positive/case_4.yaml
@@ -0,0 +1,8 @@
+description: Attempt to save HC map with A.X and A.Y on first host and A.Y on second host - should succeed
+positive: True
+hc_map:
+ first_host:
+ - service_1.component_1_1
+ - service_1.component_1_2
+ second_host:
+ - service_1.component_1_2
diff --git a/tests/functional/test_related_hc_data/bundle_configs/requires/A_X_to_A_Y_direct_declaration/cases/positive/case_5.yaml b/tests/functional/test_related_hc_data/bundle_configs/requires/A_X_to_A_Y_direct_declaration/cases/positive/case_5.yaml
new file mode 100644
index 0000000000..d87b8b96ab
--- /dev/null
+++ b/tests/functional/test_related_hc_data/bundle_configs/requires/A_X_to_A_Y_direct_declaration/cases/positive/case_5.yaml
@@ -0,0 +1,9 @@
+description: Attempt to save HC map with A.X and A.Y on first and second hosts - should succeed
+positive: True
+hc_map:
+ first_host:
+ - service_1.component_1_1
+ - service_1.component_1_2
+ second_host:
+ - service_1.component_1_1
+ - service_1.component_1_2
diff --git a/tests/functional/test_related_hc_data/bundle_configs/requires/A_X_to_A_Y_direct_declaration/cases/positive/case_6.yaml b/tests/functional/test_related_hc_data/bundle_configs/requires/A_X_to_A_Y_direct_declaration/cases/positive/case_6.yaml
new file mode 100644
index 0000000000..232dfe4c56
--- /dev/null
+++ b/tests/functional/test_related_hc_data/bundle_configs/requires/A_X_to_A_Y_direct_declaration/cases/positive/case_6.yaml
@@ -0,0 +1,7 @@
+description: Attempt to save HC map with A.X on first host and A.Y on second host - should succeed
+positive: True
+hc_map:
+ first_host:
+ - service_1.component_1_1
+ second_host:
+ - service_1.component_1_2
diff --git a/tests/functional/test_related_hc_data/bundle_configs/requires/A_X_to_A_Y_direct_declaration/config.yaml b/tests/functional/test_related_hc_data/bundle_configs/requires/A_X_to_A_Y_direct_declaration/config.yaml
new file mode 100644
index 0000000000..31d68d1b6d
--- /dev/null
+++ b/tests/functional/test_related_hc_data/bundle_configs/requires/A_X_to_A_Y_direct_declaration/config.yaml
@@ -0,0 +1,27 @@
+# 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: some_name
+ version: 1
+
+- type: service
+ name: service_1
+ version: 1.0
+ components:
+ component_1_1:
+ requires:
+ - service: service_1
+ component: component_1_2
+
+ component_1_2:
diff --git a/tests/functional/test_related_hc_data/bundle_configs/requires/A_X_to_A_Y_indirect_declaration/cases/negative/case_1.yaml b/tests/functional/test_related_hc_data/bundle_configs/requires/A_X_to_A_Y_indirect_declaration/cases/negative/case_1.yaml
new file mode 100644
index 0000000000..68e0966cfa
--- /dev/null
+++ b/tests/functional/test_related_hc_data/bundle_configs/requires/A_X_to_A_Y_indirect_declaration/cases/negative/case_1.yaml
@@ -0,0 +1,5 @@
+description: Attempt to save HC map with only A.X on 1 host - should fail
+positive: False
+hc_map:
+ first_host:
+ - service_1.component_1_1
diff --git a/tests/functional/test_related_hc_data/bundle_configs/requires/A_X_to_A_Y_indirect_declaration/cases/positive/case_2.yaml b/tests/functional/test_related_hc_data/bundle_configs/requires/A_X_to_A_Y_indirect_declaration/cases/positive/case_2.yaml
new file mode 100644
index 0000000000..c8dd48e994
--- /dev/null
+++ b/tests/functional/test_related_hc_data/bundle_configs/requires/A_X_to_A_Y_indirect_declaration/cases/positive/case_2.yaml
@@ -0,0 +1,5 @@
+description: Attempt to save HC map with only A.Y on 1 host - should succeed
+positive: True
+hc_map:
+ first_host:
+ - service_1.component_1_2
diff --git a/tests/functional/test_related_hc_data/bundle_configs/requires/A_X_to_A_Y_indirect_declaration/cases/positive/case_3.yaml b/tests/functional/test_related_hc_data/bundle_configs/requires/A_X_to_A_Y_indirect_declaration/cases/positive/case_3.yaml
new file mode 100644
index 0000000000..dae15972d6
--- /dev/null
+++ b/tests/functional/test_related_hc_data/bundle_configs/requires/A_X_to_A_Y_indirect_declaration/cases/positive/case_3.yaml
@@ -0,0 +1,6 @@
+description: Attempt to save HC map with A.X and A.Y on 1 host - should succeed
+positive: True
+hc_map:
+ first_host:
+ - service_1.component_1_1
+ - service_1.component_1_2
diff --git a/tests/functional/test_related_hc_data/bundle_configs/requires/A_X_to_A_Y_indirect_declaration/cases/positive/case_4.yaml b/tests/functional/test_related_hc_data/bundle_configs/requires/A_X_to_A_Y_indirect_declaration/cases/positive/case_4.yaml
new file mode 100644
index 0000000000..fc897ccb6b
--- /dev/null
+++ b/tests/functional/test_related_hc_data/bundle_configs/requires/A_X_to_A_Y_indirect_declaration/cases/positive/case_4.yaml
@@ -0,0 +1,8 @@
+description: Attempt to save HC map with A.X and A.Y on first host and A.Y on second host - should succeed
+positive: True
+hc_map:
+ first_host:
+ - service_1.component_1_1
+ - service_1.component_1_2
+ second_host:
+ - service_1.component_1_2
diff --git a/tests/functional/test_related_hc_data/bundle_configs/requires/A_X_to_A_Y_indirect_declaration/cases/positive/case_5.yaml b/tests/functional/test_related_hc_data/bundle_configs/requires/A_X_to_A_Y_indirect_declaration/cases/positive/case_5.yaml
new file mode 100644
index 0000000000..d87b8b96ab
--- /dev/null
+++ b/tests/functional/test_related_hc_data/bundle_configs/requires/A_X_to_A_Y_indirect_declaration/cases/positive/case_5.yaml
@@ -0,0 +1,9 @@
+description: Attempt to save HC map with A.X and A.Y on first and second hosts - should succeed
+positive: True
+hc_map:
+ first_host:
+ - service_1.component_1_1
+ - service_1.component_1_2
+ second_host:
+ - service_1.component_1_1
+ - service_1.component_1_2
diff --git a/tests/functional/test_related_hc_data/bundle_configs/requires/A_X_to_A_Y_indirect_declaration/cases/positive/case_6.yaml b/tests/functional/test_related_hc_data/bundle_configs/requires/A_X_to_A_Y_indirect_declaration/cases/positive/case_6.yaml
new file mode 100644
index 0000000000..232dfe4c56
--- /dev/null
+++ b/tests/functional/test_related_hc_data/bundle_configs/requires/A_X_to_A_Y_indirect_declaration/cases/positive/case_6.yaml
@@ -0,0 +1,7 @@
+description: Attempt to save HC map with A.X on first host and A.Y on second host - should succeed
+positive: True
+hc_map:
+ first_host:
+ - service_1.component_1_1
+ second_host:
+ - service_1.component_1_2
diff --git a/tests/functional/test_related_hc_data/bundle_configs/requires/A_X_to_A_Y_indirect_declaration/config.yaml b/tests/functional/test_related_hc_data/bundle_configs/requires/A_X_to_A_Y_indirect_declaration/config.yaml
new file mode 100644
index 0000000000..782cf95716
--- /dev/null
+++ b/tests/functional/test_related_hc_data/bundle_configs/requires/A_X_to_A_Y_indirect_declaration/config.yaml
@@ -0,0 +1,26 @@
+# 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: some_name
+ version: 1
+
+- type: service
+ name: service_1
+ version: 1.0
+ components:
+ component_1_1:
+ requires:
+ - component: component_1_2
+
+ component_1_2:
diff --git a/tests/functional/test_related_hc_data/bundle_configs/requires/A_X_to_A_Y_indirect_declaration_with_constraint/cases/negative/case_1.yaml b/tests/functional/test_related_hc_data/bundle_configs/requires/A_X_to_A_Y_indirect_declaration_with_constraint/cases/negative/case_1.yaml
new file mode 100644
index 0000000000..68e0966cfa
--- /dev/null
+++ b/tests/functional/test_related_hc_data/bundle_configs/requires/A_X_to_A_Y_indirect_declaration_with_constraint/cases/negative/case_1.yaml
@@ -0,0 +1,5 @@
+description: Attempt to save HC map with only A.X on 1 host - should fail
+positive: False
+hc_map:
+ first_host:
+ - service_1.component_1_1
diff --git a/tests/functional/test_related_hc_data/bundle_configs/requires/A_X_to_A_Y_indirect_declaration_with_constraint/cases/negative/case_4.yaml b/tests/functional/test_related_hc_data/bundle_configs/requires/A_X_to_A_Y_indirect_declaration_with_constraint/cases/negative/case_4.yaml
new file mode 100644
index 0000000000..700739c63b
--- /dev/null
+++ b/tests/functional/test_related_hc_data/bundle_configs/requires/A_X_to_A_Y_indirect_declaration_with_constraint/cases/negative/case_4.yaml
@@ -0,0 +1,8 @@
+description: Attempt to save HC map with A.X and A.Y on first host and A.Y on second host - should fail
+positive: False
+hc_map:
+ first_host:
+ - service_1.component_1_1
+ - service_1.component_1_2
+ second_host:
+ - service_1.component_1_2
diff --git a/tests/functional/test_related_hc_data/bundle_configs/requires/A_X_to_A_Y_indirect_declaration_with_constraint/cases/negative/case_5.yaml b/tests/functional/test_related_hc_data/bundle_configs/requires/A_X_to_A_Y_indirect_declaration_with_constraint/cases/negative/case_5.yaml
new file mode 100644
index 0000000000..dd47e51ef0
--- /dev/null
+++ b/tests/functional/test_related_hc_data/bundle_configs/requires/A_X_to_A_Y_indirect_declaration_with_constraint/cases/negative/case_5.yaml
@@ -0,0 +1,9 @@
+description: Attempt to save HC map with A.X and A.Y on first and second hosts - should fail
+positive: False
+hc_map:
+ first_host:
+ - service_1.component_1_1
+ - service_1.component_1_2
+ second_host:
+ - service_1.component_1_1
+ - service_1.component_1_2
diff --git a/tests/functional/test_related_hc_data/bundle_configs/requires/A_X_to_A_Y_indirect_declaration_with_constraint/cases/negative/case_7.yaml b/tests/functional/test_related_hc_data/bundle_configs/requires/A_X_to_A_Y_indirect_declaration_with_constraint/cases/negative/case_7.yaml
new file mode 100644
index 0000000000..d3bdd3c495
--- /dev/null
+++ b/tests/functional/test_related_hc_data/bundle_configs/requires/A_X_to_A_Y_indirect_declaration_with_constraint/cases/negative/case_7.yaml
@@ -0,0 +1,7 @@
+description: Attempt to save HC map with 2 A.Y on 2 hosts - should fail
+positive: False
+hc_map:
+ first_host:
+ - service_1.component_1_2
+ second_host:
+ - service_1.component_1_2
diff --git a/tests/functional/test_related_hc_data/bundle_configs/requires/A_X_to_A_Y_indirect_declaration_with_constraint/cases/positive/case_2.yaml b/tests/functional/test_related_hc_data/bundle_configs/requires/A_X_to_A_Y_indirect_declaration_with_constraint/cases/positive/case_2.yaml
new file mode 100644
index 0000000000..c8dd48e994
--- /dev/null
+++ b/tests/functional/test_related_hc_data/bundle_configs/requires/A_X_to_A_Y_indirect_declaration_with_constraint/cases/positive/case_2.yaml
@@ -0,0 +1,5 @@
+description: Attempt to save HC map with only A.Y on 1 host - should succeed
+positive: True
+hc_map:
+ first_host:
+ - service_1.component_1_2
diff --git a/tests/functional/test_related_hc_data/bundle_configs/requires/A_X_to_A_Y_indirect_declaration_with_constraint/cases/positive/case_3.yaml b/tests/functional/test_related_hc_data/bundle_configs/requires/A_X_to_A_Y_indirect_declaration_with_constraint/cases/positive/case_3.yaml
new file mode 100644
index 0000000000..dae15972d6
--- /dev/null
+++ b/tests/functional/test_related_hc_data/bundle_configs/requires/A_X_to_A_Y_indirect_declaration_with_constraint/cases/positive/case_3.yaml
@@ -0,0 +1,6 @@
+description: Attempt to save HC map with A.X and A.Y on 1 host - should succeed
+positive: True
+hc_map:
+ first_host:
+ - service_1.component_1_1
+ - service_1.component_1_2
diff --git a/tests/functional/test_related_hc_data/bundle_configs/requires/A_X_to_A_Y_indirect_declaration_with_constraint/cases/positive/case_6.yaml b/tests/functional/test_related_hc_data/bundle_configs/requires/A_X_to_A_Y_indirect_declaration_with_constraint/cases/positive/case_6.yaml
new file mode 100644
index 0000000000..232dfe4c56
--- /dev/null
+++ b/tests/functional/test_related_hc_data/bundle_configs/requires/A_X_to_A_Y_indirect_declaration_with_constraint/cases/positive/case_6.yaml
@@ -0,0 +1,7 @@
+description: Attempt to save HC map with A.X on first host and A.Y on second host - should succeed
+positive: True
+hc_map:
+ first_host:
+ - service_1.component_1_1
+ second_host:
+ - service_1.component_1_2
diff --git a/tests/functional/test_related_hc_data/bundle_configs/requires/A_X_to_A_Y_indirect_declaration_with_constraint/config.yaml b/tests/functional/test_related_hc_data/bundle_configs/requires/A_X_to_A_Y_indirect_declaration_with_constraint/config.yaml
new file mode 100644
index 0000000000..a288eae3a4
--- /dev/null
+++ b/tests/functional/test_related_hc_data/bundle_configs/requires/A_X_to_A_Y_indirect_declaration_with_constraint/config.yaml
@@ -0,0 +1,27 @@
+# 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: some_name
+ version: 1
+
+- type: service
+ name: service_1
+ version: 1.0
+ components:
+ component_1_1:
+ requires:
+ - component: component_1_2
+
+ component_1_2:
+ constraint: [0,1]
diff --git a/tests/functional/test_related_hc_data/bundle_configs/requires/A_X_to_B_Y/cases/negative/case_1.yaml b/tests/functional/test_related_hc_data/bundle_configs/requires/A_X_to_B_Y/cases/negative/case_1.yaml
new file mode 100644
index 0000000000..68e0966cfa
--- /dev/null
+++ b/tests/functional/test_related_hc_data/bundle_configs/requires/A_X_to_B_Y/cases/negative/case_1.yaml
@@ -0,0 +1,5 @@
+description: Attempt to save HC map with only A.X on 1 host - should fail
+positive: False
+hc_map:
+ first_host:
+ - service_1.component_1_1
diff --git a/tests/functional/test_related_hc_data/bundle_configs/requires/A_X_to_B_Y/cases/positive/case_2.yaml b/tests/functional/test_related_hc_data/bundle_configs/requires/A_X_to_B_Y/cases/positive/case_2.yaml
new file mode 100644
index 0000000000..eea2684bf2
--- /dev/null
+++ b/tests/functional/test_related_hc_data/bundle_configs/requires/A_X_to_B_Y/cases/positive/case_2.yaml
@@ -0,0 +1,5 @@
+description: Attempt to save HC map with only B.Y on 1 host - should succeed
+positive: True
+hc_map:
+ first_host:
+ - service_2.component_2_1
diff --git a/tests/functional/test_related_hc_data/bundle_configs/requires/A_X_to_B_Y/cases/positive/case_3.yaml b/tests/functional/test_related_hc_data/bundle_configs/requires/A_X_to_B_Y/cases/positive/case_3.yaml
new file mode 100644
index 0000000000..8922315e63
--- /dev/null
+++ b/tests/functional/test_related_hc_data/bundle_configs/requires/A_X_to_B_Y/cases/positive/case_3.yaml
@@ -0,0 +1,6 @@
+description: Attempt to save HC map with A.X and B.Y on 1 host - should succeed
+positive: True
+hc_map:
+ first_host:
+ - service_1.component_1_1
+ - service_2.component_2_1
diff --git a/tests/functional/test_related_hc_data/bundle_configs/requires/A_X_to_B_Y/cases/positive/case_4.yaml b/tests/functional/test_related_hc_data/bundle_configs/requires/A_X_to_B_Y/cases/positive/case_4.yaml
new file mode 100644
index 0000000000..83d64c4d55
--- /dev/null
+++ b/tests/functional/test_related_hc_data/bundle_configs/requires/A_X_to_B_Y/cases/positive/case_4.yaml
@@ -0,0 +1,8 @@
+description: Attempt to save HC map with A.X and B.Y on first host and B.Y on second host - should succeed
+positive: True
+hc_map:
+ first_host:
+ - service_1.component_1_1
+ - service_2.component_2_1
+ second_host:
+ - service_2.component_2_1
diff --git a/tests/functional/test_related_hc_data/bundle_configs/requires/A_X_to_B_Y/cases/positive/case_5.yaml b/tests/functional/test_related_hc_data/bundle_configs/requires/A_X_to_B_Y/cases/positive/case_5.yaml
new file mode 100644
index 0000000000..3b5d8090e0
--- /dev/null
+++ b/tests/functional/test_related_hc_data/bundle_configs/requires/A_X_to_B_Y/cases/positive/case_5.yaml
@@ -0,0 +1,9 @@
+description: Attempt to save HC map with A.X and B.Y on first and second hosts - should succeed
+positive: True
+hc_map:
+ first_host:
+ - service_1.component_1_1
+ - service_2.component_2_1
+ second_host:
+ - service_1.component_1_1
+ - service_2.component_2_1
diff --git a/tests/functional/test_related_hc_data/bundle_configs/requires/A_X_to_B_Y/cases/positive/case_6.yaml b/tests/functional/test_related_hc_data/bundle_configs/requires/A_X_to_B_Y/cases/positive/case_6.yaml
new file mode 100644
index 0000000000..3e193f0720
--- /dev/null
+++ b/tests/functional/test_related_hc_data/bundle_configs/requires/A_X_to_B_Y/cases/positive/case_6.yaml
@@ -0,0 +1,7 @@
+description: Attempt to save HC map with A.X on first host and B.Y on second host - should succeed
+positive: True
+hc_map:
+ first_host:
+ - service_1.component_1_1
+ second_host:
+ - service_2.component_2_1
diff --git a/tests/functional/test_related_hc_data/bundle_configs/requires/A_X_to_B_Y/config.yaml b/tests/functional/test_related_hc_data/bundle_configs/requires/A_X_to_B_Y/config.yaml
new file mode 100644
index 0000000000..0f62a4b44e
--- /dev/null
+++ b/tests/functional/test_related_hc_data/bundle_configs/requires/A_X_to_B_Y/config.yaml
@@ -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.
+---
+
+- type: cluster
+ name: some_name
+ version: 1
+
+- type: service
+ name: service_1
+ version: 1.0
+ components:
+ component_1_1:
+ requires:
+ - service: service_2
+ component: component_2_1
+
+- type: service
+ name: service_2
+ version: 2.0
+ components:
+ component_2_1:
diff --git a/tests/functional/test_related_hc_data/bundle_configs/requires/A_X_to_B_Y_and_B_Y_to_A_X/cases/negative/case_1.yaml b/tests/functional/test_related_hc_data/bundle_configs/requires/A_X_to_B_Y_and_B_Y_to_A_X/cases/negative/case_1.yaml
new file mode 100644
index 0000000000..68e0966cfa
--- /dev/null
+++ b/tests/functional/test_related_hc_data/bundle_configs/requires/A_X_to_B_Y_and_B_Y_to_A_X/cases/negative/case_1.yaml
@@ -0,0 +1,5 @@
+description: Attempt to save HC map with only A.X on 1 host - should fail
+positive: False
+hc_map:
+ first_host:
+ - service_1.component_1_1
diff --git a/tests/functional/test_related_hc_data/bundle_configs/requires/A_X_to_B_Y_and_B_Y_to_A_X/cases/negative/case_2.yaml b/tests/functional/test_related_hc_data/bundle_configs/requires/A_X_to_B_Y_and_B_Y_to_A_X/cases/negative/case_2.yaml
new file mode 100644
index 0000000000..c8eb54b700
--- /dev/null
+++ b/tests/functional/test_related_hc_data/bundle_configs/requires/A_X_to_B_Y_and_B_Y_to_A_X/cases/negative/case_2.yaml
@@ -0,0 +1,5 @@
+description: Attempt to save HC map with only B.Y on 1 host - should fail
+positive: False
+hc_map:
+ first_host:
+ - service_2.component_2_1
diff --git a/tests/functional/test_related_hc_data/bundle_configs/requires/A_X_to_B_Y_and_B_Y_to_A_X/cases/positive/case_3.yaml b/tests/functional/test_related_hc_data/bundle_configs/requires/A_X_to_B_Y_and_B_Y_to_A_X/cases/positive/case_3.yaml
new file mode 100644
index 0000000000..8922315e63
--- /dev/null
+++ b/tests/functional/test_related_hc_data/bundle_configs/requires/A_X_to_B_Y_and_B_Y_to_A_X/cases/positive/case_3.yaml
@@ -0,0 +1,6 @@
+description: Attempt to save HC map with A.X and B.Y on 1 host - should succeed
+positive: True
+hc_map:
+ first_host:
+ - service_1.component_1_1
+ - service_2.component_2_1
diff --git a/tests/functional/test_related_hc_data/bundle_configs/requires/A_X_to_B_Y_and_B_Y_to_A_X/cases/positive/case_4.yaml b/tests/functional/test_related_hc_data/bundle_configs/requires/A_X_to_B_Y_and_B_Y_to_A_X/cases/positive/case_4.yaml
new file mode 100644
index 0000000000..3e193f0720
--- /dev/null
+++ b/tests/functional/test_related_hc_data/bundle_configs/requires/A_X_to_B_Y_and_B_Y_to_A_X/cases/positive/case_4.yaml
@@ -0,0 +1,7 @@
+description: Attempt to save HC map with A.X on first host and B.Y on second host - should succeed
+positive: True
+hc_map:
+ first_host:
+ - service_1.component_1_1
+ second_host:
+ - service_2.component_2_1
diff --git a/tests/functional/test_related_hc_data/bundle_configs/requires/A_X_to_B_Y_and_B_Y_to_A_X/config.yaml b/tests/functional/test_related_hc_data/bundle_configs/requires/A_X_to_B_Y_and_B_Y_to_A_X/config.yaml
new file mode 100644
index 0000000000..913f0026d2
--- /dev/null
+++ b/tests/functional/test_related_hc_data/bundle_configs/requires/A_X_to_B_Y_and_B_Y_to_A_X/config.yaml
@@ -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.
+---
+
+- type: cluster
+ name: some_name
+ version: 1
+
+- type: service
+ name: service_1
+ version: 1.0
+ components:
+ component_1_1:
+ requires:
+ - service: service_2
+ component: component_2_1
+
+- type: service
+ name: service_2
+ version: 1.0
+ components:
+ component_2_1:
+ requires:
+ - service: service_1
+ component: component_1_1
diff --git a/tests/functional/test_related_hc_data/bundle_configs/requires/A_X_to_B_Y_and_B_Y_to_C_Z/cases/negative/case_1.yaml b/tests/functional/test_related_hc_data/bundle_configs/requires/A_X_to_B_Y_and_B_Y_to_C_Z/cases/negative/case_1.yaml
new file mode 100644
index 0000000000..68e0966cfa
--- /dev/null
+++ b/tests/functional/test_related_hc_data/bundle_configs/requires/A_X_to_B_Y_and_B_Y_to_C_Z/cases/negative/case_1.yaml
@@ -0,0 +1,5 @@
+description: Attempt to save HC map with only A.X on 1 host - should fail
+positive: False
+hc_map:
+ first_host:
+ - service_1.component_1_1
diff --git a/tests/functional/test_related_hc_data/bundle_configs/requires/A_X_to_B_Y_and_B_Y_to_C_Z/cases/negative/case_2.yaml b/tests/functional/test_related_hc_data/bundle_configs/requires/A_X_to_B_Y_and_B_Y_to_C_Z/cases/negative/case_2.yaml
new file mode 100644
index 0000000000..c8eb54b700
--- /dev/null
+++ b/tests/functional/test_related_hc_data/bundle_configs/requires/A_X_to_B_Y_and_B_Y_to_C_Z/cases/negative/case_2.yaml
@@ -0,0 +1,5 @@
+description: Attempt to save HC map with only B.Y on 1 host - should fail
+positive: False
+hc_map:
+ first_host:
+ - service_2.component_2_1
diff --git a/tests/functional/test_related_hc_data/bundle_configs/requires/A_X_to_B_Y_and_B_Y_to_C_Z/cases/negative/case_4.yaml b/tests/functional/test_related_hc_data/bundle_configs/requires/A_X_to_B_Y_and_B_Y_to_C_Z/cases/negative/case_4.yaml
new file mode 100644
index 0000000000..757c289cfb
--- /dev/null
+++ b/tests/functional/test_related_hc_data/bundle_configs/requires/A_X_to_B_Y_and_B_Y_to_C_Z/cases/negative/case_4.yaml
@@ -0,0 +1,6 @@
+description: Attempt to save HC map with A.X and B.Y on 1 host - should fail
+positive: False
+hc_map:
+ first_host:
+ - service_1.component_1_1
+ - service_2.component_2_1
diff --git a/tests/functional/test_related_hc_data/bundle_configs/requires/A_X_to_B_Y_and_B_Y_to_C_Z/cases/negative/case_5.yaml b/tests/functional/test_related_hc_data/bundle_configs/requires/A_X_to_B_Y_and_B_Y_to_C_Z/cases/negative/case_5.yaml
new file mode 100644
index 0000000000..4e88ad79ed
--- /dev/null
+++ b/tests/functional/test_related_hc_data/bundle_configs/requires/A_X_to_B_Y_and_B_Y_to_C_Z/cases/negative/case_5.yaml
@@ -0,0 +1,6 @@
+description: Attempt to save HC map with A.X and C.Z on 1 host - should fail
+positive: False
+hc_map:
+ first_host:
+ - service_1.component_1_1
+ - service_3.component_3_1
diff --git a/tests/functional/test_related_hc_data/bundle_configs/requires/A_X_to_B_Y_and_B_Y_to_C_Z/cases/positive/case_3.yaml b/tests/functional/test_related_hc_data/bundle_configs/requires/A_X_to_B_Y_and_B_Y_to_C_Z/cases/positive/case_3.yaml
new file mode 100644
index 0000000000..1c0b9bb5ca
--- /dev/null
+++ b/tests/functional/test_related_hc_data/bundle_configs/requires/A_X_to_B_Y_and_B_Y_to_C_Z/cases/positive/case_3.yaml
@@ -0,0 +1,5 @@
+description: Attempt to save HC map with only C.Z on 1 host - should succeed
+positive: True
+hc_map:
+ first_host:
+ - service_3.component_3_1
diff --git a/tests/functional/test_related_hc_data/bundle_configs/requires/A_X_to_B_Y_and_B_Y_to_C_Z/cases/positive/case_6.yaml b/tests/functional/test_related_hc_data/bundle_configs/requires/A_X_to_B_Y_and_B_Y_to_C_Z/cases/positive/case_6.yaml
new file mode 100644
index 0000000000..a773e60ca7
--- /dev/null
+++ b/tests/functional/test_related_hc_data/bundle_configs/requires/A_X_to_B_Y_and_B_Y_to_C_Z/cases/positive/case_6.yaml
@@ -0,0 +1,6 @@
+description: Attempt to save HC map with B.Y and C.Z on 1 host - should succeed
+positive: True
+hc_map:
+ first_host:
+ - service_2.component_2_1
+ - service_3.component_3_1
diff --git a/tests/functional/test_related_hc_data/bundle_configs/requires/A_X_to_B_Y_and_B_Y_to_C_Z/cases/positive/case_7.yaml b/tests/functional/test_related_hc_data/bundle_configs/requires/A_X_to_B_Y_and_B_Y_to_C_Z/cases/positive/case_7.yaml
new file mode 100644
index 0000000000..a3e748e0ca
--- /dev/null
+++ b/tests/functional/test_related_hc_data/bundle_configs/requires/A_X_to_B_Y_and_B_Y_to_C_Z/cases/positive/case_7.yaml
@@ -0,0 +1,7 @@
+description: Attempt to save HC map with A.X and B.Y and C.Z on 1 host - should succeed
+positive: True
+hc_map:
+ first_host:
+ - service_1.component_1_1
+ - service_2.component_2_1
+ - service_3.component_3_1
diff --git a/tests/functional/test_related_hc_data/bundle_configs/requires/A_X_to_B_Y_and_B_Y_to_C_Z/cases/positive/case_8.yaml b/tests/functional/test_related_hc_data/bundle_configs/requires/A_X_to_B_Y_and_B_Y_to_C_Z/cases/positive/case_8.yaml
new file mode 100644
index 0000000000..35d6276f2e
--- /dev/null
+++ b/tests/functional/test_related_hc_data/bundle_configs/requires/A_X_to_B_Y_and_B_Y_to_C_Z/cases/positive/case_8.yaml
@@ -0,0 +1,9 @@
+description: Attempt to save HC map with A.X on first host and B.Y on second host and C.Z on third host - should succeed
+positive: True
+hc_map:
+ first_host:
+ - service_1.component_1_1
+ second_host:
+ - service_2.component_2_1
+ third_host:
+ - service_3.component_3_1
diff --git a/tests/functional/test_related_hc_data/bundle_configs/requires/A_X_to_B_Y_and_B_Y_to_C_Z/config.yaml b/tests/functional/test_related_hc_data/bundle_configs/requires/A_X_to_B_Y_and_B_Y_to_C_Z/config.yaml
new file mode 100644
index 0000000000..b97a30ae5b
--- /dev/null
+++ b/tests/functional/test_related_hc_data/bundle_configs/requires/A_X_to_B_Y_and_B_Y_to_C_Z/config.yaml
@@ -0,0 +1,40 @@
+# 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: some_name
+ version: 1
+
+- type: service
+ name: service_1
+ version: 1.0
+ components:
+ component_1_1:
+ requires:
+ - service: service_2
+ component: component_2_1
+
+- type: service
+ name: service_2
+ version: 1.0
+ components:
+ component_2_1:
+ requires:
+ - service: service_3
+ component: component_3_1
+
+- type: service
+ name: service_3
+ version: 1.0
+ components:
+ component_3_1:
diff --git a/tests/functional/test_related_hc_data/bundle_configs/requires/A_X_to_B_Y_and_B_Y_to_C_Z_and_C_Z_to_A_X/cases/negative/case_1.yaml b/tests/functional/test_related_hc_data/bundle_configs/requires/A_X_to_B_Y_and_B_Y_to_C_Z_and_C_Z_to_A_X/cases/negative/case_1.yaml
new file mode 100644
index 0000000000..68e0966cfa
--- /dev/null
+++ b/tests/functional/test_related_hc_data/bundle_configs/requires/A_X_to_B_Y_and_B_Y_to_C_Z_and_C_Z_to_A_X/cases/negative/case_1.yaml
@@ -0,0 +1,5 @@
+description: Attempt to save HC map with only A.X on 1 host - should fail
+positive: False
+hc_map:
+ first_host:
+ - service_1.component_1_1
diff --git a/tests/functional/test_related_hc_data/bundle_configs/requires/A_X_to_B_Y_and_B_Y_to_C_Z_and_C_Z_to_A_X/cases/negative/case_2.yaml b/tests/functional/test_related_hc_data/bundle_configs/requires/A_X_to_B_Y_and_B_Y_to_C_Z_and_C_Z_to_A_X/cases/negative/case_2.yaml
new file mode 100644
index 0000000000..c8eb54b700
--- /dev/null
+++ b/tests/functional/test_related_hc_data/bundle_configs/requires/A_X_to_B_Y_and_B_Y_to_C_Z_and_C_Z_to_A_X/cases/negative/case_2.yaml
@@ -0,0 +1,5 @@
+description: Attempt to save HC map with only B.Y on 1 host - should fail
+positive: False
+hc_map:
+ first_host:
+ - service_2.component_2_1
diff --git a/tests/functional/test_related_hc_data/bundle_configs/requires/A_X_to_B_Y_and_B_Y_to_C_Z_and_C_Z_to_A_X/cases/negative/case_3.yaml b/tests/functional/test_related_hc_data/bundle_configs/requires/A_X_to_B_Y_and_B_Y_to_C_Z_and_C_Z_to_A_X/cases/negative/case_3.yaml
new file mode 100644
index 0000000000..2cef826b2d
--- /dev/null
+++ b/tests/functional/test_related_hc_data/bundle_configs/requires/A_X_to_B_Y_and_B_Y_to_C_Z_and_C_Z_to_A_X/cases/negative/case_3.yaml
@@ -0,0 +1,5 @@
+description: Attempt to save HC map with only C.Z on 1 host - should fail
+positive: False
+hc_map:
+ first_host:
+ - service_3.component_3_1
diff --git a/tests/functional/test_related_hc_data/bundle_configs/requires/A_X_to_B_Y_and_B_Y_to_C_Z_and_C_Z_to_A_X/cases/negative/case_4.yaml b/tests/functional/test_related_hc_data/bundle_configs/requires/A_X_to_B_Y_and_B_Y_to_C_Z_and_C_Z_to_A_X/cases/negative/case_4.yaml
new file mode 100644
index 0000000000..757c289cfb
--- /dev/null
+++ b/tests/functional/test_related_hc_data/bundle_configs/requires/A_X_to_B_Y_and_B_Y_to_C_Z_and_C_Z_to_A_X/cases/negative/case_4.yaml
@@ -0,0 +1,6 @@
+description: Attempt to save HC map with A.X and B.Y on 1 host - should fail
+positive: False
+hc_map:
+ first_host:
+ - service_1.component_1_1
+ - service_2.component_2_1
diff --git a/tests/functional/test_related_hc_data/bundle_configs/requires/A_X_to_B_Y_and_B_Y_to_C_Z_and_C_Z_to_A_X/cases/negative/case_5.yaml b/tests/functional/test_related_hc_data/bundle_configs/requires/A_X_to_B_Y_and_B_Y_to_C_Z_and_C_Z_to_A_X/cases/negative/case_5.yaml
new file mode 100644
index 0000000000..4e88ad79ed
--- /dev/null
+++ b/tests/functional/test_related_hc_data/bundle_configs/requires/A_X_to_B_Y_and_B_Y_to_C_Z_and_C_Z_to_A_X/cases/negative/case_5.yaml
@@ -0,0 +1,6 @@
+description: Attempt to save HC map with A.X and C.Z on 1 host - should fail
+positive: False
+hc_map:
+ first_host:
+ - service_1.component_1_1
+ - service_3.component_3_1
diff --git a/tests/functional/test_related_hc_data/bundle_configs/requires/A_X_to_B_Y_and_B_Y_to_C_Z_and_C_Z_to_A_X/cases/negative/case_6.yaml b/tests/functional/test_related_hc_data/bundle_configs/requires/A_X_to_B_Y_and_B_Y_to_C_Z_and_C_Z_to_A_X/cases/negative/case_6.yaml
new file mode 100644
index 0000000000..6b8e66fbc6
--- /dev/null
+++ b/tests/functional/test_related_hc_data/bundle_configs/requires/A_X_to_B_Y_and_B_Y_to_C_Z_and_C_Z_to_A_X/cases/negative/case_6.yaml
@@ -0,0 +1,6 @@
+description: Attempt to save HC map with B.Y and C.Z on 1 host - should fail
+positive: False
+hc_map:
+ first_host:
+ - service_2.component_2_1
+ - service_3.component_3_1
diff --git a/tests/functional/test_related_hc_data/bundle_configs/requires/A_X_to_B_Y_and_B_Y_to_C_Z_and_C_Z_to_A_X/cases/positive/case_7.yaml b/tests/functional/test_related_hc_data/bundle_configs/requires/A_X_to_B_Y_and_B_Y_to_C_Z_and_C_Z_to_A_X/cases/positive/case_7.yaml
new file mode 100644
index 0000000000..a3e748e0ca
--- /dev/null
+++ b/tests/functional/test_related_hc_data/bundle_configs/requires/A_X_to_B_Y_and_B_Y_to_C_Z_and_C_Z_to_A_X/cases/positive/case_7.yaml
@@ -0,0 +1,7 @@
+description: Attempt to save HC map with A.X and B.Y and C.Z on 1 host - should succeed
+positive: True
+hc_map:
+ first_host:
+ - service_1.component_1_1
+ - service_2.component_2_1
+ - service_3.component_3_1
diff --git a/tests/functional/test_related_hc_data/bundle_configs/requires/A_X_to_B_Y_and_B_Y_to_C_Z_and_C_Z_to_A_X/cases/positive/case_8.yaml b/tests/functional/test_related_hc_data/bundle_configs/requires/A_X_to_B_Y_and_B_Y_to_C_Z_and_C_Z_to_A_X/cases/positive/case_8.yaml
new file mode 100644
index 0000000000..35d6276f2e
--- /dev/null
+++ b/tests/functional/test_related_hc_data/bundle_configs/requires/A_X_to_B_Y_and_B_Y_to_C_Z_and_C_Z_to_A_X/cases/positive/case_8.yaml
@@ -0,0 +1,9 @@
+description: Attempt to save HC map with A.X on first host and B.Y on second host and C.Z on third host - should succeed
+positive: True
+hc_map:
+ first_host:
+ - service_1.component_1_1
+ second_host:
+ - service_2.component_2_1
+ third_host:
+ - service_3.component_3_1
diff --git a/tests/functional/test_related_hc_data/bundle_configs/requires/A_X_to_B_Y_and_B_Y_to_C_Z_and_C_Z_to_A_X/config.yaml b/tests/functional/test_related_hc_data/bundle_configs/requires/A_X_to_B_Y_and_B_Y_to_C_Z_and_C_Z_to_A_X/config.yaml
new file mode 100644
index 0000000000..6765201624
--- /dev/null
+++ b/tests/functional/test_related_hc_data/bundle_configs/requires/A_X_to_B_Y_and_B_Y_to_C_Z_and_C_Z_to_A_X/config.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.
+---
+
+- type: cluster
+ name: some_name
+ version: 1
+
+- type: service
+ name: service_1
+ version: 1.0
+ components:
+ component_1_1:
+ requires:
+ - service: service_2
+ component: component_2_1
+
+- type: service
+ name: service_2
+ version: 1.0
+ components:
+ component_2_1:
+ requires:
+ - service: service_3
+ component: component_3_1
+
+- type: service
+ name: service_3
+ version: 1.0
+ components:
+ component_3_1:
+ requires:
+ - service: service_1
+ component: component_1_1
diff --git a/tests/functional/test_related_hc_data/bundle_configs/requires/A_X_to_B_Y_and_C_Z_to_B_Y/cases/negative/case_1.yaml b/tests/functional/test_related_hc_data/bundle_configs/requires/A_X_to_B_Y_and_C_Z_to_B_Y/cases/negative/case_1.yaml
new file mode 100644
index 0000000000..68e0966cfa
--- /dev/null
+++ b/tests/functional/test_related_hc_data/bundle_configs/requires/A_X_to_B_Y_and_C_Z_to_B_Y/cases/negative/case_1.yaml
@@ -0,0 +1,5 @@
+description: Attempt to save HC map with only A.X on 1 host - should fail
+positive: False
+hc_map:
+ first_host:
+ - service_1.component_1_1
diff --git a/tests/functional/test_related_hc_data/bundle_configs/requires/A_X_to_B_Y_and_C_Z_to_B_Y/cases/negative/case_3.yaml b/tests/functional/test_related_hc_data/bundle_configs/requires/A_X_to_B_Y_and_C_Z_to_B_Y/cases/negative/case_3.yaml
new file mode 100644
index 0000000000..2cef826b2d
--- /dev/null
+++ b/tests/functional/test_related_hc_data/bundle_configs/requires/A_X_to_B_Y_and_C_Z_to_B_Y/cases/negative/case_3.yaml
@@ -0,0 +1,5 @@
+description: Attempt to save HC map with only C.Z on 1 host - should fail
+positive: False
+hc_map:
+ first_host:
+ - service_3.component_3_1
diff --git a/tests/functional/test_related_hc_data/bundle_configs/requires/A_X_to_B_Y_and_C_Z_to_B_Y/cases/negative/case_5.yaml b/tests/functional/test_related_hc_data/bundle_configs/requires/A_X_to_B_Y_and_C_Z_to_B_Y/cases/negative/case_5.yaml
new file mode 100644
index 0000000000..4e88ad79ed
--- /dev/null
+++ b/tests/functional/test_related_hc_data/bundle_configs/requires/A_X_to_B_Y_and_C_Z_to_B_Y/cases/negative/case_5.yaml
@@ -0,0 +1,6 @@
+description: Attempt to save HC map with A.X and C.Z on 1 host - should fail
+positive: False
+hc_map:
+ first_host:
+ - service_1.component_1_1
+ - service_3.component_3_1
diff --git a/tests/functional/test_related_hc_data/bundle_configs/requires/A_X_to_B_Y_and_C_Z_to_B_Y/cases/positive/case_2.yaml b/tests/functional/test_related_hc_data/bundle_configs/requires/A_X_to_B_Y_and_C_Z_to_B_Y/cases/positive/case_2.yaml
new file mode 100644
index 0000000000..eea2684bf2
--- /dev/null
+++ b/tests/functional/test_related_hc_data/bundle_configs/requires/A_X_to_B_Y_and_C_Z_to_B_Y/cases/positive/case_2.yaml
@@ -0,0 +1,5 @@
+description: Attempt to save HC map with only B.Y on 1 host - should succeed
+positive: True
+hc_map:
+ first_host:
+ - service_2.component_2_1
diff --git a/tests/functional/test_related_hc_data/bundle_configs/requires/A_X_to_B_Y_and_C_Z_to_B_Y/cases/positive/case_4.yaml b/tests/functional/test_related_hc_data/bundle_configs/requires/A_X_to_B_Y_and_C_Z_to_B_Y/cases/positive/case_4.yaml
new file mode 100644
index 0000000000..8922315e63
--- /dev/null
+++ b/tests/functional/test_related_hc_data/bundle_configs/requires/A_X_to_B_Y_and_C_Z_to_B_Y/cases/positive/case_4.yaml
@@ -0,0 +1,6 @@
+description: Attempt to save HC map with A.X and B.Y on 1 host - should succeed
+positive: True
+hc_map:
+ first_host:
+ - service_1.component_1_1
+ - service_2.component_2_1
diff --git a/tests/functional/test_related_hc_data/bundle_configs/requires/A_X_to_B_Y_and_C_Z_to_B_Y/cases/positive/case_6.yaml b/tests/functional/test_related_hc_data/bundle_configs/requires/A_X_to_B_Y_and_C_Z_to_B_Y/cases/positive/case_6.yaml
new file mode 100644
index 0000000000..a773e60ca7
--- /dev/null
+++ b/tests/functional/test_related_hc_data/bundle_configs/requires/A_X_to_B_Y_and_C_Z_to_B_Y/cases/positive/case_6.yaml
@@ -0,0 +1,6 @@
+description: Attempt to save HC map with B.Y and C.Z on 1 host - should succeed
+positive: True
+hc_map:
+ first_host:
+ - service_2.component_2_1
+ - service_3.component_3_1
diff --git a/tests/functional/test_related_hc_data/bundle_configs/requires/A_X_to_B_Y_and_C_Z_to_B_Y/cases/positive/case_7.yaml b/tests/functional/test_related_hc_data/bundle_configs/requires/A_X_to_B_Y_and_C_Z_to_B_Y/cases/positive/case_7.yaml
new file mode 100644
index 0000000000..a3e748e0ca
--- /dev/null
+++ b/tests/functional/test_related_hc_data/bundle_configs/requires/A_X_to_B_Y_and_C_Z_to_B_Y/cases/positive/case_7.yaml
@@ -0,0 +1,7 @@
+description: Attempt to save HC map with A.X and B.Y and C.Z on 1 host - should succeed
+positive: True
+hc_map:
+ first_host:
+ - service_1.component_1_1
+ - service_2.component_2_1
+ - service_3.component_3_1
diff --git a/tests/functional/test_related_hc_data/bundle_configs/requires/A_X_to_B_Y_and_C_Z_to_B_Y/cases/positive/case_8.yaml b/tests/functional/test_related_hc_data/bundle_configs/requires/A_X_to_B_Y_and_C_Z_to_B_Y/cases/positive/case_8.yaml
new file mode 100644
index 0000000000..35d6276f2e
--- /dev/null
+++ b/tests/functional/test_related_hc_data/bundle_configs/requires/A_X_to_B_Y_and_C_Z_to_B_Y/cases/positive/case_8.yaml
@@ -0,0 +1,9 @@
+description: Attempt to save HC map with A.X on first host and B.Y on second host and C.Z on third host - should succeed
+positive: True
+hc_map:
+ first_host:
+ - service_1.component_1_1
+ second_host:
+ - service_2.component_2_1
+ third_host:
+ - service_3.component_3_1
diff --git a/tests/functional/test_related_hc_data/bundle_configs/requires/A_X_to_B_Y_and_C_Z_to_B_Y/config.yaml b/tests/functional/test_related_hc_data/bundle_configs/requires/A_X_to_B_Y_and_C_Z_to_B_Y/config.yaml
new file mode 100644
index 0000000000..623ebe8773
--- /dev/null
+++ b/tests/functional/test_related_hc_data/bundle_configs/requires/A_X_to_B_Y_and_C_Z_to_B_Y/config.yaml
@@ -0,0 +1,40 @@
+# 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: some_name
+ version: 1
+
+- type: service
+ name: service_1
+ version: 1.0
+ components:
+ component_1_1:
+ requires:
+ - service: service_2
+ component: component_2_1
+
+- type: service
+ name: service_2
+ version: 1.0
+ components:
+ component_2_1:
+
+- type: service
+ name: service_3
+ version: 1.0
+ components:
+ component_3_1:
+ requires:
+ - service: service_2
+ component: component_2_1
diff --git a/tests/functional/test_bound_hc_data/provider/config.yaml b/tests/functional/test_related_hc_data/provider/config.yaml
similarity index 100%
rename from tests/functional/test_bound_hc_data/provider/config.yaml
rename to tests/functional/test_related_hc_data/provider/config.yaml
diff --git a/tests/functional/test_sdk.py b/tests/functional/test_sdk.py
index 5ae81c6175..3f55c97697 100644
--- a/tests/functional/test_sdk.py
+++ b/tests/functional/test_sdk.py
@@ -24,7 +24,7 @@
def schema():
filename = get_data_dir(__file__) + "/schema.yaml"
with open(filename, 'r') as f:
- return yaml.load(f)
+ return yaml.safe_load(f)
def test_bundle_upload(sdk_client_fs: ADCMClient):
@@ -255,7 +255,7 @@ def test_action_fail(sdk_client_fs: ADCMClient):
cluster = bundle.cluster_create(name="sample cluster")
with allure.step('Check action fail'):
with pytest.raises(TaskFailed):
- cluster.action_run(name="fail").try_wait()
+ cluster.action(name="fail").run().try_wait()
def test_cluster_upgrade(sdk_client_fs: ADCMClient):
diff --git a/tests/functional/test_stacks.py b/tests/functional/test_stacks.py
index 6d51c4fdc7..9413f760de 100644
--- a/tests/functional/test_stacks.py
+++ b/tests/functional/test_stacks.py
@@ -9,445 +9,313 @@
# 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 random
+from typing import Tuple, List
import allure
import coreapi
import pytest
+from _pytest.mark import ParameterSet
from adcm_client.objects import ADCMClient
from adcm_pytest_plugin import utils
from jsonschema import validate
# pylint: disable=W0611, W0621, W0212
-from tests.library import errorcodes, steps
+from tests.library import errorcodes
+from tests.library.errorcodes import ADCMError
SCHEMAS = utils.get_data_dir(__file__, "schemas/")
-@allure.step('Create cluster')
-def create_cluster(bundle):
- return bundle.cluster_create(utils.random_string())
-
-
-@allure.step('Load default stack')
-def load_default_stack(client):
- client.stack.load.update()
- return client.stack.host.list()
-
-
-@pytest.fixture()
-def client(sdk_client_fs: ADCMClient):
- return sdk_client_fs.adcm()._api.objects
-
-
-def test_didnot_load_stack(client):
- stack_dir = utils.get_data_dir(__file__, 'did_not_load')
- with pytest.raises(coreapi.exceptions.ErrorMessage) as e:
- steps.upload_bundle(client, stack_dir)
- with allure.step('Check error: no config files in stack directory'):
- errorcodes.STACK_LOAD_ERROR.equal(e, 'no config files in stack directory')
-
-
def test_service_wo_name(sdk_client_fs: ADCMClient):
- stack_dir = utils.get_data_dir(__file__, 'service_wo_name')
+ stack_dir = utils.get_data_dir(__file__, "service_wo_name")
with pytest.raises(coreapi.exceptions.ErrorMessage) as e:
sdk_client_fs.upload_from_fs(stack_dir)
- with allure.step('Check error: no name in service definition'):
- errorcodes.INVALID_OBJECT_DEFINITION.equal(e, 'No name in service definition:')
+ with allure.step("Check error: no name in service definition"):
+ errorcodes.INVALID_OBJECT_DEFINITION.equal(e, "There is no required key \"name\" in map.")
def test_service_wo_version(sdk_client_fs: ADCMClient):
- stack_dir = utils.get_data_dir(__file__, 'service_wo_version')
+ stack_dir = utils.get_data_dir(__file__, "service_wo_version")
with pytest.raises(coreapi.exceptions.ErrorMessage) as e:
sdk_client_fs.upload_from_fs(stack_dir)
- with allure.step('Check error: no version in service'):
- errorcodes.INVALID_OBJECT_DEFINITION.equal(e, 'No version in service')
+ with allure.step("Check error: no version in service"):
+ errorcodes.INVALID_OBJECT_DEFINITION.equal(
+ e, 'There is no required key "version" in map.'
+ )
-def test_service_wo_actions(client):
- stack_dir = utils.get_data_dir(__file__, 'service_wo_action')
- steps.upload_bundle(client, stack_dir)
- with allure.step('Get service without actions'):
- service_prototype = client.stack.service.list()[0]
- schema = json.load(open(SCHEMAS + '/stack_list_item_schema.json'))
- with allure.step('Check service'):
+def test_service_wo_actions(sdk_client_fs: ADCMClient):
+ stack_dir = utils.get_data_dir(__file__, "service_wo_action")
+ sdk_client_fs.upload_from_fs(stack_dir)
+ with allure.step("Get service without actions"):
+ service_prototype = sdk_client_fs.service_prototype()._data
+ schema = json.load(open(SCHEMAS + "/stack_list_item_schema.json"))
+ with allure.step("Check service"):
assert validate(service_prototype, schema) is None
-def test_cluster_proto_wo_actions(client):
- stack_dir = utils.get_data_dir(__file__, 'cluster_proto_wo_actions')
- steps.upload_bundle(client, stack_dir)
- with allure.step('Get cluster without actions'):
- cluster_prototype = client.stack.cluster.list()[0]
- schema = json.load(open(SCHEMAS + '/stack_list_item_schema.json'))
- with allure.step('Check cluster'):
+def test_cluster_proto_wo_actions(sdk_client_fs: ADCMClient):
+ stack_dir = utils.get_data_dir(__file__, "cluster_proto_wo_actions")
+ sdk_client_fs.upload_from_fs(stack_dir)
+ with allure.step("Get cluster without actions"):
+ cluster_prototype = sdk_client_fs.cluster_prototype()._data
+ schema = json.load(open(SCHEMAS + "/stack_list_item_schema.json"))
+ with allure.step("Check cluster"):
assert validate(cluster_prototype, schema) is None
-def test_host_proto_wo_actions(client):
- stack_dir = utils.get_data_dir(__file__, 'host_proto_wo_action')
- steps.upload_bundle(client, stack_dir)
- with allure.step('Get host without actions'):
- host_prototype = client.stack.host.list()[0]
- schema = json.load(open(SCHEMAS + '/stack_list_item_schema.json'))
- with allure.step('Check host'):
+def test_host_proto_wo_actions(sdk_client_fs: ADCMClient):
+ stack_dir = utils.get_data_dir(__file__, "host_proto_wo_action")
+ sdk_client_fs.upload_from_fs(stack_dir)
+ with allure.step("Get host without actions"):
+ host_prototype = sdk_client_fs.host_prototype()._data
+ schema = json.load(open(SCHEMAS + "/stack_list_item_schema.json"))
+ with allure.step("Check host prototype"):
assert validate(host_prototype, schema) is None
def test_service_wo_type(sdk_client_fs: ADCMClient):
- stack_dir = utils.get_data_dir(__file__, 'service_wo_type')
+ stack_dir = utils.get_data_dir(__file__, "service_wo_type")
with pytest.raises(coreapi.exceptions.ErrorMessage) as e:
sdk_client_fs.upload_from_fs(stack_dir)
- with allure.step('Check error: No type in object definition'):
- errorcodes.INVALID_OBJECT_DEFINITION.equal(e, 'No type in object definition:')
+ with allure.step("Check error: No type in object definition"):
+ errorcodes.INVALID_OBJECT_DEFINITION.equal(e, "There is no key \"type\" in map.")
def test_service_unknown_type(sdk_client_fs: ADCMClient):
- stack_dir = utils.get_data_dir(__file__, 'service_unknown_type')
+ stack_dir = utils.get_data_dir(__file__, "service_unknown_type")
with pytest.raises(coreapi.exceptions.ErrorMessage) as e:
sdk_client_fs.upload_from_fs(stack_dir)
- with allure.step('Check error: Unknown type'):
- errorcodes.INVALID_OBJECT_DEFINITION.equal(e, 'Unknown type')
-
-
-def test_yaml_parser_error(client):
- stack_dir = utils.get_data_dir(__file__, 'yaml_parser_error')
- with pytest.raises(coreapi.exceptions.ErrorMessage) as e:
- steps.upload_bundle(client, stack_dir)
- with allure.step('Check error: YAML decode'):
- errorcodes.STACK_LOAD_ERROR.equal(e, 'YAML decode')
-
-
-def test_toml_parser_error(client):
- stack_dir = utils.get_data_dir(__file__, 'toml_parser_error')
- with pytest.raises(coreapi.exceptions.ErrorMessage) as e:
- steps.upload_bundle(client, stack_dir)
- with allure.step('Check error: TOML decode'):
- errorcodes.STACK_LOAD_ERROR.equal(e, 'TOML decode')
+ with allure.step("Check error: Unknown type"):
+ errorcodes.INVALID_OBJECT_DEFINITION.equal(e, "Value \"unknown\" is not allowed")
def test_stack_hasnt_script_mandatory_key(sdk_client_fs: ADCMClient):
- stack_dir = utils.get_data_dir(__file__, 'script_mandatory_key')
+ stack_dir = utils.get_data_dir(__file__, "script_mandatory_key")
with pytest.raises(coreapi.exceptions.ErrorMessage) as e:
sdk_client_fs.upload_from_fs(stack_dir)
- with allure.step('Check error: has no mandatory'):
- errorcodes.DEFINITION_KEY_ERROR.equal(e, 'has no mandatory \"script\"')
+ with allure.step("Check error: has no mandatory"):
+ errorcodes.INVALID_OBJECT_DEFINITION.equal(e, 'There is no required key "script" in map.')
def test_stack_hasnt_scripttype_mandatory_key(sdk_client_fs: ADCMClient):
- stack_dir = utils.get_data_dir(__file__, 'scripttype_mandatory_key')
+ stack_dir = utils.get_data_dir(__file__, "scripttype_mandatory_key")
with pytest.raises(coreapi.exceptions.ErrorMessage) as e:
sdk_client_fs.upload_from_fs(stack_dir)
- with allure.step('Check error: has no mandatory'):
- errorcodes.DEFINITION_KEY_ERROR.equal(e, 'has no mandatory \"script_type\"')
+ with allure.step("Check error: has no mandatory"):
+ errorcodes.INVALID_OBJECT_DEFINITION.equal(e, 'There is no '
+ 'required key "script_type" in map.')
def test_playbook_path(sdk_client_fs: ADCMClient):
- stack_dir = utils.get_data_dir(__file__, 'playbook_path_test')
+ stack_dir = utils.get_data_dir(__file__, "playbook_path_test")
sdk_client_fs.upload_from_fs(stack_dir)
- with allure.step('Check service prototype list'):
+ with allure.step("Check service prototype list"):
assert sdk_client_fs.service_prototype_list() is not None
def test_empty_default_config_value(sdk_client_fs: ADCMClient):
- stack_dir = utils.get_data_dir(__file__, 'empty_default_config_value')
+ stack_dir = utils.get_data_dir(__file__, "empty_default_config_value")
sdk_client_fs.upload_from_fs(stack_dir)
- with allure.step('Check service prototype list'):
+ with allure.step("Check service prototype list"):
assert sdk_client_fs.service_prototype_list() is not None
-def test_load_stack_w_empty_config_field(client):
- stack_dir = utils.get_data_dir(__file__, 'empty_config_field')
- steps.upload_bundle(client, stack_dir)
- with allure.step('Get cluster list'):
- cluster_proto = client.stack.cluster.list()[0]
- schema = json.load(open(SCHEMAS + '/stack_list_item_schema.json'))
- with allure.step('Check cluster'):
- assert validate(cluster_proto, schema) is None
-
-
-def test_yaml_decode_duplicate_anchor(client):
- stack_dir = utils.get_data_dir(__file__, 'yaml_decode_duplicate_anchor')
- with pytest.raises(coreapi.exceptions.ErrorMessage) as e:
- steps.upload_bundle(client, stack_dir)
- with allure.step('Check error: found duplicate anchor'):
- errorcodes.STACK_LOAD_ERROR.equal(e, 'found duplicate anchor')
-
-
-def test_raises_error_expected_colon(client):
- stack_dir = utils.get_data_dir(__file__, 'expected_colon')
+def test_load_stack_w_empty_config_field(sdk_client_fs: ADCMClient):
+ stack_dir = utils.get_data_dir(__file__, "empty_config_field")
with pytest.raises(coreapi.exceptions.ErrorMessage) as e:
- steps.upload_bundle(client, stack_dir)
- with allure.step('Check error: could not find expected'):
- errorcodes.STACK_LOAD_ERROR.equal(e, 'could not find expected \':\'')
+ sdk_client_fs.upload_from_fs(stack_dir)
+ with allure.step("Check error: config field should not be empty"):
+ errorcodes.INVALID_OBJECT_DEFINITION.equal(e, 'None of the variants '
+ 'for rule "config_obj" match')
def test_shouldn_load_config_with_wrong_name(sdk_client_fs: ADCMClient):
- stack_dir = utils.get_data_dir(__file__, 'parsing_scalar_wrong_name')
+ stack_dir = utils.get_data_dir(__file__, "parsing_scalar_wrong_name")
with pytest.raises(coreapi.exceptions.ErrorMessage) as e:
sdk_client_fs.upload_from_fs(stack_dir)
- with allure.step('Check error: Config key is incorrect'):
- errorcodes.WRONG_NAME.equal(e, 'Config key', ' is incorrect')
-
-
-def test_load_stack_with_lost_whitespace(client):
- stack_dir = utils.get_data_dir(__file__, 'missed_whitespace')
- with pytest.raises(coreapi.exceptions.ErrorMessage) as e:
- steps.upload_bundle(client, stack_dir)
- with allure.step('Check error: mapping values are not allowed here'):
- errorcodes.STACK_LOAD_ERROR.equal(e, 'mapping values are not allowed here')
-
-
-def test_load_stack_expected_block_end(client):
- stack_dir = utils.get_data_dir(__file__, 'expected_block_end')
- with pytest.raises(coreapi.exceptions.ErrorMessage) as e:
- steps.upload_bundle(client, stack_dir)
- with allure.step('Check error: expected , but found'):
- errorcodes.STACK_LOAD_ERROR.equal(e, 'expected , but found \'-\'')
+ with allure.step("Check error: Config key is incorrect"):
+ errorcodes.INVALID_OBJECT_DEFINITION.equal(e, 'There is no key "type" in map')
def test_load_stack_wo_type_in_config_key(sdk_client_fs: ADCMClient):
- stack_dir = utils.get_data_dir(__file__, 'no_type_in_config_key')
+ stack_dir = utils.get_data_dir(__file__, "no_type_in_config_key")
with pytest.raises(coreapi.exceptions.ErrorMessage) as e:
sdk_client_fs.upload_from_fs(stack_dir)
- with allure.step('Check error: No type in config key'):
- errorcodes.INVALID_CONFIG_DEFINITION.equal(e, 'No type in config key')
-
-
-def test_when_config_has_incorrect_option_definition(client):
- stack_dir = utils.get_data_dir(__file__, 'incorrect_option_definition')
- with pytest.raises(coreapi.exceptions.ErrorMessage) as e:
- steps.upload_bundle(client, stack_dir)
- with allure.step('Check error: found unhashable key'):
- errorcodes.STACK_LOAD_ERROR.equal(e, 'found unhashable key')
+ with allure.step("Check error: No type in config key"):
+ errorcodes.INVALID_OBJECT_DEFINITION.equal(e, 'There is no key "type" in map.')
def test_when_config_has_two_identical_service_proto(sdk_client_fs: ADCMClient):
- stack_dir = utils.get_data_dir(__file__, 'two_identical_services')
+ stack_dir = utils.get_data_dir(__file__, "two_identical_services")
with pytest.raises(coreapi.exceptions.ErrorMessage) as e:
sdk_client_fs.upload_from_fs(stack_dir)
- with allure.step('Check error: Duplicate definition of service'):
- errorcodes.INVALID_OBJECT_DEFINITION.equal(e, 'Duplicate definition of service')
+ with allure.step("Check error: Duplicate definition of service"):
+ errorcodes.INVALID_OBJECT_DEFINITION.equal(e, "Duplicate definition of service")
-@pytest.mark.parametrize('entity', [('host'), ('provider')])
-def test_config_has_one_definition_and_two_diff_types(sdk_client_fs: ADCMClient, entity):
- name = 'cluster_has_a_' + entity + '_definition'
+@pytest.mark.parametrize("entity", ["host", "provider"])
+def test_config_has_one_definition_and_two_diff_types(
+ sdk_client_fs: ADCMClient, entity
+):
+ name = "cluster_has_a_" + entity + "_definition"
stack_dir = utils.get_data_dir(__file__, name)
with pytest.raises(coreapi.exceptions.ErrorMessage) as e:
sdk_client_fs.upload_from_fs(stack_dir)
- with allure.step('Check error: definition in cluster type bundle'):
- errorcodes.BUNDLE_ERROR.equal(e, entity + ' definition in cluster type bundle')
+ with allure.step("Check error: definition in cluster type bundle"):
+ errorcodes.BUNDLE_ERROR.equal(e, entity + " definition in cluster type bundle")
# TODO: Fix assertion after completed ADCM-146
-@pytest.mark.skip(reason="no way of currently testing this")
-def test_add_config_parameter_in_cluster_proto_and_update(client):
- volumes = {
- utils.get_data_dir(__file__) + 'add_param_in_cluster_proto': {
- 'bind': '/adcm/stack/', 'mode': 'rw'}
- }
-
- path = next(iter(volumes))
- config = path + '/config.yaml'
- updated = path + '/updated_config.yaml'
- proto_list = load_default_stack(client)
- os.rename(config, path + '/config.bak')
- os.rename(updated, config)
- client.stack.load.update()
- with allure.step('Updated cluster'):
- updated_cluster = client.stack.cluster.read(prototype_id=proto_list[0]['id'])
- with allure.step('Check updated cluster'):
- expected = [d['name'] for d in updated_cluster['config']]
- assert ('test_key' in expected) is True
-
-
-@pytest.mark.skip(reason="no way of currently testing this")
-def test_add_config_parameter_in_host_proto_and_update(client):
- volumes = {
- utils.get_data_dir(__file__) + 'add_param_in_host_proto': {
- 'bind': '/adcm/stack/', 'mode': 'rw'}
- }
-
- path = next(iter(volumes))
- config = path + '/config.yaml'
- updated = path + '/updated_config.yaml'
- proto_list = load_default_stack(client)
- os.rename(config, path + '/config.bak')
- os.rename(updated, config)
- client.stack.load.update()
- with allure.step('Updated host'):
- updated_proto = client.stack.host.read(prototype_id=proto_list[0]['id'])
- with allure.step('Check updated host'):
- expected = [d['name'] for d in updated_proto['config']]
- assert ('test_key' in expected) is True
-
-
-@pytest.mark.skip(reason="no way of currently testing this")
-def test_add_config_parameter_in_service_prototype_and_update(client):
- volumes = {
- utils.get_data_dir(__file__) + 'add_param_in_service_proto': {
- 'bind': '/adcm/stack/', 'mode': 'rw'}
- }
- path = next(iter(volumes))
- config = path + '/config.yaml'
- updated = path + '/updated_config.yaml'
- proto_list = load_default_stack(client)
- os.rename(config, path + '/config.bak')
- os.rename(updated, config)
- client.stack.load.update()
- with allure.step('Update service'):
- updated_proto = client.stack.service.read(service_id=proto_list[0]['id'])
- with allure.step('Check updated service'):
- expected = [d['name'] for d in updated_proto['config']]
- assert ('test_key' in expected) is True
-
-
def test_check_cluster_bundle_versions_as_a_string(sdk_client_fs: ADCMClient):
- stack_dir = utils.get_data_dir(__file__, 'cluster_service_versions_as_a_string')
+ stack_dir = utils.get_data_dir(__file__, "cluster_service_versions_as_a_string")
sdk_client_fs.upload_from_fs(stack_dir)
- with allure.step('Check bundle versions'):
- prototype = random.choice(sdk_client_fs.service_prototype_list())
- assert isinstance(prototype.version, str) is True
- assert isinstance(prototype.version, str) is True
+ with allure.step("Check bundle versions"):
+ prototype = sdk_client_fs.service_prototype()
+ assert isinstance(prototype.version, str)
def test_check_host_bundle_versions_as_a_string(sdk_client_fs: ADCMClient):
- stack_dir = utils.get_data_dir(__file__, 'host_version_as_a_string')
+ stack_dir = utils.get_data_dir(__file__, "host_version_as_a_string")
sdk_client_fs.upload_from_fs(stack_dir)
- with allure.step('Check host versions'):
- assert isinstance(random.choice(sdk_client_fs.host_prototype_list()).version, str) is True
+ with allure.step("Check host versions"):
+ prototype = sdk_client_fs.host_prototype()
+ assert isinstance(prototype.version, str)
def test_cluster_bundle_can_be_on_any_level(sdk_client_fs: ADCMClient):
- stack_dir = utils.get_data_dir(__file__, 'cluster_bundle_on_any_level')
+ stack_dir = utils.get_data_dir(__file__, "cluster_bundle_on_any_level")
sdk_client_fs.upload_from_fs(stack_dir)
- with allure.step('Check cluster bundle'):
+ with allure.step("Check cluster bundle"):
assert sdk_client_fs.service_prototype_list()
assert sdk_client_fs.cluster_prototype_list()
def test_host_bundle_can_be_on_any_level(sdk_client_fs: ADCMClient):
- stack_dir = utils.get_data_dir(__file__, 'host_bundle_on_any_level')
+ stack_dir = utils.get_data_dir(__file__, "host_bundle_on_any_level")
sdk_client_fs.upload_from_fs(stack_dir)
- with allure.step('Check host bundle'):
+ with allure.step("Check host bundle"):
assert sdk_client_fs.host_prototype_list()
-@allure.issue('https://jira.arenadata.io/browse/ADCM-184')
+@allure.issue("https://jira.arenadata.io/browse/ADCM-184")
def test_cluster_config_without_required_parent_key(sdk_client_fs: ADCMClient):
- stack_dir = utils.get_data_dir(__file__, 'cluster_config_without_required_parent_key')
+ stack_dir = utils.get_data_dir(
+ __file__, "cluster_config_without_required_parent_key"
+ )
bundle = sdk_client_fs.upload_from_fs(stack_dir)
- cluster = create_cluster(bundle)
+ cluster = bundle.cluster_create(name=utils.random_string())
with allure.step('Set config "str-key": "string"'):
config = cluster.config_set({"str-key": "string"})
- with allure.step('Check config'):
+ with allure.step("Check config"):
expected = cluster.config()
assert config == expected
def test_cluster_bundle_definition_shouldnt_contain_host(sdk_client_fs: ADCMClient):
- stack_dir = utils.get_data_dir(__file__, 'cluster_bundle_with_host_definition')
+ stack_dir = utils.get_data_dir(__file__, "cluster_bundle_with_host_definition")
with pytest.raises(coreapi.exceptions.ErrorMessage) as e:
sdk_client_fs.upload_from_fs(stack_dir)
- with allure.step('Check error: There are 1 host definition in cluster type'):
- errorcodes.BUNDLE_ERROR.equal(e, 'There are 1 host definition in cluster type')
+ with allure.step("Check error: There are 1 host definition in cluster type"):
+ errorcodes.BUNDLE_ERROR.equal(e, "There are 1 host definition in cluster type")
def test_when_cluster_config_must_contains_some_subkeys(sdk_client_fs: ADCMClient):
- stack_dir = utils.get_data_dir(__file__, 'cluster_config_with_empty_subkeys')
+ stack_dir = utils.get_data_dir(__file__, "cluster_config_with_empty_subkeys")
bundle = sdk_client_fs.upload_from_fs(stack_dir)
bad_config = {"str-key": "bluh", "subkeys": {}}
- cluster = create_cluster(bundle)
- with allure.step('Set bad config'):
+ cluster = bundle.cluster_create(name=utils.random_string())
+ with allure.step("Set bad config"):
with pytest.raises(coreapi.exceptions.ErrorMessage) as e:
cluster.config_set(bad_config)
- with allure.step('Check error: should contains some subkeys'):
- errorcodes.CONFIG_KEY_ERROR.equal(e, 'should contains some subkeys')
+ with allure.step("Check error: should contains some subkeys"):
+ errorcodes.CONFIG_KEY_ERROR.equal(e, "should contains some subkeys")
def test_when_host_config_must_contains_some_subkeys(sdk_client_fs: ADCMClient):
- stack_dir = utils.get_data_dir(__file__, 'host_config_with_empty_subkeys')
+ stack_dir = utils.get_data_dir(__file__, "host_config_with_empty_subkeys")
bundle = sdk_client_fs.upload_from_fs(stack_dir)
- with allure.step('Create provider'):
+ with allure.step("Create provider"):
bad_config = {"str-key": "bluh", "subkeys": {}}
provider = bundle.provider_create(utils.random_string())
- with allure.step('Create host'):
+ with allure.step("Create host"):
host = provider.host_create(utils.random_string())
with allure.step('Set bad config: "str-key": "bluh", "subkeys": {}'):
with pytest.raises(coreapi.exceptions.ErrorMessage) as e:
host.config_set(bad_config)
- with allure.step('Check error: should contains some subkeys'):
- errorcodes.CONFIG_KEY_ERROR.equal(e, 'should contains some subkeys')
+ with allure.step("Check error: should contains some subkeys"):
+ errorcodes.CONFIG_KEY_ERROR.equal(e, "should contains some subkeys")
def test_host_bundle_shouldnt_contains_service_definition(sdk_client_fs: ADCMClient):
- stack_dir = utils.get_data_dir(__file__, 'host_bundle_with_service_definition')
+ stack_dir = utils.get_data_dir(__file__, "host_bundle_with_service_definition")
with pytest.raises(coreapi.exceptions.ErrorMessage) as e:
sdk_client_fs.upload_from_fs(stack_dir)
- with allure.step('Check error: service definition in host provider type bundle'):
- errorcodes.BUNDLE_ERROR.equal(e, 'service definition in host provider type bundle')
+ with allure.step("Check error: service definition in host provider type bundle"):
+ errorcodes.BUNDLE_ERROR.equal(
+ e, "service definition in host provider type bundle"
+ )
def test_service_job_should_run_success(sdk_client_fs: ADCMClient):
- stack_dir = utils.get_data_dir(__file__, 'job_should_run_success')
+ stack_dir = utils.get_data_dir(__file__, "job_should_run_success")
bundle = sdk_client_fs.upload_from_fs(stack_dir)
- cluster = create_cluster(bundle)
- with allure.step('Add service'):
+ cluster = bundle.cluster_create(name=utils.random_string())
+ with allure.step("Add service"):
service = cluster.service_add(name="zookeeper")
- with allure.step('Run action: install'):
- action_run = service.action_run(name='install')
+ with allure.step("Run action: install"):
+ action_run = service.action(name="install").run()
action_run.try_wait()
- with allure.step('Check if state is success'):
- assert action_run.status == 'success'
+ with allure.step("Check if state is success"):
+ assert action_run.status == "success"
def test_service_job_should_run_failed(sdk_client_fs: ADCMClient):
- stack_dir = utils.get_data_dir(__file__, 'job_should_run_failed')
+ stack_dir = utils.get_data_dir(__file__, "job_should_run_failed")
bundle = sdk_client_fs.upload_from_fs(stack_dir)
- cluster = create_cluster(bundle)
- with allure.step('Add service'):
+ cluster = bundle.cluster_create(name=utils.random_string())
+ with allure.step("Add service"):
service = cluster.service_add(name="zookeeper")
- with allure.step('Run action: should_be_failed'):
- action_run = service.action_run(name='should_be_failed')
+ with allure.step("Run action: should_be_failed"):
+ action_run = service.action(name="should_be_failed").run()
action_run.wait()
- with allure.step('Check if state is failed'):
- assert action_run.status == 'failed'
+ with allure.step("Check if state is failed"):
+ assert action_run.status == "failed"
def test_cluster_action_run_should_be_success(sdk_client_fs: ADCMClient):
- stack_dir = utils.get_data_dir(__file__, 'cluster_action_run_should_be_success')
+ stack_dir = utils.get_data_dir(__file__, "cluster_action_run_should_be_success")
bundle = sdk_client_fs.upload_from_fs(stack_dir)
- cluster = create_cluster(bundle)
- with allure.step('Run action: install'):
- action_run = cluster.action_run(name='install')
+ cluster = bundle.cluster_create(name=utils.random_string())
+ with allure.step("Run action: install"):
+ action_run = cluster.action(name="install").run()
action_run.try_wait()
- with allure.step('Check if state is success'):
- assert action_run.status == 'success'
+ with allure.step("Check if state is success"):
+ assert action_run.status == "success"
def test_cluster_action_run_should_be_failed(sdk_client_fs: ADCMClient):
- stack_dir = utils.get_data_dir(__file__, 'cluster_action_run_should_be_success')
+ stack_dir = utils.get_data_dir(__file__, "cluster_action_run_should_be_success")
bundle = sdk_client_fs.upload_from_fs(stack_dir)
- cluster = create_cluster(bundle)
- with allure.step('Run action: run_fail'):
- action_run = cluster.action_run(name='run_fail')
+ cluster = bundle.cluster_create(name=utils.random_string())
+ with allure.step("Run action: run_fail"):
+ action_run = cluster.action(name="run_fail").run()
action_run.wait()
- with allure.step('Check if state is failed'):
- assert action_run.status == 'failed'
+ with allure.step("Check if state is failed"):
+ assert action_run.status == "failed"
def test_should_return_job_log_files(sdk_client_fs: ADCMClient):
- stack_dir = utils.get_data_dir(__file__, 'return_job_log_files')
+ stack_dir = utils.get_data_dir(__file__, "return_job_log_files")
bundle = sdk_client_fs.upload_from_fs(stack_dir)
- cluster = create_cluster(bundle)
- with allure.step('Run action'):
- action_run = cluster.action_run()
+ cluster = bundle.cluster_create(name=utils.random_string())
+ with allure.step("Run action"):
+ action_run = cluster.action().run()
action_run.wait()
job = action_run.job()
- with allure.step('Check log iles'):
+ with allure.step("Check log iles"):
log_files_list = job.log_files
log_list = job.log_list()
assert log_files_list is not None, log_files_list
@@ -457,26 +325,90 @@ def test_should_return_job_log_files(sdk_client_fs: ADCMClient):
def test_load_bundle_with_undefined_config_parameter(sdk_client_fs: ADCMClient):
- stack_dir = utils.get_data_dir(__file__, 'param_not_defined')
+ stack_dir = utils.get_data_dir(__file__, "param_not_defined")
with pytest.raises(coreapi.exceptions.ErrorMessage) as e:
sdk_client_fs.upload_from_fs(stack_dir)
- with allure.step('Check error: should be a map'):
- errorcodes.INVALID_CONFIG_DEFINITION.equal(e, 'Config definition of cluster',
- 'should be a map')
+ with allure.step("Check error: should be a map"):
+ errorcodes.INVALID_OBJECT_DEFINITION.equal(
+ e, "None of the variants", 'for rule "config_dict_obj" match'
+ )
-def test_when_import_has_unknown_config_parameter_shouldnt_be_loaded(sdk_client_fs: ADCMClient):
- bundledir = utils.get_data_dir(__file__, 'import_has_unknown_parameter')
+def test_when_import_has_unknown_config_parameter_shouldnt_be_loaded(
+ sdk_client_fs: ADCMClient,
+):
+ bundledir = utils.get_data_dir(__file__, "import_has_unknown_parameter")
with pytest.raises(coreapi.exceptions.ErrorMessage) as e:
sdk_client_fs.upload_from_fs(bundledir)
- with allure.step('Check error: does not has config group'):
- errorcodes.INVALID_OBJECT_DEFINITION.equal(e, 'cluster ', ' does not has ', ' config group')
+ with allure.step("Check error: does not has config group"):
+ errorcodes.INVALID_OBJECT_DEFINITION.equal(
+ e, "cluster ", " does not has ", " config group"
+ )
def test_when_bundle_hasnt_only_host_definition(sdk_client_fs: ADCMClient):
- bundledir = utils.get_data_dir(__file__, 'host_wo_provider')
+ bundledir = utils.get_data_dir(__file__, "host_wo_provider")
with pytest.raises(coreapi.exceptions.ErrorMessage) as e:
sdk_client_fs.upload_from_fs(bundledir)
- with allure.step('Check error: There isnt any cluster or host provider definition in bundle'):
- errorcodes.BUNDLE_ERROR.equal(e, "There isn't any cluster or "
- "host provider definition in bundle")
+ with allure.step(
+ "Check error: There isnt any cluster or host provider definition in bundle"
+ ):
+ errorcodes.BUNDLE_ERROR.equal(
+ e, "There isn't any cluster or " "host provider definition in bundle"
+ )
+
+
+def _get_invalid_bundle_config_params() -> List[ParameterSet]:
+ def get_pytest_param(bundle_name, adcm_err: ADCMError, msg: str):
+ return pytest.param(
+ utils.get_data_dir(__file__, bundle_name), (adcm_err, msg), id=bundle_name
+ )
+
+ return [
+ get_pytest_param(
+ "did_not_load",
+ errorcodes.STACK_LOAD_ERROR,
+ "no config files in stack directory",
+ ),
+ get_pytest_param(
+ "yaml_parser_error", errorcodes.STACK_LOAD_ERROR, "YAML decode"
+ ),
+ get_pytest_param(
+ "yaml_decode_duplicate_anchor",
+ errorcodes.STACK_LOAD_ERROR,
+ "found duplicate anchor",
+ ),
+ get_pytest_param(
+ "expected_colon", errorcodes.STACK_LOAD_ERROR, "could not find expected ':'"
+ ),
+ get_pytest_param(
+ "missed_whitespace",
+ errorcodes.STACK_LOAD_ERROR,
+ "mapping values are not allowed here",
+ ),
+ get_pytest_param(
+ "expected_block_end",
+ errorcodes.STACK_LOAD_ERROR,
+ "expected , but found '-'",
+ ),
+ get_pytest_param(
+ "incorrect_option_definition",
+ errorcodes.INVALID_OBJECT_DEFINITION,
+ "Value of map key \"required\" should be a ",
+ ),
+ ]
+
+
+@pytest.mark.parametrize(
+ ("bundle_archive", "expected_error"),
+ _get_invalid_bundle_config_params(),
+ indirect=["bundle_archive"],
+)
+def test_invalid_bundle_config(
+ sdk_client_fs: ADCMClient, bundle_archive, expected_error: Tuple[ADCMError, str]
+):
+ (adcm_error, expected_msg) = expected_error
+ with pytest.raises(coreapi.exceptions.ErrorMessage) as e:
+ sdk_client_fs.upload_from_fs(bundle_archive)
+ with allure.step("Check error"):
+ adcm_error.equal(e, expected_msg)
diff --git a/tests/functional/test_stacks_data/add_param_in_cluster_proto/config.yaml b/tests/functional/test_stacks_data/add_param_in_cluster_proto/config.yaml
index 38972e82ef..0f3f6f1678 100644
--- a/tests/functional/test_stacks_data/add_param_in_cluster_proto/config.yaml
+++ b/tests/functional/test_stacks_data/add_param_in_cluster_proto/config.yaml
@@ -20,7 +20,7 @@
re-start-zookeper:
type: job
script: stack/extcode/job.py
- script_type: task_generator
+ script_type: ansible
params:
qwe: 31
config:
diff --git a/tests/functional/test_stacks_data/add_param_in_cluster_proto/updated_config.yaml b/tests/functional/test_stacks_data/add_param_in_cluster_proto/updated_config.yaml
index 8f754f59f6..79b4d893bd 100644
--- a/tests/functional/test_stacks_data/add_param_in_cluster_proto/updated_config.yaml
+++ b/tests/functional/test_stacks_data/add_param_in_cluster_proto/updated_config.yaml
@@ -20,7 +20,7 @@
re-start-zookeper:
type: job
script: stack/extcode/job.py
- script_type: task_generator
+ script_type: ansible
params:
qwe: 31
config:
diff --git a/tests/functional/test_stacks_data/add_param_in_host_proto/config.yaml b/tests/functional/test_stacks_data/add_param_in_host_proto/config.yaml
index 50d576bb39..1f2f572c5f 100644
--- a/tests/functional/test_stacks_data/add_param_in_host_proto/config.yaml
+++ b/tests/functional/test_stacks_data/add_param_in_host_proto/config.yaml
@@ -20,7 +20,7 @@
do-something:
type: job
script: job.py
- script_type: task_generator
+ script_type: ansible
config:
required:
type: integer
diff --git a/tests/functional/test_stacks_data/add_param_in_host_proto/updated_config.yaml b/tests/functional/test_stacks_data/add_param_in_host_proto/updated_config.yaml
index d7b00b4a45..40212826b6 100644
--- a/tests/functional/test_stacks_data/add_param_in_host_proto/updated_config.yaml
+++ b/tests/functional/test_stacks_data/add_param_in_host_proto/updated_config.yaml
@@ -20,7 +20,7 @@
do-something:
type: job
script: job.py
- script_type: task_generator
+ script_type: ansible
config:
required:
type: integer
diff --git a/tests/functional/test_stacks_data/add_param_in_service_proto/config.yaml b/tests/functional/test_stacks_data/add_param_in_service_proto/config.yaml
index 14f4811559..d6fc926f33 100644
--- a/tests/functional/test_stacks_data/add_param_in_service_proto/config.yaml
+++ b/tests/functional/test_stacks_data/add_param_in_service_proto/config.yaml
@@ -20,7 +20,7 @@
install:
type: job
script: job.py
- script_type: task_generator
+ script_type: ansible
config:
required:
type: integer
diff --git a/tests/functional/test_stacks_data/add_param_in_service_proto/updated_config.yaml b/tests/functional/test_stacks_data/add_param_in_service_proto/updated_config.yaml
index 9f678c65df..dbcd09b6f2 100644
--- a/tests/functional/test_stacks_data/add_param_in_service_proto/updated_config.yaml
+++ b/tests/functional/test_stacks_data/add_param_in_service_proto/updated_config.yaml
@@ -20,7 +20,7 @@
install:
type: job
script: job.py
- script_type: task_generator
+ script_type: ansible
config:
required:
type: integer
diff --git a/tests/functional/test_stacks_data/cluster_action_run_should_be_success/config.yaml b/tests/functional/test_stacks_data/cluster_action_run_should_be_success/config.yaml
index e642e7bc2b..5c99935585 100644
--- a/tests/functional/test_stacks_data/cluster_action_run_should_be_success/config.yaml
+++ b/tests/functional/test_stacks_data/cluster_action_run_should_be_success/config.yaml
@@ -61,8 +61,6 @@
description: Arenadata Nothing
version: 1.0
- config:
-
actions:
install:
type: job
diff --git a/tests/functional/test_stacks_data/cluster_bundle_on_any_level/cluster/config.yaml b/tests/functional/test_stacks_data/cluster_bundle_on_any_level/cluster/config.yaml
index 0b36d1261b..eaf2b82bf7 100644
--- a/tests/functional/test_stacks_data/cluster_bundle_on_any_level/cluster/config.yaml
+++ b/tests/functional/test_stacks_data/cluster_bundle_on_any_level/cluster/config.yaml
@@ -20,7 +20,7 @@
re-start-zookeper:
type: job
script: stack/extcode/job.py
- script_type: task_generator
+ script_type: ansible
params:
qwe: 31
do-somethng:
diff --git a/tests/functional/test_stacks_data/cluster_bundle_on_any_level/services/service_group/simple/config.yaml b/tests/functional/test_stacks_data/cluster_bundle_on_any_level/services/service_group/simple/config.yaml
index 25199d15e1..404663fba5 100644
--- a/tests/functional/test_stacks_data/cluster_bundle_on_any_level/services/service_group/simple/config.yaml
+++ b/tests/functional/test_stacks_data/cluster_bundle_on_any_level/services/service_group/simple/config.yaml
@@ -19,7 +19,7 @@
install:
type: job
script: stack/extcode/job.py
- script_type: task_generator
+ script_type: ansible
params:
aaa: 31
bbb: place your text here
diff --git a/tests/functional/test_stacks_data/cluster_bundle_with_host_definition/config.yaml b/tests/functional/test_stacks_data/cluster_bundle_with_host_definition/config.yaml
index ff244db77f..657df1f134 100644
--- a/tests/functional/test_stacks_data/cluster_bundle_with_host_definition/config.yaml
+++ b/tests/functional/test_stacks_data/cluster_bundle_with_host_definition/config.yaml
@@ -31,7 +31,6 @@
name: simple ssh
version: .01
- actions:
config:
ansible_user:
default: root
diff --git a/tests/functional/test_stacks_data/cluster_config_with_empty_subkeys/config.yaml b/tests/functional/test_stacks_data/cluster_config_with_empty_subkeys/config.yaml
index e26f3a9fd3..54dc01094f 100644
--- a/tests/functional/test_stacks_data/cluster_config_with_empty_subkeys/config.yaml
+++ b/tests/functional/test_stacks_data/cluster_config_with_empty_subkeys/config.yaml
@@ -9,61 +9,57 @@
# 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: ZOOKEEPER
- type: service
- description: AllKeeper
- version: '1.2'
+- name: ZOOKEEPER
+ type: service
+ description: AllKeeper
+ version: '1.2'
- actions:
- install:
- states:
- available: [created]
- on_success: all_installed
- on_fail: cluster_install_fail
- type: job
- script: stack/extcode/cook.py
- script_type: task_generator
- components:
- ZOOKEEPER_CLIENT:
- config:
- ssh-key: {default: TItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAA, type: string, required: no}
- integer-key: {default: 24, max: 48, min: 2, type: integer, required: no}
+ actions:
+ install:
+ states:
+ available: [ created ]
+ on_success: all_installed
+ on_fail: cluster_install_fail
+ type: job
+ script: stack/extcode/cook.py
+ script_type: ansible
+ components:
+ ZOOKEEPER_CLIENT:
+ config:
+ ssh-key: { default: TItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAA, type: string, required: no }
+ integer-key: { default: 24, max: 48, min: 2, type: integer, required: no }
--
- name: ALLKEEPER
- type: service
- description: AllKeeper
- version: '5'
+- name: ALLKEEPER
+ type: service
+ description: AllKeeper
+ version: '5'
- actions:
- install:
- type: job
- script: cook.py
- script_type: task_generator
- components:
- ZOOKEEPER_CLIENT:
- config:
- ssh-key: {default: TItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAA, type: string, required: false}
+ actions:
+ install:
+ type: job
+ script: cook.py
+ script_type: ansible
+ components:
+ ZOOKEEPER_CLIENT:
+ config:
+ ssh-key: { default: TItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAA, type: string, required: false }
--
+- type: cluster
+ name: ADH
+ version: 1.5
- type: cluster
- name: ADH
- version: 1.5
-
- actions:
- re-start-zookeper:
- type: job
- script: stack/job.py
- script_type: task_generator
- config:
- str-key:
- default: value
- type: string
- required: false
- subkeys:
- key1:
- default: 0
- type: integer
- required: No
+ actions:
+ re-start-zookeper:
+ type: job
+ script: stack/job.py
+ script_type: ansible
+ config:
+ str-key:
+ default: value
+ type: string
+ required: false
+ subkeys:
+ key1:
+ default: 0
+ type: integer
+ required: No
diff --git a/tests/functional/test_stacks_data/cluster_has_a_host_definition/config.yaml b/tests/functional/test_stacks_data/cluster_has_a_host_definition/config.yaml
index ec3b194d4d..013cc6556c 100644
--- a/tests/functional/test_stacks_data/cluster_has_a_host_definition/config.yaml
+++ b/tests/functional/test_stacks_data/cluster_has_a_host_definition/config.yaml
@@ -20,7 +20,7 @@
re-start-zookeper:
type: job
script: stack/extcode/job.py
- script_type: task_generator
+ script_type: ansible
params:
qwe: 31
config:
diff --git a/tests/functional/test_stacks_data/cluster_has_a_provider_definition/config.yaml b/tests/functional/test_stacks_data/cluster_has_a_provider_definition/config.yaml
index a81dd868f6..55ca1dd087 100644
--- a/tests/functional/test_stacks_data/cluster_has_a_provider_definition/config.yaml
+++ b/tests/functional/test_stacks_data/cluster_has_a_provider_definition/config.yaml
@@ -20,7 +20,7 @@
re-start-zookeper:
type: job
script: stack/extcode/job.py
- script_type: task_generator
+ script_type: ansible
config:
str-key:
default: value
diff --git a/tests/functional/test_stacks_data/cluster_proto_wo_actions/services/zooyaml/config.yaml b/tests/functional/test_stacks_data/cluster_proto_wo_actions/services/zooyaml/config.yaml
index b9abac9a92..d3d18e86f5 100644
--- a/tests/functional/test_stacks_data/cluster_proto_wo_actions/services/zooyaml/config.yaml
+++ b/tests/functional/test_stacks_data/cluster_proto_wo_actions/services/zooyaml/config.yaml
@@ -9,40 +9,40 @@
# 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: ZOOKEEPER
-type: service
-description: AllKeeper
-version: 'new_version'
+- name: ZOOKEEPER
+ type: service
+ description: AllKeeper
+ version: 'new_version'
-actions:
+ actions:
install:
- type: job
- script: cook.py
- script_type: task_generator
+ type: job
+ script: cook.py
+ script_type: ansible
start:
- type: job
- log_files: [remote]
- script: services/zooyaml/start.yaml
- script_type: ansible
+ type: job
+ log_files: [ remote ]
+ script: services/zooyaml/start.yaml
+ script_type: ansible
stop:
- type: job
- log_files: [remote]
- script: services/zooyaml/stop.yaml
- script_type: ansible
+ type: job
+ log_files: [ remote ]
+ script: services/zooyaml/stop.yaml
+ script_type: ansible
-components:
+ components:
ZOOKEEPER_CLIENT:
-config:
- ssh-key: {default: TItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAA, type: string, required: no}
- integer-key: {default: 24, max: 48, min: 2, type: integer, required: no}
- float-key: {default: 2.4, type: float}
+ config:
+ ssh-key: { default: TItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAA, type: string, required: no }
+ integer-key: { default: 24, max: 48, min: 2, type: integer, required: no }
+ float-key: { default: 2.4, type: float }
zoo.cfg:
- autopurge.purgeInterval: {default: 24, max: 48, min: 2, type: integer}
- dataDir: {default: /hadoop/zookeeper, type: string}
- port:
- required: no
- default: 80
- option: {http: 80, https: 443}
- type: option
- required-key: {default: value, type: string}
+ autopurge.purgeInterval: { default: 24, max: 48, min: 2, type: integer }
+ dataDir: { default: /hadoop/zookeeper, type: string }
+ port:
+ required: no
+ default: 80
+ option: { http: 80, https: 443 }
+ type: option
+ required-key: { default: value, type: string }
diff --git a/tests/functional/test_stacks_data/cluster_service_versions_as_a_string/config.yaml b/tests/functional/test_stacks_data/cluster_service_versions_as_a_string/config.yaml
index daac6560bf..d74388621e 100644
--- a/tests/functional/test_stacks_data/cluster_service_versions_as_a_string/config.yaml
+++ b/tests/functional/test_stacks_data/cluster_service_versions_as_a_string/config.yaml
@@ -20,7 +20,7 @@
re-start-zookeper:
type: job
script: stack/extcode/job.py
- script_type: task_generator
+ script_type: ansible
params:
qwe: 31
do-somethng:
@@ -73,7 +73,7 @@
install:
type: job
script: stack/extcode/job.py
- script_type: task_generator
+ script_type: ansible
params:
aaa: 31
bbb: place your text here
diff --git a/tests/functional/test_stacks_data/host_bundle_with_service_definition/config.yaml b/tests/functional/test_stacks_data/host_bundle_with_service_definition/config.yaml
index c35f535a4b..c8d88766fe 100644
--- a/tests/functional/test_stacks_data/host_bundle_with_service_definition/config.yaml
+++ b/tests/functional/test_stacks_data/host_bundle_with_service_definition/config.yaml
@@ -24,7 +24,6 @@
- type: provider
name: sample_provider
version: 0.1.0
- config:
-
type: host
diff --git a/tests/functional/test_stacks_data/host_config_with_empty_subkeys/config.yaml b/tests/functional/test_stacks_data/host_config_with_empty_subkeys/config.yaml
index 2abdc07d3b..5f0479b383 100644
--- a/tests/functional/test_stacks_data/host_config_with_empty_subkeys/config.yaml
+++ b/tests/functional/test_stacks_data/host_config_with_empty_subkeys/config.yaml
@@ -13,7 +13,6 @@
- type: provider
name: sample_provider
version: 0.1.0
- config:
-
type: host
diff --git a/tests/functional/test_stacks_data/host_proto_wo_action/services/host_proto/config.yaml b/tests/functional/test_stacks_data/host_proto_wo_action/services/host_proto/config.yaml
index 93aef32588..e9eea609dc 100644
--- a/tests/functional/test_stacks_data/host_proto_wo_action/services/host_proto/config.yaml
+++ b/tests/functional/test_stacks_data/host_proto_wo_action/services/host_proto/config.yaml
@@ -13,8 +13,7 @@
- type: provider
name: sample_provider
version: 0.1.0
- config:
-
+
- type: host
name: host1
version: 1.0
diff --git a/tests/functional/test_stacks_data/host_version_as_a_string/config.yaml b/tests/functional/test_stacks_data/host_version_as_a_string/config.yaml
index 269ee7c8ce..a30c37c867 100644
--- a/tests/functional/test_stacks_data/host_version_as_a_string/config.yaml
+++ b/tests/functional/test_stacks_data/host_version_as_a_string/config.yaml
@@ -13,13 +13,11 @@
- type: provider
name: sample_provider
version: 0.1.0
- config:
-
type: host
name: simple ssh
version: .01
- actions:
config:
ansible_user:
default: root
diff --git a/tests/functional/test_stacks_data/incorrect_option_definition/config.yaml b/tests/functional/test_stacks_data/incorrect_option_definition/config.yaml
index 05c8466bd0..d28434ce8a 100644
--- a/tests/functional/test_stacks_data/incorrect_option_definition/config.yaml
+++ b/tests/functional/test_stacks_data/incorrect_option_definition/config.yaml
@@ -19,7 +19,7 @@
re-start-zookeper:
type: job
script: stack/job.py
- script_type: task_generator
+ script_type: ansible
params:
qwe: 31
config:
diff --git a/tests/functional/test_stacks_data/job_should_run_failed/config.yaml b/tests/functional/test_stacks_data/job_should_run_failed/config.yaml
index 83da4d11b7..eef6fb62a9 100644
--- a/tests/functional/test_stacks_data/job_should_run_failed/config.yaml
+++ b/tests/functional/test_stacks_data/job_should_run_failed/config.yaml
@@ -61,8 +61,6 @@
description: Arenadata Nothing
version: 1.0
- config:
-
actions:
install:
type: job
diff --git a/tests/functional/test_stacks_data/job_should_run_success/config.yaml b/tests/functional/test_stacks_data/job_should_run_success/config.yaml
index 890e9f2b58..51bdf05bae 100644
--- a/tests/functional/test_stacks_data/job_should_run_success/config.yaml
+++ b/tests/functional/test_stacks_data/job_should_run_success/config.yaml
@@ -47,8 +47,6 @@
description: Arenadata Nothing
version: 1.0
- config:
-
actions:
install:
type: job
diff --git a/tests/functional/test_stacks_data/playbook_path_test/services/zoo/config.yaml b/tests/functional/test_stacks_data/playbook_path_test/services/zoo/config.yaml
index 2090b2d3c8..21e8e367a6 100644
--- a/tests/functional/test_stacks_data/playbook_path_test/services/zoo/config.yaml
+++ b/tests/functional/test_stacks_data/playbook_path_test/services/zoo/config.yaml
@@ -17,7 +17,7 @@
do-something:
type: job
script: stack/service/cluster/job.py
- script_type: task_generator
+ script_type: ansible
params:
create-something:
type: job
@@ -38,7 +38,7 @@
install:
type: job
script: cook.py
- script_type: task_generator
+ script_type: ansible
start:
type: job
log_files: [remote]
diff --git a/tests/functional/test_stacks_data/script_mandatory_key/services/zoo/config.yaml b/tests/functional/test_stacks_data/script_mandatory_key/services/zoo/config.yaml
index 5a2299ddf4..9dce01eb67 100644
--- a/tests/functional/test_stacks_data/script_mandatory_key/services/zoo/config.yaml
+++ b/tests/functional/test_stacks_data/script_mandatory_key/services/zoo/config.yaml
@@ -23,7 +23,7 @@
install:
type: job
script: cook.py
- script_type: task_generator
+ script_type: ansible
start:
type: job
script_type: ansible
diff --git a/tests/functional/test_stacks_data/service_wo_action/services/ZOOKEEPER/config.yaml b/tests/functional/test_stacks_data/service_wo_action/services/ZOOKEEPER/config.yaml
index 219be8051a..aa40675f0e 100644
--- a/tests/functional/test_stacks_data/service_wo_action/services/ZOOKEEPER/config.yaml
+++ b/tests/functional/test_stacks_data/service_wo_action/services/ZOOKEEPER/config.yaml
@@ -13,7 +13,6 @@
type: cluster
name: ADH
version: 1.4
- actions:
config:
key: {}
diff --git a/tests/functional/test_stacks_data/toml_parser_error/services/simple_service/config.toml b/tests/functional/test_stacks_data/toml_parser_error/services/simple_service/config.toml
deleted file mode 100644
index 09a53ea695..0000000000
--- a/tests/functional/test_stacks_data/toml_parser_error/services/simple_service/config.toml
+++ /dev/null
@@ -1,86 +0,0 @@
-type = "cluster"
-name = "cluster"
-description = "cluster"
-version = "1.0"
-
-type = "service"
-name = "simple_service"
-description = "simple_service"
-version = "1.0"
-
-[actions.install]
- type = "sequence"
-[[actions.install.commands]
- name = "DATANODE"
- command = "INSTALL"
-[[actions.install.commands]]
- name = "HDFS_CLIENT"
- command = "INSTALL"
-[[actions.install.commands]]
- name = "NAMENODE"
- command = "INSTALL"
-[[actions.install.commands]]
- name = "SECONDARY_NAMENODE"
- command = "INSTALL"
-
-[actions.start]
- type = "sequence"
-[[actions.start.commands]]
- name = "DATANODE"
- command = "START"
-[[actions.start.commands]]
- name = "NAMENODE"
- command = "START"
-[[actions.start.commands]]
- name = "SECONDARY_NAMENODE"
- command = "START"
-
-[actions.stop]
- type = "sequence"
-[[actions.stop.commands]]
- name = "SECONDARY_NAMENODE"
- command = "STOP"
-[[actions.stop.commands]]
- name = "NAMENODE"
- command = "STOP"
-[[actions.stop.commands]]
- name = "DATANODE"
- command = "STOP"
-
-
-[components.DATANODE]
-playbook = "run_agent_command.yml"
-log_files = ["remote"]
-[components.DATANODE.config]
- folder = "stacks/ADH/1.0/services/HDFS/package"
- script = "scripts/datanode.py"
- packages = ["hadoop", "ranger-hdfs-plugin", "hadoop-client", "snappy", "snappy-devel", "lzo", "hadoop-libhdfs"]
-
-[components.NAMENODE]
-playbook = "run_agent_command.yml"
-log_files = ["remote"]
-[components.NAMENODE.config]
- folder = "stacks/ADH/1.0/services/HDFS/package"
- script = "scripts/namenode.py"
- packages = ["hadoop", "ranger-hdfs-plugin", "hadoop-client", "snappy", "snappy-devel", "lzo", "hadoop-libhdfs"]
-
-[components.SECONDARY_NAMENODE]
-playbook = "run_agent_command.yml"
-log_files = ["remote"]
-[components.SECONDARY_NAMENODE.config]
- folder = "stacks/ADH/1.0/services/HDFS/package"
- script = "scripts/snamenode.py"
- packages = ["hadoop", "ranger-hdfs-plugin", "hadoop-client", "snappy", "snappy-devel", "lzo", "hadoop-libhdfs"]
-
-
-[components.HDFS_CLIENT]
-playbook = "run_agent_command.yml"
-log_files = ["remote"]
-hostgroup = "HDFS.DATANODE"
-[components.HDFS_CLIENT.config]
- folder = "stacks/ADH/1.0/services/HDFS/package"
- script = "scripts/hdfs_client.py"
- packages = ["hadoop", "ranger-hdfs-plugin", "hadoop-client", "snappy", "snappy-devel", "lzo", "hadoop-libhdfs"]
-
-
-
diff --git a/tests/functional/test_stacks_data/two_identical_services/config.yaml b/tests/functional/test_stacks_data/two_identical_services/config.yaml
index a1dcbf0888..a9bc97e98c 100644
--- a/tests/functional/test_stacks_data/two_identical_services/config.yaml
+++ b/tests/functional/test_stacks_data/two_identical_services/config.yaml
@@ -23,7 +23,7 @@
install:
type: job
script: stack/extcode/cook.py
- script_type: task_generator
+ script_type: ansible
components:
ZOOKEEPER_CLIENT:
config:
@@ -40,7 +40,7 @@
install:
type: job
script: stack/extcode/cook.py
- script_type: task_generator
+ script_type: ansible
components:
ZOOKEEPER_CLIENT:
config:
diff --git a/tests/functional/test_stacks_data/yaml_parser_error/services/ZOOKEEPER/config.yaml b/tests/functional/test_stacks_data/yaml_parser_error/services/ZOOKEEPER/config.yaml
index 436fea4740..9019c54d7e 100644
--- a/tests/functional/test_stacks_data/yaml_parser_error/services/ZOOKEEPER/config.yaml
+++ b/tests/functional/test_stacks_data/yaml_parser_error/services/ZOOKEEPER/config.yaml
@@ -9,28 +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.
-name: service_name
-type: service
-description: ZooKeeper
-version: '1.2'
+- name: service_name
+ type: service
+ description: ZooKeeper
+ version: '1.2'
-actions:
+ actions:
install:
- commands:
- - {command: INSTALL, component: ZOOKEEPER_CLIENT}
- - {command: INSTALL, component: ZOOKEEPER_SERVER}
- type: sequence
+ commands:
+ - { command: INSTALL, component: ZOOKEEPER_CLIENT }
+ - { command: INSTALL, component: ZOOKEEPER_SERVER }
+ type: sequence
start:
- type: job
- script_type: ansible
- config:
- folder: stacks/ADH/1.0/services/ZOOKEEPER/package
- packages: [zookeeper][
+ type: job
+ script_type: ansible
+ config:
+ folder: stacks/ADH/1.0/services/ZOOKEEPER/package
+ packages: [ zookeeper ][
script: scripts/zookeeper_server.py
- log_files: [remote]
- script: services/zoo/start.yaml
- stop:
- type: job
- script_type: ansible
- log_files: [remote]
- script: services/zoo/stop.yaml
+ log_files: [ remote ]
+ script: services/zoo/start.yaml
+ stop:
+ type: job
+ script_type: ansible
+ log_files: [ remote ]
+ script: services/zoo/stop.yaml
diff --git a/tests/functional/test_upgrade_hostprovider_data/hostprovider/config.yaml b/tests/functional/test_upgrade_hostprovider_data/hostprovider/config.yaml
index 2fed33c993..3713a14cd0 100644
--- a/tests/functional/test_upgrade_hostprovider_data/hostprovider/config.yaml
+++ b/tests/functional/test_upgrade_hostprovider_data/hostprovider/config.yaml
@@ -14,7 +14,7 @@
type: provider
name: sample hostprovider
- version: '1.0'
+ version: "1.0"
config:
required:
type: integer
diff --git a/tests/functional/test_yet_another_tests.py b/tests/functional/test_yet_another_tests.py
index b072928868..0a00f29f6b 100644
--- a/tests/functional/test_yet_another_tests.py
+++ b/tests/functional/test_yet_another_tests.py
@@ -14,29 +14,34 @@
import pytest
# pylint: disable=W0611, W0621
from adcm_pytest_plugin import utils
+from adcm_client.packer.bundle_build import build
-from tests.library import steps
from tests.library.errorcodes import BUNDLE_ERROR, INVALID_OBJECT_DEFINITION
testcases = ["cluster", "host"]
@pytest.mark.parametrize('testcase', testcases)
-def test_handle_unknown_words_in_bundle(sdk_client_ms, testcase):
+def test_handle_unknown_words_in_bundle(sdk_client_fs, testcase):
with allure.step('Try to upload bundle with unknown words'):
dir_name = 'unknown_words_in_' + testcase
bundledir = utils.get_data_dir(__file__, dir_name)
with pytest.raises(coreapi.exceptions.ErrorMessage) as e:
- sdk_client_ms.upload_from_fs(bundledir)
+ sdk_client_fs.upload_from_fs(bundledir)
with allure.step('Check error: Not allowed key'):
- INVALID_OBJECT_DEFINITION.equal(e, 'Not allowed key', 'in ' + testcase)
+ INVALID_OBJECT_DEFINITION.equal(e, 'Map key "confi" is not allowed here')
-def test_shouldnt_load_same_bundle_twice(sdk_client_ms):
- with allure.step('Try to upload same bundle twice'):
+def test_shouldnt_load_same_bundle_twice(sdk_client_fs):
+ with allure.step('Build bundle'):
bundledir = utils.get_data_dir(__file__, 'bundle_directory_exist')
- sdk_client_ms.upload_from_fs(bundledir)
+ for path, steram in build(repopath=bundledir).items():
+ with open(path, 'wb') as file:
+ file.write(steram.read())
+ bundle_tar_path = path
+ with allure.step('Try to upload same bundle twice'):
+ sdk_client_fs.upload_from_fs(bundle_tar_path)
with pytest.raises(coreapi.exceptions.ErrorMessage) as e:
- sdk_client_ms.upload_from_fs(bundledir)
+ sdk_client_fs.upload_from_fs(bundle_tar_path)
with allure.step('Check error: bundle directory already exists'):
BUNDLE_ERROR.equal(e, 'bundle directory', 'already exists')
diff --git a/tests/functional/test_yet_another_tests_data/bundle_directory_exist/config.yaml b/tests/functional/test_yet_another_tests_data/bundle_directory_exist/config.yaml
index cd7e4c28db..8082f8689c 100644
--- a/tests/functional/test_yet_another_tests_data/bundle_directory_exist/config.yaml
+++ b/tests/functional/test_yet_another_tests_data/bundle_directory_exist/config.yaml
@@ -13,7 +13,6 @@
type: cluster
name: ADH
version: 1.5
- actions:
config:
required:
type: integer
diff --git a/tests/functional/test_yet_another_tests_data/unknown_words_in_cluster/config.yaml b/tests/functional/test_yet_another_tests_data/unknown_words_in_cluster/config.yaml
index 6e2787a25f..22e6be68a1 100644
--- a/tests/functional/test_yet_another_tests_data/unknown_words_in_cluster/config.yaml
+++ b/tests/functional/test_yet_another_tests_data/unknown_words_in_cluster/config.yaml
@@ -60,8 +60,6 @@
description: Arenadata Nothing
version: 1.0
- config:
-
confi:
bluh:
type: string
diff --git a/tests/library/errorcodes.py b/tests/library/errorcodes.py
index 97c60c5c44..f17351f1ea 100644
--- a/tests/library/errorcodes.py
+++ b/tests/library/errorcodes.py
@@ -18,24 +18,24 @@ def __init__(self, title, code):
self.code = code
def equal(self, e, *args):
+ title = e.value.error.title
+ code = e.value.error.get("code", "")
+ desc = e.value.error.get("desc", "")
+ error_args = e.value.error.get("args", "")
expect(
- e.value.error.title == self.title,
- 'Expected title is "{}", actual is "{}"'.format(
- self.title, e.value.error.title
- )
+ title == self.title,
+ f'Expected title is "{self.title}", actual is "{title}"'
)
expect(
- e.value.error['code'] == self.code,
- 'Expected error code is "{}", actual is "{}"'.format(
- self.code, e.value.error['code']
- )
+ code == self.code,
+ f'Expected error code is "{self.code}", actual is "{code}"'
)
for i in args:
expect(
- i in e.value.error['desc'],
- 'Expected part of desc is "{}", actual desc is "{}"'.format(
- i, e.value.error['desc']
- )
+ i in desc or i in error_args,
+ f'Expected part of desc or args is "{i}", '
+ f'actual desc is: \n"{desc}", '
+ f'\nargs is: \n"{error_args or None}"'
)
assert_expectations()
@@ -145,6 +145,11 @@ def equal(self, e, *args):
'CLUSTER_NOT_FOUND',
)
+CLUSTER_SERVICE_NOT_FOUND = ADCMError(
+ '404 Not Found',
+ 'CLUSTER_SERVICE_NOT_FOUND',
+)
+
SERVICE_NOT_FOUND = ADCMError(
'404 Not Found',
'SERVICE_NOT_FOUND',
diff --git a/tests/library/steps.py b/tests/library/steps.py
deleted file mode 100644
index 5d1498efaf..0000000000
--- a/tests/library/steps.py
+++ /dev/null
@@ -1,168 +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 os
-import tarfile
-import tempfile
-
-import allure
-from adcm_pytest_plugin import utils
-
-from .utils import (
- get_host_by_fqdn,
- get_random_service,
- get_service_id_by_name,
- get_random_host_prototype,
- get_random_cluster_prototype
-)
-
-
-@allure.step('Pack bundle form {bundledir}')
-def _pack_bundle(bundledir):
- tempdir = tempfile.mkdtemp(prefix="test")
- tarfilename = os.path.join(tempdir, os.path.basename(bundledir) + '.tar')
- with tarfile.open(tarfilename, "w") as tar:
- for sub in os.listdir(bundledir):
- tar.add(os.path.join(bundledir, sub), arcname=sub)
- tar.close()
- return tarfilename
-
-
-@allure.step('Upload bundle "{1}"')
-def upload_bundle(client, bundledir):
- try:
- if os.path.isdir(bundledir):
- archfile = _pack_bundle(bundledir)
- else:
- archfile = bundledir
- file = open(archfile, 'rb')
- client.stack.upload.create(file=file)
- client.stack.load.create(bundle_file=os.path.basename(archfile))
- finally:
- os.remove(archfile)
- os.rmdir(os.path.dirname(archfile))
-
-
-@allure.step('Create cluster')
-def create_cluster(client):
- prototype = get_random_cluster_prototype(client)
- return client.cluster.create(prototype_id=prototype['id'],
- name=utils.random_string())
-
-
-@allure.step('Create host {1}')
-def create_host_w_default_provider(client, fqdn):
- proto = get_random_host_prototype(client)
- prototype_id = client.stack.provider.list()[0]['id']
- provider = client.provider.create(prototype_id=prototype_id,
- name=utils.random_string())
- return client.host.create(prototype_id=proto['id'],
- provider_id=provider['id'], fqdn=fqdn)
-
-
-@allure.step('Create hostprovider')
-def create_hostprovider(client):
- prototype_id = client.stack.provider.list()[0]['id']
- return client.provider.create(name=utils.random_string(),
- prototype_id=prototype_id)
-
-
-@allure.step('Add host {1} to cluster {2}')
-def add_host_to_cluster(client, host, cluster):
- client.cluster.host.create(cluster_id=cluster['id'], host_id=host['id'])
- host = client.host.read(host_id=host['id'])
- return host
-
-
-@allure.step('Update cluster to {2}')
-def partial_update_cluster(client, cluster, name, desc=None):
- if desc is None:
- return client.cluster.partial_update(cluster_id=cluster['id'],
- name=name)
- else:
- return client.cluster.partial_update(cluster_id=cluster['id'],
- name=name, description=desc)
-
-
-@allure.step('Create service {1} in cluster {0}')
-def create_service_by_name(client, cluster_id, service_name):
- service_id = get_service_id_by_name(client, service_name)
- return client.cluster.service.create(cluster_id=cluster_id,
- prototype_id=service_id)
-
-
-@allure.step('Create random service in cluster {0}')
-def create_random_service(client, cluster_id):
- service = get_random_service(client)['name']
- return create_service_by_name(client, cluster_id, service)
-
-
-@allure.step('Read Service with id= {0}')
-def read_service(client, identifer):
- return client.stack.service.read(prototype_id=identifer)
-
-
-@allure.step('Read cluster with id {0}')
-def read_cluster(client, identifier):
- return client.cluster.read(cluster_id=identifier)
-
-
-@allure.step('Read host with id {0}')
-def read_host(client, identifier):
- return client.host.read(host_id=identifier)
-
-
-@allure.step('Delete host {0}')
-def delete_host(client, fqdn):
- host_id = get_host_by_fqdn(client, fqdn)['id']
- return client.host.delete(host_id=host_id)
-
-
-@allure.step('Delete all clusters')
-def delete_all_clusters(client):
- for cluster in client.cluster.list():
- client.cluster.delete(cluster_id=cluster['id'])
-
-
-@allure.step('Delete all hosts')
-def delete_all_hosts(client):
- for host in client.host.list():
- client.host.delete(host_id=host['id'])
-
-
-@allure.step('Delete all hostcomponents')
-def delete_all_hostcomponents(client):
- for hostcomponent in client.hostcomponent.list():
- client.hostcomponent.delete(id=hostcomponent['id'])
-
-
-@allure.step('Delete {0}')
-def delete_all_data(client):
- with allure.step('Cleaning all data'):
- # delete_all_hostcomponents()
- delete_all_clusters(client)
- delete_all_hosts(client)
-
-
-@allure.step('Create hostservice in specific cluster')
-def create_hostcomponent_in_cluster(client, cluster, host, service, component):
- hc = [{"host_id": host['id'],
- "service_id": service['id'],
- "component_id": component['id']}]
- return client.cluster.hostcomponent.create(cluster_id=cluster['id'], hc=hc)
-
-
-@allure.step('Check if action {action_name} state is {state_expected}')
-def check_action_state(action_name: str, state_current: str,
- state_expected: str) -> None:
- assert state_current == state_expected, \
- f'Current action {action_name} status {state_current}. ' \
- f'Expected: {state_expected}'
diff --git a/tests/stack/cluster_bundle/services/ZOOKEEPER/config.yaml b/tests/stack/cluster_bundle/services/ZOOKEEPER/config.yaml
index 8763583343..b294247cb8 100644
--- a/tests/stack/cluster_bundle/services/ZOOKEEPER/config.yaml
+++ b/tests/stack/cluster_bundle/services/ZOOKEEPER/config.yaml
@@ -9,49 +9,49 @@
# 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: ZOOKEEPER
-type: service
-description: ZooKeeper
-version: '1.2'
+- name: ZOOKEEPER
+ type: service
+ description: ZooKeeper
+ version: '1.2'
-actions:
+ actions:
start:
- type: job
- params:
- folder: stacks/ADH/1.0/services/ZOOKEEPER/package
- packages: [zookeeper]
- script: scripts/zookeeper_server.py
- log_files: [remote]
- script: services/zoo/start.yaml
- script_type: ansible
+ type: job
+ params:
+ folder: stacks/ADH/1.0/services/ZOOKEEPER/package
+ packages: [ zookeeper ]
+ script: scripts/zookeeper_server.py
+ log_files: [ remote ]
+ script: services/zoo/start.yaml
+ script_type: ansible
stop:
- type: job
- log_files: [remote]
- script: services/zoo/stop.yaml
- script_type: ansible
+ type: job
+ log_files: [ remote ]
+ script: services/zoo/stop.yaml
+ script_type: ansible
-components:
+ components:
ZOOKEEPER_CLIENT:
- params:
- folder: stacks/ADH/1.0/services/ZOOKEEPER/package
- packages: [zookeeper]
- script: scripts/zookeeper_client.py
+ params:
+ folder: stacks/ADH/1.0/services/ZOOKEEPER/package
+ packages: [ zookeeper ]
+ script: scripts/zookeeper_client.py
ZOOKEEPER_SERVER:
- params:
- folder: stacks/ADH/1.0/services/ZOOKEEPER/package
- packages: [zookeeper]
- script: scripts/zookeeper_server.py
+ params:
+ folder: stacks/ADH/1.0/services/ZOOKEEPER/package
+ packages: [ zookeeper ]
+ script: scripts/zookeeper_server.py
-config:
- ssh-key: {default: TItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAA, type: string, required: false}
- integer-key: {default: 24, max: 48, min: 2, type: integer, required: false}
- float-key: {default: 4.4, max: 50.0, min: 4.0, type: float, required: false}
+ config:
+ ssh-key: { default: TItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAA, type: string, required: false }
+ integer-key: { default: 24, max: 48, min: 2, type: integer, required: false }
+ float-key: { default: 4.4, max: 50.0, min: 4.0, type: float, required: false }
zoo.cfg:
- autopurge.purgeInterval: {default: 24, max: 48, min: 2, type: integer}
- dataDir: {default: /hadoop/zookeeper, type: string}
- port:
- required: false
- default: 80
- option: {http: 80, https: 443}
- type: option
- required-key: {default: value, type: string}
+ autopurge.purgeInterval: { default: 24, max: 48, min: 2, type: integer }
+ dataDir: { default: /hadoop/zookeeper, type: string }
+ port:
+ required: false
+ default: 80
+ option: { http: 80, https: 443 }
+ type: option
+ required-key: { default: value, type: string }
diff --git a/tests/stack/hostprovider_bundle/provider/config.yaml b/tests/stack/hostprovider_bundle/provider/config.yaml
index 19dc8a0121..ff0dcc02c1 100644
--- a/tests/stack/hostprovider_bundle/provider/config.yaml
+++ b/tests/stack/hostprovider_bundle/provider/config.yaml
@@ -12,28 +12,28 @@
---
- type: provider
name: provider_sample
- version: &version 0.3
+ version: &version "0.3"
upgrade:
- name: *version
- versions: {min: 0.1, max_strict: *version}
+ versions: { min: "0.1", max_strict: *version }
description: sample upgrade for sample
- states: {available: any}
+ states: { available: any }
actions:
init: &init
type: job
script_type: ansible
script: provider/init.yaml
states:
- available: [created]
+ available: [ created ]
on_success: initiated
status_check: &status_check
- type: job
- script_type: ansible
- script: provider/check_status.yaml
- states: { available: any }
- log_files:
- -check
+ type: job
+ script_type: ansible
+ script: provider/check_status.yaml
+ states: { available: any }
+ log_files:
+ - check
sleep: &sleep_action
type: job
@@ -86,48 +86,48 @@
version: &host_ver 1.01
config:
required:
- type: integer
- required: yes
- default: 40
+ type: integer
+ required: yes
+ default: 40
str-key:
- default: value
- type: string
- required: false
+ default: value
+ type: string
+ required: false
int_key:
- type: integer
- required: NO
- default:
+ type: integer
+ required: NO
+ default: 1
fkey:
- type: float
- required: false
- default: 1
+ type: float
+ required: false
+ default: 1
bool:
- type: boolean
- required : no
- default: false
+ type: boolean
+ required: no
+ default: false
option:
+ type: option
+ option:
+ http: 8080
+ https: 4043
+ ftp: my.host
+ required: FALSE
+ sub:
+ sub1:
type: option
option:
- http: 8080
- https: 4043
- ftp: my.host
- required: FALSE
- sub:
- sub1:
- type: option
- option:
- a: 1
- s: 2
- d: 3
- required: no
+ a: 1
+ s: 2
+ d: 3
+ required: no
password:
- type: password
- required: false
- default: q1w2e3r4t5y6
+ type: password
+ required: false
+ default: q1w2e3r4t5y6
json:
- type: json
- required: false
- default: {"foo": "bar"}
+ type: json
+ required: false
+ default: { "foo": "bar" }
credentials:
ansible_host:
display_name: "Hostname"
@@ -156,7 +156,7 @@
type: json
default: *connect_type
description: "Change it before hostprovider will be initiated"
- writable: [created]
+ writable: [ created ]
file_with_ssh:
type: file
required: false
diff --git a/tests/ui_tests/app/app.py b/tests/ui_tests/app/app.py
index 20d8ecc485..cf6f2b83ce 100644
--- a/tests/ui_tests/app/app.py
+++ b/tests/ui_tests/app/app.py
@@ -24,13 +24,12 @@
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.ui import WebDriverWait as WDW
-from tests.library import steps
from tests.ui_tests.app.pages import Ui, ClustersList
class ADCMTest:
- __slots__ = ('opts', 'capabilities', 'driver', 'ui', 'adcm', '_client', 'selenoid')
+ __slots__ = ('opts', 'capabilities', 'driver', 'ui', 'adcm', 'selenoid')
def __init__(self, browser='Chrome'):
self.opts = FirefoxOptions() if browser == 'Firefox' else ChromeOptions()
@@ -51,7 +50,6 @@ def __init__(self, browser='Chrome'):
self.driver = None
self.ui = None
self.adcm = None
- self._client = None
def create_driver(self):
if self.selenoid['host']:
@@ -72,22 +70,11 @@ def create_driver(self):
@allure.step('Attache ADCM')
def attache_adcm(self, adcm: ADCM):
self.adcm = adcm
- self._client = adcm.api.objects
@allure.step('Get Clusters List')
def clusters_page(self):
return ClustersList(self)
- def create_cluster(self):
- return steps.create_cluster(self._client)['name']
-
- def create_host(self, fqdn):
- steps.create_host_w_default_provider(self._client, fqdn)
- return fqdn
-
- def create_provider(self):
- return steps.create_hostprovider(self._client)['name']
-
def wait_for(self, condition: EC, locator: tuple, timer=5):
def get_element(el):
return WDW(self.driver, timer).until(condition(el))
diff --git a/tests/ui_tests/app/configuration.py b/tests/ui_tests/app/configuration.py
index 457a7ec0ad..8accad283f 100644
--- a/tests/ui_tests/app/configuration.py
+++ b/tests/ui_tests/app/configuration.py
@@ -1,8 +1,7 @@
import json
import allure
-from retrying import retry
-from selenium.common.exceptions import NoSuchElementException, StaleElementReferenceException, \
+from selenium.common.exceptions import NoSuchElementException, \
TimeoutException
from selenium.webdriver.common.action_chains import ActionChains
from selenium.webdriver.remote.webelement import WebElement
@@ -11,11 +10,6 @@
from tests.ui_tests.app.pages import BasePage
-def retry_on_exception(exc):
- return any((isinstance(exc, StaleElementReferenceException),
- isinstance(exc, NoSuchElementException)))
-
-
# pylint: disable=R0904
class Configuration(BasePage):
"""Class for configuration page."""
@@ -169,7 +163,6 @@ def get_field_checkbox(field):
def get_field_groups(self):
return self.driver.find_elements(*ConfigurationLocators.field_group)
- @retry(retry_on_exception=retry_on_exception, stop_max_delay=10 * 1000)
@allure.step('Get save button status')
def save_button_status(self):
self._wait_element(ConfigurationLocators.config_save_button)
@@ -289,7 +282,10 @@ def get_textboxes(self):
@allure.step('Get password elements')
def get_password_elements(self):
- return self.driver.find_elements(*ConfigurationLocators.app_fields_password)
+ base_password_fields = self.driver.find_elements(*ConfigurationLocators.app_fields_password)
+ return base_password_fields[0].find_elements(
+ *ConfigurationLocators.displayed_password_fields
+ )
@allure.step('Get display names')
def get_display_names(self):
diff --git a/tests/ui_tests/app/locators.py b/tests/ui_tests/app/locators.py
index 0eea0f082c..c73e84951a 100644
--- a/tests/ui_tests/app/locators.py
+++ b/tests/ui_tests/app/locators.py
@@ -140,6 +140,7 @@ class ConfigurationLocators:
map_key_field = bys.by_class('key-field')
map_value_field = bys.by_class('value-field')
load_marker = bys.by_class('load_complete')
+ displayed_password_fields = bys.by_xpath("//div[@style='display: flex;']")
class ActionPageLocators:
diff --git a/tests/ui_tests/app/pages.py b/tests/ui_tests/app/pages.py
index 9094737f50..a0dc13d35b 100644
--- a/tests/ui_tests/app/pages.py
+++ b/tests/ui_tests/app/pages.py
@@ -14,7 +14,6 @@
import allure
# Created by a1wen at 05.03.19
-from retrying import retry
from selenium.common.exceptions import TimeoutException, InvalidElementStateException, \
NoSuchElementException, StaleElementReferenceException
from selenium.webdriver.common.action_chains import ActionChains
@@ -23,15 +22,10 @@
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.ui import WebDriverWait as WDW
-from cm.errors import ERRORS
from tests.ui_tests.app.helpers import bys
from tests.ui_tests.app.locators import Menu, Common, Cluster, Provider, Host, Service
-def ui_retry(func):
- return retry(stop_max_delay=15 * 1000)(func)
-
-
def element_text(e):
if not e:
raise NoSuchElementException("Asked for text of None element")
@@ -48,13 +42,11 @@ class BasePage:
def __init__(self, driver):
self.driver = driver
- @ui_retry
def get(self, url, url_path=None, timeout=5):
if self.driver.current_url != url:
self.driver.get(url)
self._contains_url(url_path, timer=timeout)
- @ui_retry
@allure.step('Find elements')
def _elements(self, locator: tuple, f, **kwargs):
"""Find elements
@@ -195,15 +187,6 @@ def clear_input_element(self, element):
self.clear_element(input_element)
return element
- def _error_handler(self, error: ERRORS):
- if error in ERRORS.keys():
- e = self._getelement(Common.error).text
- return bool(error in e), e
-
- @allure.step('Check error')
- def check_error(self, error: ERRORS):
- return self._error_handler(error)
-
def _click_with_offset(self, element: tuple, x_offset, y_offset):
actions = ActionChains(self.driver)
actions.move_to_element_with_offset(self._getelement(element),
@@ -213,7 +196,7 @@ def _contains_url(self, url: str, timer=5):
WDW(self.driver, timer).until(EC.url_contains(url))
return self.driver.current_url
- def _is_element_clickable(self, locator: tuple, timer=5) -> WebElement:
+ def _is_element_clickable(self, locator: tuple, timer=5) -> bool:
return bool(WDW(self.driver, timer).until(EC.element_to_be_clickable(locator)))
def _menu_click(self, locator: tuple):
diff --git a/tests/ui_tests/test_app_crud.py b/tests/ui_tests/test_app_crud.py
deleted file mode 100644
index 98e9612bfc..0000000000
--- a/tests/ui_tests/test_app_crud.py
+++ /dev/null
@@ -1,167 +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.
-
-# Created by a1wen at 27.02.19
-
-import os
-import time
-
-import allure
-import pytest
-from adcm_pytest_plugin import utils
-from adcm_pytest_plugin.docker_utils import DockerWrapper
-from selenium.common.exceptions import TimeoutException
-
-# pylint: disable=W0611, W0621
-from tests.library import steps
-from tests.ui_tests.app.app import ADCMTest
-
-DATADIR = utils.get_data_dir(__file__)
-BUNDLES = os.path.join(os.path.dirname(__file__), "../stack/")
-
-pytestmark = pytest.mark.skip(reason="It is flaky. Just skip this")
-
-
-@pytest.fixture()
-@allure.step('Create cluster and provider')
-def adcm(image, adcm_credentials):
- repo, tag = image
- dw = DockerWrapper()
- adcm = dw.run_adcm(image=repo, tag=tag, pull=False)
- adcm.api.auth(**adcm_credentials)
- cluster_bundle = os.path.join(DATADIR, 'cluster_bundle')
- provider_bundle = os.path.join(DATADIR, 'hostprovider')
- steps.upload_bundle(adcm.api.objects, cluster_bundle)
- steps.upload_bundle(adcm.api.objects, provider_bundle)
- yield adcm
- adcm.stop()
-
-
-@pytest.fixture()
-def app(adcm, request):
- app = ADCMTest()
- app.attache_adcm(adcm)
- app.base_page()
- request.addfinalizer(app.destroy)
- return app
-
-
-@pytest.fixture()
-@allure.step('Create cluster')
-def cluster(app):
- return app.create_cluster()
-
-
-@pytest.fixture()
-@allure.step('Create hostprovider')
-def hostprovider(app):
- return app.create_provider()
-
-
-@pytest.fixture()
-@allure.step('Create host')
-def host(app):
- return app.create_host(utils.random_string())
-
-
-@pytest.fixture()
-def data():
- return {'name': utils.random_string(), 'description': utils.random_string()}
-
-
-def test_run_app(app, adcm_credentials):
- app.contains_url('/login')
- app.ui.session.login(**adcm_credentials)
- with allure.step('Check that login successful'):
- assert app.contains_url('/admin')
-
-
-def test_cluster_creation(app, data, adcm_credentials):
- app.ui.session.login(**adcm_credentials)
- app.ui.clusters.add_new_cluster(**data)
- app.ui.clusters.list_element_contains(data['name'])
-
-
-def test_delete_first_cluster(app, data, adcm_credentials):
- app.ui.session.login(**adcm_credentials)
- app.ui.clusters.add_new_cluster(**data)
- app.ui.clusters.delete_first_cluster()
- app.ui.clusters.list_is_empty()
-
-
-def test_provider_creation(app, data, adcm_credentials):
- app.ui.session.login(**adcm_credentials)
- app.ui.providers.add_new_provider(**data)
- app.ui.providers.list_element_contains(data['name'])
-
-
-def test_delete_first_provider(app, data, adcm_credentials):
- app.ui.session.login(**adcm_credentials)
- app.ui.providers.add_new_provider(**data)
- try:
- app.ui.providers.list_element_contains(data['name'])
- except TimeoutException:
- pytest.xfail("Flaky test")
- app.ui.providers.delete_first_provider()
- app.ui.providers.list_is_empty()
-
-
-provider = data
-
-
-def test_host_creation(app, provider, data, adcm_credentials):
- app.ui.session.login(**adcm_credentials)
- app.ui.providers.add_new_provider(**provider)
- app.ui.hosts.add_new_host(data['name'])
- app.ui.hosts.list_element_contains(data['name'])
-
-
-def test_host_creation_from_cluster_details(app, cluster, hostprovider, data, adcm_credentials):
- app.ui.session.login(**adcm_credentials)
- app.ui.clusters.details.create_host_from_cluster(hostprovider, data['name'])
- app.ui.clusters.details.host_tab.list_element_contains(data['name'])
-
-
-def test_host_deletion(app, provider, data, adcm_credentials):
- app.ui.session.login(**adcm_credentials)
- app.ui.providers.add_new_provider(**provider)
- app.ui.hosts.add_new_host(data['name'])
- try:
- app.ui.hosts.delete_first_host()
- except TimeoutException:
- pytest.xfail("Flaky test")
-
-
-def test_deletion_provider_while_it_has_host(app, provider, data, adcm_credentials):
- app.ui.session.login(**adcm_credentials)
- app.ui.providers.add_new_provider(**provider)
- app.ui.hosts.add_new_host(data['name'])
- time.sleep(10)
- try:
- app.ui.providers.delete_first_provider()
- error = app.ui.providers.check_error('PROVIDER_CONFLICT')
- assert error[0], error[1]
- except (AssertionError, TimeoutException):
- pytest.xfail("Flaky test")
-
-
-def test_addition_host_to_cluster(app, cluster, host, adcm_credentials):
- app.ui.session.login(**adcm_credentials)
- app.ui.clusters.details.add_host_in_cluster()
- app.ui.clusters.details.host_tab.list_element_contains(host)
-
-
-def test_cluster_action_must_be_run(app, cluster, adcm_credentials):
- action = 'install'
- app.ui.session.login(**adcm_credentials)
- app.ui.clusters.details.run_action_by_name(action)
- app.ui.jobs.check_task(action)
diff --git a/tests/ui_tests/test_app_crud_data/hostprovider/config.yaml b/tests/ui_tests/test_app_crud_data/hostprovider/config.yaml
index 50ba4676ea..343ddf3bf7 100644
--- a/tests/ui_tests/test_app_crud_data/hostprovider/config.yaml
+++ b/tests/ui_tests/test_app_crud_data/hostprovider/config.yaml
@@ -16,7 +16,7 @@
upgrade:
- name: *version
versions: { min: 0.1, max_strict: *version }
- description: yopta upgrade
+ description: some upgrade
states: { available: any }
- name: new_upgrade
versions:
diff --git a/tests/ui_tests/test_ui_config_hell_data/config.yaml b/tests/ui_tests/test_ui_config_hell_data/config.yaml
index b386527c17..6570fe0d90 100644
--- a/tests/ui_tests/test_ui_config_hell_data/config.yaml
+++ b/tests/ui_tests/test_ui_config_hell_data/config.yaml
@@ -30,7 +30,6 @@
default: qwerty1234
required: false
svc-ro-created:
- display_name:
type: string
default: bluh
required: false
@@ -849,8 +848,7 @@
required: no
ui_options:
invisible: true
- <<: *group_of_visible
- <<: *group_on_invisible
+ <<: [*group_of_visible, *group_on_invisible]
- type: service
diff --git a/web/.prettierrc b/web/.prettierrc
deleted file mode 100644
index f3902342b5..0000000000
--- a/web/.prettierrc
+++ /dev/null
@@ -1,6 +0,0 @@
-{
- "tabWidth": 2,
- "useTabs": false,
- "singleQuote": true,
- "printWidth": 120
-}
diff --git a/web/angular.json b/web/angular.json
index 4ba7a79a4d..4459e704e6 100644
--- a/web/angular.json
+++ b/web/angular.json
@@ -11,6 +11,7 @@
"build": {
"builder": "@angular-devkit/build-angular:browser",
"options": {
+ "preserveSymlinks": true,
"aot": true,
"outputPath": "dist",
"index": "src/index.html",
@@ -22,7 +23,8 @@
],
"styles": [
"src/adcm-theme.scss",
- "src/styles.scss"
+ "src/styles.scss",
+ "src/adcm2.scss"
],
"scripts": []
},
@@ -71,6 +73,7 @@
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
+ "preserveSymlinks": true,
"main": "src/test.ts",
"karmaConfig": "./karma.conf.js",
"polyfills": "src/polyfills.ts",
@@ -78,7 +81,8 @@
"scripts": [],
"styles": [
"src/adcm-theme.scss",
- "src/styles.scss"
+ "src/styles.scss",
+ "src/adcm2.scss"
],
"assets": [
"src/assets"
diff --git a/web/build.sh b/web/build.sh
index df0ca27bdf..d4953444ce 100755
--- a/web/build.sh
+++ b/web/build.sh
@@ -11,6 +11,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
+npm config set registry https://rt.adsw.io/artifactory/api/npm/arenadata-npm/
npm install
export PATH=./node_modules/.bin/:$PATH
ng build --prod --progress=false --no-delete-output-path --output-path /wwwroot adcm
diff --git a/web/ng_test.sh b/web/ng_test.sh
new file mode 100755
index 0000000000..df6e54c0c5
--- /dev/null
+++ b/web/ng_test.sh
@@ -0,0 +1,7 @@
+#!/usr/bin/env sh
+set -eu
+
+npm config set registry https://rt.adsw.io/artifactory/api/npm/arenadata-npm/
+export CHROME_BIN=/usr/bin/google-chrome
+npm install
+ng test --watch=false
diff --git a/web/npm_check.sh b/web/npm_check.sh
new file mode 100755
index 0000000000..1df82c9669
--- /dev/null
+++ b/web/npm_check.sh
@@ -0,0 +1,9 @@
+#!/usr/bin/env sh
+set -euo pipefail
+
+npm install npm-check
+export PATH=$PATH:./node_modules/.bin/
+npm config set registry https://rt.adsw.io/artifactory/api/npm/arenadata-npm/
+npm i --production
+npm-check --production --skip-unused
+npm audit
diff --git a/web/package-lock.json b/web/package-lock.json
index a747798b8a..99e5a0be14 100644
--- a/web/package-lock.json
+++ b/web/package-lock.json
@@ -4,6 +4,14 @@
"lockfileVersion": 1,
"requires": true,
"dependencies": {
+ "@adwp-ui/widgets": {
+ "version": "0.0.36",
+ "resolved": "https://rt.adsw.io/artifactory/api/npm/arenadata-npm/@adwp-ui/widgets/-/@adwp-ui/widgets-0.0.36.tgz",
+ "integrity": "sha1-BTLGMBOYqcvDLvT0tU7gpTrR2FM=",
+ "requires": {
+ "tslib": "^2.0.0"
+ }
+ },
"@angular-devkit/architect": {
"version": "0.1101.4",
"resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1101.4.tgz",
@@ -91,6 +99,17 @@
"webpack-sources": "2.2.0",
"webpack-subresource-integrity": "1.5.2",
"worker-plugin": "5.0.0"
+ },
+ "dependencies": {
+ "sass": {
+ "version": "1.32.4",
+ "resolved": "https://rt.adsw.io/artifactory/api/npm/arenadata-npm/sass/-/sass-1.32.4.tgz",
+ "integrity": "sha1-MIvyndf1PUSuTwZYDpqRCtmqQR4=",
+ "dev": true,
+ "requires": {
+ "chokidar": ">=2.0.0 <4.0.0"
+ }
+ }
}
},
"@angular-devkit/build-optimizer": {
@@ -2214,12 +2233,6 @@
"integrity": "sha1-l6ERlkmyEa0zaR2fn0hqjsn74KM=",
"dev": true
},
- "amdefine": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz",
- "integrity": "sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU=",
- "dev": true
- },
"ansi-colors": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz",
@@ -2260,7 +2273,6 @@
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.1.tgz",
"integrity": "sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg==",
- "dev": true,
"requires": {
"normalize-path": "^3.0.0",
"picomatch": "^2.0.4"
@@ -2363,12 +2375,6 @@
"integrity": "sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=",
"dev": true
},
- "array-find-index": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/array-find-index/-/array-find-index-1.0.2.tgz",
- "integrity": "sha1-3wEKoSh+Fku9pvlyOwqWoexBh6E=",
- "dev": true
- },
"array-flatten": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-2.1.2.tgz",
@@ -2506,12 +2512,6 @@
"integrity": "sha512-z/WhQ5FPySLdvREByI2vZiTWwCnF0moMJ1hK9YQwDTHKh6I7/uSckMetoRGb5UBZPC1z0jlw+n/XCgjeH7y1AQ==",
"dev": true
},
- "async-foreach": {
- "version": "0.1.3",
- "resolved": "https://registry.npmjs.org/async-foreach/-/async-foreach-0.1.3.tgz",
- "integrity": "sha1-NhIfhFwFeBct5Bmpfb6x0W7DRUI=",
- "dev": true
- },
"async-limiter": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz",
@@ -2717,8 +2717,7 @@
"binary-extensions": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz",
- "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==",
- "dev": true
+ "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA=="
},
"bindings": {
"version": "1.5.0",
@@ -2747,15 +2746,6 @@
"integrity": "sha512-gaqbzQPqOoamawKg0LGVd7SzLgXS+JH61oWprSLH+P+abTczqJbhTR8CmJ2u9/bUYNmHTGJx/UEmn6doAvvuig==",
"dev": true
},
- "block-stream": {
- "version": "0.0.9",
- "resolved": "https://registry.npmjs.org/block-stream/-/block-stream-0.0.9.tgz",
- "integrity": "sha1-E+v+d4oDIFz+A3UUgeu0szAMEmo=",
- "dev": true,
- "requires": {
- "inherits": "~2.0.0"
- }
- },
"blocking-proxy": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/blocking-proxy/-/blocking-proxy-1.0.1.tgz",
@@ -2852,7 +2842,6 @@
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
"integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
- "dev": true,
"requires": {
"fill-range": "^7.0.1"
}
@@ -3130,24 +3119,6 @@
"integrity": "sha512-c7wVvbw3f37nuobQNtgsgG9POC9qMbNuMQmTCqZv23b6MIz0fcYpBiOlv9gEN/hdLdnZTDQhg6e9Dq5M1vKvfg==",
"dev": true
},
- "camelcase-keys": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-2.1.0.tgz",
- "integrity": "sha1-MIvur/3ygRkFHvodkyITyRuPkuc=",
- "dev": true,
- "requires": {
- "camelcase": "^2.0.0",
- "map-obj": "^1.0.0"
- },
- "dependencies": {
- "camelcase": {
- "version": "2.1.1",
- "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-2.1.1.tgz",
- "integrity": "sha1-fB0W1nmhu+WcoCys7PsBHiAfWh8=",
- "dev": true
- }
- }
- },
"caniuse-api": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/caniuse-api/-/caniuse-api-3.0.0.tgz",
@@ -3199,7 +3170,6 @@
"version": "3.5.1",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.1.tgz",
"integrity": "sha512-9+s+Od+W0VJJzawDma/gvBNQqkTiqYTWLuZoyAsivsI4AaWTCzHG06/TMjsf1cYe9Cb97UCEhjz7HvnPk2p/tw==",
- "dev": true,
"requires": {
"anymatch": "~3.1.1",
"braces": "~3.0.2",
@@ -3215,7 +3185,6 @@
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
- "dev": true,
"optional": true
}
}
@@ -4429,15 +4398,6 @@
}
}
},
- "currently-unhandled": {
- "version": "0.4.1",
- "resolved": "https://registry.npmjs.org/currently-unhandled/-/currently-unhandled-0.4.1.tgz",
- "integrity": "sha1-mI3zP+qxke95mmE2nddsF635V+o=",
- "dev": true,
- "requires": {
- "array-find-index": "^1.0.1"
- }
- },
"custom-event": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/custom-event/-/custom-event-1.0.1.tgz",
@@ -5942,7 +5902,6 @@
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
"integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
- "dev": true,
"requires": {
"to-regex-range": "^5.0.1"
}
@@ -6215,38 +6174,6 @@
"dev": true,
"optional": true
},
- "fstream": {
- "version": "1.0.12",
- "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.12.tgz",
- "integrity": "sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg==",
- "dev": true,
- "requires": {
- "graceful-fs": "^4.1.2",
- "inherits": "~2.0.0",
- "mkdirp": ">=0.5 0",
- "rimraf": "2"
- },
- "dependencies": {
- "mkdirp": {
- "version": "0.5.5",
- "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz",
- "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==",
- "dev": true,
- "requires": {
- "minimist": "^1.2.5"
- }
- },
- "rimraf": {
- "version": "2.7.1",
- "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz",
- "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==",
- "dev": true,
- "requires": {
- "glob": "^7.1.3"
- }
- }
- }
- },
"function-bind": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
@@ -6312,15 +6239,6 @@
}
}
},
- "gaze": {
- "version": "1.1.3",
- "resolved": "https://registry.npmjs.org/gaze/-/gaze-1.1.3.tgz",
- "integrity": "sha512-BRdNm8hbWzFzWHERTrejLqwHDfS4GibPoq5wjTPIoJHoBtKGPg3xAFfxmM+9ztbXelxcf2hwQcaz1PtmFeue8g==",
- "dev": true,
- "requires": {
- "globule": "^1.0.0"
- }
- },
"gensync": {
"version": "1.0.0-beta.2",
"resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
@@ -6344,12 +6262,6 @@
"has-symbols": "^1.0.1"
}
},
- "get-stdin": {
- "version": "4.0.1",
- "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-4.0.1.tgz",
- "integrity": "sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4=",
- "dev": true
- },
"get-stream": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz",
@@ -6392,7 +6304,6 @@
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.1.tgz",
"integrity": "sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ==",
- "dev": true,
"requires": {
"is-glob": "^4.0.1"
}
@@ -6417,17 +6328,6 @@
"slash": "^3.0.0"
}
},
- "globule": {
- "version": "1.3.2",
- "resolved": "https://registry.npmjs.org/globule/-/globule-1.3.2.tgz",
- "integrity": "sha512-7IDTQTIu2xzXkT+6mlluidnWo+BypnbSoEVVQCGfzqnl5Ik8d3e1d4wycb8Rj9tWW+Z39uPWsdlquqiqPCd/pA==",
- "dev": true,
- "requires": {
- "glob": "~7.1.1",
- "lodash": "~4.17.10",
- "minimatch": "~3.0.2"
- }
- },
"graceful-fs": {
"version": "4.2.5",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.5.tgz",
@@ -7047,12 +6947,6 @@
"integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=",
"dev": true
},
- "in-publish": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/in-publish/-/in-publish-2.0.1.tgz",
- "integrity": "sha512-oDM0kUSNFC31ShNxHKUyfZKy8ZeXZBWMjMdZHKLOk13uvT27VTL/QzRGfRUcevJhpkZAvlhPYuXkF7eNWrtyxQ==",
- "dev": true
- },
"indent-string": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz",
@@ -7244,7 +7138,6 @@
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
"integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
- "dev": true,
"requires": {
"binary-extensions": "^2.0.0"
}
@@ -7350,14 +7243,7 @@
"is-extglob": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
- "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=",
- "dev": true
- },
- "is-finite": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/is-finite/-/is-finite-1.1.0.tgz",
- "integrity": "sha512-cdyMtqX/BOqqNBBiKlIVkytNHm49MtMlYyn1zxzvJKWmFMlGzm+ry5BBfYyeY9YmNKbRSo/o7OX9w9ale0wg3w==",
- "dev": true
+ "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI="
},
"is-fullwidth-code-point": {
"version": "3.0.0",
@@ -7369,7 +7255,6 @@
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz",
"integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==",
- "dev": true,
"requires": {
"is-extglob": "^2.1.1"
}
@@ -7395,8 +7280,7 @@
"is-number": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
- "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
- "dev": true
+ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="
},
"is-obj": {
"version": "2.0.0",
@@ -7483,12 +7367,6 @@
"integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=",
"dev": true
},
- "is-utf8": {
- "version": "0.2.1",
- "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz",
- "integrity": "sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI=",
- "dev": true
- },
"is-what": {
"version": "3.12.0",
"resolved": "https://registry.npmjs.org/is-what/-/is-what-3.12.0.tgz",
@@ -7724,12 +7602,6 @@
}
}
},
- "js-base64": {
- "version": "2.6.4",
- "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-2.6.4.tgz",
- "integrity": "sha512-pZe//GGmwJndub7ZghVHz7vjb2LgC1m8B07Au3eYqeqv9emhESByMXxaEgkUkEqJe87oBbSniGYoQNIBklc7IQ==",
- "dev": true
- },
"js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
@@ -8216,36 +8088,6 @@
"integrity": "sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA=",
"dev": true
},
- "load-json-file": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz",
- "integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=",
- "dev": true,
- "requires": {
- "graceful-fs": "^4.1.2",
- "parse-json": "^2.2.0",
- "pify": "^2.0.0",
- "pinkie-promise": "^2.0.0",
- "strip-bom": "^2.0.0"
- },
- "dependencies": {
- "parse-json": {
- "version": "2.2.0",
- "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz",
- "integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=",
- "dev": true,
- "requires": {
- "error-ex": "^1.2.0"
- }
- },
- "pify": {
- "version": "2.3.0",
- "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
- "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=",
- "dev": true
- }
- }
- },
"loader-runner": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-2.4.0.tgz",
@@ -8377,16 +8219,6 @@
"integrity": "sha512-Hesni4s5UkWkwCGJMQGAh71PaLUmKFM60dHvq0zi/vDhhrzuk+4GgNbTXJ12YYQJn6ZKBDNIjYcuQGKudvqrIw==",
"dev": true
},
- "loud-rejection": {
- "version": "1.6.0",
- "resolved": "https://registry.npmjs.org/loud-rejection/-/loud-rejection-1.6.0.tgz",
- "integrity": "sha1-W0b4AUft7leIcPCG0Eghz5mOVR8=",
- "dev": true,
- "requires": {
- "currently-unhandled": "^0.4.1",
- "signal-exit": "^3.0.0"
- }
- },
"lru-cache": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
@@ -8396,6 +8228,11 @@
"yallist": "^4.0.0"
}
},
+ "luxon": {
+ "version": "1.25.0",
+ "resolved": "https://rt.adsw.io/artifactory/api/npm/arenadata-npm/luxon/-/luxon-1.25.0.tgz",
+ "integrity": "sha1-2GIZ6QvAECwOspnWWy9ele/h/nI="
+ },
"magic-string": {
"version": "0.25.7",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.7.tgz",
@@ -8457,12 +8294,6 @@
"integrity": "sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=",
"dev": true
},
- "map-obj": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz",
- "integrity": "sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0=",
- "dev": true
- },
"map-visit": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz",
@@ -8531,24 +8362,6 @@
}
}
},
- "meow": {
- "version": "3.7.0",
- "resolved": "https://registry.npmjs.org/meow/-/meow-3.7.0.tgz",
- "integrity": "sha1-cstmi0JSKCkKu/qFaJJYcwioAfs=",
- "dev": true,
- "requires": {
- "camelcase-keys": "^2.0.0",
- "decamelize": "^1.1.2",
- "loud-rejection": "^1.0.0",
- "map-obj": "^1.0.1",
- "minimist": "^1.1.3",
- "normalize-package-data": "^2.3.4",
- "object-assign": "^4.0.1",
- "read-pkg-up": "^1.0.1",
- "redent": "^1.0.0",
- "trim-newlines": "^1.0.0"
- }
- },
"merge-descriptors": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz",
@@ -8900,7 +8713,8 @@
"version": "2.14.2",
"resolved": "https://registry.npmjs.org/nan/-/nan-2.14.2.tgz",
"integrity": "sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ==",
- "dev": true
+ "dev": true,
+ "optional": true
},
"nanoid": {
"version": "3.1.20",
@@ -9098,163 +8912,6 @@
"integrity": "sha512-Slf2s69+2/uAD79pVVQo8uSiC34+g8GWY8UH2Qtqv34ZfhYrxpYpfzs9Js9d6O0mbDmALuxaTlplnBTnSELcrw==",
"dev": true
},
- "node-sass": {
- "version": "4.14.1",
- "resolved": "https://registry.npmjs.org/node-sass/-/node-sass-4.14.1.tgz",
- "integrity": "sha512-sjCuOlvGyCJS40R8BscF5vhVlQjNN069NtQ1gSxyK1u9iqvn6tf7O1R4GNowVZfiZUCRt5MmMs1xd+4V/7Yr0g==",
- "dev": true,
- "requires": {
- "async-foreach": "^0.1.3",
- "chalk": "^1.1.1",
- "cross-spawn": "^3.0.0",
- "gaze": "^1.0.0",
- "get-stdin": "^4.0.1",
- "glob": "^7.0.3",
- "in-publish": "^2.0.0",
- "lodash": "^4.17.15",
- "meow": "^3.7.0",
- "mkdirp": "^0.5.1",
- "nan": "^2.13.2",
- "node-gyp": "^3.8.0",
- "npmlog": "^4.0.0",
- "request": "^2.88.0",
- "sass-graph": "2.2.5",
- "stdout-stream": "^1.4.0",
- "true-case-path": "^1.0.2"
- },
- "dependencies": {
- "ansi-regex": {
- "version": "2.1.1",
- "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz",
- "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=",
- "dev": true
- },
- "ansi-styles": {
- "version": "2.2.1",
- "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz",
- "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=",
- "dev": true
- },
- "chalk": {
- "version": "1.1.3",
- "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz",
- "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=",
- "dev": true,
- "requires": {
- "ansi-styles": "^2.2.1",
- "escape-string-regexp": "^1.0.2",
- "has-ansi": "^2.0.0",
- "strip-ansi": "^3.0.0",
- "supports-color": "^2.0.0"
- }
- },
- "cross-spawn": {
- "version": "3.0.1",
- "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-3.0.1.tgz",
- "integrity": "sha1-ElYDfsufDF9549bvE14wdwGEuYI=",
- "dev": true,
- "requires": {
- "lru-cache": "^4.0.1",
- "which": "^1.2.9"
- }
- },
- "lru-cache": {
- "version": "4.1.5",
- "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz",
- "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==",
- "dev": true,
- "requires": {
- "pseudomap": "^1.0.2",
- "yallist": "^2.1.2"
- }
- },
- "mkdirp": {
- "version": "0.5.5",
- "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz",
- "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==",
- "dev": true,
- "requires": {
- "minimist": "^1.2.5"
- }
- },
- "node-gyp": {
- "version": "3.8.0",
- "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-3.8.0.tgz",
- "integrity": "sha512-3g8lYefrRRzvGeSowdJKAKyks8oUpLEd/DyPV4eMhVlhJ0aNaZqIrNUIPuEWWTAoPqyFkfGrM67MC69baqn6vA==",
- "dev": true,
- "requires": {
- "fstream": "^1.0.0",
- "glob": "^7.0.3",
- "graceful-fs": "^4.1.2",
- "mkdirp": "^0.5.0",
- "nopt": "2 || 3",
- "npmlog": "0 || 1 || 2 || 3 || 4",
- "osenv": "0",
- "request": "^2.87.0",
- "rimraf": "2",
- "semver": "~5.3.0",
- "tar": "^2.0.0",
- "which": "1"
- }
- },
- "nopt": {
- "version": "3.0.6",
- "resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz",
- "integrity": "sha1-xkZdvwirzU2zWTF/eaxopkayj/k=",
- "dev": true,
- "requires": {
- "abbrev": "1"
- }
- },
- "rimraf": {
- "version": "2.7.1",
- "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz",
- "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==",
- "dev": true,
- "requires": {
- "glob": "^7.1.3"
- }
- },
- "semver": {
- "version": "5.3.0",
- "resolved": "https://registry.npmjs.org/semver/-/semver-5.3.0.tgz",
- "integrity": "sha1-myzl094C0XxgEq0yaqa00M9U+U8=",
- "dev": true
- },
- "strip-ansi": {
- "version": "3.0.1",
- "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
- "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=",
- "dev": true,
- "requires": {
- "ansi-regex": "^2.0.0"
- }
- },
- "supports-color": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz",
- "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=",
- "dev": true
- },
- "tar": {
- "version": "2.2.2",
- "resolved": "https://registry.npmjs.org/tar/-/tar-2.2.2.tgz",
- "integrity": "sha512-FCEhQ/4rE1zYv9rYXJw/msRqsnmlje5jHP6huWeBZ704jUTy02c5AZyWujpMR1ax6mVw9NyJMfuK2CMDWVIfgA==",
- "dev": true,
- "requires": {
- "block-stream": "*",
- "fstream": "^1.0.12",
- "inherits": "2"
- }
- },
- "yallist": {
- "version": "2.1.2",
- "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz",
- "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=",
- "dev": true
- }
- }
- },
"nopt": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz",
@@ -9264,37 +8921,10 @@
"abbrev": "1"
}
},
- "normalize-package-data": {
- "version": "2.5.0",
- "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz",
- "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==",
- "dev": true,
- "requires": {
- "hosted-git-info": "^2.1.4",
- "resolve": "^1.10.0",
- "semver": "2 || 3 || 4 || 5",
- "validate-npm-package-license": "^3.0.1"
- },
- "dependencies": {
- "hosted-git-info": {
- "version": "2.8.8",
- "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.8.tgz",
- "integrity": "sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg==",
- "dev": true
- },
- "semver": {
- "version": "5.7.1",
- "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
- "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
- "dev": true
- }
- }
- },
"normalize-path": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
- "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
- "dev": true
+ "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="
},
"normalize-range": {
"version": "0.1.2",
@@ -9704,28 +9334,12 @@
"integrity": "sha1-hUNzx/XCMVkU/Jv8a9gjj92h7Cc=",
"dev": true
},
- "os-homedir": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz",
- "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=",
- "dev": true
- },
"os-tmpdir": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz",
"integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=",
"dev": true
},
- "osenv": {
- "version": "0.1.5",
- "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz",
- "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==",
- "dev": true,
- "requires": {
- "os-homedir": "^1.0.0",
- "os-tmpdir": "^1.0.0"
- }
- },
"p-finally": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz",
@@ -10048,8 +9662,7 @@
"picomatch": {
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.2.tgz",
- "integrity": "sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==",
- "dev": true
+ "integrity": "sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg=="
},
"pify": {
"version": "4.0.1",
@@ -11843,12 +11456,6 @@
"integrity": "sha1-0/wRS6BplaRexok/SEzrHXj19HY=",
"dev": true
},
- "pseudomap": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz",
- "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=",
- "dev": true
- },
"psl": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz",
@@ -12053,67 +11660,6 @@
"npm-normalize-package-bin": "^1.0.1"
}
},
- "read-pkg": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz",
- "integrity": "sha1-9f+qXs0pyzHAR0vKfXVra7KePyg=",
- "dev": true,
- "requires": {
- "load-json-file": "^1.0.0",
- "normalize-package-data": "^2.3.2",
- "path-type": "^1.0.0"
- },
- "dependencies": {
- "path-type": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz",
- "integrity": "sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE=",
- "dev": true,
- "requires": {
- "graceful-fs": "^4.1.2",
- "pify": "^2.0.0",
- "pinkie-promise": "^2.0.0"
- }
- },
- "pify": {
- "version": "2.3.0",
- "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
- "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=",
- "dev": true
- }
- }
- },
- "read-pkg-up": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-1.0.1.tgz",
- "integrity": "sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI=",
- "dev": true,
- "requires": {
- "find-up": "^1.0.0",
- "read-pkg": "^1.0.0"
- },
- "dependencies": {
- "find-up": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz",
- "integrity": "sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=",
- "dev": true,
- "requires": {
- "path-exists": "^2.0.0",
- "pinkie-promise": "^2.0.0"
- }
- },
- "path-exists": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz",
- "integrity": "sha1-D+tsZPD8UY2adU3V77YscCJ2H0s=",
- "dev": true,
- "requires": {
- "pinkie-promise": "^2.0.0"
- }
- }
- }
- },
"readable-stream": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz",
@@ -12141,32 +11687,10 @@
"version": "3.5.0",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.5.0.tgz",
"integrity": "sha512-cMhu7c/8rdhkHXWsY+osBhfSy0JikwpHK/5+imo+LpeasTF8ouErHrlYkwT0++njiyuDvc7OFY5T3ukvZ8qmFQ==",
- "dev": true,
"requires": {
"picomatch": "^2.2.1"
}
},
- "redent": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/redent/-/redent-1.0.0.tgz",
- "integrity": "sha1-z5Fqsf1fHxbfsggi3W7H9zDCr94=",
- "dev": true,
- "requires": {
- "indent-string": "^2.1.0",
- "strip-indent": "^1.0.1"
- },
- "dependencies": {
- "indent-string": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-2.1.0.tgz",
- "integrity": "sha1-ji1INIdCEhtKghi3oTfppSBJ3IA=",
- "dev": true,
- "requires": {
- "repeating": "^2.0.0"
- }
- }
- }
- },
"reflect-metadata": {
"version": "0.1.13",
"resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.13.tgz",
@@ -12290,15 +11814,6 @@
"integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=",
"dev": true
},
- "repeating": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/repeating/-/repeating-2.0.1.tgz",
- "integrity": "sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo=",
- "dev": true,
- "requires": {
- "is-finite": "^1.0.0"
- }
- },
"request": {
"version": "2.88.2",
"resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz",
@@ -12655,26 +12170,13 @@
"dev": true
},
"sass": {
- "version": "1.32.4",
- "resolved": "https://registry.npmjs.org/sass/-/sass-1.32.4.tgz",
- "integrity": "sha512-N0BT0PI/t3+gD8jKa83zJJUb7ssfQnRRfqN+GIErokW6U4guBpfYl8qYB+OFLEho+QvnV5ZH1R9qhUC/Z2Ch9w==",
- "dev": true,
+ "version": "1.32.8",
+ "resolved": "https://rt.adsw.io/artifactory/api/npm/arenadata-npm/sass/-/sass-1.32.8.tgz",
+ "integrity": "sha1-8WqavY3FMK3Yg05QaHiigIwDe9w=",
"requires": {
"chokidar": ">=2.0.0 <4.0.0"
}
},
- "sass-graph": {
- "version": "2.2.5",
- "resolved": "https://registry.npmjs.org/sass-graph/-/sass-graph-2.2.5.tgz",
- "integrity": "sha512-VFWDAHOe6mRuT4mZRd4eKE+d8Uedrk6Xnh7Sh9b4NGufQLQjOrvf/MQoOdx+0s92L89FeyUUNfU597j/3uNpag==",
- "dev": true,
- "requires": {
- "glob": "^7.0.0",
- "lodash": "^4.0.0",
- "scss-tokenizer": "^0.2.3",
- "yargs": "^13.3.2"
- }
- },
"sass-loader": {
"version": "10.1.1",
"resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-10.1.1.tgz",
@@ -12757,27 +12259,6 @@
"ajv-keywords": "^3.5.2"
}
},
- "scss-tokenizer": {
- "version": "0.2.3",
- "resolved": "https://registry.npmjs.org/scss-tokenizer/-/scss-tokenizer-0.2.3.tgz",
- "integrity": "sha1-jrBtualyMzOCTT9VMGQRSYR85dE=",
- "dev": true,
- "requires": {
- "js-base64": "^2.1.8",
- "source-map": "^0.4.2"
- },
- "dependencies": {
- "source-map": {
- "version": "0.4.4",
- "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.4.4.tgz",
- "integrity": "sha1-66T12pwNyZneaAMti092FzZSA2s=",
- "dev": true,
- "requires": {
- "amdefine": ">=0.0.4"
- }
- }
- }
- },
"select-hose": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz",
@@ -13573,38 +13054,6 @@
"integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==",
"dev": true
},
- "spdx-correct": {
- "version": "3.1.1",
- "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.1.tgz",
- "integrity": "sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w==",
- "dev": true,
- "requires": {
- "spdx-expression-parse": "^3.0.0",
- "spdx-license-ids": "^3.0.0"
- }
- },
- "spdx-exceptions": {
- "version": "2.3.0",
- "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz",
- "integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==",
- "dev": true
- },
- "spdx-expression-parse": {
- "version": "3.0.1",
- "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz",
- "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==",
- "dev": true,
- "requires": {
- "spdx-exceptions": "^2.1.0",
- "spdx-license-ids": "^3.0.0"
- }
- },
- "spdx-license-ids": {
- "version": "3.0.7",
- "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.7.tgz",
- "integrity": "sha512-U+MTEOO0AiDzxwFvoa4JVnMV6mZlJKk2sBLt90s7G0Gd0Mlknc7kxEn3nuDPNZRta7O2uy8oLcZLVT+4sqNZHQ==",
- "dev": true
- },
"spdy": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/spdy/-/spdy-4.0.2.tgz",
@@ -13715,41 +13164,6 @@
"integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=",
"dev": true
},
- "stdout-stream": {
- "version": "1.4.1",
- "resolved": "https://registry.npmjs.org/stdout-stream/-/stdout-stream-1.4.1.tgz",
- "integrity": "sha512-j4emi03KXqJWcIeF8eIXkjMFN1Cmb8gUlDYGeBALLPo5qdyTfA9bOtl8m33lRoC+vFMkP3gl0WsDr6+gzxbbTA==",
- "dev": true,
- "requires": {
- "readable-stream": "^2.0.1"
- },
- "dependencies": {
- "readable-stream": {
- "version": "2.3.7",
- "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz",
- "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==",
- "dev": true,
- "requires": {
- "core-util-is": "~1.0.0",
- "inherits": "~2.0.3",
- "isarray": "~1.0.0",
- "process-nextick-args": "~2.0.0",
- "safe-buffer": "~5.1.1",
- "string_decoder": "~1.1.1",
- "util-deprecate": "~1.0.1"
- }
- },
- "string_decoder": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
- "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
- "dev": true,
- "requires": {
- "safe-buffer": "~5.1.0"
- }
- }
- }
- },
"stream-browserify": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-2.0.2.tgz",
@@ -13928,30 +13342,12 @@
"ansi-regex": "^5.0.0"
}
},
- "strip-bom": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz",
- "integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=",
- "dev": true,
- "requires": {
- "is-utf8": "^0.2.0"
- }
- },
"strip-eof": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz",
"integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=",
"dev": true
},
- "strip-indent": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-1.0.1.tgz",
- "integrity": "sha1-DHlipq3vp7vUrDZkYKY4VSrhoKI=",
- "dev": true,
- "requires": {
- "get-stdin": "^4.0.1"
- }
- },
"strip-json-comments": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
@@ -14393,7 +13789,6 @@
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
- "dev": true,
"requires": {
"is-number": "^7.0.0"
}
@@ -14426,21 +13821,6 @@
"integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==",
"dev": true
},
- "trim-newlines": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-1.0.0.tgz",
- "integrity": "sha1-WIeWa7WCpFA6QetST301ARgVphM=",
- "dev": true
- },
- "true-case-path": {
- "version": "1.0.3",
- "resolved": "https://registry.npmjs.org/true-case-path/-/true-case-path-1.0.3.tgz",
- "integrity": "sha512-m6s2OdQe5wgpFMC+pAJ+q9djG82O2jcHPOI6RNg1yy9rCYR+WD6Nbpl32fDpfC56nirdRy+opFa/Vk7HYhqaew==",
- "dev": true,
- "requires": {
- "glob": "^7.1.2"
- }
- },
"ts-node": {
"version": "9.0.0",
"resolved": "https://registry.npmjs.org/ts-node/-/ts-node-9.0.0.tgz",
@@ -14864,16 +14244,6 @@
"integrity": "sha512-gTpR5XQNKFwOd4clxfnhaqvfqMpqEwr4tOtCyz4MtYZX2JYhfr1JvBFKdS+7K/9rfpZR3VLX+YWBbKoxCgS43Q==",
"dev": true
},
- "validate-npm-package-license": {
- "version": "3.0.4",
- "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz",
- "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==",
- "dev": true,
- "requires": {
- "spdx-correct": "^3.0.0",
- "spdx-expression-parse": "^3.0.0"
- }
- },
"validate-npm-package-name": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-3.0.0.tgz",
diff --git a/web/package.json b/web/package.json
index 2693ebf9e1..3624a08230 100644
--- a/web/package.json
+++ b/web/package.json
@@ -13,6 +13,7 @@
},
"private": true,
"dependencies": {
+ "@adwp-ui/widgets": "0.0.37",
"@angular/animations": "^11.1.1",
"@angular/cdk": "^11.1.1",
"@angular/common": "^11.1.1",
@@ -28,15 +29,17 @@
"@ngrx/router-store": "^10.1.2",
"@ngrx/store": "^10.1.2",
"@ngrx/store-devtools": "^10.1.2",
+ "luxon": "^1.25.0",
"rxjs": "^6.5.5",
+ "sass": "^1.32.8",
"tslib": "^2.0.0",
"zone.js": "~0.10.3"
},
"devDependencies": {
"@angular-devkit/build-angular": "~0.1101.2",
"@angular/cli": "^11.1.2",
- "@angular/language-service": "11.1.1",
"@angular/compiler-cli": "11.1.1",
+ "@angular/language-service": "11.1.1",
"@ngrx/schematics": "^10.0.0",
"@types/jasmine": "~3.6.0",
"@types/jasminewd2": "~2.0.8",
@@ -52,7 +55,6 @@
"karma-jasmine": "~4.0.0",
"karma-jasmine-html-reporter": "^1.5.0",
"protractor": "~7.0.0",
- "node-sass": "^4.14.1",
"ts-node": "~9.0.0",
"tslint": "~6.1.0",
"typescript": "~4.0.2",
diff --git a/web/src/adcm-theme.scss b/web/src/adcm-theme.scss
index e4c46a2f40..ca2689895b 100644
--- a/web/src/adcm-theme.scss
+++ b/web/src/adcm-theme.scss
@@ -91,7 +91,7 @@ a.active .mat-list-item-content {
color: mat-color($mat-green, A400);
}
-.mat-nav-list a:not(.top) {
+.mat-nav-list a:not(.top, .issue) {
color: mat-color($mat-blue-grey, 200) !important;
&:hover {
@@ -104,3 +104,31 @@ a.active .mat-list-item-content {
width: auto;
height: 40px;
}
+
+// BEGIN: Paginator
+
+@mixin paginator {
+ font-size: 14px;
+ margin: 0 6px;
+ color: #bbb;
+}
+
+.page-button {
+ @include paginator;
+ cursor: pointer;
+}
+
+.page-button:hover {
+ text-decoration: none;
+ color: #ccc;
+}
+
+.page-button.current {
+ color: mat-color($mat-orange, 500);
+}
+
+.page-additional {
+ @include paginator;
+}
+
+// END: Paginator
diff --git a/web/src/adcm2.scss b/web/src/adcm2.scss
new file mode 100644
index 0000000000..e9bd7726ba
--- /dev/null
+++ b/web/src/adcm2.scss
@@ -0,0 +1,34 @@
+mat-header-cell.list-control, mat-cell.list-control {
+ flex-grow: 0;
+ flex-basis: 60px;
+ text-align: center;
+}
+
+.width100 {
+ flex-grow: 0;
+ flex-basis: 100px;
+}
+
+.width30pr {
+ flex-grow: 1;
+ flex-basis: 30%;
+ width: 100px
+}
+
+/* BEGIN: Material header arrow crutch */
+.mat-header-cell .mat-sort-header-container.mat-sort-header-sorted .mat-sort-header-arrow {
+ opacity: 1 !important;
+}
+/* END: Material header arrow crutch */
+
+mat-cell {
+ padding: 0 2px;
+}
+
+mat-header-cell {
+ padding: 0 2px;
+}
+
+.expandedRow {
+ min-height: 0;
+}
diff --git a/web/src/app/abstract-directives/adwp-base-list.directive.ts b/web/src/app/abstract-directives/adwp-base-list.directive.ts
new file mode 100644
index 0000000000..4a12ab7565
--- /dev/null
+++ b/web/src/app/abstract-directives/adwp-base-list.directive.ts
@@ -0,0 +1,73 @@
+import { BehaviorSubject } from 'rxjs';
+import { Paging } from '@adwp-ui/widgets';
+import { Sort } from '@angular/material/sort';
+import { ParamMap } from '@angular/router';
+
+import { BaseListDirective } from '@app/shared/components/list/base-list.directive';
+import { Host as AdcmHost, TypeName } from '@app/core/types';
+import { AdwpListDirective } from '@app/abstract-directives/adwp-list.directive';
+import { IHost } from '@app/models/host';
+import { ICluster } from '@app/models/cluster';
+import { IListResult } from '@adwp-ui/widgets';
+import { ListDirective } from '@app/abstract-directives/list.directive';
+import { ListService } from '@app/shared/components/list/list.service';
+import { Store } from '@ngrx/store';
+import { SocketState } from '@app/core/store';
+import { ApiService } from '@app/core/api';
+
+export class AdwpBaseListDirective extends BaseListDirective {
+
+ paging: BehaviorSubject;
+ sorting: BehaviorSubject = new BehaviorSubject(null);
+
+ constructor(
+ protected parent: ListDirective,
+ protected service: ListService,
+ protected store: Store,
+ private api: ApiService,
+ ) {
+ super(parent, service, store);
+ }
+
+ checkType(typeName: string, referenceTypeName: TypeName): boolean {
+ if (referenceTypeName === 'servicecomponent') {
+ return typeName === 'component';
+ }
+
+ return (referenceTypeName ? referenceTypeName.split('2')[0] : referenceTypeName) === typeName;
+ }
+
+ routeListener(limit: number, page: number, ordering: string, params: ParamMap) {
+ this.paging.next({ pageIndex: page + 1, pageSize: limit });
+ if (ordering) {
+ const direction = ordering[0] === '-' ? 'desc' : 'asc';
+ const active = ordering[0] === '-' ? ordering.substr(1) : ordering;
+ this.sorting.next({ direction, active });
+ }
+
+ this.listParams = params;
+ this.refresh();
+ }
+
+ addCluster(id: number) {
+ if (id) {
+ this.service.addClusterToHost(id, this.row as AdcmHost)
+ .subscribe((host) => {
+ if ((this.parent as AdwpListDirective)?.data$?.value?.results) {
+ this.api.getOne('cluster', host.cluster_id).subscribe((cluster: ICluster) => {
+ const tableData = Object.assign({}, (this.parent as AdwpListDirective).data$.value);
+ const index = tableData.results.findIndex(item => item.id === host.id);
+ const row = Object.assign({}, tableData.results[index]);
+
+ row.cluster_id = cluster.id;
+ row.cluster_name = cluster.name;
+
+ tableData.results.splice(index, 1, row);
+ (this.parent as AdwpListDirective).reload(tableData as IListResult);
+ });
+ }
+ });
+ }
+ }
+
+}
diff --git a/web/src/app/abstract-directives/adwp-list.directive.ts b/web/src/app/abstract-directives/adwp-list.directive.ts
new file mode 100644
index 0000000000..22b0630d92
--- /dev/null
+++ b/web/src/app/abstract-directives/adwp-list.directive.ts
@@ -0,0 +1,76 @@
+import { Directive, OnInit } from '@angular/core';
+import { BehaviorSubject } from 'rxjs';
+import { IColumns, IListResult, Paging, RowEventData } from '@adwp-ui/widgets';
+import { Sort } from '@angular/material/sort';
+import { PageEvent } from '@angular/material/paginator';
+
+import { ListDirective } from './list.directive';
+import { AdwpBaseListDirective } from './adwp-base-list.directive';
+import { Entities } from '@app/core/types';
+
+@Directive({
+ selector: '[appAdwpList]',
+})
+export abstract class AdwpListDirective extends ListDirective implements OnInit {
+
+ abstract listColumns: IColumns;
+
+ data$: BehaviorSubject> = new BehaviorSubject(null);
+
+ paging: BehaviorSubject = new BehaviorSubject(null);
+ sorting: BehaviorSubject = new BehaviorSubject(null);
+
+ defaultSort: Sort = { active: 'id', direction: 'desc' };
+
+ reload(data: IListResult) {
+ this.data$.next(data as any);
+ }
+
+ ngOnInit() {
+ this.baseListDirective = new AdwpBaseListDirective(this, this.service, this.store, this.api);
+ this.baseListDirective.typeName = this.type;
+ this.baseListDirective.reload = this.reload.bind(this);
+ (this.baseListDirective as AdwpBaseListDirective).paging = this.paging;
+ (this.baseListDirective as AdwpBaseListDirective).sorting = this.sorting;
+ this.baseListDirective.init();
+ }
+
+ clickRow(data: RowEventData) {
+ this.clickCell(data.event, 'title', data.row);
+ }
+
+ auxclickRow(data: RowEventData) {
+ this.clickCell(data.event, 'new-tab', data.row);
+ }
+
+ changeCount(count: number) {}
+
+ getPageIndex(): number {
+ return this.paging.value.pageIndex - 1;
+ }
+
+ getPageSize(): number {
+ return this.paging.value.pageSize;
+ }
+
+ onChangePaging(paging: Paging): void {
+ this.paging.next(paging);
+
+ const pageEvent = new PageEvent();
+ pageEvent.pageIndex = this.getPageIndex();
+ pageEvent.length = this.data$.value.count;
+ pageEvent.pageSize = this.getPageSize();
+
+ this.pageHandler(pageEvent);
+ }
+
+ onChangeSort(sort: Sort): void {
+ this.sorting.next(sort);
+ this.changeSorting(sort);
+ }
+
+ getSort(): Sort {
+ return this.sorting.value;
+ }
+
+}
diff --git a/web/src/app/abstract-directives/list.directive.ts b/web/src/app/abstract-directives/list.directive.ts
new file mode 100644
index 0000000000..b817882b58
--- /dev/null
+++ b/web/src/app/abstract-directives/list.directive.ts
@@ -0,0 +1,183 @@
+import { Directive, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core';
+import { filter } from 'rxjs/operators';
+import { Store } from '@ngrx/store';
+import { ActivatedRoute, Router } from '@angular/router';
+import { MatPaginator, PageEvent } from '@angular/material/paginator';
+import { MatSort, Sort } from '@angular/material/sort';
+import { MatTableDataSource } from '@angular/material/table';
+import { MatDialog } from '@angular/material/dialog';
+import { BaseDirective, EventHelper } from '@adwp-ui/widgets';
+
+import { EmmitRow, TypeName } from '@app/core/types';
+import { BaseListDirective } from '@app/shared/components/list/base-list.directive';
+import { SocketState } from '@app/core/store';
+import { ListService } from '@app/shared/components/list/list.service';
+import { DialogComponent } from '@app/shared/components';
+import { StatusData } from '@app/components/columns/status-column/status-column.component';
+import { ICluster } from '@app/models/cluster';
+import { ApiService } from '@app/core/api';
+
+enum Direction {
+ '' = '',
+ 'asc' = '',
+ 'desc' = '-',
+}
+
+@Directive({
+ selector: '[appAbstractList]',
+})
+export abstract class ListDirective extends BaseDirective implements OnInit, OnDestroy {
+
+ @Input() type: TypeName;
+
+ baseListDirective: BaseListDirective;
+
+ current: any = {};
+
+ @Input()
+ columns: Array;
+
+ @Output()
+ listItemEvt = new EventEmitter();
+
+ paginator: MatPaginator;
+
+ sort: MatSort;
+
+ data: MatTableDataSource = new MatTableDataSource([]);
+
+ @Output() pageEvent = new EventEmitter();
+
+ addToSorting = false;
+
+ @Input()
+ set dataSource(data: { results: any; count: number }) {
+ if (data) {
+ const list = data.results;
+ this.data = new MatTableDataSource(list);
+ this.changeCount(data.count);
+ this.listItemEvt.emit({ cmd: 'onLoad', row: list[0] });
+ }
+ }
+
+ sortParam = '';
+
+ constructor(
+ protected service: ListService,
+ protected store: Store,
+ public route: ActivatedRoute,
+ public router: Router,
+ public dialog: MatDialog,
+ protected api: ApiService,
+ ) {
+ super();
+ }
+
+ changeCount(count: number) {
+ this.paginator.length = count;
+ }
+
+ clickCell($e: MouseEvent, cmd?: string, row?: any, item?: any) {
+ EventHelper.stopPropagation($e);
+ this.current = row;
+ this.listItemEvt.emit({ cmd, row, item });
+ }
+
+ ngOnInit() {
+ this.baseListDirective = new BaseListDirective(this, this.service, this.store);
+ this.baseListDirective.typeName = this.type;
+ this.baseListDirective.init();
+ }
+
+ ngOnDestroy() {
+ this.baseListDirective.destroy();
+ }
+
+ delete($event: MouseEvent, row: any) {
+ EventHelper.stopPropagation($event);
+ this.dialog
+ .open(DialogComponent, {
+ data: {
+ title: `Deleting "${row.name || row.fqdn}"`,
+ text: 'Are you sure?',
+ controls: ['Yes', 'No'],
+ },
+ })
+ .beforeClosed()
+ .pipe(filter((yes) => yes))
+ .subscribe(() => this.listItemEvt.emit({ cmd: 'delete', row }));
+ }
+
+ getPageIndex(): number {
+ return this.paginator.pageIndex;
+ }
+
+ getPageSize(): number {
+ return this.paginator.pageSize;
+ }
+
+ changeSorting(sort: Sort) {
+ const _filter = this.route.snapshot.paramMap.get('filter') || '';
+ const pageIndex = this.getPageIndex();
+ const pageSize = this.getPageSize();
+ const ordering = this.getSortParam(sort);
+
+ this.router.navigate(
+ [
+ './',
+ {
+ page: pageIndex,
+ limit: pageSize,
+ filter: _filter,
+ ordering,
+ },
+ ],
+ { relativeTo: this.route }
+ );
+
+ this.sortParam = ordering;
+ }
+
+ getSortParam(a: Sort) {
+ const penis: { [key: string]: string[] } = {
+ prototype_version: ['prototype_display_name', 'prototype_version'],
+ };
+
+ if (a) {
+ const dumb = penis[a.active] ? penis[a.active] : [a.active],
+ active = dumb.map((b: string) => `${Direction[a.direction]}${b}`).join(',');
+
+ const current = this.sortParam;
+ if (current && this.addToSorting) {
+ const result = current
+ .split(',')
+ .filter((b) => dumb.every((d) => d !== b.replace('-', '')))
+ .join(',');
+ return [result, a.direction ? active : ''].filter((e) => e).join(',');
+ }
+
+ return a.direction ? active : '';
+ } else {
+ return '';
+ }
+ }
+
+ getSort(): Sort {
+ return this.sort;
+ }
+
+ pageHandler(pageEvent: PageEvent) {
+ 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 }], {
+ relativeTo: this.route,
+ });
+ }
+
+ gotoStatus(data: StatusData) {
+ this.clickCell(data.event, data.action, data.row);
+ }
+
+}
diff --git a/web/src/app/abstract-directives/popover-content.directive.ts b/web/src/app/abstract-directives/popover-content.directive.ts
new file mode 100644
index 0000000000..f52c430603
--- /dev/null
+++ b/web/src/app/abstract-directives/popover-content.directive.ts
@@ -0,0 +1,13 @@
+import { Directive } from '@angular/core';
+import { BaseDirective } from '@adwp-ui/widgets';
+
+import { PopoverInput } from '../directives/popover.directive';
+
+@Directive({
+ selector: '[appAbstractPopoverContent]',
+})
+export abstract class PopoverContentDirective extends BaseDirective {
+
+ abstract data: PopoverInput;
+
+}
diff --git a/web/src/app/abstract/entity-service.ts b/web/src/app/abstract/entity-service.ts
new file mode 100644
index 0000000000..1017ceceb7
--- /dev/null
+++ b/web/src/app/abstract/entity-service.ts
@@ -0,0 +1,14 @@
+import { Observable } from 'rxjs';
+
+import { ApiService } from '@app/core/api';
+
+export abstract class EntityService {
+
+ constructor(
+ protected api: ApiService,
+ ) {
+ }
+
+ abstract get(id: number, params: { [key: string]: string }): Observable;
+
+}
diff --git a/web/src/app/admin/admin.module.ts b/web/src/app/admin/admin.module.ts
index 23fc8570e1..c60b0c1da7 100644
--- a/web/src/app/admin/admin.module.ts
+++ b/web/src/app/admin/admin.module.ts
@@ -13,7 +13,7 @@ import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { AuthGuard } from '@app/core';
-import { SharedModule } from '@app/shared';
+import { SharedModule } from '@app/shared/shared.module';
import { IntroComponent } from './intro.component';
import { PatternComponent } from './pattern.component';
diff --git a/web/src/app/admin/intro.component.ts b/web/src/app/admin/intro.component.ts
index 69f7a6bf45..a208080444 100644
--- a/web/src/app/admin/intro.component.ts
+++ b/web/src/app/admin/intro.component.ts
@@ -20,9 +20,6 @@ import { Component } from '@angular/core';
- -
- We will send anonymous statistic about number of bundles your use and number of hosts and clusters, but without any config or names.
-
-
We have to know ADCM's Url [ {{ adcm_url }} ] to send information from host. We try to gues that information from url you enter in
diff --git a/web/src/app/admin/pattern.component.ts b/web/src/app/admin/pattern.component.ts
index 0e2f441f06..ff88da5927 100644
--- a/web/src/app/admin/pattern.component.ts
+++ b/web/src/app/admin/pattern.component.ts
@@ -13,7 +13,7 @@ import { Component, OnDestroy, OnInit } from '@angular/core';
import { NavigationEnd, Router } from '@angular/router';
import { ApiService } from '@app/core/api';
import { getProfileSelector, settingsSave, State } from '@app/core/store';
-import { BaseDirective } from '@app/shared';
+import { BaseDirective } from '@app/shared/directives';
import { IConfig } from '@app/shared/configuration/types';
import { select, Store } from '@ngrx/store';
import { exhaustMap, filter } from 'rxjs/operators';
@@ -85,7 +85,6 @@ export class PatternComponent extends BaseDirective implements OnInit, OnDestroy
const config = c.config;
const global = config['global'] || {};
global.adcm_url = global.adcm_url || `${location.protocol}//${location.host}`;
- global.send_stats = true;
return this.api.post('/api/v1/adcm/1/config/history/', c);
})
)
diff --git a/web/src/app/admin/settings.component.ts b/web/src/app/admin/settings.component.ts
index 74c42edd76..14fc585fde 100644
--- a/web/src/app/admin/settings.component.ts
+++ b/web/src/app/admin/settings.component.ts
@@ -11,13 +11,13 @@
// limitations under the License.
import { Component, OnInit } from '@angular/core';
import { ApiService } from '@app/core/api';
-import { sendMetrics, State } from '@app/core/store';
+import { settingsSave, State } from '@app/core/store';
import { ApiBase } from '@app/core/types/api';
-import { DynamicEvent } from '@app/shared';
+import { DynamicEvent } from '@app/shared/directives';
import { Store } from '@ngrx/store';
import { Observable } from 'rxjs';
import { map, switchMap } from 'rxjs/operators';
-import { FormGroup } from '@angular/forms';
+
@Component({
selector: 'app-settings',
@@ -37,6 +37,6 @@ export class SettingsComponent implements OnInit {
}
onEvent(e: DynamicEvent) {
- if (e.name === 'send') this.store.dispatch(sendMetrics({ metrics: (e.data.form.controls['global'] as FormGroup).controls['send_stats'].value }));
+ if (e.name === 'send') this.store.dispatch(settingsSave({ isSet: true }));
}
}
diff --git a/web/src/app/admin/users/users.component.ts b/web/src/app/admin/users/users.component.ts
index 7c794a2a3d..5465ed28b4 100644
--- a/web/src/app/admin/users/users.component.ts
+++ b/web/src/app/admin/users/users.component.ts
@@ -11,12 +11,12 @@
// limitations under the License.
import { Component, OnInit } from '@angular/core';
import { FormControl, FormGroup, NgModel, Validators } from '@angular/forms';
-import { MatDialog } from '@angular/material/dialog';
-import { Router } from '@angular/router';
-import { AuthService } from '@app/core';
-import { DialogComponent } from '@app/shared';
import { map } from 'rxjs/operators';
+import { Router } from '@angular/router';
+import { MatDialog } from '@angular/material/dialog';
+import { AuthService } from '@app/core';
+import { DialogComponent } from '@app/shared/components';
import { User, UsersService } from './users.service';
@Component({
diff --git a/web/src/app/app.module.ts b/web/src/app/app.module.ts
index 362eda272c..7a0282b6e8 100644
--- a/web/src/app/app.module.ts
+++ b/web/src/app/app.module.ts
@@ -25,11 +25,15 @@ import { EntryModule } from './entry/entry.module';
import { MainModule } from './main/main.module';
import { SharedModule } from './shared/shared.module';
import { LogComponent } from './ws-logs/log.component';
+import { AdwpUiWidgetsModule } from '@adwp-ui/widgets';
//registerLocaleData(localeRu, 'ru');
@NgModule({
- declarations: [AppComponent, LogComponent],
+ declarations: [
+ AppComponent,
+ LogComponent,
+ ],
imports: [
BrowserModule,
BrowserAnimationsModule,
@@ -42,6 +46,8 @@ import { LogComponent } from './ws-logs/log.component';
EffectsModule.forRoot(StoreEffects),
// StoreRouterConnectingModule.forRoot(),
!environment.production ? StoreDevtoolsModule.instrument() : [],
+
+ AdwpUiWidgetsModule,
],
bootstrap: [AppComponent],
providers: [
diff --git a/web/src/app/components/actions-button/actions-button.component.html b/web/src/app/components/actions-button/actions-button.component.html
new file mode 100644
index 0000000000..d30df3fe63
--- /dev/null
+++ b/web/src/app/components/actions-button/actions-button.component.html
@@ -0,0 +1,19 @@
+
+
+
+
+
diff --git a/web/src/app/components/actions-button/actions-button.component.scss b/web/src/app/components/actions-button/actions-button.component.scss
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/web/src/app/components/actions-button/actions-button.component.spec.ts b/web/src/app/components/actions-button/actions-button.component.spec.ts
new file mode 100644
index 0000000000..918518aa3f
--- /dev/null
+++ b/web/src/app/components/actions-button/actions-button.component.spec.ts
@@ -0,0 +1,25 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { ActionsButtonComponent } from './actions-button.component';
+
+describe('ActionsButtonComponent', () => {
+ let component: ActionsButtonComponent;
+ let fixture: ComponentFixture>;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ declarations: [ ActionsButtonComponent ]
+ })
+ .compileComponents();
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(ActionsButtonComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/web/src/app/components/actions-button/actions-button.component.ts b/web/src/app/components/actions-button/actions-button.component.ts
new file mode 100644
index 0000000000..911d0cf7d5
--- /dev/null
+++ b/web/src/app/components/actions-button/actions-button.component.ts
@@ -0,0 +1,31 @@
+import { Component, Input } from '@angular/core';
+import { EventHelper } from '@adwp-ui/widgets';
+
+import { isIssue, Issue } from '@app/core/types';
+import { IssuesComponent } from '@app/components/issues/issues.component';
+import { IssueType } from '@app/models/issue';
+
+@Component({
+ selector: 'app-actions-button',
+ templateUrl: './actions-button.component.html',
+ styleUrls: ['./actions-button.component.scss']
+})
+export class ActionsButtonComponent {
+
+ IssuesComponent = IssuesComponent;
+ EventHelper = EventHelper;
+
+ @Input() row: T;
+ @Input() issueType: IssueType;
+
+ notIssue(issue: Issue): boolean {
+ return !isIssue(issue);
+ }
+
+ getClusterData(row: any) {
+ const { id, hostcomponent } = row.cluster || row;
+ const { action } = row;
+ return { id, hostcomponent, action };
+ }
+
+}
diff --git a/web/src/app/components/cluster/hcmap/hcmap.component.ts b/web/src/app/components/cluster/hcmap/hcmap.component.ts
new file mode 100644
index 0000000000..8720c8a385
--- /dev/null
+++ b/web/src/app/components/cluster/hcmap/hcmap.component.ts
@@ -0,0 +1,17 @@
+import { Component, OnInit } from '@angular/core';
+
+import { ClusterService } from '@app/core/services/cluster.service';
+
+@Component({
+ template: ` `,
+ styles: [':host { flex: 1; }'],
+})
+export class HcmapComponent implements OnInit {
+ cluster: { id: number; hostcomponent: string };
+ constructor(private service: ClusterService) {}
+
+ ngOnInit() {
+ const { id, hostcomponent } = { ...this.service.Cluster };
+ this.cluster = { id, hostcomponent };
+ }
+}
diff --git a/web/src/app/components/cluster/host/host.component.ts b/web/src/app/components/cluster/host/host.component.ts
new file mode 100644
index 0000000000..c2d826bede
--- /dev/null
+++ b/web/src/app/components/cluster/host/host.component.ts
@@ -0,0 +1,52 @@
+import { Component } from '@angular/core';
+import { IColumns } from '@adwp-ui/widgets';
+
+import { AdwpListDirective } from '@app/abstract-directives/adwp-list.directive';
+import { TypeName } from '@app/core/types';
+import { IHost } from '@app/models/host';
+import { ListFactory } from '@app/factories/list-factory';
+
+@Component({
+ selector: 'app-cluster-host',
+ template: `
+ Add hosts
+
+
+ `,
+ styles: [':host { flex: 1; }', '.add-button {position:fixed; right: 20px;top:120px;}'],
+})
+export class HostComponent extends AdwpListDirective {
+
+ type: TypeName = 'host2cluster';
+
+ listColumns = [
+ ListFactory.fqdnColumn(),
+ ListFactory.providerColumn(),
+ ListFactory.stateColumn(),
+ ListFactory.statusColumn(this),
+ ListFactory.actionsColumn(),
+ ListFactory.configColumn(this),
+ {
+ type: 'buttons',
+ className: 'list-control',
+ headerClassName: 'list-control',
+ buttons: [{
+ icon: 'link_off',
+ tooltip: 'Remove from cluster',
+ callback: (row, event) => this.delete(event, row),
+ }],
+ }
+ ] as IColumns;
+
+}
diff --git a/web/src/app/components/cluster/services/services.component.ts b/web/src/app/components/cluster/services/services.component.ts
new file mode 100644
index 0000000000..7b49b69f17
--- /dev/null
+++ b/web/src/app/components/cluster/services/services.component.ts
@@ -0,0 +1,46 @@
+import { Component } from '@angular/core';
+import { IColumns } from '@adwp-ui/widgets';
+
+import { TypeName } from '@app/core/types';
+import { AdwpListDirective } from '@app/abstract-directives/adwp-list.directive';
+import { ListFactory } from '@app/factories/list-factory';
+import { IClusterService } from '@app/models/cluster-service';
+
+@Component({
+ selector: 'app-services',
+ template: `
+ Add services
+
+
+ `,
+ styles: [':host { flex: 1; }', '.add-button {position:fixed; right: 20px;top:120px;}'],
+})
+export class ServicesComponent extends AdwpListDirective {
+
+ type: TypeName = 'service2cluster';
+
+ listColumns = [
+ ListFactory.nameColumn('display_name'),
+ {
+ label: 'Version',
+ value: (row) => row.version,
+ },
+ ListFactory.stateColumn(),
+ ListFactory.statusColumn(this),
+ ListFactory.actionsButton('service'),
+ ListFactory.importColumn(this),
+ ListFactory.configColumn(this),
+ ] as IColumns;
+
+}
diff --git a/web/src/app/components/columns/actions-column/actions-column.component.html b/web/src/app/components/columns/actions-column/actions-column.component.html
new file mode 100644
index 0000000000..7ed341016e
--- /dev/null
+++ b/web/src/app/components/columns/actions-column/actions-column.component.html
@@ -0,0 +1,12 @@
+
+
+
+
diff --git a/web/src/app/components/columns/actions-column/actions-column.component.scss b/web/src/app/components/columns/actions-column/actions-column.component.scss
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/web/src/app/components/columns/actions-column/actions-column.component.spec.ts b/web/src/app/components/columns/actions-column/actions-column.component.spec.ts
new file mode 100644
index 0000000000..9517ec2d40
--- /dev/null
+++ b/web/src/app/components/columns/actions-column/actions-column.component.spec.ts
@@ -0,0 +1,31 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { MatIconModule } from '@angular/material/icon';
+
+import { ActionsColumnComponent } from './actions-column.component';
+
+describe('ActionsColumnComponent', () => {
+ let component: ActionsColumnComponent;
+ let fixture: ComponentFixture>;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ declarations: [
+ ActionsColumnComponent,
+ ],
+ imports: [
+ MatIconModule,
+ ]
+ })
+ .compileComponents();
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(ActionsColumnComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/web/src/app/components/columns/actions-column/actions-column.component.ts b/web/src/app/components/columns/actions-column/actions-column.component.ts
new file mode 100644
index 0000000000..54c03375fa
--- /dev/null
+++ b/web/src/app/components/columns/actions-column/actions-column.component.ts
@@ -0,0 +1,27 @@
+import { Component, Input } from '@angular/core';
+import { EventHelper } from '@adwp-ui/widgets';
+
+import { isIssue, Issue } from '@app/core/types';
+
+@Component({
+ selector: 'app-actions-column',
+ templateUrl: './actions-column.component.html',
+ styleUrls: ['./actions-column.component.scss']
+})
+export class ActionsColumnComponent {
+
+ EventHelper = EventHelper;
+
+ @Input() row: T;
+
+ notIssue(issue: Issue): boolean {
+ return !isIssue(issue);
+ }
+
+ getClusterData(row: any) {
+ const { id, hostcomponent } = row.cluster || row;
+ const { action } = row;
+ return { id, hostcomponent, action };
+ }
+
+}
diff --git a/web/src/app/components/columns/cluster-column/cluster-column.component.ts b/web/src/app/components/columns/cluster-column/cluster-column.component.ts
new file mode 100644
index 0000000000..60c4896ef1
--- /dev/null
+++ b/web/src/app/components/columns/cluster-column/cluster-column.component.ts
@@ -0,0 +1,69 @@
+import { Component, Input, Output, EventEmitter } from '@angular/core';
+import { AdwpCellComponent, ILinkColumn, EventHelper } from '@adwp-ui/widgets';
+
+import { IHost } from '@app/models/host';
+import { UniversalAdcmEventData } from '@app/models/universal-adcm-event-data';
+import { ICluster } from '@app/models/cluster';
+
+export interface AddClusterEventData extends UniversalAdcmEventData {
+ cluster: ICluster;
+}
+
+@Component({
+ selector: 'app-cluster-column',
+ template: `
+
+
+
+
+
+ ...
+
+ {{ item.title }}
+
+
+
+
+
+ `,
+ styles: [`
+ :host {
+ width: 100%;
+ }
+ `],
+})
+export class ClusterColumnComponent implements AdwpCellComponent {
+
+ EventHelper = EventHelper;
+
+ @Input() row: IHost;
+
+ @Output() onGetNextPageCluster = new EventEmitter>();
+ @Output() onGetClusters = new EventEmitter>();
+ @Output() onAddCluster = new EventEmitter();
+
+ linkColumn: ILinkColumn = {
+ label: '',
+ type: 'link',
+ value: (row) => row.cluster_name,
+ url: (row) => `/cluster/${row.cluster_id}`,
+ };
+
+ getNextPageCluster(event: MouseEvent) {
+ this.onGetNextPageCluster.emit({ event, action: 'getNextPageCluster', row: this.row });
+ }
+
+ getClusters(event: MouseEvent) {
+ this.onGetClusters.emit({ event, action: 'getClusters', row: this.row });
+ }
+
+ addCluster(event: MouseEvent, cluster: ICluster) {
+ this.onAddCluster.emit({ event, action: 'addCluster', row: this.row, cluster });
+ }
+
+}
diff --git a/web/src/app/components/columns/edition-column/edition-column.component.ts b/web/src/app/components/columns/edition-column/edition-column.component.ts
new file mode 100644
index 0000000000..a39a6fa710
--- /dev/null
+++ b/web/src/app/components/columns/edition-column/edition-column.component.ts
@@ -0,0 +1,29 @@
+import { Component, EventEmitter, Input, Output } from '@angular/core';
+import { AdwpCellComponent } from '@adwp-ui/widgets';
+
+import { IBundle } from '@app/models/bundle';
+import { StatusData } from '@app/components/columns/status-column/status-column.component';
+
+@Component({
+ selector: 'app-edition-column',
+ template: `
+ {{ row.edition }}
+
+
+
+ `,
+})
+export class EditionColumnComponent implements AdwpCellComponent {
+
+ @Input() row: IBundle;
+
+ @Output() onClick = new EventEmitter>();
+
+ clickCell(event: MouseEvent, action: string, row: IBundle): void {
+ this.onClick.emit({ event, action, row });
+ }
+
+}
diff --git a/web/src/app/components/columns/job-status-column/job-status-column.component.scss b/web/src/app/components/columns/job-status-column/job-status-column.component.scss
new file mode 100644
index 0000000000..4f9cb7d898
--- /dev/null
+++ b/web/src/app/components/columns/job-status-column/job-status-column.component.scss
@@ -0,0 +1,6 @@
+.mat-icon {
+ vertical-align: middle;
+ font-size: 1.2em;
+ width: auto;
+ height: auto;
+}
diff --git a/web/src/app/components/columns/job-status-column/job-status-column.component.ts b/web/src/app/components/columns/job-status-column/job-status-column.component.ts
new file mode 100644
index 0000000000..93c15cf6e1
--- /dev/null
+++ b/web/src/app/components/columns/job-status-column/job-status-column.component.ts
@@ -0,0 +1,27 @@
+import { Component } from '@angular/core';
+import { AdwpCellComponent } from '@adwp-ui/widgets';
+
+import { Job } from '@app/core/types';
+
+@Component({
+ selector: 'app-job-status-column',
+ template: `
+
+ {{ iconDisplay[row.status] }}
+
+ `,
+ styleUrls: ['./job-status-column.component.scss']
+})
+export class JobStatusColumnComponent implements AdwpCellComponent {
+
+ row: Job;
+
+ iconDisplay = {
+ created: 'watch_later',
+ running: 'autorenew',
+ success: 'done',
+ failed: 'error',
+ aborted: 'block'
+ };
+
+}
diff --git a/web/src/app/components/columns/state-column/state-column.component.html b/web/src/app/components/columns/state-column/state-column.component.html
new file mode 100644
index 0000000000..df986f1e8e
--- /dev/null
+++ b/web/src/app/components/columns/state-column/state-column.component.html
@@ -0,0 +1,2 @@
+autorenew
+{{ row?.state }}
diff --git a/web/src/app/components/columns/state-column/state-column.component.scss b/web/src/app/components/columns/state-column/state-column.component.scss
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/web/src/app/components/columns/state-column/state-column.component.spec.ts b/web/src/app/components/columns/state-column/state-column.component.spec.ts
new file mode 100644
index 0000000000..d032e18fcf
--- /dev/null
+++ b/web/src/app/components/columns/state-column/state-column.component.spec.ts
@@ -0,0 +1,31 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { MatIconModule } from '@angular/material/icon';
+
+import { StateColumnComponent } from './state-column.component';
+
+describe('StateColumnComponent', () => {
+ let component: StateColumnComponent;
+ let fixture: ComponentFixture>;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ declarations: [
+ StateColumnComponent,
+ ],
+ imports: [
+ MatIconModule,
+ ],
+ })
+ .compileComponents();
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(StateColumnComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/web/src/app/components/columns/state-column/state-column.component.ts b/web/src/app/components/columns/state-column/state-column.component.ts
new file mode 100644
index 0000000000..e3199713c3
--- /dev/null
+++ b/web/src/app/components/columns/state-column/state-column.component.ts
@@ -0,0 +1,12 @@
+import { Component, Input } from '@angular/core';
+
+@Component({
+ selector: 'app-state-column',
+ templateUrl: './state-column.component.html',
+ styleUrls: ['./state-column.component.scss']
+})
+export class StateColumnComponent {
+
+ @Input() row: T;
+
+}
diff --git a/web/src/app/components/columns/status-column/status-column.component.html b/web/src/app/components/columns/status-column/status-column.component.html
new file mode 100644
index 0000000000..9bd20013ad
--- /dev/null
+++ b/web/src/app/components/columns/status-column/status-column.component.html
@@ -0,0 +1,11 @@
+{{ row?.status }}
+
+
+
+
diff --git a/web/src/app/components/columns/status-column/status-column.component.scss b/web/src/app/components/columns/status-column/status-column.component.scss
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/web/src/app/components/columns/status-column/status-column.component.spec.ts b/web/src/app/components/columns/status-column/status-column.component.spec.ts
new file mode 100644
index 0000000000..762eda2e28
--- /dev/null
+++ b/web/src/app/components/columns/status-column/status-column.component.spec.ts
@@ -0,0 +1,31 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { StatusColumnComponent } from './status-column.component';
+import { MatIconModule } from '@angular/material/icon';
+
+describe('StatusColumnComponent', () => {
+ let component: StatusColumnComponent;
+ let fixture: ComponentFixture>;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ declarations: [
+ StatusColumnComponent,
+ ],
+ imports: [
+ MatIconModule,
+ ]
+ })
+ .compileComponents();
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(StatusColumnComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/web/src/app/components/columns/status-column/status-column.component.ts b/web/src/app/components/columns/status-column/status-column.component.ts
new file mode 100644
index 0000000000..1eb5815b53
--- /dev/null
+++ b/web/src/app/components/columns/status-column/status-column.component.ts
@@ -0,0 +1,25 @@
+import { Component, Input, Output, EventEmitter } from '@angular/core';
+import { AdwpCellComponent } from '@adwp-ui/widgets/public-api';
+
+export interface StatusData {
+ event: MouseEvent;
+ action: string;
+ row: T;
+}
+
+@Component({
+ selector: 'app-status-column',
+ templateUrl: './status-column.component.html',
+ styleUrls: ['./status-column.component.scss']
+})
+export class StatusColumnComponent implements AdwpCellComponent {
+
+ @Input() row: T;
+
+ @Output() onClick = new EventEmitter>();
+
+ clickCell(event: MouseEvent, action: string, row: T): void {
+ this.onClick.emit({ event, action, row });
+ }
+
+}
diff --git a/web/src/app/components/columns/task-name/task-name.component.scss b/web/src/app/components/columns/task-name/task-name.component.scss
new file mode 100644
index 0000000000..c8343eedbe
--- /dev/null
+++ b/web/src/app/components/columns/task-name/task-name.component.scss
@@ -0,0 +1,14 @@
+:host {
+ width: 100%;
+}
+
+.multi-title {
+ cursor: pointer;
+}
+
+.mat-icon {
+ vertical-align: middle;
+ font-size: 1.2em;
+ width: auto;
+ height: auto;
+}
diff --git a/web/src/app/components/columns/task-name/task-name.component.ts b/web/src/app/components/columns/task-name/task-name.component.ts
new file mode 100644
index 0000000000..e000fc7d28
--- /dev/null
+++ b/web/src/app/components/columns/task-name/task-name.component.ts
@@ -0,0 +1,41 @@
+import { Component, Input } from '@angular/core';
+import { AdwpCellComponent, ILinkColumn } from '@adwp-ui/widgets';
+import { BehaviorSubject } from 'rxjs';
+
+import { Task } from '@app/core/types';
+
+@Component({
+ selector: 'app-task-name',
+ template: `
+
+
+
+ {{ row.action?.display_name || 'unknown' }}
+
+
+ {{ (expandedTask | async) && (expandedTask | async).id === row.id ? 'expand_less' : 'expand_more' }}
+
+
+
+ `,
+ styleUrls: ['./task-name.component.scss']
+})
+export class TaskNameComponent implements AdwpCellComponent {
+
+ row: Task;
+
+ linkColumn: ILinkColumn = {
+ label: '',
+ type: 'link',
+ value: (row) => row.action?.display_name || 'unknown',
+ url: (row) => `/job/${row.jobs[0].id}`,
+ };
+
+ @Input() expandedTask: BehaviorSubject;
+ @Input() toggleExpand: (row: Task) => void;
+
+}
diff --git a/web/src/app/components/columns/task-objects/task-objects.component.html b/web/src/app/components/columns/task-objects/task-objects.component.html
new file mode 100644
index 0000000000..477c4795ce
--- /dev/null
+++ b/web/src/app/components/columns/task-objects/task-objects.component.html
@@ -0,0 +1,7 @@
+
+
+ /
+
diff --git a/web/src/app/components/columns/task-objects/task-objects.component.scss b/web/src/app/components/columns/task-objects/task-objects.component.scss
new file mode 100644
index 0000000000..2684ebc430
--- /dev/null
+++ b/web/src/app/components/columns/task-objects/task-objects.component.scss
@@ -0,0 +1,7 @@
+adwp-link-cell {
+ display: inline;
+}
+
+:host ::ng-deep adwp-link-cell a {
+ display: inline !important;
+}
diff --git a/web/src/app/components/columns/task-objects/task-objects.component.ts b/web/src/app/components/columns/task-objects/task-objects.component.ts
new file mode 100644
index 0000000000..77a52ad87e
--- /dev/null
+++ b/web/src/app/components/columns/task-objects/task-objects.component.ts
@@ -0,0 +1,15 @@
+import { Component } from '@angular/core';
+import { AdwpCellComponent } from '@adwp-ui/widgets/public-api';
+
+import { Task } from '@app/core/types';
+
+@Component({
+ selector: 'app-task-objects',
+ templateUrl: './task-objects.component.html',
+ styleUrls: ['./task-objects.component.scss']
+})
+export class TaskObjectsComponent implements AdwpCellComponent {
+
+ row: Task;
+
+}
diff --git a/web/src/app/components/columns/task-status-column/task-status-column.component.html b/web/src/app/components/columns/task-status-column/task-status-column.component.html
new file mode 100644
index 0000000000..44d4e75a09
--- /dev/null
+++ b/web/src/app/components/columns/task-status-column/task-status-column.component.html
@@ -0,0 +1,23 @@
+
+
+
+ autorenew
+
+
+
+
+ block
+
+ done_all
+
+
+
diff --git a/web/src/app/components/columns/task-status-column/task-status-column.component.scss b/web/src/app/components/columns/task-status-column/task-status-column.component.scss
new file mode 100644
index 0000000000..4f9cb7d898
--- /dev/null
+++ b/web/src/app/components/columns/task-status-column/task-status-column.component.scss
@@ -0,0 +1,6 @@
+.mat-icon {
+ vertical-align: middle;
+ font-size: 1.2em;
+ width: auto;
+ height: auto;
+}
diff --git a/web/src/app/components/columns/task-status-column/task-status-column.component.ts b/web/src/app/components/columns/task-status-column/task-status-column.component.ts
new file mode 100644
index 0000000000..a62c94d1e5
--- /dev/null
+++ b/web/src/app/components/columns/task-status-column/task-status-column.component.ts
@@ -0,0 +1,40 @@
+import { Component } from '@angular/core';
+import { AdwpCellComponent } from '@adwp-ui/widgets';
+import { filter, switchMap } from 'rxjs/operators';
+import { MatDialog } from '@angular/material/dialog';
+
+import { Task } from '@app/core/types';
+import { DialogComponent } from '@app/shared/components';
+import { ApiService } from '@app/core/api';
+
+@Component({
+ selector: 'app-task-status-column',
+ templateUrl: './task-status-column.component.html',
+ styleUrls: ['./task-status-column.component.scss']
+})
+export class TaskStatusColumnComponent implements AdwpCellComponent {
+
+ constructor(
+ public dialog: MatDialog,
+ private api: ApiService,
+ ) {}
+
+ row: Task;
+
+ cancelTask(url: string) {
+ this.dialog
+ .open(DialogComponent, {
+ data: {
+ text: 'Are you sure?',
+ controls: ['Yes', 'No'],
+ },
+ })
+ .beforeClosed()
+ .pipe(
+ filter((yes) => yes),
+ switchMap(() => this.api.put(url, {}))
+ )
+ .subscribe();
+ }
+
+}
diff --git a/web/src/app/components/host-list/host-list.component.ts b/web/src/app/components/host-list/host-list.component.ts
new file mode 100644
index 0000000000..c182e41dc1
--- /dev/null
+++ b/web/src/app/components/host-list/host-list.component.ts
@@ -0,0 +1,76 @@
+import { Component, ComponentRef } from '@angular/core';
+import { IColumns } from '@adwp-ui/widgets';
+
+import { TypeName } from '@app/core/types';
+import { AdwpListDirective } from '@app/abstract-directives/adwp-list.directive';
+import { IHost } from '@app/models/host';
+import { ListFactory } from '@app/factories/list-factory';
+import { AddClusterEventData, ClusterColumnComponent } from '@app/components/columns/cluster-column/cluster-column.component';
+import { UniversalAdcmEventData } from '@app/models/universal-adcm-event-data';
+
+@Component({
+ selector: 'app-host-list',
+ template: `
+
+
+ Create {{ type }}
+
+
+
+ `,
+ styles: [':host { flex: 1; }'],
+})
+export class HostListComponent extends AdwpListDirective {
+
+ type: TypeName = 'host';
+
+ listColumns = [
+ ListFactory.fqdnColumn(),
+ ListFactory.providerColumn(),
+ {
+ type: 'component',
+ label: 'Cluster',
+ sort: 'cluster_name',
+ component: ClusterColumnComponent,
+ instanceTaken: (componentRef: ComponentRef) => {
+ componentRef.instance
+ .onGetNextPageCluster
+ .pipe(this.takeUntil())
+ .subscribe((data: UniversalAdcmEventData) => {
+ this.clickCell(data.event, data.action, data.row);
+ });
+
+ componentRef.instance
+ .onGetClusters
+ .pipe(this.takeUntil())
+ .subscribe((data: UniversalAdcmEventData) => {
+ this.clickCell(data.event, data.action, data.row);
+ });
+
+ componentRef.instance
+ .onAddCluster
+ .pipe(this.takeUntil())
+ .subscribe((data: AddClusterEventData) => {
+ this.clickCell(data.event, data.action, data.row, data.cluster);
+ });
+ }
+ },
+ ListFactory.stateColumn(),
+ ListFactory.statusColumn(this),
+ ListFactory.actionsColumn(),
+ ListFactory.configColumn(this),
+ ListFactory.deleteColumn(this),
+ ] as IColumns;
+
+}
diff --git a/web/src/app/components/hostprovider/hostprovider.component.ts b/web/src/app/components/hostprovider/hostprovider.component.ts
new file mode 100644
index 0000000000..f20284c988
--- /dev/null
+++ b/web/src/app/components/hostprovider/hostprovider.component.ts
@@ -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.
+import { Component } from '@angular/core';
+import { IColumns } from '@adwp-ui/widgets';
+
+import { TypeName } from '@app/core/types';
+import { AdwpListDirective } from '@app/abstract-directives/adwp-list.directive';
+import { ListFactory } from '@app/factories/list-factory';
+
+@Component({
+ selector: 'app-hostprovider',
+ template: `
+
+
+ Create {{ type }}
+
+
+
+ `,
+ styles: [':host { flex: 1; }'],
+})
+export class HostproviderComponent extends AdwpListDirective {
+
+ type: TypeName = 'provider';
+
+ listColumns = [
+ ListFactory.nameColumn(),
+ ListFactory.bundleColumn(),
+ ListFactory.stateColumn(),
+ ListFactory.actionsColumn(),
+ ListFactory.updateColumn(),
+ ListFactory.configColumn(this),
+ ListFactory.deleteColumn(this),
+ ] as IColumns;
+
+}
diff --git a/web/src/app/components/issues/issues.component.spec.ts b/web/src/app/components/issues/issues.component.spec.ts
new file mode 100644
index 0000000000..8b2cb84fb6
--- /dev/null
+++ b/web/src/app/components/issues/issues.component.spec.ts
@@ -0,0 +1,29 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { IssuesComponent } from './issues.component';
+import { KeysPipe } from '@app/pipes/keys.pipe';
+
+describe('IssuesComponent', () => {
+ let component: IssuesComponent;
+ let fixture: ComponentFixture;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ declarations: [
+ IssuesComponent,
+ KeysPipe,
+ ]
+ })
+ .compileComponents();
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(IssuesComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/web/src/app/components/issues/issues.component.ts b/web/src/app/components/issues/issues.component.ts
new file mode 100644
index 0000000000..cc524a7dec
--- /dev/null
+++ b/web/src/app/components/issues/issues.component.ts
@@ -0,0 +1,64 @@
+import { Component, Input } from '@angular/core';
+
+import { PopoverContentDirective } from '@app/abstract-directives/popover-content.directive';
+import { PopoverInput } from '@app/directives/popover.directive';
+import { IssueEntity, IssueType } from '@app/models/issue';
+
+interface IssuesInput extends PopoverInput {
+ row: IssueEntity;
+ issueType: IssueType;
+}
+
+@Component({
+ selector: 'app-issues',
+ template: `
+ {{ intro }}
+
+ `,
+ styles: [`
+ :host{
+ cursor: auto;
+ }
+ a, .item-step {
+ display: block;
+ margin: 6px 0 8px 12px;
+ white-space: nowrap;
+ }
+ a.issue {
+ color: #90caf9 !important;
+ text-decoration: none;
+ cursor: pointer;
+ }
+ a.issue:hover {
+ color: #64b5f6 !important;
+ text-decoration: underline;
+ }
+ `],
+})
+export class IssuesComponent extends PopoverContentDirective {
+
+ @Input() intro = 'Issues in:';
+
+ readonly IssueNames = {
+ config: 'Configuration',
+ host_component: 'Host - Components',
+ required_service: 'Required a service',
+ required_import: 'Required a import',
+ };
+
+ @Input() data: IssuesInput;
+
+}
diff --git a/web/src/app/components/navigation/navigation.component.spec.ts b/web/src/app/components/navigation/navigation.component.spec.ts
new file mode 100644
index 0000000000..07f73656d9
--- /dev/null
+++ b/web/src/app/components/navigation/navigation.component.spec.ts
@@ -0,0 +1,35 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { MatListModule } from '@angular/material/list';
+import { MatIconModule } from '@angular/material/icon';
+
+import { NavigationComponent } from './navigation.component';
+import { NavItemPipe } from '@app/pipes/nav-item.pipe';
+
+describe('NavigationComponent', () => {
+ let component: NavigationComponent;
+ let fixture: ComponentFixture;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ declarations: [
+ NavigationComponent,
+ NavItemPipe,
+ ],
+ imports: [
+ MatListModule,
+ MatIconModule,
+ ],
+ })
+ .compileComponents();
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(NavigationComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/web/src/app/components/navigation/navigation.component.ts b/web/src/app/components/navigation/navigation.component.ts
new file mode 100644
index 0000000000..cd8e2bb5cb
--- /dev/null
+++ b/web/src/app/components/navigation/navigation.component.ts
@@ -0,0 +1,129 @@
+import { Component, Input } from '@angular/core';
+import { Observable } from 'rxjs';
+import { BaseDirective } from '@adwp-ui/widgets';
+
+import { AdcmTypedEntity } from '@app/models/entity';
+import { IAction } from '@app/core/types';
+import { IIssues } from '@app/models/issue';
+
+@Component({
+ selector: 'app-navigation',
+ template: `
+
+ apps
+ /
+
+
+
+
+
+
+ /
+
+
+ `,
+ styles: [`
+ :host {
+ font-size: 14px;
+ margin-left: 8px;
+ width: 100%;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ }
+
+ mat-nav-list {
+ padding-top: 0;
+ display: flex;
+ align-items: center;
+ max-width: 100%;
+ overflow: hidden;
+ flex-flow: row nowrap;
+ justify-content: flex-start;
+ }
+
+ mat-nav-list > * {
+ display: block;
+ box-sizing: border-box;
+ }
+
+ mat-nav-list a {
+ display: flex;
+ align-items: center;
+ line-height: normal;
+ }
+
+ .mat-nav-list .entity {
+ border: 1px solid #54646E;
+ border-radius: 5px;
+ padding: 2px 0 2px 8px;
+ display: flex;
+ align-items: center;
+ justify-content: space-around;
+ flex: 0 1 auto;
+ overflow: hidden;
+
+ }
+
+ .mat-nav-list .entity.last {
+ flex: 0 0 auto;
+ }
+
+ .mat-nav-list .entity * {
+ flex: 0 0 auto;
+ }
+
+ .mat-nav-list .entity .link {
+ flex: 0 1 auto;
+ overflow: hidden;
+ }
+
+ .mat-nav-list .entity a {
+ line-height: normal;
+ text-overflow: ellipsis;
+ overflow: hidden;
+ display: block;
+ }
+
+ .mat-nav-list app-upgrade {
+ margin-left: -8px;
+ }
+
+ `],
+})
+export class NavigationComponent extends BaseDirective {
+
+ actionFlag = false;
+ actionLink: string;
+ actions: IAction[] = [];
+ state: string;
+ disabled: boolean;
+ cluster: { id: number; hostcomponent: string };
+
+ private ownPath: Observable;
+
+ isIssue = (issue: IIssues): boolean => !!(issue && Object.keys(issue).length);
+
+ @Input() set path(path: Observable) {
+ this.ownPath = path;
+ this.ownPath.pipe(this.takeUntil()).subscribe((lPath) => {
+ if (lPath) {
+ const last = lPath[lPath.length - 1];
+ const exclude = ['bundle', 'job'];
+ this.actionFlag = !exclude.includes(last.typeName);
+ this.actionLink = (last).action;
+ this.actions = (last).actions;
+ this.state = (last).state;
+ this.disabled = this.isIssue((last).issue) || (last).state === 'locked';
+ const { id, hostcomponent } = lPath[0];
+ this.cluster = { id, hostcomponent };
+ }
+ });
+ }
+ get path(): Observable {
+ return this.ownPath;
+ }
+
+}
diff --git a/web/src/app/components/popover/popover.component.scss b/web/src/app/components/popover/popover.component.scss
new file mode 100644
index 0000000000..5965a46faa
--- /dev/null
+++ b/web/src/app/components/popover/popover.component.scss
@@ -0,0 +1,26 @@
+:host {
+ line-height: normal;
+ text-align: left;
+ font-family: Roboto, "Helvetica Neue", sans-serif;
+ color: #fff;
+ font-size: 14px;
+ position: absolute;
+ display: block;
+ border: solid 1px #455A64;
+ padding: 0;
+ background-color: #37474F;
+ border-radius: 5px;
+ box-shadow: 0 3px 1px -2px rgba(0, 0, 0, 0.2),
+ 0px 2px 2px 0px rgba(0, 0, 0, 0.14),
+ 0px 1px 5px 0px rgba(0, 0, 0, 0.12);
+ height: auto;
+ z-index: 1100;
+ overflow: auto;
+ box-sizing: content-box;
+ cursor: auto;
+}
+
+.container {
+ margin: 8px 14px;
+ box-sizing: content-box;
+}
diff --git a/web/src/app/components/popover/popover.component.ts b/web/src/app/components/popover/popover.component.ts
new file mode 100644
index 0000000000..b77acf1eb9
--- /dev/null
+++ b/web/src/app/components/popover/popover.component.ts
@@ -0,0 +1,52 @@
+import {
+ Component,
+ ViewChild,
+ ViewContainerRef,
+ ComponentRef,
+ Input,
+ ComponentFactory,
+ ComponentFactoryResolver,
+ AfterViewInit,
+ Type,
+ HostListener,
+} from '@angular/core';
+import { EventHelper } from '@adwp-ui/widgets';
+
+import { PopoverContentDirective } from '@app/abstract-directives/popover-content.directive';
+import { PopoverInput } from '@app/directives/popover.directive';
+
+@Component({
+ selector: 'app-popover',
+ template: `
+
+
+
+ `,
+ styleUrls: ['./popover.component.scss']
+})
+export class PopoverComponent implements AfterViewInit {
+
+ @ViewChild('container', { read: ViewContainerRef }) container: ViewContainerRef;
+ containerRef: ComponentRef;
+
+ @Input() component: Type;
+ @Input() data: PopoverInput = {};
+
+ @HostListener('click', ['$event']) click(event: MouseEvent) {
+ EventHelper.stopPropagation(event);
+ }
+
+ constructor(
+ private componentFactoryResolver: ComponentFactoryResolver,
+ ) {}
+
+ ngAfterViewInit() {
+ setTimeout(() => {
+ const factory: ComponentFactory = this.componentFactoryResolver.resolveComponentFactory(this.component);
+ this.container.clear();
+ this.containerRef = this.container.createComponent(factory);
+ this.containerRef.instance.data = this.data;
+ });
+ }
+
+}
diff --git a/web/src/app/components/service-components.component.ts b/web/src/app/components/service-components.component.ts
new file mode 100644
index 0000000000..b4b2cc6994
--- /dev/null
+++ b/web/src/app/components/service-components.component.ts
@@ -0,0 +1,61 @@
+import { IColumns } from '@adwp-ui/widgets';
+import { Component, OnInit } from '@angular/core';
+import { Store } from '@ngrx/store';
+import { ActivatedRoute, Router } from '@angular/router';
+import { MatDialog } from '@angular/material/dialog';
+
+import { AdwpListDirective } from '@app/abstract-directives/adwp-list.directive';
+import { ListService } from '@app/shared/components/list/list.service';
+import { SocketState } from '@app/core/store';
+import { ApiService } from '@app/core/api';
+import { TypeName } from '@app/core/types';
+import { ListFactory } from '@app/factories/list-factory';
+
+@Component({
+ selector: 'app-service-components',
+ template: `
+
+ `,
+ styles: [`
+ :host { flex: 1; }
+ `],
+})
+export class ServiceComponentsComponent extends AdwpListDirective implements OnInit {
+
+ type: TypeName = 'servicecomponent';
+
+ listColumns = [
+ ListFactory.nameColumn('display_name'),
+ ListFactory.stateColumn(),
+ ListFactory.statusColumn(this),
+ ListFactory.actionsButton('servicecomponent'),
+ ListFactory.configColumn(this),
+ ] as IColumns;
+
+ constructor(
+ protected service: ListService,
+ protected store: Store,
+ public route: ActivatedRoute,
+ public router: Router,
+ public dialog: MatDialog,
+ protected api: ApiService,
+ ) {
+ super(service, store, route, router, dialog, api);
+ }
+
+ ngOnInit() {
+ super.ngOnInit();
+ }
+
+}
diff --git a/web/src/app/components/task/jobs/jobs.component.scss b/web/src/app/components/task/jobs/jobs.component.scss
new file mode 100644
index 0000000000..882bdeaa08
--- /dev/null
+++ b/web/src/app/components/task/jobs/jobs.component.scss
@@ -0,0 +1,36 @@
+:host {
+ width: 100%;
+}
+
+adwp-table {
+ width: 100%;
+}
+
+:host adwp-table ::ng-deep mat-table {
+ width: 100%;
+}
+
+:host ::ng-deep .action_date {
+ width: 200px;
+ flex-basis: 200px;
+ flex-grow: 0;
+}
+
+:host ::ng-deep .padding20 {
+ padding-right: 20px;
+}
+
+:host ::ng-deep .table-end {
+ width: 50px;
+ flex-basis: 50px;
+ flex-grow: 0;
+}
+
+:host ::ng-deep .center {
+ text-align: center;
+}
+
+:host ::ng-deep .status {
+ display: flex;
+ justify-content: center;
+}
diff --git a/web/src/app/components/task/jobs/jobs.component.ts b/web/src/app/components/task/jobs/jobs.component.ts
new file mode 100644
index 0000000000..424c4fbe4d
--- /dev/null
+++ b/web/src/app/components/task/jobs/jobs.component.ts
@@ -0,0 +1,79 @@
+import { Component, Input } from '@angular/core';
+import { animate, state, style, transition, trigger } from '@angular/animations';
+import { IColumns, AdwpComponentHolder } from '@adwp-ui/widgets';
+import { BehaviorSubject } from 'rxjs';
+
+import { Job, Task } from '@app/core/types';
+import { DateHelper } from '@app/helpers/date-helper';
+import { JobStatusColumnComponent } from '@app/components/columns/job-status-column/job-status-column.component';
+
+@Component({
+ selector: 'app-jobs',
+ animations: [
+ trigger('jobsExpand', [
+ state('collapsed', style({ height: '0px', minHeight: '0' })),
+ state('expanded', style({ height: '*' })),
+ transition('expanded <=> collapsed', animate('225ms cubic-bezier(0.4, 0.0, 0.2, 1)')),
+ ]),
+ ],
+ template: `
+
+
1"
+ [columns]="columns"
+ [dataSource]="data"
+ headerRowClassName="hidden"
+ >
+
+ `,
+ styleUrls: ['./jobs.component.scss'],
+})
+export class JobsComponent implements AdwpComponentHolder {
+
+ columns: IColumns = [
+ {
+ type: 'link',
+ label: '',
+ value: (row) => row.display_name || row.id,
+ url: (row) => `/job/${row.id}`,
+ },
+ {
+ label: '',
+ value: (row) => {
+ return row.status !== 'created' ? DateHelper.short(row.start_date) : '';
+ },
+ className: 'action_date',
+ headerClassName: 'action_date',
+ },
+ {
+ label: '',
+ value: (row) => row.status === 'success' || row.status === 'failed' ? DateHelper.short(row.finish_date) : '',
+ className: 'action_date',
+ headerClassName: 'action_date',
+ },
+ {
+ label: '',
+ type: 'component',
+ component: JobStatusColumnComponent,
+ className: 'table-end center status',
+ headerClassName: 'table-end center status',
+ }
+ ];
+
+ private ownData: { results: Job[], count: number };
+ get data(): { results: Job[], count: number } {
+ return this.ownData;
+ }
+
+ private ownRow: Task;
+ @Input() set row(row: Task) {
+ this.ownRow = row;
+ this.ownData = { results: this.ownRow?.jobs, count: 0 };
+ }
+ get row(): Task {
+ return this.ownRow;
+ }
+
+ @Input() expandedTask: BehaviorSubject;
+
+}
diff --git a/web/src/app/core/core.module.ts b/web/src/app/core/core.module.ts
index 4d42418120..2806f9e7eb 100644
--- a/web/src/app/core/core.module.ts
+++ b/web/src/app/core/core.module.ts
@@ -9,20 +9,23 @@
// 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 { HttpClientModule } from '@angular/common/http';
+import { HTTP_INTERCEPTORS, HttpClientModule } from '@angular/common/http';
import { NgModule } from '@angular/core';
import { ApiService } from './api/api.service';
import { AuthGuard } from './auth/auth.guard';
import { AuthService } from './auth/auth.service';
-import { httpInterseptorProviders, RequestCache, RequestCacheService } from './http-interseptors';
+import { RequestCacheService, RequestCache } from '@app/core/http-interseptors/request-cache.service';
+import { CachingInterseptor } from '@app/core/http-interseptors/caching-interseptor';
+import { AuthInterceptor } from '@app/core/http-interseptors/auth-interseptor';
@NgModule({
imports: [HttpClientModule],
providers: [
ApiService,
{ provide: RequestCache, useClass: RequestCacheService },
- httpInterseptorProviders,
+ { provide: HTTP_INTERCEPTORS, useClass: CachingInterseptor, multi: true },
+ { provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true },
AuthGuard,
AuthService
],
diff --git a/web/src/app/core/http-interseptors/index.ts b/web/src/app/core/http-interseptors/index.ts
deleted file mode 100644
index dc56b50edd..0000000000
--- a/web/src/app/core/http-interseptors/index.ts
+++ /dev/null
@@ -1,21 +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 { HTTP_INTERCEPTORS } from '@angular/common/http';
-import { AuthInterceptor } from './auth-interseptor';
-import { CachingInterseptor } from './caching-interseptor';
-
-export const httpInterseptorProviders = [
- { provide: HTTP_INTERCEPTORS, useClass: CachingInterseptor, multi: true },
- { provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true }
-];
-
-export * from './request-cache.service';
diff --git a/web/src/app/core/services/detail.service.ts b/web/src/app/core/services/cluster.service.ts
similarity index 86%
rename from web/src/app/core/services/detail.service.ts
rename to web/src/app/core/services/cluster.service.ts
index 3c0e7aa328..3c1c7c661f 100644
--- a/web/src/app/core/services/detail.service.ts
+++ b/web/src/app/core/services/cluster.service.ts
@@ -12,12 +12,15 @@
import { Injectable } from '@angular/core';
import { ParamMap } from '@angular/router';
import { ApiService } from '@app/core/api';
-import { Bundle, Cluster, Entities, Host, IAction, IImport, Job, LogFile, Provider, Service, TypeName } from '@app/core/types';
-import { environment } from '@env/environment';
import { BehaviorSubject, EMPTY, forkJoin, Observable, of } from 'rxjs';
import { filter, map, switchMap, tap } from 'rxjs/operators';
+import { Store } from '@ngrx/store';
-const EntitiNames: TypeName[] = ['host', 'service', 'cluster', 'provider', 'job', 'task', 'bundle'];
+import { Bundle, Cluster, Entities, Host, IAction, IImport, Job, LogFile, Provider, Service } from '@app/core/types';
+import { environment } from '@env/environment';
+import { ServiceComponentService } from '@app/services/service-component.service';
+import { setPathOfRoute } from '@app/store/navigation/navigation.store';
+import { EntityNames } from '@app/models/entity-names';
export interface WorkerInstance {
current: Entities;
@@ -49,7 +52,11 @@ export class ClusterService {
return this.worker ? this.worker.current : null;
}
- constructor(private api: ApiService) {}
+ constructor(
+ protected api: ApiService,
+ protected serviceComponentService: ServiceComponentService,
+ protected store: Store,
+ ) {}
clearWorker() {
this.worker = null;
@@ -88,14 +95,24 @@ export class ClusterService {
}
getContext(param: ParamMap): Observable {
- const typeName = EntitiNames.find((a) => param.keys.some((b) => a === b));
+ this.store.dispatch(setPathOfRoute({ params: param }));
+
+ const typeName = EntityNames.find((a) => param.keys.some((b) => a === b));
const id = +param.get(typeName);
const cluster$ = param.has('cluster') ? this.api.getOne('cluster', +param.get('cluster')) : of(null);
return cluster$
.pipe(
tap((cluster) => (this.Cluster = cluster)),
- switchMap((cluster) => (cluster && typeName !== 'cluster' ? this.api.get(`${cluster[typeName]}${id}/`) : this[`one_${typeName}`](id)))
+ switchMap((cluster) => {
+ if (cluster && typeName === 'servicecomponent') {
+ return this.serviceComponentService.get(id);
+ } else if (cluster && typeName !== 'cluster') {
+ return this.api.get(`${cluster[typeName]}${id}/`);
+ } else {
+ return this[`one_${typeName}`](id);
+ }
+ })
)
.pipe(
map((a: Entities) => {
diff --git a/web/src/app/core/services/index.ts b/web/src/app/core/services/index.ts
index 04f138da24..09aa4875e5 100644
--- a/web/src/app/core/services/index.ts
+++ b/web/src/app/core/services/index.ts
@@ -11,7 +11,6 @@
// limitations under the License.
export * from './preloader.service';
export * from './config.service';
-export * from './detail.service';
export * from './stack.service';
export * from './channel.service';
export * from './app/app.service';
diff --git a/web/src/app/core/store/index.ts b/web/src/app/core/store/index.ts
index 422cc9e208..69aec66b6c 100644
--- a/web/src/app/core/store/index.ts
+++ b/web/src/app/core/store/index.ts
@@ -19,6 +19,7 @@ import { IssueEffect, issueReducer, IssueState } from './issue';
import { ProfileEffects, profileReducer, ProfileState } from './profile';
import { SocketEffect } from './sockets/socket.effect';
import { socketReducer, SocketState } from './sockets/socket.reducer';
+import { NavigationEffects, navigationReducer, NavigationState } from '@app/store/navigation/navigation.store';
export interface State {
auth: AuthState;
@@ -26,6 +27,7 @@ export interface State {
api: ApiState;
profile: ProfileState;
issue: IssueState;
+ navigation: NavigationState,
}
export const reducers: ActionReducerMap = {
@@ -34,11 +36,12 @@ export const reducers: ActionReducerMap = {
api: apiReducer,
profile: profileReducer,
issue: issueReducer,
+ navigation: navigationReducer,
};
export const metaReducers: MetaReducer[] = !environment.production ? [] : [];
-export const StoreEffects = [AuthEffects, ApiEffects, ProfileEffects, IssueEffect, SocketEffect];
+export const StoreEffects = [AuthEffects, ApiEffects, ProfileEffects, IssueEffect, SocketEffect, NavigationEffects];
export * from '../api/api.reducer';
export * from '../auth/auth.store';
@@ -47,3 +50,4 @@ export * from './profile/profile.service';
export * from './issue';
export * from './sockets/socket.service';
export * from './sockets/socket.reducer';
+export * from '@app/store/navigation/navigation.store';
diff --git a/web/src/app/core/store/profile/index.ts b/web/src/app/core/store/profile/index.ts
index 3bc04b483b..49c7f9f64e 100644
--- a/web/src/app/core/store/profile/index.ts
+++ b/web/src/app/core/store/profile/index.ts
@@ -11,10 +11,9 @@
// limitations under the License.
import { Injectable } from '@angular/core';
import { Actions, createEffect, ofType } from '@ngrx/effects';
-import { Action, createAction, createFeatureSelector, createReducer, createSelector, on, props, Store } from '@ngrx/store';
+import { Action, createAction, createFeatureSelector, createReducer, createSelector, on, props } from '@ngrx/store';
import { exhaustMap, map } from 'rxjs/operators';
-import { State } from '../../store';
import { IUser, ProfileService } from './profile.service';
export type ProfileState = IUser;
@@ -25,7 +24,6 @@ const InitState = {
profile: {
dashboard: [],
textarea: {},
- metrics: true, // deprecated
settingsSaved: false,
},
};
@@ -35,7 +33,6 @@ export const clearProfile = createAction('[Profile] ClearProfile');
export const saveDashboard = createAction('[Profile] SaveDashboard', props<{ dashboard: any[] }>());
export const loadProfileSuccess = createAction('[Profile] LoadSuccess', props<{ profile: IUser }>());
export const setTextareaHeight = createAction('[Profile] SetTextareaHeight', props<{ key: string; value: number }>());
-export const sendMetrics = createAction('[Profile] SendMetrics', props<{ metrics: boolean }>());
export const settingsSave = createAction('[Profile] SettingsSave', props<{ isSet: boolean }>());
const reducer = createReducer(
@@ -43,7 +40,6 @@ const reducer = createReducer(
on(loadProfileSuccess, (state, { profile }) => ({ ...profile })),
on(setTextareaHeight, state => ({ ...state })),
on(saveDashboard, (state, { dashboard }) => ({ ...state, dashboard })),
- on(sendMetrics, (state, { metrics }) => ({ ...state, metrics })),
on(settingsSave, (state, { isSet }) => ({ ...state, isSet })),
on(clearProfile, () => InitState)
);
@@ -80,14 +76,6 @@ export class ProfileEffects {
)
);
- sendMetrics$ = createEffect(() =>
- this.actions$.pipe(
- ofType(sendMetrics),
- map(a => this.service.setUser('metrics', a.metrics)),
- exhaustMap(() => this.service.setProfile().pipe(map(user => loadProfileSuccess({ profile: user }))))
- )
- );
-
saveSettings$ = createEffect(() =>
this.actions$.pipe(
ofType(settingsSave),
@@ -96,7 +84,7 @@ export class ProfileEffects {
)
);
- constructor(private actions$: Actions, private service: ProfileService, private store: Store) {}
+ constructor(private actions$: Actions, private service: ProfileService) {}
}
export const getProfileSelector = createFeatureSelector('profile');
diff --git a/web/src/app/core/store/profile/profile.service.ts b/web/src/app/core/store/profile/profile.service.ts
index 38a26c5fae..89240764ec 100644
--- a/web/src/app/core/store/profile/profile.service.ts
+++ b/web/src/app/core/store/profile/profile.service.ts
@@ -23,7 +23,6 @@ const PROFILE_LINK = `${environment.apiRoot}profile/`;
export interface IProfile {
dashboard: any[];
textarea: { [key: string]: number };
- metrics: boolean;
settingsSaved: boolean;
}
@@ -48,7 +47,7 @@ export class ProfileService {
}
emptyProfile() {
- return { dashboard: PROFILE_DASHBOARD_DEFAULT, textarea: {}, metrics: null, settingsSaved: false };
+ return { dashboard: PROFILE_DASHBOARD_DEFAULT, textarea: {}, settingsSaved: false };
}
setUser(key: string, value: string | boolean | { [key: string]: number }) {
diff --git a/web/src/app/core/store/sockets/socket.reducer.ts b/web/src/app/core/store/sockets/socket.reducer.ts
index d9a3938579..70f2258fd6 100644
--- a/web/src/app/core/store/sockets/socket.reducer.ts
+++ b/web/src/app/core/store/sockets/socket.reducer.ts
@@ -10,7 +10,18 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import { TypeName } from '@app/core/types';
-import { Action, createAction, createFeatureSelector, createReducer, createSelector, on, props } from '@ngrx/store';
+import {
+ Action,
+ createAction,
+ createFeatureSelector,
+ createReducer,
+ createSelector,
+ on,
+ props,
+ select
+} from '@ngrx/store';
+import { pipe } from 'rxjs';
+import { skip } from 'rxjs/operators';
/**
* Event Message Object dispatched from socket
@@ -25,21 +36,23 @@ export interface IEMObject {
};
}
+export type EntityEvent =
+ | 'add'
+ | 'add_job_log'
+ | 'create'
+ | 'delete'
+ | 'remove'
+ | 'change_config'
+ | 'change_state'
+ | 'change_status'
+ | 'change_job_status'
+ | 'change_hostcomponentmap'
+ | 'raise_issue'
+ | 'clear_issue'
+ | 'upgrade';
+
export interface EventMessage {
- event:
- | 'add'
- | 'add_job_log'
- | 'create'
- | 'delete'
- | 'remove'
- | 'change_config'
- | 'change_state'
- | 'change_status'
- | 'change_job_status'
- | 'change_hostcomponentmap'
- | 'raise_issue'
- | 'clear_issue'
- | 'upgrade';
+ event: EntityEvent;
object?: IEMObject;
}
@@ -86,3 +99,7 @@ export function socketReducer(state: SocketState, action: Action) {
export const getSocketState = createFeatureSelector('socket');
export const getConnectStatus = createSelector(getSocketState, (state: SocketState) => state.status);
export const getMessage = createSelector(getSocketState, (state) => state.message);
+export const selectMessage = pipe(
+ select(getMessage),
+ skip(1),
+);
diff --git a/web/src/app/core/types/api.ts b/web/src/app/core/types/api.ts
index 34eba40a79..335a93c315 100644
--- a/web/src/app/core/types/api.ts
+++ b/web/src/app/core/types/api.ts
@@ -14,7 +14,23 @@ import { IComponent } from './host-component';
import { Issue } from './issue';
import { Job, Task } from './task-job';
-export type TypeName = 'bundle' | 'cluster' | 'host' | 'provider' | 'service' | 'job' | 'task' | 'user' | 'profile' | 'adcm' | 'stats' | 'hostcomponent' | 'component';
+export type TypeName =
+ 'bundle' |
+ 'cluster' |
+ 'host' |
+ 'provider' |
+ 'service' |
+ 'job' |
+ 'task' |
+ 'user' |
+ 'profile' |
+ 'adcm' |
+ 'stats' |
+ 'hostcomponent' |
+ 'service2cluster' |
+ 'host2cluster' |
+ 'servicecomponent' |
+ 'component';
export type Entities = Cluster | Service | Host | Provider | Job | Task | Bundle;
/**
@@ -78,6 +94,7 @@ export interface Service extends ApiBase {
status: number;
hostcomponent: string;
display_name: string;
+ cluster_id?: number;
}
export interface Bundle extends ApiBase {
diff --git a/web/src/app/core/types/task-job.ts b/web/src/app/core/types/task-job.ts
index d040b974d8..29afb43d7a 100644
--- a/web/src/app/core/types/task-job.ts
+++ b/web/src/app/core/types/task-job.ts
@@ -13,10 +13,12 @@ import { ApiBase } from './api';
export type JobStatus = 'created' | 'running' | 'failed' | 'success' | 'aborted';
+export type JobType = 'component' | 'service' | 'cluster';
+
export interface JobObject {
id: number;
name: string;
- type: string;
+ type: JobType;
url?: string[];
}
@@ -31,7 +33,7 @@ interface TaskBase {
}
export interface JobAction {
- prototype_name?: string;
+ prototype_name?: string;
prototype_version?: string;
bundle_id?: number;
display_name: string;
@@ -56,7 +58,7 @@ export interface LogFile {
type: string;
format: 'txt' | 'json';
download_url: string;
- content: string | CheckLog[];
+ content: string | CheckLog[];
}
export interface CheckLog {
diff --git a/web/src/app/directives/popover.directive.ts b/web/src/app/directives/popover.directive.ts
new file mode 100644
index 0000000000..0278c72139
--- /dev/null
+++ b/web/src/app/directives/popover.directive.ts
@@ -0,0 +1,76 @@
+import {
+ Directive,
+ ElementRef,
+ HostListener,
+ Input,
+ ViewContainerRef,
+ ComponentFactoryResolver,
+ ComponentFactory,
+ ComponentRef,
+ OnDestroy,
+ OnInit,
+ Renderer2, Type,
+} from '@angular/core';
+import { BaseDirective } from '@adwp-ui/widgets';
+
+import { PopoverComponent } from '@app/components/popover/popover.component';
+import { PopoverContentDirective } from '@app/abstract-directives/popover-content.directive';
+
+export interface PopoverInput { [inputKey: string]: any; }
+
+@Directive({
+ selector: '[appPopover]'
+})
+export class PopoverDirective extends BaseDirective implements OnInit, OnDestroy {
+
+ containerRef: ComponentRef;
+ factory: ComponentFactory;
+ leaveListener: () => void;
+
+ @Input() component: Type;
+ @Input() data: PopoverInput = {};
+
+ constructor(
+ private elementRef: ElementRef,
+ public viewContainer: ViewContainerRef,
+ public componentFactoryResolver: ComponentFactoryResolver,
+ public renderer: Renderer2,
+ ) {
+ super();
+ }
+
+ ngOnInit() {
+ this.factory = this.componentFactoryResolver.resolveComponentFactory(PopoverComponent);
+ }
+
+ @HostListener('mouseenter') mouseEnter() {
+ if (this.component) {
+ this.containerRef = this.viewContainer.createComponent(this.factory);
+ this.containerRef.instance.component = this.component;
+ this.containerRef.instance.data = this.data;
+ this.leaveListener = this.renderer.listen(
+ this.elementRef.nativeElement.parentElement,
+ 'mouseleave',
+ () => this.clear(),
+ );
+ }
+ }
+
+ clear() {
+ if (this.containerRef) {
+ this.containerRef.destroy();
+ }
+
+ this.viewContainer.clear();
+
+ if (this.leaveListener) {
+ this.elementRef.nativeElement.parentElement.removeEventListener('mouseleave', this.leaveListener);
+ }
+ }
+
+ ngOnDestroy() {
+ super.ngOnDestroy();
+ this.clear();
+ }
+
+}
diff --git a/web/src/app/entry/bundle/bundle.module.ts b/web/src/app/entry/bundle/bundle.module.ts
index e71dfe0d50..0b3c5ba848 100644
--- a/web/src/app/entry/bundle/bundle.module.ts
+++ b/web/src/app/entry/bundle/bundle.module.ts
@@ -12,9 +12,11 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { Routes, RouterModule } from '@angular/router';
+
import { AuthGuard } from '@app/core';
import { StackComponent, MainComponent } from './stack.component';
-import { DetailComponent, SharedModule } from '@app/shared';
+import { DetailComponent } from '@app/shared/details/detail.component';
+import { SharedModule } from '@app/shared/shared.module';
const routes: Routes = [
{
diff --git a/web/src/app/entry/bundle/stack.component.ts b/web/src/app/entry/bundle/stack.component.ts
index 914aa10c76..930e4e8dda 100644
--- a/web/src/app/entry/bundle/stack.component.ts
+++ b/web/src/app/entry/bundle/stack.component.ts
@@ -9,8 +9,22 @@
// 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, OnInit, ViewChild } from '@angular/core';
-import { ClusterService, StackService } from '@app/core';
+import { Component, ComponentRef, OnInit, ViewChild } from '@angular/core';
+import { Store } from '@ngrx/store';
+import { ActivatedRoute, Router } from '@angular/router';
+import { MatDialog } from '@angular/material/dialog';
+import { IColumns } from '@adwp-ui/widgets';
+
+import { StackService } from '@app/core';
+import { ClusterService } from '@app/core/services/cluster.service';
+import { AdwpListDirective } from '@app/abstract-directives/adwp-list.directive';
+import { ListService } from '@app/shared/components/list/list.service';
+import { SocketState } from '@app/core/store';
+import { TypeName } from '@app/core/types';
+import { IBundle } from '@app/models/bundle';
+import { ListFactory } from '@app/factories/list-factory';
+import { EditionColumnComponent } from '@app/components/columns/edition-column/edition-column.component';
+import { ApiService } from '@app/core/api';
@Component({
selector: 'app-stack',
@@ -19,17 +33,68 @@ import { ClusterService, StackService } from '@app/core';
-
+
+
`,
styles: [':host { flex: 1; }'],
})
-export class StackComponent {
- typeName = 'bundle';
+export class StackComponent extends AdwpListDirective {
+
+ type: TypeName = 'bundle';
+
+ listColumns = [
+ ListFactory.nameColumn(),
+ {
+ label: 'Version',
+ sort: 'version',
+ value: row => row.version,
+ },
+ {
+ label: 'Edition',
+ type: 'component',
+ component: EditionColumnComponent,
+ instanceTaken: (componentRef: ComponentRef) => {
+ componentRef.instance.onClick
+ .pipe(this.takeUntil())
+ .subscribe(
+ (data: { event: MouseEvent, action: string, row: any }) =>
+ this.clickCell(data.event, data.action, data.row)
+ );
+ }
+ },
+ ListFactory.descriptionColumn(),
+ ListFactory.deleteColumn(this),
+ ] as IColumns;
+
@ViewChild('uploadBtn', { static: true }) uploadBtn: any;
- constructor(private stack: StackService) {}
+
+ constructor(
+ private stack: StackService,
+ protected service: ListService,
+ protected store: Store,
+ public route: ActivatedRoute,
+ public router: Router,
+ public dialog: MatDialog,
+ protected api: ApiService,
+ ) {
+ super(service, store, route, router, dialog, api);
+ }
+
upload(data: FormData[]) {
this.stack.upload(data).subscribe();
}
+
}
@Component({
diff --git a/web/src/app/entry/cluster/cluster.component.ts b/web/src/app/entry/cluster/cluster.component.ts
index d9c81212e9..1c869072d1 100644
--- a/web/src/app/entry/cluster/cluster.component.ts
+++ b/web/src/app/entry/cluster/cluster.component.ts
@@ -9,53 +9,57 @@
// 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, OnInit } from '@angular/core';
-import { ClusterService } from '@app/core';
+import { Component } from '@angular/core';
+import { IColumns } from '@adwp-ui/widgets';
-@Component({
- selector: 'app-cluster-host',
- template: `
- Add hosts
-
- `,
- styles: [':host { flex: 1; }', '.add-button {position:fixed; right: 20px;top:120px;}'],
-})
-export class HostComponent {}
-
-@Component({
- selector: 'app-services',
- template: `
- Add services
-
- `,
- styles: [':host { flex: 1; }', '.add-button {position:fixed; right: 20px;top:120px;}'],
-})
-export class ServicesComponent {}
+import { ICluster } from '@app/models/cluster';
+import { TypeName } from '@app/core/types';
+import { AdwpListDirective } from '@app/abstract-directives/adwp-list.directive';
+import { ListFactory } from '@app/factories/list-factory';
@Component({
template: `
- Create {{ typeName }}
+ Create {{ type }}
-
+
+
`,
- styles: [':host { flex: 1; }'],
+ styles: [`
+ :host {
+ flex: 1;
+ max-width: 100%;
+ }
+ `],
})
-export class ClusterListComponent {
- typeName = 'cluster';
-}
+export class ClusterListComponent extends AdwpListDirective {
-@Component({
- template: ` `,
- styles: [':host { flex: 1; }'],
-})
-export class HcmapComponent implements OnInit {
- cluster: { id: number; hostcomponent: string };
- constructor(private service: ClusterService) {}
+ type: TypeName = 'cluster';
+
+ listColumns = [
+ ListFactory.nameColumn(),
+ ListFactory.bundleColumn(),
+ ListFactory.descriptionColumn(),
+ ListFactory.stateColumn(),
+ ListFactory.statusColumn(this),
+ ListFactory.actionsButton('cluster'),
+ ListFactory.importColumn(this),
+ ListFactory.updateColumn(),
+ ListFactory.configColumn(this),
+ ListFactory.deleteColumn(this),
+ ] as IColumns;
- ngOnInit() {
- const { id, hostcomponent } = { ...this.service.Cluster };
- this.cluster = { id, hostcomponent };
- }
}
+
diff --git a/web/src/app/entry/cluster/cluster.module.ts b/web/src/app/entry/cluster/cluster.module.ts
index d919b81289..9fe42d13d8 100644
--- a/web/src/app/entry/cluster/cluster.module.ts
+++ b/web/src/app/entry/cluster/cluster.module.ts
@@ -12,11 +12,19 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
-import { SharedModule, DetailComponent, MainInfoComponent, ConfigComponent, StatusComponent, ImportComponent } from '@app/shared';
-import { ClusterListComponent, HcmapComponent, HostComponent, ServicesComponent } from './cluster.component';
+import { DetailComponent } from '@app/shared/details/detail.component';
+import { ConfigComponent } from '@app/shared/configuration/main/main.component';
+import { MainInfoComponent, StatusComponent, ImportComponent } from '@app/shared/components';
+import { SharedModule } from '@app/shared/shared.module';
+
+import { ClusterListComponent } from './cluster.component';
+import { HcmapComponent } from '@app/components/cluster/hcmap/hcmap.component';
+import { HostComponent } from '@app/components/cluster/host/host.component';
+import { ServicesComponent } from '@app/components/cluster/services/services.component';
import { AuthGuard } from '@app/core';
import { ActionCardComponent } from '@app/shared/components/actions/action-card/action-card.component';
+import { ServiceComponentsComponent } from '@app/components/service-components.component';
const clusterRoutes: Routes = [
@@ -54,6 +62,20 @@ const clusterRoutes: Routes = [
{ path: 'status', component: StatusComponent },
{ path: 'import', component: ImportComponent },
{ path: 'action', component: ActionCardComponent },
+ { path: 'component', component: ServiceComponentsComponent },
+ ],
+ },
+ {
+ path: ':cluster/service/:service/component/:servicecomponent',
+ component: DetailComponent,
+ canActivate: [AuthGuard],
+ canActivateChild: [AuthGuard],
+ children: [
+ { path: '', redirectTo: 'main', pathMatch: 'full' },
+ { path: 'main', component: MainInfoComponent },
+ { path: 'config', component: ConfigComponent },
+ { path: 'status', component: StatusComponent },
+ { path: 'action', component: ActionCardComponent },
],
},
{
@@ -72,7 +94,9 @@ const clusterRoutes: Routes = [
];
@NgModule({
- imports: [RouterModule.forChild(clusterRoutes)],
+ imports: [
+ RouterModule.forChild(clusterRoutes),
+ ],
exports: [RouterModule],
})
export class ClusterRoutingModule {}
diff --git a/web/src/app/entry/entry.module.ts b/web/src/app/entry/entry.module.ts
index 3a6da2cf7c..7bd4ae36d6 100644
--- a/web/src/app/entry/entry.module.ts
+++ b/web/src/app/entry/entry.module.ts
@@ -13,10 +13,15 @@ import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { RouterModule } from '@angular/router';
import { AuthGuard } from '@app/core';
-import { ConfigComponent, DetailComponent, MainInfoComponent, SharedModule, StatusComponent } from '@app/shared';
+import { DetailComponent } from '@app/shared/details/detail.component';
+import { ConfigComponent } from '@app/shared/configuration/main/main.component';
+import { MainInfoComponent, StatusComponent } from '@app/shared/components';
+import { SharedModule } from '@app/shared/shared.module';
import { ListEntryComponent } from './list.component';
import { ActionCardComponent } from '@app/shared/components/actions/action-card/action-card.component';
+import { HostproviderComponent } from '@app/components/hostprovider/hostprovider.component';
+import { HostListComponent } from '@app/components/host-list/host-list.component';
const entryRouter = [
{
@@ -37,7 +42,7 @@ const entryRouter = [
},
{
path: 'host',
- component: ListEntryComponent,
+ component: HostListComponent,
canActivate: [AuthGuard],
},
{
@@ -63,7 +68,7 @@ const entryRouter = [
{
path: 'provider',
canActivate: [AuthGuard],
- component: ListEntryComponent,
+ component: HostproviderComponent,
},
{
path: 'provider/:provider',
@@ -80,6 +85,6 @@ const entryRouter = [
@NgModule({
imports: [CommonModule, SharedModule, RouterModule.forChild(entryRouter)],
- declarations: [ListEntryComponent],
+ declarations: [ListEntryComponent, HostproviderComponent, HostListComponent],
})
export class EntryModule {}
diff --git a/web/src/app/entry/job/job.component.ts b/web/src/app/entry/job/job.component.ts
index e5d131b75c..014b3a4635 100644
--- a/web/src/app/entry/job/job.component.ts
+++ b/web/src/app/entry/job/job.component.ts
@@ -11,18 +11,19 @@
// limitations under the License.
import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
-import { ClusterService } from '@app/core';
+import { filter } from 'rxjs/operators';
+
+import { ClusterService } from '@app/core/services/cluster.service';
import { Job } from '@app/core/types';
-import { BaseDirective } from '@app/shared';
+import { BaseDirective } from '@app/shared/directives';
import { ListComponent } from '@app/shared/components/list/list.component';
-import { filter } from 'rxjs/operators';
@Component({
selector: 'app-job',
template: `
`,
})
diff --git a/web/src/app/entry/job/job.module.ts b/web/src/app/entry/job/job.module.ts
index 967c868886..f4f9c8db32 100644
--- a/web/src/app/entry/job/job.module.ts
+++ b/web/src/app/entry/job/job.module.ts
@@ -12,8 +12,8 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { RouterModule } from '@angular/router';
-import { SharedModule } from '@app/shared';
+import { SharedModule } from '@app/shared/shared.module';
import { JobInfoComponent } from './job-info.component';
import { JobRoutingModule } from './job-routing.module';
import { JobComponent, MainComponent } from './job.component';
diff --git a/web/src/app/entry/job/log/log.component.spec.ts b/web/src/app/entry/job/log/log.component.spec.ts
index 8ccd24ec18..3797c6d9d0 100644
--- a/web/src/app/entry/job/log/log.component.spec.ts
+++ b/web/src/app/entry/job/log/log.component.spec.ts
@@ -12,7 +12,7 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { MatIconModule } from '@angular/material/icon';
import { ActivatedRoute, convertToParamMap } from '@angular/router';
-import { ClusterService } from '@app/core';
+import { ClusterService } from '@app/core/services/cluster.service';
import { ApiService } from '@app/core/api';
import { EventMessage, getMessage, SocketState } from '@app/core/store';
import { Job, LogFile } from '@app/core/types';
diff --git a/web/src/app/entry/job/log/log.component.ts b/web/src/app/entry/job/log/log.component.ts
index 32df0328bd..d32623d9a5 100644
--- a/web/src/app/entry/job/log/log.component.ts
+++ b/web/src/app/entry/job/log/log.component.ts
@@ -11,13 +11,14 @@
// limitations under the License.
import { Component, OnInit, ViewChild } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
-import { ClusterService } from '@app/core';
-import { EventMessage, SocketState } from '@app/core/store';
-import { Job, JobStatus, LogFile } from '@app/core/types';
-import { SocketListenerDirective } from '@app/shared';
import { Store } from '@ngrx/store';
import { Subject } from 'rxjs';
+import { ClusterService } from '@app/core/services/cluster.service';
+import { EventMessage, SocketState } from '@app/core/store';
+import { Job, JobStatus, LogFile } from '@app/core/types';
+import { SocketListenerDirective } from '@app/shared/directives';
+
import { TextComponent } from './text.component';
export interface ITimeInfo {
diff --git a/web/src/app/entry/job/log/text.component.ts b/web/src/app/entry/job/log/text.component.ts
index 1718df0039..96f11a238d 100644
--- a/web/src/app/entry/job/log/text.component.ts
+++ b/web/src/app/entry/job/log/text.component.ts
@@ -10,10 +10,11 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import { Component, DoCheck, ElementRef, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core';
-import { JobStatus } from '@app/core/types/task-job';
-import { BaseDirective } from '@app/shared';
import { interval, Subscription } from 'rxjs';
+import { JobStatus } from '@app/core/types/task-job';
+import { BaseDirective } from '@app/shared/directives';
+
@Component({
selector: 'app-log-text',
styles: [
diff --git a/web/src/app/entry/list.component.ts b/web/src/app/entry/list.component.ts
index 46fbfb7d05..e4978f2080 100644
--- a/web/src/app/entry/list.component.ts
+++ b/web/src/app/entry/list.component.ts
@@ -19,7 +19,7 @@ import { ActivatedRoute } from '@angular/router';
Create {{ typeName }}
-
+
`,
styles: [':host { flex: 1; }'],
})
diff --git a/web/src/app/entry/task/hover.directive.spec.ts b/web/src/app/entry/task/hover.directive.spec.ts
index 2a4b35634b..ecac251e0e 100644
--- a/web/src/app/entry/task/hover.directive.spec.ts
+++ b/web/src/app/entry/task/hover.directive.spec.ts
@@ -13,7 +13,7 @@ import { Component, DebugElement } from '@angular/core';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
-import { SharedModule } from '@app/shared';
+import { SharedModule } from '@app/shared/shared.module';
import { HoverDirective } from './hover.directive';
@@ -53,7 +53,7 @@ describe('HoverStatusTaskDirective', () => {
});
// mousehover
- it('should change icon `block` by mouseover', () => {
+ it('should change icon `block` by mouseover', () => {
a.triggerEventHandler('mouseover', {});
fixture.detectChanges();
expect(a.nativeElement.querySelector('mat-icon').innerText).toEqual('block');
diff --git a/web/src/app/entry/task/inner.component.spec.ts b/web/src/app/entry/task/inner.component.spec.ts
deleted file mode 100644
index 0235f71668..0000000000
--- a/web/src/app/entry/task/inner.component.spec.ts
+++ /dev/null
@@ -1,94 +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 { ComponentFixture, TestBed } from '@angular/core/testing';
-import { MatIconModule } from '@angular/material/icon';
-import { MatTableModule } from '@angular/material/table';
-import { MatTooltipModule } from '@angular/material/tooltip';
-import { RouterTestingModule } from '@angular/router/testing';
-import { Job } from '@app/core/types';
-
-import { InnerComponent } from './inner.component';
-
-describe('InnerComponent', () => {
- let component: InnerComponent;
- let fixture: ComponentFixture;
-
- beforeEach(async () => {
- TestBed.configureTestingModule({
- imports: [MatTableModule, MatIconModule, MatTooltipModule, RouterTestingModule],
- declarations: [InnerComponent],
- }).compileComponents();
- });
-
- beforeEach(() => {
- fixture = TestBed.createComponent(InnerComponent);
- component = fixture.componentInstance;
- fixture.detectChanges();
- });
-
- it('should create', () => {
- expect(component).toBeTruthy();
- });
-
- it('job should display as row', () => {
- component.dataSource = [{ status: 'created', display_name: 'job_test', id: 1, start_date: '2020-07-30T12:28:59.431072Z', finish_date: '2020-07-30T12:29:00.222917Z' }] as Job[];
- fixture.detectChanges();
- const rows = fixture.nativeElement.querySelectorAll('table tr');
- expect(rows.length).toBe(1);
- });
-
- it('changing job status should display the appropriate icon', () => {
- const getIcon = () => fixture.nativeElement.querySelector('table tr td:last-child mat-icon');
- component.dataSource = [{ status: 'created', display_name: 'job_test', id: 1, start_date: '2020-07-30T12:28:59.431072Z', finish_date: '2020-07-30T12:29:00.222917Z' }] as Job[];
- fixture.detectChanges();
- const last_icon = getIcon();
- expect(last_icon.innerText).toBe('watch_later');
-
- component.dataSource[0].status = 'running';
- fixture.detectChanges();
- const last_icon2 = getIcon();
- expect(last_icon2.innerText).toBe('autorenew');
-
- component.dataSource[0].status = 'success';
- fixture.detectChanges();
- const last_icon3 = getIcon();
- expect(last_icon3.innerText).toBe('done');
-
- component.dataSource[0].status = 'failed';
- fixture.detectChanges();
- const last_icon4 = getIcon();
- expect(last_icon4.innerText).toBe('error');
-
- component.dataSource[0].status = 'aborted';
- fixture.detectChanges();
- const last_icon5 = getIcon();
- expect(last_icon5.innerText).toBe('block');
- });
-
- it('job property should dislplay in the columns of row table', () => {
- component.dataSource = [{ status: 'created', display_name: 'job_test', id: 1, start_date: '2020-07-30T12:28:59.431072Z', finish_date: '2020-07-30T12:29:00.222917Z' }] as Job[];
- fixture.detectChanges();
- const tds = fixture.nativeElement.querySelectorAll('table tr td');
- expect(tds[0].innerText).toBe('job_test');
- expect(tds[1].innerText).toBeFalsy();
- expect(tds[2].innerText).toBeFalsy();
-
- component.dataSource[0].status = 'running';
- fixture.detectChanges();
- expect(tds[1].innerText).toBeTruthy(); //.toBe('Jul 30, 2020, 3:28:59 PM');
-
- component.dataSource[0].status = 'success';
- fixture.detectChanges();
- expect(tds[1].innerText).toBeTruthy(); //.toBe('Jul 30, 2020, 3:28:59 PM');
- expect(tds[2].innerText).toBeTruthy(); //.toBe('Jul 30, 2020, 3:29:00 PM');
- });
-});
diff --git a/web/src/app/entry/task/inner.component.ts b/web/src/app/entry/task/inner.component.ts
deleted file mode 100644
index a227966530..0000000000
--- a/web/src/app/entry/task/inner.component.ts
+++ /dev/null
@@ -1,61 +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 { Component, Input } from '@angular/core';
-import { Job } from '@app/core/types';
-
-@Component({
- selector: 'app-task-inner',
- template: `
-
-
-
- {{ row.display_name || row.id }}
-
- {{ row.display_name || row.id }}
-
- |
-
-
-
- {{ row.status !== 'created' ? (row.start_date | date: 'medium') : '' }}
- |
-
-
-
- {{ row.status === 'success' || row.status === 'failed' ? (row.finish_date | date: 'medium') : '' }}
- |
-
-
-
-
- {{ iconDisplay[row.status] }}
-
- |
-
-
-
- `,
- styleUrls: ['./task.component.scss']
-})
-export class InnerComponent {
- displayColumns = ['job_name', 'start_date_job', 'finish_date_job', 'status_job'];
-
- @Input() dataSource: Job[];
-
- iconDisplay = {
- created: 'watch_later',
- running: 'autorenew',
- success: 'done',
- failed: 'error',
- aborted: 'block'
- };
-}
diff --git a/web/src/app/entry/task/task.component.html b/web/src/app/entry/task/task.component.html
deleted file mode 100644
index f6d736ce56..0000000000
--- a/web/src/app/entry/task/task.component.html
+++ /dev/null
@@ -1,89 +0,0 @@
-
-
-
-
-
-
-
-
- # |
-
- {{ row.id }}
- |
-
-
-
- Action name |
-
-
- {{ element.action?.display_name || 'unknown' }}
-
-
-
- {{ element.action?.display_name || 'unknown' }}
-
- {{ expandedTask?.id === element.id ? 'expand_less' : 'expand_more' }}
-
-
- |
-
-
-
- Objects |
-
-
- {{ obj.name }}
- /
-
- |
-
-
-
- Start date |
- {{ element.start_date | date: 'medium' }} |
-
-
-
- Finish date |
-
- {{ row.status === 'success' || row.status === 'failed' ? (row.finish_date | date: 'medium') : '' }}
- |
-
-
-
- Status |
-
-
-
-
- autorenew
-
-
-
-
- {{ getIcon(row.status) }}
-
-
- |
-
-
-
-
-
- |
-
-
-
-
-
-
-
-
-
-
diff --git a/web/src/app/entry/task/task.component.scss b/web/src/app/entry/task/task.component.scss
deleted file mode 100644
index c4cf50b8b2..0000000000
--- a/web/src/app/entry/task/task.component.scss
+++ /dev/null
@@ -1,68 +0,0 @@
-:host {
- flex: 1;
-}
-
-.multi-title {
- cursor: pointer;
-}
-
-.mat-icon {
- vertical-align: middle;
- font-size: 1.2em;
- width: auto;
- height: auto;
-}
-
-table.main {
- width: 100%;
-
- & .first-child {
- width: 20px;
- padding-right: 10px;
- }
-}
-
-.action_date {
- width: 200px;
-}
-
-.padding20 {
- padding-right: 20px;
-}
-
-.parent-end {
- width: 84px;
-}
-
-.end {
- width: 40px;
- padding-left: 30px !important;
-}
-
-.center {
- text-align: center;
-}
-
-table.inner {
- width: 100%;
- margin-bottom: 10px;
-
- & tr:last-child td {
- border-bottom-width: 0;
- }
-}
-
-td.mat-cell,
-th.mat-header-cell {
- white-space: nowrap;
- padding: 0 10px;
-}
-
-tr.jobs-row {
- height: 0;
-}
-
-.expand-jobs {
- overflow: hidden;
- padding: 0 12px;
-}
diff --git a/web/src/app/entry/task/task.module.ts b/web/src/app/entry/task/task.module.ts
index 1fc6c32498..94a82d96f3 100644
--- a/web/src/app/entry/task/task.module.ts
+++ b/web/src/app/entry/task/task.module.ts
@@ -13,11 +13,19 @@ import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { AuthGuard } from '@app/core';
-import { SharedModule } from '@app/shared';
+import { SharedModule } from '@app/shared/shared.module';
import { HoverDirective } from './hover.directive';
import { TasksComponent } from './tasks.component';
-import { InnerComponent } from './inner.component';
+import { TaskObjectsComponent } from '@app/components/columns/task-objects/task-objects.component';
+import { ObjectLinkColumnPipe } from '@app/pipes/object-link-column.pipe';
+import { SortObjectsPipe } from '@app/pipes/sort-objects.pipe';
+import { TaskStatusColumnComponent } from '@app/components/columns/task-status-column/task-status-column.component';
+import { JobsComponent } from '@app/components/task/jobs/jobs.component';
+import { JobStatusColumnComponent } from '@app/components/columns/job-status-column/job-status-column.component';
+import { TaskNameComponent } from '@app/components/columns/task-name/task-name.component';
+import { TaskService } from '@app/services/task.service';
+import { JobService } from '@app/services/job.service';
const routes: Routes = [
{
@@ -28,13 +36,25 @@ const routes: Routes = [
];
@NgModule({
- imports: [RouterModule.forChild(routes)],
- exports: [RouterModule]
-})
-export class TaskRoutingModule {}
-
-@NgModule({
- imports: [CommonModule, TaskRoutingModule, SharedModule],
- declarations: [TasksComponent, HoverDirective, InnerComponent]
+ imports: [
+ CommonModule,
+ SharedModule,
+ RouterModule.forChild(routes),
+ ],
+ declarations: [
+ TasksComponent,
+ HoverDirective,
+ TaskObjectsComponent,
+ ObjectLinkColumnPipe,
+ SortObjectsPipe,
+ TaskStatusColumnComponent,
+ TaskNameComponent,
+ JobsComponent,
+ JobStatusColumnComponent,
+ ],
+ providers: [
+ TaskService,
+ JobService,
+ ],
})
export class TaskModule {}
diff --git a/web/src/app/entry/task/tasks.component.html b/web/src/app/entry/task/tasks.component.html
new file mode 100644
index 0000000000..94d399c614
--- /dev/null
+++ b/web/src/app/entry/task/tasks.component.html
@@ -0,0 +1,33 @@
+
+
+
+
+ All
+ In progress
+ Success
+ Failed
+
+
+
+
diff --git a/web/src/app/entry/task/tasks.component.scss b/web/src/app/entry/task/tasks.component.scss
new file mode 100644
index 0000000000..b4065f3b6a
--- /dev/null
+++ b/web/src/app/entry/task/tasks.component.scss
@@ -0,0 +1,109 @@
+:host {
+ flex: 1;
+}
+
+.multi-title {
+ cursor: pointer;
+}
+
+.mat-icon {
+ vertical-align: middle;
+ font-size: 1.2em;
+ width: auto;
+ height: auto;
+}
+
+table.main {
+ width: 100%;
+}
+
+:host ::ng-deep .first-child {
+ width: 20px;
+ padding-right: 10px;
+ flex-grow: 0;
+ flex-basis: 20px;
+}
+
+:host ::ng-deep .action_date {
+ width: 200px;
+ flex-basis: 200px;
+ flex-grow: 0;
+}
+
+:host ::ng-deep .regular-table {
+ mat-row:not(:hover) {
+ background-color: #303030 !important;
+ }
+ mat-cell {
+ cursor: auto;
+ }
+ adwp-link-cell {
+ cursor: pointer;
+ }
+}
+
+:host ::ng-deep .expandedRow > mat-cell {
+ padding: 0;
+}
+
+.padding20 {
+ padding-right: 20px;
+}
+
+:host ::ng-deep .parent-end {
+ width: 84px;
+ flex-basis: 84px;
+ flex-grow: 0;
+}
+
+:host ::ng-deep .table-end {
+ width: 50px;
+ flex-basis: 50px;
+ flex-grow: 0;
+}
+
+.end {
+ width: 40px;
+ padding-left: 30px !important;
+}
+
+:host ::ng-deep .center {
+ text-align: center;
+}
+
+:host ::ng-deep .status {
+ display: flex;
+ justify-content: center;
+}
+
+table.inner {
+ width: 100%;
+ margin-bottom: 10px;
+
+ & tr:last-child td {
+ border-bottom-width: 0;
+ }
+}
+
+td.mat-cell,
+th.mat-header-cell {
+ white-space: nowrap;
+ padding: 0 10px;
+}
+
+tr.jobs-row {
+ height: 0;
+}
+
+.expand-jobs {
+ overflow: hidden;
+ padding: 0 12px;
+}
+
+.toggle {
+ font-size: small;
+}
+
+:host ::ng-deep .toggle .mat-button-toggle-label-content {
+ line-height: 36px;
+}
diff --git a/web/src/app/entry/task/tasks.component.ts b/web/src/app/entry/task/tasks.component.ts
index 1ce1583869..06278d22e6 100644
--- a/web/src/app/entry/task/tasks.component.ts
+++ b/web/src/app/entry/task/tasks.component.ts
@@ -9,169 +9,267 @@
// 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 { animate, state, style, transition, trigger } from '@angular/animations';
-import { Component, ElementRef, OnInit, QueryList, ViewChild, ViewChildren } from '@angular/core';
-import { MatDialog } from '@angular/material/dialog';
-import { MatPaginator, PageEvent } from '@angular/material/paginator';
-import { MatSort, MatSortHeader } from '@angular/material/sort';
-import { MatTableDataSource } from '@angular/material/table';
-import { ActivatedRoute, ParamMap, Router } from '@angular/router';
-import { ApiService } from '@app/core/api';
-import { EventMessage, SocketState } from '@app/core/store';
-import { JobStatus, Task, JobObject } from '@app/core/types';
-import { DialogComponent, SocketListenerDirective } from '@app/shared';
-import { Store } from '@ngrx/store';
-import { filter, switchMap } from 'rxjs/operators';
+import { Component, OnInit, ComponentRef } from '@angular/core';
+import { ActivatedRoute, Router } from '@angular/router';
+import { filter, tap } from 'rxjs/operators';
+import { BehaviorSubject, Observable } from 'rxjs';
+import { BaseDirective, IColumns, IListResult, InstanceTakenFunc, Paging } from '@adwp-ui/widgets';
+import { DateHelper } from '@app/helpers/date-helper';
+
+import { EventMessage } from '@app/core/store';
+import { JobStatus, Task, Job } from '@app/core/types';
+import { TaskObjectsComponent } from '@app/components/columns/task-objects/task-objects.component';
+import { TaskStatusColumnComponent } from '@app/components/columns/task-status-column/task-status-column.component';
+import { JobsComponent } from '@app/components/task/jobs/jobs.component';
+import { TaskNameComponent } from '@app/components/columns/task-name/task-name.component';
+import { TaskService } from '@app/services/task.service';
+import { JobService } from '@app/services/job.service';
+import { MatButtonToggleChange } from '@angular/material/button-toggle';
+
+type TaskStatus = '' | 'running' | 'success' | 'failed';
@Component({
selector: 'app-tasks',
- templateUrl: './task.component.html',
- styleUrls: ['./task.component.scss'],
- animations: [
- trigger('jobsExpand', [
- state('collapsed', style({ height: '0px', minHeight: '0' })),
- state('expanded', style({ height: '*' })),
- transition('expanded <=> collapsed', animate('225ms cubic-bezier(0.4, 0.0, 0.2, 1)')),
- ]),
- ],
+ templateUrl: './tasks.component.html',
+ styleUrls: ['./tasks.component.scss'],
})
-export class TasksComponent extends SocketListenerDirective implements OnInit {
- isDisabled = false;
-
- dataSource = new MatTableDataSource([]);
- columnsToDisplay = ['id', 'name', 'objects', 'start_date', 'finish_date', 'status'];
- expandedTask: Task | null;
+export class TasksComponent extends BaseDirective implements OnInit {
- paramMap: ParamMap;
- dataCount = 0;
+ JobsComponent = JobsComponent;
+ expandedTask = new BehaviorSubject(null);
- @ViewChild(MatPaginator, { static: true })
- paginator: MatPaginator;
+ data$: BehaviorSubject> = new BehaviorSubject(null);
+ paging: BehaviorSubject = new BehaviorSubject(null);
+ status: TaskStatus = '';
- @ViewChild(MatSort, { static: true })
- sort: MatSort;
+ listColumns = [
+ {
+ label: '#',
+ value: (row) => row.id,
+ className: 'first-child',
+ headerClassName: 'first-child',
+ },
+ {
+ type: 'component',
+ label: 'Action name',
+ component: TaskNameComponent,
+ instanceTaken: (componentRef: ComponentRef) => {
+ componentRef.instance.expandedTask = this.expandedTask;
+ componentRef.instance.toggleExpand = (row) => {
+ this.expandedTask.next(
+ this.expandedTask.value && this.expandedTask.value.id === row.id ? null : row
+ );
+ };
+ },
+ },
+ {
+ type: 'component',
+ label: 'Objects',
+ component: TaskObjectsComponent,
+ },
+ {
+ label: 'Start date',
+ value: row => DateHelper.short(row.start_date),
+ className: 'action_date',
+ headerClassName: 'action_date',
+ },
+ {
+ label: 'Finish date',
+ value: row => row.status === 'success' || row.status === 'failed' ? DateHelper.short(row.finish_date) : '',
+ className: 'action_date',
+ headerClassName: 'action_date',
+ },
+ {
+ type: 'component',
+ label: 'Status',
+ component: TaskStatusColumnComponent,
+ className: 'table-end center status',
+ headerClassName: 'table-end center status',
+ }
+ ] as IColumns;
- @ViewChildren(MatSortHeader, { read: ElementRef }) matSortHeader: QueryList;
+ jobsTableInstanceTaken: InstanceTakenFunc = (componentRef: ComponentRef>) => {
+ componentRef.instance.expandedTask = this.expandedTask;
+ }
- constructor(private api: ApiService, protected store: Store, public router: Router, public route: ActivatedRoute, public dialog: MatDialog) {
- super(store);
+ constructor(
+ private route: ActivatedRoute,
+ private router: Router,
+ private taskService: TaskService,
+ private jobService: JobService,
+ ) {
+ super();
}
- getIcon(status: string) {
- switch (status) {
- case 'aborted':
- return 'block';
- default:
- return 'done_all';
+ addTask(event: EventMessage): void {
+ if (this.data$.value.results.some((task) => task.id === event.object.id)) {
+ return;
}
- }
- ngOnInit() {
- const limit = +localStorage.getItem('limit');
- if (!limit) localStorage.setItem('limit', '10');
- this.paginator.pageSize = +localStorage.getItem('limit');
-
- this.route.paramMap.pipe(this.takeUntil()).subscribe((p) => {
- this.paramMap = p;
- if (+p.get('page') === 0) {
- this.paginator.firstPage();
+ const data: IListResult = Object.assign({}, this.data$.value);
+ this.taskService.get(event.object.id).subscribe((task) => {
+ if (data.results.length < this.paging.value.pageSize) {
+ data.count++;
+ } else {
+ data.results.splice(data.results.length - 1, 1);
}
- const ordering = p.get('ordering');
- if (ordering && !this.sort.active) {
- this.sort.direction = ordering[0] === '-' ? 'desc' : 'asc';
- this.sort.active = ordering[0] === '-' ? ordering.substr(1) : ordering;
- }
-
- this.refresh();
+ data.results = [task, ...data.results];
+ this.data$.next(data);
});
-
- super.startListenSocket();
}
- cancelTask(url: string) {
- this.dialog
- .open(DialogComponent, {
- data: {
- text: 'Are you sure?',
- controls: ['Yes', 'No'],
- },
- })
- .beforeClosed()
- .pipe(
- filter((yes) => yes),
- switchMap(() => this.api.put(url, {}))
- )
- .subscribe();
+ deleteTask(event: EventMessage): void {
+ const data: IListResult = Object.assign({}, this.data$.value);
+ const index = data.results.findIndex((task) => task.id === event.object.id);
+ if (index > -1) {
+ data.results.splice(index, 1);
+ data.count--;
+ this.data$.next(data);
+ }
}
- socketListener(m: EventMessage) {
- if (m.object.type === 'task' && m.event === 'change_job_status' && m.object.details.type === 'status' && m.object.details.value === 'created') {
- this.addTask(m.object.id);
- return;
+ changeTask(event: EventMessage): void {
+ const data: IListResult = Object.assign({}, this.data$.value);
+ const index = data.results.findIndex((a) => a.id === event.object.id);
+ if (index > -1) {
+ const task: Task = Object.assign({}, data.results[index]);
+ task.finish_date = new Date().toISOString();
+ task.status = event.object.details.value as JobStatus;
+ data.results.splice(index, 1, task);
+ this.data$.next(data);
}
+ }
- const row = this.dataSource.data.find((a) => a.id === m.object.id);
- if (m.event === 'change_job_status') {
- if (row && m.object.type === 'task') {
- row.finish_date = new Date().toISOString();
- row.status = m.object.details.value as JobStatus;
- }
- if (m.object.type === 'job') {
- const task = this.dataSource.data.find((a) => a.jobs.some((b) => b.id === m.object.id));
- if (task) {
- const job = task.jobs.find((a) => a.id === m.object.id);
- if (job) {
- job.status = m.object.details.value as JobStatus;
- if (m.object.details.type === 'status' && m.object.details.value === 'running') job.start_date = new Date().toISOString();
- if (m.object.details.type === 'status' && (m.object.details.value === 'success' || m.object.details.value === 'failed')) job.finish_date = new Date().toISOString();
- }
+ jobChanged(event: EventMessage): void {
+ const data: IListResult = Object.assign({}, this.data$.value);
+ const taskIndex = data.results.findIndex(
+ (item) => item.jobs.some((job) => job.id === event.object.id)
+ );
+ if (taskIndex > -1) {
+ const task: Task = Object.assign({}, data.results[taskIndex]);
+ const jobIndex = task.jobs.findIndex((item) => item.id === event.object.id);
+ if (jobIndex > -1) {
+ const job: Job = Object.assign({}, task.jobs[jobIndex]);
+ job.status = event.object.details.value as JobStatus;
+ if (event.object.details.type === 'status' && event.object.details.value === 'running') {
+ job.start_date = new Date().toISOString();
}
+ if (
+ event.object.details.type === 'status'
+ && (event.object.details.value === 'success' || event.object.details.value === 'failed')
+ ) {
+ job.finish_date = new Date().toISOString();
+ }
+ task.jobs.splice(jobIndex, 1, job);
+ data.results.splice(taskIndex, 1, task);
+ this.data$.next(data);
}
}
}
- addTask(id: number) {
- this.isDisabled = true;
- this.api.getOne('task', id).subscribe((task) => {
- if (this.dataSource.data.some((a) => a.id === id)) return;
- this.paginator.length = ++this.dataCount;
- task.objects = this.buildLink(task.objects);
- if (this.paginator.pageSize > this.dataSource.data.length) this.dataSource.data = [task, ...this.dataSource.data];
- else {
- const [last, ...ost] = this.dataSource.data.reverse();
- this.dataSource.data = [task, ...ost.reverse()];
- }
- this.dataSource._updateChangeSubscription();
- setTimeout((_) => (this.isDisabled = false), 500);
+ startListen() {
+ this.taskService.events(['change_job_status'])
+ .pipe(
+ this.takeUntil(),
+ )
+ .subscribe(event => {
+ if (event.object.details.type === 'status') {
+ switch (event.object.details.value) {
+ case 'created':
+ if (['', 'running'].includes(this.status)) {
+ this.addTask(event);
+ }
+ break;
+ case 'running':
+ if (['', 'running'].includes(this.status)) {
+ this.changeTask(event);
+ }
+ break;
+ case 'success':
+ if (this.status === '') {
+ this.changeTask(event);
+ } else if (this.status === 'running') {
+ this.deleteTask(event);
+ } else if (this.status === 'success') {
+ this.addTask(event);
+ }
+ break;
+ case 'failed':
+ if (this.status === '') {
+ this.changeTask(event);
+ } else if (this.status === 'running') {
+ this.deleteTask(event);
+ } else if (this.status === 'failed') {
+ this.addTask(event);
+ }
+ break;
+ }
+ } else {
+ this.changeTask(event);
+ }
+ });
+
+ this.jobService.events(['change_job_status'])
+ .pipe(this.takeUntil())
+ .subscribe(event => this.jobChanged(event));
+ }
+
+ refreshList(page: number, limit: number, status: TaskStatus): Observable> {
+ const params: any = {
+ limit: limit.toString(),
+ offset: ((page - 1) * limit).toString(),
+ };
+
+ if (status) {
+ params.status = status.toString();
+ }
+
+ this.router.navigate([], {
+ relativeTo: this.route,
+ queryParams: {
+ page,
+ limit,
+ status,
+ },
+ queryParamsHandling: 'merge',
});
+
+ return this.taskService.list(params).pipe(tap(resp => this.data$.next(resp)));
}
- buildLink(items: JobObject[]) {
- const c = items.find((a) => a.type === 'cluster');
- const url = (a: JobObject): string[] => (a.type === 'cluster' || !c ? ['/', a.type, `${a.id}`] : ['/', 'cluster', `${c.id}`, a.type, `${a.id}`]);
- return items.map((a) => ({ ...a, url: url(a) }));
+ initPaging() {
+ this.paging.pipe(
+ this.takeUntil(),
+ filter(paging => !!paging),
+ ).subscribe((paging) => this.refreshList(paging.pageIndex, paging.pageSize, this.status).subscribe());
}
- refresh() {
- this.api.root.pipe(switchMap((root) => this.api.getList(root.task, this.paramMap))).subscribe((data) => {
- this.dataSource.data = data.results.map((a) => ({ ...a, objects: this.buildLink(a.objects) }));
- this.paginator.length = data.count;
- this.dataCount = data.count;
- if (data.results.length) localStorage.setItem('lastJob', data.results[0].id.toString());
- this.dataSource._updateChangeSubscription();
- });
+ getLimit(): number {
+ const p = this.route.snapshot.queryParamMap;
+ return p.get('limit') ? +p.get('limit') : +localStorage.getItem('limit');
}
- pageHandler(pageEvent: PageEvent) {
- localStorage.setItem('limit', String(pageEvent.pageSize));
- const f = this.route.snapshot.paramMap.get('filter') || '';
- const ordering = null; // this.getSortParam(this.sort);
- this.router.navigate(['./', { page: pageEvent.pageIndex, limit: pageEvent.pageSize, filter: f, ordering }], {
- relativeTo: this.route,
+ ngOnInit() {
+ this.initPaging();
+
+ if (!localStorage.getItem('limit')) localStorage.setItem('limit', '10');
+
+ this.route.queryParamMap.pipe(this.takeUntil()).subscribe((p) => {
+ const page = +p.get('page') ? +p.get('page') : 1;
+ const limit = this.getLimit();
+ if (limit) {
+ localStorage.setItem('limit', limit.toString());
+ }
+ this.status = (p.get('status') || '') as TaskStatus;
+ this.paging.next({ pageIndex: page, pageSize: limit });
});
+
+ this.startListen();
}
- trackBy(item: any) {
- return item.id || item;
+ filterChanged(event: MatButtonToggleChange) {
+ this.status = event.value;
+ this.paging.next({ pageIndex: 1, pageSize: this.getLimit() });
}
+
}
diff --git a/web/src/app/factories/list-factory.ts b/web/src/app/factories/list-factory.ts
new file mode 100644
index 0000000000..5281ca73ad
--- /dev/null
+++ b/web/src/app/factories/list-factory.ts
@@ -0,0 +1,155 @@
+import { IComponentColumn, IValueColumn, IButtonsColumn, ILinkColumn } from '@adwp-ui/widgets';
+import { ComponentRef } from '@angular/core';
+
+import { StateColumnComponent } from '@app/components/columns/state-column/state-column.component';
+import { StatusColumnComponent, StatusData } from '@app/components/columns/status-column/status-column.component';
+import { ActionsColumnComponent } from '@app/components/columns/actions-column/actions-column.component';
+import { AdwpListDirective } from '@app/abstract-directives/adwp-list.directive';
+import { UpgradeComponent } from '@app/shared/components';
+import { ActionsButtonComponent } from '@app/components/actions-button/actions-button.component';
+import { IssueType } from '@app/models/issue';
+
+export class ListFactory {
+
+ static nameColumn(sort: string = 'name'): IValueColumn {
+ return {
+ label: 'Name',
+ sort,
+ value: (row) => row.display_name || row.name,
+ };
+ }
+
+ static fqdnColumn(): IValueColumn {
+ return {
+ label: 'FQDN',
+ sort: 'fqdn',
+ className: 'width30pr',
+ headerClassName: 'width30pr',
+ value: row => row.fqdn,
+ };
+ }
+
+ static descriptionColumn(): IValueColumn {
+ return {
+ label: 'Description',
+ value: (row) => row.description,
+ };
+ }
+
+ static stateColumn(): IComponentColumn {
+ return {
+ label: 'State',
+ sort: 'state',
+ type: 'component',
+ className: 'width100',
+ headerClassName: 'width100',
+ component: StateColumnComponent,
+ };
+ }
+
+ static statusColumn(listDirective: AdwpListDirective): IComponentColumn {
+ return {
+ label: 'Status',
+ sort: 'status',
+ type: 'component',
+ className: 'list-control',
+ headerClassName: 'list-control',
+ component: StatusColumnComponent,
+ instanceTaken: (componentRef: ComponentRef>) => {
+ componentRef.instance.onClick
+ .pipe(listDirective.takeUntil())
+ .subscribe((data: StatusData) => listDirective.gotoStatus(data));
+ }
+ };
+ }
+
+ static actionsColumn(): IComponentColumn {
+ return {
+ label: 'Actions',
+ type: 'component',
+ className: 'list-control',
+ headerClassName: 'list-control',
+ component: ActionsColumnComponent,
+ };
+ }
+
+ static actionsButton(type: IssueType): IComponentColumn {
+ return {
+ label: 'Actions',
+ type: 'component',
+ className: 'list-control',
+ headerClassName: 'list-control',
+ component: ActionsButtonComponent,
+ instanceTaken: (componentRef: ComponentRef>) => {
+ componentRef.instance.issueType = type;
+ }
+ };
+ }
+
+ static importColumn(listDirective: AdwpListDirective): IButtonsColumn {
+ return {
+ label: 'Import',
+ type: 'buttons',
+ className: 'list-control',
+ headerClassName: 'list-control',
+ buttons: [{
+ icon: 'import_export',
+ callback: (row) => listDirective.baseListDirective.listEvents({ cmd: 'import', row }),
+ }]
+ };
+ }
+
+ static configColumn(listDirective: AdwpListDirective): IButtonsColumn {
+ return {
+ label: 'Config',
+ type: 'buttons',
+ className: 'list-control',
+ headerClassName: 'list-control',
+ buttons: [{
+ icon: 'settings',
+ callback: (row) => listDirective.baseListDirective.listEvents({ cmd: 'config', row }),
+ }]
+ };
+ }
+
+ static bundleColumn(): IValueColumn {
+ return {
+ label: 'Bundle',
+ sort: 'prototype_version',
+ value: (row) => [row.prototype_display_name || row.prototype_name, row.prototype_version, row.edition].join(' '),
+ };
+ }
+
+ static updateColumn(): IComponentColumn {
+ return {
+ label: 'Upgrade',
+ type: 'component',
+ className: 'list-control',
+ headerClassName: 'list-control',
+ component: UpgradeComponent,
+ };
+ }
+
+ static deleteColumn(listDirective: AdwpListDirective): IButtonsColumn {
+ return {
+ type: 'buttons',
+ className: 'list-control',
+ headerClassName: 'list-control',
+ buttons: [{
+ icon: 'delete',
+ callback: (row, event) => listDirective.delete(event, row),
+ }]
+ };
+ }
+
+ static providerColumn(): ILinkColumn {
+ return {
+ type: 'link',
+ label: 'Provider',
+ sort: 'provider_name',
+ value: row => row.provider_name,
+ url: row => `/provider/${row.provider_id}`,
+ };
+ }
+
+}
diff --git a/web/src/app/helpers/date-helper.ts b/web/src/app/helpers/date-helper.ts
new file mode 100644
index 0000000000..e1a5cbbd67
--- /dev/null
+++ b/web/src/app/helpers/date-helper.ts
@@ -0,0 +1,9 @@
+import { DateTime } from 'luxon';
+
+export class DateHelper {
+
+ static short(date: string) {
+ return DateTime.fromISO(date).setLocale('en').toFormat('FF');
+ }
+
+}
diff --git a/web/src/app/helpers/objects-helper.ts b/web/src/app/helpers/objects-helper.ts
new file mode 100644
index 0000000000..82421edf03
--- /dev/null
+++ b/web/src/app/helpers/objects-helper.ts
@@ -0,0 +1,9 @@
+import { JobObject, JobType } from '../core/types';
+
+export class ObjectsHelper {
+
+ static getObject(objects: JobObject[], type: JobType): JobObject {
+ return objects.find(object => object.type === type);
+ }
+
+}
diff --git a/web/src/app/main/login/login.component.ts b/web/src/app/main/login/login.component.ts
index 5cbe272098..360822bd85 100644
--- a/web/src/app/main/login/login.component.ts
+++ b/web/src/app/main/login/login.component.ts
@@ -11,15 +11,16 @@
// limitations under the License.
import { Component, OnDestroy, OnInit } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
-import { AuthService } from '@app/core';
-import { authLogin, authLogout, AuthState, getAuthState } from '@app/core/auth/auth.store';
-import { clearProfile } from '@app/core/store';
-import { BaseDirective } from '@app/shared';
import { Store } from '@ngrx/store';
import { Observable } from 'rxjs';
import { filter } from 'rxjs/operators';
import { FormGroup, FormControl, Validators } from '@angular/forms';
+import { AuthService } from '@app/core';
+import { authLogin, authLogout, AuthState, getAuthState } from '@app/core/auth/auth.store';
+import { clearProfile } from '@app/core/store';
+import { BaseDirective } from '@app/shared/directives';
+
@Component({
selector: 'app-login',
templateUrl: './login.component.html',
diff --git a/web/src/app/main/main.module.ts b/web/src/app/main/main.module.ts
index 554986b981..b71ec46889 100644
--- a/web/src/app/main/main.module.ts
+++ b/web/src/app/main/main.module.ts
@@ -12,7 +12,7 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
-import { SharedModule } from '@app/shared';
+import { SharedModule } from '@app/shared/shared.module';
import { MainRoutingModule } from '@app/main/routing.module';
import { LoginComponent } from './login/login.component';
@@ -35,6 +35,6 @@ import { PageNotFoundComponent, FatalErrorComponent, GatewayTimeoutComponent } f
ProgressComponent,
],
exports: [TopComponent, ProgressComponent],
-
+
})
export class MainModule {}
diff --git a/web/src/app/main/profile/profile.component.html b/web/src/app/main/profile/profile.component.html
deleted file mode 100644
index 3949cd7db6..0000000000
--- a/web/src/app/main/profile/profile.component.html
+++ /dev/null
@@ -1,33 +0,0 @@
-
-
-
You are authorized as [ {{ user.username }} ]
-
-
-
Change Password
-
-
-
-
-
-
-
-
-
-
-
-
Profile
-
Dashboard
-
-
Widgets
-
- - title: {{ widget.title }}, type: {{ widget.type }}
-
-
-
Metrics
-
Allow ADCM collect and send non-personalized information? - {{ user.profile.metrics? 'Yes': 'No' }}
-
-
-
-
diff --git a/web/src/app/main/profile/profile.component.ts b/web/src/app/main/profile/profile.component.ts
index bf541794d8..656fe038a4 100644
--- a/web/src/app/main/profile/profile.component.ts
+++ b/web/src/app/main/profile/profile.component.ts
@@ -12,11 +12,12 @@
import { Component, OnDestroy, OnInit } from '@angular/core';
import { FormControl, FormGroup, Validators } from '@angular/forms';
import { Router } from '@angular/router';
-import { getProfileSelector, ProfileService, ProfileState } from '@app/core/store';
-import { BaseDirective } from '@app/shared';
import { Store } from '@ngrx/store';
import { Observable } from 'rxjs';
+import { getProfileSelector, ProfileService, ProfileState } from '@app/core/store';
+import { BaseDirective } from '@app/shared/directives';
+
@Component({
selector: 'app-profile',
template: `
diff --git a/web/src/app/main/top/top.component.html b/web/src/app/main/top/top.component.html
index 53d6ee40f9..7599e804a9 100644
--- a/web/src/app/main/top/top.component.html
+++ b/web/src/app/main/top/top.component.html
@@ -23,16 +23,36 @@
-
diff --git a/web/src/app/models/action.ts b/web/src/app/models/action.ts
new file mode 100644
index 0000000000..fe951219b9
--- /dev/null
+++ b/web/src/app/models/action.ts
@@ -0,0 +1,13 @@
+export interface IActionConfig {
+ attr: any;
+ config: any[];
+}
+
+export interface IAction {
+ button: any;
+ config: IActionConfig;
+ display_name: string;
+ hostcomponentmap: any[];
+ name: string;
+ run: string;
+}
diff --git a/web/src/app/models/bundle.ts b/web/src/app/models/bundle.ts
new file mode 100644
index 0000000000..18d24fccf3
--- /dev/null
+++ b/web/src/app/models/bundle.ts
@@ -0,0 +1,17 @@
+import { AdcmEntity } from './entity';
+
+export interface IBundle extends AdcmEntity {
+ adcm_min_version: string;
+ date: string;
+ description: string;
+ edition: string;
+ hash: string;
+ license: string;
+ license_hash: any;
+ license_path: any;
+ license_url: string;
+ name: string;
+ update: string;
+ url: string;
+ version: string;
+}
diff --git a/web/src/app/models/cluster-service.ts b/web/src/app/models/cluster-service.ts
new file mode 100644
index 0000000000..0997fdd919
--- /dev/null
+++ b/web/src/app/models/cluster-service.ts
@@ -0,0 +1,22 @@
+import { IComponent } from './component';
+import { IssueEntity } from './issue';
+
+export interface IClusterService extends IssueEntity {
+ action: string;
+ actions: any[];
+ bind: string;
+ bundle_id: number;
+ cluster_id: number;
+ component: string;
+ components: IComponent[];
+ config: string;
+ description: string;
+ imports: string;
+ monitoring: string;
+ prototype: string;
+ prototype_id: number;
+ state: string;
+ status: number;
+ url: string;
+ version: string;
+}
diff --git a/web/src/app/models/cluster.ts b/web/src/app/models/cluster.ts
new file mode 100644
index 0000000000..7d60a9da9f
--- /dev/null
+++ b/web/src/app/models/cluster.ts
@@ -0,0 +1,30 @@
+import { IAction } from './action';
+import { IssueEntity } from '@app/models/issue';
+
+export interface ICluster extends IssueEntity {
+ action: string;
+ actions: IAction[];
+ bind: string;
+ bundle_id: number;
+ config: string;
+ description: string;
+ edition: string;
+ host: string;
+ hostcomponent: string;
+ imports: string;
+ license: string;
+ name: string;
+ prototype: string;
+ prototype_display_name: string;
+ prototype_id: number;
+ prototype_name: string;
+ prototype_version: string;
+ service: string;
+ serviceprototype: string;
+ state: string;
+ status: number;
+ status_url: string;
+ upgradable: boolean;
+ upgrade: string;
+ url: string;
+}
diff --git a/web/src/app/models/component.ts b/web/src/app/models/component.ts
new file mode 100644
index 0000000000..5be6f42526
--- /dev/null
+++ b/web/src/app/models/component.ts
@@ -0,0 +1,14 @@
+import { AdcmEntity } from './entity';
+
+export interface IComponent extends AdcmEntity {
+ action: string;
+ bound_to: any;
+ config: string;
+ constraint: Array;
+ description: string;
+ monitoring: string;
+ prototype_id: number;
+ requires: any[];
+ status: number;
+ url: string;
+}
diff --git a/web/src/app/models/entity-names.ts b/web/src/app/models/entity-names.ts
new file mode 100644
index 0000000000..f1fcb9f755
--- /dev/null
+++ b/web/src/app/models/entity-names.ts
@@ -0,0 +1,3 @@
+import { TypeName } from '../core/types';
+
+export const EntityNames: TypeName[] = ['servicecomponent', 'host', 'service', 'cluster', 'provider', 'job', 'task', 'bundle'];
diff --git a/web/src/app/models/entity.ts b/web/src/app/models/entity.ts
new file mode 100644
index 0000000000..0c8bc56a10
--- /dev/null
+++ b/web/src/app/models/entity.ts
@@ -0,0 +1,11 @@
+import { Entity } from '@adwp-ui/widgets';
+import { TypeName } from '@app/core/types';
+
+export interface AdcmEntity extends Entity {
+ name?: string;
+ display_name?: string;
+}
+
+export interface AdcmTypedEntity extends AdcmEntity {
+ typeName: TypeName;
+}
diff --git a/web/src/app/models/eventable-service.ts b/web/src/app/models/eventable-service.ts
new file mode 100644
index 0000000000..fa4ca39a58
--- /dev/null
+++ b/web/src/app/models/eventable-service.ts
@@ -0,0 +1,9 @@
+import { Observable } from 'rxjs';
+
+import { EntityEvent, EventMessage } from '@app/core/store';
+
+export interface EventableService {
+
+ events(events: EntityEvent[]): Observable;
+
+}
diff --git a/web/src/app/models/host.ts b/web/src/app/models/host.ts
new file mode 100644
index 0000000000..bd30f8b30b
--- /dev/null
+++ b/web/src/app/models/host.ts
@@ -0,0 +1,28 @@
+import { Entity } from '@adwp-ui/widgets';
+import { IAction } from './action';
+import { IIssues } from './issue';
+
+export interface IHost extends Entity {
+ action: string;
+ actions: IAction[];
+ cluster_id?: number;
+ cluster_url?: string;
+ cluster_name?: string;
+ clusters: any[];
+ config: string;
+ fqdn: string;
+ host_id: number;
+ host_url: string;
+ issue: IIssues;
+ monitoring: string;
+ prototype_display_name: string;
+ prototype_id: number;
+ prototype_name: string;
+ prototype_version: string;
+ provider_id: number;
+ provider_name: number;
+ state: string;
+ status: number;
+ upgradable: boolean;
+ url: string;
+}
diff --git a/web/src/app/models/issue.ts b/web/src/app/models/issue.ts
new file mode 100644
index 0000000000..bcdea6076e
--- /dev/null
+++ b/web/src/app/models/issue.ts
@@ -0,0 +1,15 @@
+import { AdcmEntity } from '@app/models/entity';
+
+export type IssueType = 'cluster' | 'service' | 'servicecomponent' | 'component';
+
+export interface IssueEntity extends AdcmEntity {
+ issue: IIssues;
+}
+
+export interface IIssues {
+ config: boolean;
+ required_import?: boolean;
+ host_component: false;
+ cluster?: IssueEntity[];
+ service?: IssueEntity[];
+}
diff --git a/web/src/app/models/service-component.ts b/web/src/app/models/service-component.ts
new file mode 100644
index 0000000000..cbbf39cb8d
--- /dev/null
+++ b/web/src/app/models/service-component.ts
@@ -0,0 +1,21 @@
+import { IssueEntity } from '@app/models/issue';
+import { IAction } from '@app/models/action';
+
+export interface IServiceComponent extends IssueEntity {
+ cluster_id: number;
+ service_id: number;
+ description: string;
+ constraint: Array;
+ monitoring: string;
+ prototype_id: number;
+ requires: Array;
+ bound_to: any;
+ status: number;
+ url: string;
+ state: string;
+ action: string;
+ config: string;
+ prototype: string;
+ actions: IAction[];
+ version: string;
+}
diff --git a/web/src/app/models/universal-adcm-event-data.ts b/web/src/app/models/universal-adcm-event-data.ts
new file mode 100644
index 0000000000..8080b65fc5
--- /dev/null
+++ b/web/src/app/models/universal-adcm-event-data.ts
@@ -0,0 +1,5 @@
+export interface UniversalAdcmEventData {
+ event: MouseEvent;
+ action: 'getNextPageCluster' | 'getClusters' | 'addCluster';
+ row: T;
+}
diff --git a/web/src/app/pipes/is-array.pipe.ts b/web/src/app/pipes/is-array.pipe.ts
new file mode 100644
index 0000000000..04f8bdcb23
--- /dev/null
+++ b/web/src/app/pipes/is-array.pipe.ts
@@ -0,0 +1,12 @@
+import { Pipe, PipeTransform } from '@angular/core';
+
+@Pipe({
+ name: 'isArray'
+})
+export class IsArrayPipe implements PipeTransform {
+
+ transform(value: any): unknown {
+ return Array.isArray(value);
+ }
+
+}
diff --git a/web/src/app/pipes/issue-path.pipe.ts b/web/src/app/pipes/issue-path.pipe.ts
new file mode 100644
index 0000000000..a0f8757e4d
--- /dev/null
+++ b/web/src/app/pipes/issue-path.pipe.ts
@@ -0,0 +1,40 @@
+import { Pipe, PipeTransform } from '@angular/core';
+import { map } from 'rxjs/operators';
+import { Observable, of } from 'rxjs';
+
+import { ServiceService } from '@app/services/service.service';
+import { IssueType } from '@app/models/issue';
+import { ServiceComponentService } from '@app/services/service-component.service';
+
+@Pipe({
+ name: 'issuePath'
+})
+export class IssuePathPipe implements PipeTransform {
+
+ constructor(
+ private serviceService: ServiceService,
+ private serviceComponentService: ServiceComponentService,
+ ) {}
+
+ transform(issueName: string, issueType: IssueType, id: number): Observable {
+ let issue = issueName;
+ if (issue === 'required_import') {
+ issue = 'import';
+ }
+
+ if (issueType === 'service') {
+ return this.serviceService.get(id)
+ .pipe(map(
+ service => `/cluster/${service.cluster_id}/${issueType}/${id}/${issue}`,
+ ));
+ } else if (issueType === 'servicecomponent' || issueType === 'component') {
+ return this.serviceComponentService.get(id)
+ .pipe(map(
+ component => `/cluster/${component.cluster_id}/service/${component.service_id}/component/${id}/${issue}`,
+ ));
+ } {
+ return of(`/${issueType}/${id}/${issue}`);
+ }
+ }
+
+}
diff --git a/web/src/app/pipes/keys.pipe.ts b/web/src/app/pipes/keys.pipe.ts
new file mode 100644
index 0000000000..a96e420ad5
--- /dev/null
+++ b/web/src/app/pipes/keys.pipe.ts
@@ -0,0 +1,12 @@
+import { Pipe, PipeTransform } from '@angular/core';
+
+@Pipe({
+ name: 'keys'
+})
+export class KeysPipe implements PipeTransform {
+
+ transform(value: any): unknown {
+ return Object.keys(value || {});
+ }
+
+}
diff --git a/web/src/app/pipes/nav-item.pipe.ts b/web/src/app/pipes/nav-item.pipe.ts
new file mode 100644
index 0000000000..8f36980e1f
--- /dev/null
+++ b/web/src/app/pipes/nav-item.pipe.ts
@@ -0,0 +1,61 @@
+import { Pipe, PipeTransform } from '@angular/core';
+
+import { AdcmTypedEntity } from '@app/models/entity';
+import { IStyledNavItem } from '@app/shared/details/navigation.service';
+import { TypeName } from '@app/core/types';
+
+@Pipe({
+ name: 'navItem'
+})
+export class NavItemPipe implements PipeTransform {
+
+ getGroupName(typeName: TypeName): string {
+ switch (typeName) {
+ case 'cluster':
+ return 'clusters';
+ case 'service':
+ return 'services';
+ case 'servicecomponent':
+ return 'components';
+ }
+ }
+
+ getLink(path: AdcmTypedEntity[], index: number, group: boolean): string {
+ switch (path[index].typeName) {
+ case 'cluster':
+ return group ? `/${path[index].typeName}` : `/${path[index].typeName}/${path[index].id}`;
+ case 'service':
+ return group ? (
+ `/${path[index - 1].typeName}/${path[index - 1].id}/service`
+ ) : (
+ `/${path[index - 1].typeName}/${path[index - 1].id}/service/${path[index].id}`
+ );
+ case 'servicecomponent':
+ return group ? (
+ `/${path[index - 2].typeName}/${path[index - 2].id}/service/${path[index - 1].id}/component`
+ ) : (
+ `/${path[index - 2].typeName}/${path[index - 2].id}/service/${path[index - 1].id}/component/${path[index].id}`
+ );
+ }
+ }
+
+ transform(path: AdcmTypedEntity[]): IStyledNavItem[] {
+ return path?.reduce((acc, item, index) => {
+ return [
+ ...acc,
+ {
+ title: this.getGroupName(item.typeName),
+ url: this.getLink(path, index, true),
+ class: 'type-name',
+ },
+ {
+ title: item.display_name || item.name,
+ url: this.getLink(path, index, false),
+ class: 'entity',
+ entity: item,
+ }
+ ] as IStyledNavItem[];
+ }, []);
+ }
+
+}
diff --git a/web/src/app/pipes/object-link-column.pipe.ts b/web/src/app/pipes/object-link-column.pipe.ts
new file mode 100644
index 0000000000..96903714fb
--- /dev/null
+++ b/web/src/app/pipes/object-link-column.pipe.ts
@@ -0,0 +1,39 @@
+import { Pipe, PipeTransform } from '@angular/core';
+import { ILinkColumn } from '@adwp-ui/widgets';
+
+import { JobObject, Task } from '../core/types';
+import { ObjectsHelper } from '../helpers/objects-helper';
+
+@Pipe({
+ name: 'objectLinkColumn'
+})
+export class ObjectLinkColumnPipe implements PipeTransform {
+
+ getCluster(task: Task) {
+ return ObjectsHelper.getObject(task.objects, 'cluster');
+ }
+
+ getService(task: Task) {
+ return ObjectsHelper.getObject(task.objects, 'service');
+ }
+
+ url(object: JobObject, task: Task): string[] {
+ if (object.type === 'cluster' || !this.getCluster(task)) {
+ return ['/', object.type, `${object.id}`];
+ } else if (object.type === 'component' && this.getService(task)) {
+ return ['/', 'cluster', `${this.getCluster(task).id}`, 'service', `${this.getService(task).id}`, object.type, `${object.id}`];
+ } else {
+ return ['/', 'cluster', `${this.getCluster(task).id}`, object.type, `${object.id}`];
+ }
+ }
+
+ transform(object: JobObject, task: Task): ILinkColumn {
+ return {
+ label: '',
+ type: 'link',
+ value: () => object.name,
+ url: () => this.url(object, task).join('/'),
+ };
+ }
+
+}
diff --git a/web/src/app/pipes/sort-objects.pipe.ts b/web/src/app/pipes/sort-objects.pipe.ts
new file mode 100644
index 0000000000..97e4273baf
--- /dev/null
+++ b/web/src/app/pipes/sort-objects.pipe.ts
@@ -0,0 +1,23 @@
+import { Pipe, PipeTransform } from '@angular/core';
+
+import { JobObject } from '../core/types';
+import { ObjectsHelper } from '../helpers/objects-helper';
+
+@Pipe({
+ name: 'sortObjects'
+})
+export class SortObjectsPipe implements PipeTransform {
+
+ transform(objects: JobObject[]): JobObject[] {
+ if (ObjectsHelper.getObject(objects, 'component')) {
+ return [
+ ObjectsHelper.getObject(objects, 'component'),
+ ObjectsHelper.getObject(objects, 'service'),
+ ObjectsHelper.getObject(objects, 'cluster'),
+ ];
+ }
+
+ return objects;
+ }
+
+}
diff --git a/web/src/app/services/job.service.ts b/web/src/app/services/job.service.ts
new file mode 100644
index 0000000000..097cf83f3f
--- /dev/null
+++ b/web/src/app/services/job.service.ts
@@ -0,0 +1,27 @@
+import { Injectable } from '@angular/core';
+import { Observable } from 'rxjs';
+import { filter } from 'rxjs/operators';
+import { Store } from '@ngrx/store';
+
+import { EventableService } from '@app/models/eventable-service';
+import { EntityEvent, EventMessage, selectMessage, SocketState } from '@app/core/store';
+
+@Injectable()
+export class JobService implements EventableService {
+
+ constructor(
+ private store: Store,
+ ) {}
+
+ events(events: EntityEvent[]): Observable {
+ const result = this.store.pipe(
+ selectMessage,
+ filter(event => event?.object?.type === 'job'),
+ );
+ if (events) {
+ result.pipe(filter(event => events.includes(event.event)));
+ }
+ return result;
+ }
+
+}
diff --git a/web/src/app/services/service-component.service.ts b/web/src/app/services/service-component.service.ts
new file mode 100644
index 0000000000..6702d79036
--- /dev/null
+++ b/web/src/app/services/service-component.service.ts
@@ -0,0 +1,26 @@
+import { Injectable } from '@angular/core';
+import { Observable } from 'rxjs';
+
+import { ApiService } from '@app/core/api';
+import { IServiceComponent } from '@app/models/service-component';
+import { EntityService } from '@app/abstract/entity-service';
+
+@Injectable({
+ providedIn: 'root',
+})
+export class ServiceComponentService extends EntityService {
+
+ constructor(
+ protected api: ApiService,
+ ) {
+ super(api);
+ }
+
+ get(
+ id: number,
+ params: { [key: string]: string } = {},
+ ): Observable {
+ return this.api.get(`api/v1/component/${id}`, params);
+ }
+
+}
diff --git a/web/src/app/services/service.service.ts b/web/src/app/services/service.service.ts
new file mode 100644
index 0000000000..49e6f7c726
--- /dev/null
+++ b/web/src/app/services/service.service.ts
@@ -0,0 +1,26 @@
+import { Injectable } from '@angular/core';
+import { Observable } from 'rxjs';
+
+import { ApiService } from '@app/core/api';
+import { IClusterService } from '@app/models/cluster-service';
+import { EntityService } from '@app/abstract/entity-service';
+
+@Injectable({
+ providedIn: 'root',
+})
+export class ServiceService extends EntityService {
+
+ constructor(
+ protected api: ApiService,
+ ) {
+ super(api);
+ }
+
+ get(
+ id: number,
+ params: { [key: string]: string } = {},
+ ): Observable {
+ return this.api.get(`api/v1/service/${id}/`, params);
+ }
+
+}
diff --git a/web/src/app/services/task.service.ts b/web/src/app/services/task.service.ts
new file mode 100644
index 0000000000..57e16b31bf
--- /dev/null
+++ b/web/src/app/services/task.service.ts
@@ -0,0 +1,42 @@
+import { Injectable } from '@angular/core';
+import { Store } from '@ngrx/store';
+import { filter } from 'rxjs/operators';
+import { Observable } from 'rxjs';
+import { IListResult } from '@adwp-ui/widgets';
+
+import { EventMessage, EntityEvent, selectMessage, SocketState } from '@app/core/store';
+import { EventableService } from '@app/models/eventable-service';
+import { Task } from '@app/core/types';
+import { ApiService } from '@app/core/api';
+import { EntityService } from '@app/abstract/entity-service';
+
+@Injectable()
+export class TaskService extends EntityService implements EventableService {
+
+ constructor(
+ private store: Store,
+ protected api: ApiService,
+ ) {
+ super(api);
+ }
+
+ get(id: number, params: { [key: string]: string } = {}): Observable {
+ return this.api.get(`api/v1/task/${id}/`, params);
+ }
+
+ list(params: { [key: string]: string } = {}): Observable> {
+ return this.api.get(`api/v1/task/`, params);
+ }
+
+ events(events: EntityEvent[]): Observable {
+ const result = this.store.pipe(
+ selectMessage,
+ filter(event => event?.object?.type === 'task'),
+ );
+ if (events) {
+ result.pipe(filter(event => events.includes(event.event)));
+ }
+ return result;
+ }
+
+}
diff --git a/web/src/app/shared/add-component/add.service.ts b/web/src/app/shared/add-component/add.service.ts
index 41ac3d67b4..14804e763a 100644
--- a/web/src/app/shared/add-component/add.service.ts
+++ b/web/src/app/shared/add-component/add.service.ts
@@ -13,14 +13,15 @@ import { EventEmitter, Injectable } from '@angular/core';
import { FormControl, FormGroup, Validators } from '@angular/forms';
import { MatDialog } from '@angular/material/dialog';
import { convertToParamMap, Params } from '@angular/router';
-import { ClusterService, StackInfo, StackService } from '@app/core';
-import { ApiService } from '@app/core/api';
-import { Host, Prototype, ServicePrototype, StackBase, TypeName } from '@app/core/types';
import { environment } from '@env/environment';
import { Observable, of, throwError, forkJoin } from 'rxjs';
import { concatAll, filter, map, switchMap, catchError } from 'rxjs/operators';
-import { DialogComponent } from '../components/dialog.component';
+import { StackInfo, StackService } from '@app/core';
+import { ClusterService } from '@app/core/services/cluster.service';
+import { ApiService } from '@app/core/api';
+import { Host, Prototype, ServicePrototype, StackBase, TypeName } from '@app/core/types';
+import { DialogComponent } from '@app/shared/components/dialog.component';
import { GenName } from './naming';
export interface FormModel {
@@ -75,7 +76,7 @@ export class AddService {
get currentPrototype(): StackBase {
return this._currentPrototype;
}
-
+
constructor(private api: ApiService, private stack: StackService, private cluster: ClusterService, public dialog: MatDialog) {}
model(name: string) {
@@ -145,7 +146,7 @@ export class AddService {
return this.api.root.pipe(switchMap((root) => this.api.getList(root[type], paramMap)));
}
- getList(type: TypeName, param: Params = {}): Observable {
+ getList(type: TypeName, param: Params = {}): Observable {
return this.getListResults(type, param).pipe(map((list) => list.results));
}
diff --git a/web/src/app/shared/add-component/host.component.ts b/web/src/app/shared/add-component/host.component.ts
index 26ec2d4699..5ad313ed87 100644
--- a/web/src/app/shared/add-component/host.component.ts
+++ b/web/src/app/shared/add-component/host.component.ts
@@ -15,6 +15,7 @@ import { openClose } from '@app/core/animations';
import { clearEmptyField, Cluster, Host, Provider } from '@app/core/types';
import { BehaviorSubject } from 'rxjs';
import { filter, tap } from 'rxjs/operators';
+import { EventHelper } from '@adwp-ui/widgets';
import { ActionsDirective } from '../components/actions/actions.directive';
import { AddService } from './add.service';
@@ -120,8 +121,8 @@ export class HostComponent extends BaseFormDirective implements OnInit {
return fi.invalid && (fi.dirty || fi.touched);
}
- showHostproviderForm(e: Event) {
- e.stopPropagation();
+ showHostproviderForm(e: MouseEvent) {
+ EventHelper.stopPropagation(e);
this.expanded = !this.expanded;
this.form.get('provider_id').setValue('');
}
diff --git a/web/src/app/shared/components/actions/action-card/action-card.component.spec.ts b/web/src/app/shared/components/actions/action-card/action-card.component.spec.ts
index d566b9c369..6e8949c01f 100644
--- a/web/src/app/shared/components/actions/action-card/action-card.component.spec.ts
+++ b/web/src/app/shared/components/actions/action-card/action-card.component.spec.ts
@@ -15,7 +15,7 @@ import { MatDialogModule } from '@angular/material/dialog';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { provideMockStore } from '@ngrx/store/testing';
-import { ClusterService } from '../../../../core/services/detail.service';
+import { ClusterService } from '../../../../core/services/cluster.service';
import { Entities } from '../../../../core/types/';
import { ActionsService } from '../actions.service';
import { ActionCardComponent } from './action-card.component';
diff --git a/web/src/app/shared/components/actions/action-card/action-card.component.ts b/web/src/app/shared/components/actions/action-card/action-card.component.ts
index 169a02d622..a4f36240b8 100644
--- a/web/src/app/shared/components/actions/action-card/action-card.component.ts
+++ b/web/src/app/shared/components/actions/action-card/action-card.component.ts
@@ -10,13 +10,13 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import { Component, OnInit } from '@angular/core';
-import { ClusterService } from '@app/core';
-import { EventMessage, SocketState } from '@app/core/store';
-import { Cluster, Entities, isIssue } from '@app/core/types';
-import { SocketListenerDirective } from '@app/shared/directives';
import { Store } from '@ngrx/store';
import { Observable, of } from 'rxjs';
+import { ClusterService } from '@app/core/services/cluster.service';
+import { EventMessage, SocketState } from '@app/core/store';
+import { Cluster, Entities, isIssue } from '@app/core/types';
+import { SocketListenerDirective } from '@app/shared/directives';
import { ActionsService } from '../actions.service';
@Component({
@@ -43,7 +43,8 @@ export class ActionCardComponent extends SocketListenerDirective implements OnIn
}
socketListener(m: EventMessage) {
- if (this.details.Current?.typeName === m.object.type && this.details.Current?.id === m.object.id && (m.event === 'change_state' || m.event === 'clear_issue')) {
+ const type = m.object.type === 'component' ? 'servicecomponent' : m.object.type;
+ if (this.details.Current?.typeName === type && this.details.Current?.id === m.object.id && (m.event === 'change_state' || m.event === 'clear_issue')) {
this.actions$ = this.service.getActions(this.details.Current.action);
}
}
diff --git a/web/src/app/shared/components/import/import.component.ts b/web/src/app/shared/components/import/import.component.ts
index ca64d2a6dc..f3a75b0cc7 100644
--- a/web/src/app/shared/components/import/import.component.ts
+++ b/web/src/app/shared/components/import/import.component.ts
@@ -12,11 +12,13 @@
import { Component, Input, OnInit } from '@angular/core';
import { AbstractControl, FormControl, FormGroup, ValidatorFn } from '@angular/forms';
import { MatCheckboxChange } from '@angular/material/checkbox';
-import { ChannelService, ClusterService, keyChannelStrim } from '@app/core';
-import { IExport, IImport } from '@app/core/types';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';
+import { ChannelService, keyChannelStrim } from '@app/core';
+import { ClusterService } from '@app/core/services/cluster.service';
+import { IExport, IImport } from '@app/core/types';
+
interface IComposite {
[key: string]: number;
}
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 4949abdaee..bb1ce5f3a8 100644
--- a/web/src/app/shared/components/list/base-list.directive.ts
+++ b/web/src/app/shared/components/list/base-list.directive.ts
@@ -9,72 +9,120 @@
// 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 { Directive, Host, Input, OnDestroy, OnInit } from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
import { ParamMap } from '@angular/router';
-import { EventMessage, SocketState } from '@app/core/store';
+import { clearMessages, EventMessage, getMessage, SocketState } from '@app/core/store';
import { Bundle, Cluster, EmmitRow, Entities, Host as AdcmHost, TypeName } from '@app/core/types';
-import { Store } from '@ngrx/store';
+import { select, Store } from '@ngrx/store';
import { filter, mergeMap, switchMap, tap } from 'rxjs/operators';
+import { IListResult } from '@adwp-ui/widgets';
+import { takeUntil } from 'rxjs/operators';
+import { Sort } from '@angular/material/sort';
+import { Observable, Subject } from 'rxjs';
-import { SocketListenerDirective } from '../../directives/socketListener.directive';
-import { DialogComponent } from '../dialog.component';
-import { ListComponent } from '../list/list.component';
+import { DialogComponent } from '@app/shared/components';
+import { ListResult } from '@app/shared/components/list/list.component';
import { ListService } from './list.service';
+import { ListDirective } from '@app/abstract-directives/list.directive';
interface IRowHost extends AdcmHost {
clusters: Partial[];
page: number;
}
-@Directive({
- selector: '[appBaseList]',
-})
-export class BaseListDirective extends SocketListenerDirective implements OnInit, OnDestroy {
+export class BaseListDirective {
+
+ socket$: Observable;
+ destroy$ = new Subject();
+
row: Entities;
listParams: ParamMap;
limit = 10;
+ typeName: TypeName;
+
+ reload: (result: ListResult) => void;
+
+ constructor(
+ protected parent: ListDirective,
+ protected service: ListService,
+ protected store: Store,
+ ) {}
+
+ takeUntil() {
+ return takeUntil(this.destroy$);
+ }
+
+ startListenSocket(): void {
+ this.socket$.pipe(tap(m => this.socketListener(m))).subscribe();
+ }
- @Input('appBaseList') typeName: TypeName;
- constructor(@Host() private parent: ListComponent, private service: ListService, protected store: Store) {
- super(store);
+ initSocket() {
+ this.socket$ = this.store.pipe(this.takeUntil(), select(getMessage), filter(m => !!m && !!m.object));
}
- ngOnInit(): void {
- this.parent.type = this.typeName;
+ initColumns() {
this.parent.columns = this.service.initInstance(this.typeName).columns;
- this.parent.listItemEvt.pipe(this.takeUntil()).subscribe({ next: (event: EmmitRow) => this.listEvents(event) });
+ }
+
+ initListItemEvent() {
+ this.parent.listItemEvt
+ .pipe(this.takeUntil())
+ .subscribe({ next: (event: EmmitRow) => this.listEvents(event) });
+ }
+
+ calcSort(ordering: string): Sort {
+ let sort: Sort;
+ if (ordering) {
+ sort = {
+ direction: ordering[0] === '-' ? 'desc' : 'asc',
+ active: ordering[0] === '-' ? ordering.substr(1) : ordering,
+ };
+ }
+
+ return sort;
+ }
+ routeListener(limit: number, page: number, ordering: string, params: ParamMap) {
+ this.parent.paginator.pageSize = limit;
+ if (page === 0) {
+ this.parent.paginator.firstPage();
+ } else {
+ this.parent.paginator.pageIndex = page;
+ }
+ if (ordering && !this.parent.sort.active) {
+ this.parent.sort.direction = ordering[0] === '-' ? 'desc' : 'asc';
+ this.parent.sort.active = ordering[0] === '-' ? ordering.substr(1) : ordering;
+ this.parent.sortParam = ordering;
+ }
+
+ this.listParams = params;
+ this.refresh();
+ }
+
+ initRouteListener() {
this.parent.route.paramMap
.pipe(
this.takeUntil(),
filter((p) => this.checkParam(p))
)
- .subscribe((p) => {
- this.parent.paginator.pageSize = +p.get('limit') || 10;
- const page = +p.get('page');
- if (page === 0) {
- this.parent.paginator.firstPage();
- } else {
- this.parent.paginator.pageIndex = page;
- }
- const ordering = p.get('ordering');
- if (ordering && !this.parent.sort.active) {
- this.parent.sort.direction = ordering[0] === '-' ? 'desc' : 'asc';
- this.parent.sort.active = ordering[0] === '-' ? ordering.substr(1) : ordering;
- this.parent.sortParam = ordering;
- }
-
- this.listParams = p;
- this.refresh();
- });
-
- super.startListenSocket();
- }
-
- ngOnDestroy() {
- super.ngOnDestroy();
+ .subscribe((p) => this.routeListener(+p.get('limit') || 10, +p.get('page'), p.get('ordering'), p));
+ }
+
+ init(): void {
+ this.initSocket();
+ this.initColumns();
+ this.initListItemEvent();
+ this.initRouteListener();
+ this.startListenSocket();
+ }
+
+ destroy() {
this.parent.listItemEvt.complete();
+
+ this.destroy$.next();
+ this.destroy$.complete();
+
+ this.store.dispatch(clearMessages());
}
checkParam(p: ParamMap): boolean {
@@ -89,9 +137,12 @@ export class BaseListDirective extends SocketListenerDirective implements OnInit
return true;
}
+ checkType(typeName: string, referenceTypeName: TypeName): boolean {
+ return (referenceTypeName ? referenceTypeName.split('2')[0] : referenceTypeName) === typeName;
+ }
+
socketListener(m: EventMessage): void {
const stype = (x: string) => `${m.object.type}${m.object.details.type ? `2${m.object.details.type}` : ''}` === x;
- const ctype = (name: string) => (name ? name.split('2')[0] : name) === m.object.type;
const checkUpgradable = () => (m.event === 'create' || m.event === 'delete') && m.object.type === 'bundle' && this.typeName === 'cluster';
const changeList = () => stype(this.typeName) && (m.event === 'create' || m.event === 'delete' || m.event === 'add' || m.event === 'remove');
@@ -111,7 +162,7 @@ export class BaseListDirective extends SocketListenerDirective implements OnInit
if (m.event === 'add' && stype('host2cluster')) rewriteRow(row);
- if (ctype(this.typeName)) {
+ if (this.checkType(m.object.type, this.typeName)) {
if (m.event === 'change_state') row.state = m.object.details.value;
if (m.event === 'change_status') row.status = +m.object.details.value;
if (m.event === 'change_job_status') row.status = m.object.details.value;
@@ -122,7 +173,12 @@ export class BaseListDirective extends SocketListenerDirective implements OnInit
refresh(id?: number) {
if (id) this.parent.current = { id };
- this.service.getList(this.listParams, this.typeName).subscribe((list) => (this.parent.dataSource = list));
+ this.service.getList(this.listParams, this.typeName).subscribe((list: IListResult) => {
+ if (this.reload) {
+ this.reload(list);
+ }
+ this.parent.dataSource = list;
+ });
}
listEvents(event: EmmitRow) {
diff --git a/web/src/app/shared/components/list/list.component.html b/web/src/app/shared/components/list/list.component.html
index c2037a9dee..dfc0bb3040 100644
--- a/web/src/app/shared/components/list/list.component.html
+++ b/web/src/app/shared/components/list/list.component.html
@@ -15,7 +15,7 @@
[indeterminate]="selection.hasValue() && !isAllSelected()">
-
@@ -113,8 +113,7 @@
State
- autorenew
- {{ row.state }}
+
@@ -131,13 +130,7 @@
Actions
-
- priority_hight
-
-
-
-
+
@@ -163,7 +156,7 @@
Upgrade
-
+
@@ -202,7 +195,7 @@
{{ row.cluster_name }}
...
@@ -217,17 +210,7 @@
Status
- {{ row.status }}
-
-
- check_circle_outline
-
-
- error_outline
-
-
+
diff --git a/web/src/app/shared/components/list/list.component.scss b/web/src/app/shared/components/list/list.component.scss
index ce87e4ba56..efc495c917 100644
--- a/web/src/app/shared/components/list/list.component.scss
+++ b/web/src/app/shared/components/list/list.component.scss
@@ -17,13 +17,3 @@ mat-cell.control {
flex-basis: 60px;
}
-.width100 {
- flex-grow: 0;
- flex-basis: 100px;
-}
-
-.width30pr {
- flex-grow: 1;
- flex-basis: 30%;
- width: 100px
-}
diff --git a/web/src/app/shared/components/list/list.component.ts b/web/src/app/shared/components/list/list.component.ts
index 9be8cbc4f4..6a8d3b2323 100644
--- a/web/src/app/shared/components/list/list.component.ts
+++ b/web/src/app/shared/components/list/list.component.ts
@@ -10,23 +10,20 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import { SelectionModel } from '@angular/cdk/collections';
-import { Component, ElementRef, EventEmitter, Input, OnInit, Output, QueryList, ViewChild, ViewChildren } from '@angular/core';
+import { Component, ElementRef, Input, OnDestroy, OnInit, QueryList, ViewChild, ViewChildren } from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
-import { MatPaginator, PageEvent } from '@angular/material/paginator';
+import { MatPaginator } from '@angular/material/paginator';
import { MatSort, MatSortHeader, Sort } from '@angular/material/sort';
-import { MatTableDataSource } from '@angular/material/table';
import { ActivatedRoute, Router } from '@angular/router';
-import { EmmitRow, isIssue, Issue, TypeName } from '@app/core/types';
import { BehaviorSubject } from 'rxjs';
-import { filter } from 'rxjs/operators';
+import { EventHelper } from '@adwp-ui/widgets';
-import { DialogComponent } from '../dialog.component';
-
-enum Direction {
- '' = '',
- 'asc' = '',
- 'desc' = '-',
-}
+import { ListDirective } from '@app/abstract-directives/list.directive';
+import { ListService } from '@app/shared/components/list/list.service';
+import { Store } from '@ngrx/store';
+import { SocketState } from '@app/core/store';
+import set = Reflect.set;
+import { ApiService } from '@app/core/api';
export interface ListResult {
count: number;
@@ -40,34 +37,20 @@ export interface ListResult {
templateUrl: './list.component.html',
styleUrls: ['./list.component.scss'],
})
-export class ListComponent implements OnInit {
+export class ListComponent extends ListDirective implements OnInit, OnDestroy {
+ EventHelper = EventHelper;
+
selection = new SelectionModel(true, []);
- current: any = {};
- type: TypeName;
clustersSubj = new BehaviorSubject<{ id: number; title: string }[]>([]);
clusters$ = this.clustersSubj.asObservable();
@Input()
currentItemId: string;
- @Input()
- columns: Array;
-
- data: MatTableDataSource = new MatTableDataSource([]);
- @Input()
- set dataSource(data: { results: any; count: number }) {
- if (data) {
- const list = data.results;
- this.data = new MatTableDataSource(list);
- this.paginator.length = data.count;
- this.listItemEvt.emit({ cmd: 'onLoad', row: list[0] });
- }
- }
- @Output()
- listItemEvt = new EventEmitter();
+ @ViewChildren(MatSortHeader, { read: ElementRef }) matSortHeader: QueryList;
- @Output() pageEvent = new EventEmitter();
+ sorting: MatSort[];
@ViewChild(MatPaginator, { static: true })
paginator: MatPaginator;
@@ -75,65 +58,21 @@ export class ListComponent implements OnInit {
@ViewChild(MatSort, { static: true })
sort: MatSort;
- @ViewChildren(MatSortHeader, { read: ElementRef }) matSortHeader: QueryList;
-
- addToSorting = false;
- sorting: MatSort[];
- sortParam = '';
-
- constructor(public dialog: MatDialog, public router: Router, public route: ActivatedRoute) {}
-
- getSortParam(a: Sort) {
- const penis: { [key: string]: string[] } = {
- prototype_version: ['prototype_display_name', 'prototype_version'],
- };
-
- const dumb = penis[a.active] ? penis[a.active] : [a.active],
- active = dumb.map((b: string) => `${Direction[a.direction]}${b}`).join(',');
-
- const current = this.sortParam;
- if (current && this.addToSorting) {
- const result = current
- .split(',')
- .filter((b) => dumb.every((d) => d !== b.replace('-', '')))
- .join(',');
- return [result, a.direction ? active : ''].filter((e) => e).join(',');
- }
-
- return a.direction ? active : '';
+ constructor(
+ protected service: ListService,
+ protected store: Store,
+ public dialog: MatDialog,
+ public route: ActivatedRoute,
+ public router: Router,
+ protected api: ApiService,
+ ) {
+ super(service, store, route, router, dialog, api);
}
ngOnInit(): void {
- this.sort.sortChange.subscribe((a: Sort) => {
- const _filter = this.route.snapshot.paramMap.get('filter') || '',
- { pageIndex, pageSize } = this.paginator,
- ordering = this.getSortParam(a);
-
- this.router.navigate(
- [
- './',
- {
- page: pageIndex,
- limit: pageSize,
- filter: _filter,
- ordering,
- },
- ],
- { relativeTo: this.route }
- );
-
- this.sortParam = ordering;
- });
- }
+ this.sort.sortChange.subscribe((sort: Sort) => this.changeSorting(sort));
- pageHandler(pageEvent: PageEvent) {
- this.pageEvent.emit(pageEvent);
- localStorage.setItem('limit', String(pageEvent.pageSize));
- const f = this.route.snapshot.paramMap.get('filter') || '';
- const ordering = this.getSortParam(this.sort);
- this.router.navigate(['./', { page: pageEvent.pageIndex, limit: pageEvent.pageSize, filter: f, ordering }], {
- relativeTo: this.route,
- });
+ super.ngOnInit();
}
trackBy(item: any) {
@@ -152,39 +91,4 @@ export class ListComponent implements OnInit {
this.isAllSelected() ? this.selection.clear() : this.data.data.forEach((row) => this.selection.select(row));
}
- getClusterData(row: any) {
- const { id, hostcomponent } = row.cluster || row;
- const { action } = row;
- return { id, hostcomponent, action };
- }
-
- stopPropagation($e: MouseEvent) {
- $e.stopPropagation();
- return $e;
- }
-
- notIssue(issue: Issue): boolean {
- return !isIssue(issue);
- }
-
- clickCell($e: MouseEvent, cmd: string, row: any, item?: any) {
- if ($e && $e.stopPropagation) $e.stopPropagation();
- this.current = row;
- this.listItemEvt.emit({ cmd, row, item });
- }
-
- delete($event: MouseEvent, row: any) {
- $event.stopPropagation();
- this.dialog
- .open(DialogComponent, {
- data: {
- title: `Deleting "${row.name || row.fqdn}"`,
- text: 'Are you sure?',
- controls: ['Yes', 'No'],
- },
- })
- .beforeClosed()
- .pipe(filter((yes) => yes))
- .subscribe(() => this.listItemEvt.emit({ cmd: 'delete', row }));
- }
}
diff --git a/web/src/app/shared/components/list/list.service.ts b/web/src/app/shared/components/list/list.service.ts
index 2af42b06bb..f9021c40ab 100644
--- a/web/src/app/shared/components/list/list.service.ts
+++ b/web/src/app/shared/components/list/list.service.ts
@@ -11,13 +11,14 @@
// limitations under the License.
import { Injectable } from '@angular/core';
import { ParamMap, Params, convertToParamMap } from '@angular/router';
-import { ApiService } from '@app/core/api';
-import { ClusterService } from '@app/core/services';
-import { Bundle, Cluster, Entities, Host, IAction, TypeName } from '@app/core/types';
-import { environment } from '@env/environment';
import { switchMap, tap, map } 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';
+import { Bundle, Cluster, Entities, Host, IAction, Service, TypeName } from '@app/core/types';
+
const COLUMNS_SET = {
cluster: ['name', 'prototype_version', 'description', 'state', 'status', 'actions', 'import', 'upgrade', 'config', 'controls'],
host2cluster: ['fqdn', 'provider_name', 'state', 'status', 'actions', 'config', 'remove'],
@@ -66,6 +67,8 @@ export class ListService {
return this.detail.getServices(p);
case 'bundle':
return this.api.getList(`${environment.apiRoot}stack/bundle/`, p);
+ case 'servicecomponent':
+ return this.api.getList(`${environment.apiRoot}cluster/${(this.detail.Current as Service).cluster_id}/service/${this.detail.Current.id}/component`, p);
default:
return this.api.root.pipe(switchMap((root) => this.api.getList(root[this.current.typeName], p)));
}
@@ -93,13 +96,13 @@ export class ListService {
.pipe(map((res) => res.results.map((a) => ({ id: a.id, title: a.name }))));
}
- addClusterToHost(cluster_id: number, row: Host) {
- this.api
+ addClusterToHost(cluster_id: number, row: Host): Observable {
+ return this.api
.post(`${environment.apiRoot}cluster/${cluster_id}/host/`, { host_id: row.id })
- .subscribe((host) => {
+ .pipe(tap((host) => {
row.cluster_id = host.cluster_id;
row.cluster_name = host.cluster;
- });
+ }));
}
checkItem(item: Entities) {
diff --git a/web/src/app/shared/components/main-info.component.ts b/web/src/app/shared/components/main-info.component.ts
index 7b5b734f07..bec7b86ca0 100644
--- a/web/src/app/shared/components/main-info.component.ts
+++ b/web/src/app/shared/components/main-info.component.ts
@@ -11,7 +11,8 @@
// limitations under the License.
import { Component, OnInit } from '@angular/core';
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
-import { ClusterService } from '@app/core';
+
+import { ClusterService } from '@app/core/services/cluster.service';
@Component({
selector: 'app-main-info',
diff --git a/web/src/app/shared/components/status/status.component.ts b/web/src/app/shared/components/status/status.component.ts
index b3f10fb5c7..77e8b84aa4 100644
--- a/web/src/app/shared/components/status/status.component.ts
+++ b/web/src/app/shared/components/status/status.component.ts
@@ -17,9 +17,9 @@ import { Store } from '@ngrx/store';
import { Observable, of } from 'rxjs';
import { map, switchMap, tap } from 'rxjs/operators';
-import { SocketListenerDirective } from '../../directives/socketListener.directive';
+import { SocketListenerDirective } from '@app/shared/directives/socketListener.directive';
import { StatusInfo, StatusService } from './status.service';
-import { ClusterService } from '@app/core';
+import { ClusterService } from '@app/core/services/cluster.service';
@Component({
selector: 'app-status',
diff --git a/web/src/app/shared/components/tooltip/tooltip.directive.ts b/web/src/app/shared/components/tooltip/tooltip.directive.ts
index 7e06508e10..ff589433c7 100644
--- a/web/src/app/shared/components/tooltip/tooltip.directive.ts
+++ b/web/src/app/shared/components/tooltip/tooltip.directive.ts
@@ -10,8 +10,9 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import { Directive, ElementRef, HostListener, Input } from '@angular/core';
-import { ApiBase } from '@app/core/types/api';
+import { EventHelper } from '@adwp-ui/widgets';
+import { ApiBase } from '@app/core/types/api';
import { ComponentName, PositionType, TooltipService } from '../tooltip/tooltip.service';
@Directive({
@@ -33,7 +34,7 @@ export class TooltipDirective {
constructor(private el: ElementRef, private tooltip: TooltipService) {}
@HostListener('mouseenter', ['$event']) menter(e: MouseEvent) {
- e.stopPropagation();
+ EventHelper.stopPropagation(e);
const options = {
content: this.appTooltip,
componentName: this.appTooltipComponent,
diff --git a/web/src/app/shared/components/upgrade.component.ts b/web/src/app/shared/components/upgrade.component.ts
index 264f22af94..c229beec2f 100644
--- a/web/src/app/shared/components/upgrade.component.ts
+++ b/web/src/app/shared/components/upgrade.component.ts
@@ -15,6 +15,7 @@ import { ApiService } from '@app/core/api';
import { EmmitRow, Issue, isIssue } from '@app/core/types';
import { concat, Observable, of } from 'rxjs';
import { filter, map, switchMap } from 'rxjs/operators';
+import { EventHelper } from '@adwp-ui/widgets';
import { BaseDirective } from '../directives';
import { DialogComponent } from './dialog.component';
@@ -46,7 +47,7 @@ interface Upgrade {
color="warn"
[disabled]="!checkIssue()"
[matMenuTriggerFor]="menu"
- (click)="$event.stopPropagation()"
+ (click)="EventHelper.stopPropagation($event)"
>
sync_problem
@@ -60,14 +61,16 @@ interface Upgrade {
`
})
export class UpgradeComponent extends BaseDirective {
+ EventHelper = EventHelper;
+
list$: Observable;
- row: UpgradeItem = { upgradable: false, upgrade: '', issue: null };
+ pRow: UpgradeItem = { upgradable: false, upgrade: '', issue: null };
@Input() xPosition = 'before';
@Input()
- set dataRow(row: UpgradeItem) {
- this.row = row;
+ set row(row: UpgradeItem) {
+ this.pRow = row;
if (row.upgrade) {
this.list$ = this.api.get(`${row.upgrade}?ordering=-name`).pipe(filter((list: Upgrade[]) => !!list.length));
}
@@ -81,7 +84,7 @@ export class UpgradeComponent extends BaseDirective {
}
checkIssue() {
- return this.row.upgradable && !isIssue(this.row.issue);
+ return this.pRow.upgradable && !isIssue(this.pRow.issue);
}
runUpgrade(item: Upgrade) {
diff --git a/web/src/app/shared/configuration/main/main.service.spec.ts b/web/src/app/shared/configuration/main/main.service.spec.ts
index 2c0666ce3f..7499adb8bc 100644
--- a/web/src/app/shared/configuration/main/main.service.spec.ts
+++ b/web/src/app/shared/configuration/main/main.service.spec.ts
@@ -14,13 +14,19 @@ import { TestBed } from '@angular/core/testing';
import { MainService } from './main.service';
import { FieldService } from '../field.service';
import { ApiService } from '@app/core/api';
+import { Store } from '@ngrx/store';
describe('MainService', () => {
let service: MainService;
beforeEach(() => {
TestBed.configureTestingModule({
- providers: [MainService, { provide: ApiService, useValue: {} }, { provide: FieldService, useValue: {} }],
+ providers: [
+ MainService,
+ { provide: ApiService, useValue: {} },
+ { provide: FieldService, useValue: {} },
+ { provide: Store, useValue: {} },
+ ],
});
service = TestBed.inject(MainService);
});
diff --git a/web/src/app/shared/configuration/main/main.service.ts b/web/src/app/shared/configuration/main/main.service.ts
index 2715687c43..7adbaf599c 100644
--- a/web/src/app/shared/configuration/main/main.service.ts
+++ b/web/src/app/shared/configuration/main/main.service.ts
@@ -11,12 +11,12 @@
// limitations under the License.
import { animate, state, style, transition, trigger } from '@angular/animations';
import { Injectable } from '@angular/core';
-import { ApiService } from '@app/core/api';
-import { ClusterService } from '@app/core/services';
-import { getRandomColor, isObject } from '@app/core/types';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
+import { ApiService } from '@app/core/api';
+import { ClusterService } from '@app/core/services/cluster.service';
+import { getRandomColor, isObject } from '@app/core/types';
import { FieldService, IOutput, TFormOptions } from '../field.service';
import { CompareConfig, IConfig, IFieldOptions, IFieldStack } from '../types';
diff --git a/web/src/app/shared/details/detail.component.html b/web/src/app/shared/details/detail.component.html
index dd569b02ef..38a7074afa 100644
--- a/web/src/app/shared/details/detail.component.html
+++ b/web/src/app/shared/details/detail.component.html
@@ -1,7 +1,20 @@
-
-
+
+
+
+
+
+
+
diff --git a/web/src/app/shared/details/detail.component.ts b/web/src/app/shared/details/detail.component.ts
index 5c683a4de4..0101d45574 100644
--- a/web/src/app/shared/details/detail.component.ts
+++ b/web/src/app/shared/details/detail.component.ts
@@ -11,15 +11,17 @@
// limitations under the License.
import { Component, OnDestroy, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
-import { ChannelService, ClusterService, keyChannelStrim, WorkerInstance } from '@app/core';
-import { EventMessage, SocketState } from '@app/core/store';
-import { Cluster, Host, IAction, Issue, Job, isIssue } from '@app/core/types';
import { Store } from '@ngrx/store';
import { Observable } from 'rxjs';
import { switchMap, tap } from 'rxjs/operators';
-import { SocketListenerDirective } from '../directives/socketListener.directive';
+import { ChannelService, keyChannelStrim } from '@app/core';
+import { WorkerInstance, ClusterService } from '@app/core/services/cluster.service';
+import { EventMessage, getNavigationPath, SocketState } from '@app/core/store';
+import { Cluster, Host, IAction, Issue, Job, isIssue } from '@app/core/types';
+import { SocketListenerDirective } from '@app/shared/directives/socketListener.directive';
import { IDetails } from './navigation.service';
+import { AdcmEntity } from '@app/models/entity';
@Component({
selector: 'app-detail',
@@ -35,7 +37,15 @@ export class DetailComponent extends SocketListenerDirective implements OnInit,
current: IDetails;
currentName = '';
- constructor(socket: Store, private route: ActivatedRoute, private service: ClusterService, private channel: ChannelService) {
+ navigationPath: Observable = this.store.select(getNavigationPath).pipe(this.takeUntil());
+
+ constructor(
+ socket: Store,
+ private route: ActivatedRoute,
+ private service: ClusterService,
+ private channel: ChannelService,
+ private store: Store,
+ ) {
super(socket);
}
@@ -61,7 +71,20 @@ export class DetailComponent extends SocketListenerDirective implements OnInit,
}
run(w: WorkerInstance) {
- const { id, name, typeName, action, actions, issue, status, prototype_name, prototype_display_name, prototype_version, bundle_id, state } = w.current;
+ const {
+ id,
+ name,
+ typeName,
+ action,
+ actions,
+ issue,
+ status,
+ prototype_name,
+ prototype_display_name,
+ prototype_version,
+ bundle_id,
+ state,
+ } = w.current;
const { upgradable, upgrade, hostcomponent } = w.current as Cluster;
const { log_files, objects } = w.current as Job;
const { provider_id } = w.current as Host;
@@ -114,7 +137,8 @@ export class DetailComponent extends SocketListenerDirective implements OnInit,
return;
}
- if (this.Current?.typeName === m.object.type && this.Current?.id === m.object.id) {
+ const type = m.object.type === 'component' ? 'servicecomponent' : m.object.type;
+ if (this.Current?.typeName === type && this.Current?.id === m.object.id) {
if (this.service.Current.typeName === 'job' && (m.event === 'change_job_status' || m.event === 'add_job_log')) {
this.reset();
return;
@@ -130,6 +154,6 @@ export class DetailComponent extends SocketListenerDirective implements OnInit,
}
// parent
- if (this.service.Cluster?.id === m.object.id && this.Current?.typeName !== 'cluster' && m.object.type === 'cluster' && m.event === 'clear_issue') this.issue = {};
+ if (this.service.Cluster?.id === m.object.id && this.Current?.typeName !== 'cluster' && type === 'cluster' && m.event === 'clear_issue') this.issue = {};
}
}
diff --git a/web/src/app/shared/details/details.module.ts b/web/src/app/shared/details/details.module.ts
index 401f6103ef..bc49d2749f 100644
--- a/web/src/app/shared/details/details.module.ts
+++ b/web/src/app/shared/details/details.module.ts
@@ -25,11 +25,44 @@ import { LeftComponent } from './left/left.component';
import { NavigationService } from './navigation.service';
import { SubtitleComponent } from './subtitle.component';
import { TopComponent } from './top/top.component';
+import { NavigationComponent } from '@app/components/navigation/navigation.component';
+import { ActionsColumnComponent } from '@app/components/columns/actions-column/actions-column.component';
+import { ActionsButtonComponent } from '@app/components/actions-button/actions-button.component';
+
+import { NavItemPipe } from '@app/pipes/nav-item.pipe';
+import { MatTooltipModule } from '@angular/material/tooltip';
@NgModule({
- imports: [CommonModule, RouterModule, StuffModule, MatCardModule, MatToolbarModule, MatSidenavModule, MatListModule, MatIconModule, MatButtonModule],
- exports: [DetailComponent],
- declarations: [DetailComponent, SubtitleComponent, LeftComponent, TopComponent],
- providers: [NavigationService],
+ imports: [
+ CommonModule,
+ RouterModule,
+ StuffModule,
+ MatCardModule,
+ MatToolbarModule,
+ MatSidenavModule,
+ MatListModule,
+ MatIconModule,
+ MatButtonModule,
+ MatTooltipModule,
+ ],
+ exports: [
+ DetailComponent,
+ ActionsColumnComponent,
+ ActionsButtonComponent,
+ ],
+ declarations: [
+ DetailComponent,
+ SubtitleComponent,
+ LeftComponent,
+ TopComponent,
+ NavigationComponent,
+ ActionsColumnComponent,
+ ActionsButtonComponent,
+
+ NavItemPipe,
+ ],
+ providers: [
+ NavigationService,
+ ],
})
export class DetailsModule {}
diff --git a/web/src/app/shared/details/left/left.component.ts b/web/src/app/shared/details/left/left.component.ts
index 0b1103ef6a..73ff94941b 100644
--- a/web/src/app/shared/details/left/left.component.ts
+++ b/web/src/app/shared/details/left/left.component.ts
@@ -10,20 +10,37 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import { Component, Input } from '@angular/core';
-import { ApiBase, Issue } from '@app/core/types';
+import { ApiBase, Issue } from '@app/core/types';
import { NavigationService, INavItem } from '../navigation.service';
@Component({
selector: 'app-details-left',
template: `
-
- {{ item.title }}
-
- cloud_download
+
+ {{ item.title }}
+
+
+ cloud_download
+
+
priority_hight
- {{ item.status === 0 ? 'check_circle_outline' : 'error_outline' }}
+
+
+ {{ item.status === 0 ? 'check_circle_outline' : 'error_outline' }}
+
`,
diff --git a/web/src/app/shared/details/navigation.service.ts b/web/src/app/shared/details/navigation.service.ts
index 0aaf7dfec5..050acb81e6 100644
--- a/web/src/app/shared/details/navigation.service.ts
+++ b/web/src/app/shared/details/navigation.service.ts
@@ -11,8 +11,9 @@
// limitations under the License.
import { Injectable } from '@angular/core';
import { ApiBase, Cluster, isIssue, Issue, Job, TypeName, IAction, LogFile, JobObject } from '@app/core/types';
+import { AdcmTypedEntity } from '@app/models/entity';
-const ISSUE_MESSAGE = 'Something is wrong with your cluster configuration, please review it.';
+export const ISSUE_MESSAGE = 'Something is wrong with your cluster configuration, please review it.';
export interface IDetails {
parent?: Cluster;
@@ -61,6 +62,11 @@ export interface INavItem {
action?: () => void;
}
+export interface IStyledNavItem {
+ class?: string;
+ entity?: AdcmTypedEntity;
+}
+
const all = [
{ id: 0, title: 'Main', url: 'main' },
{ id: 4, title: 'Configuration', url: 'config' },
@@ -74,13 +80,20 @@ const all = [
const [main, config, m_status, m_import, actions] = all;
+const components = {
+ id: 8,
+ title: 'Components',
+ url: 'component',
+};
+
export const Config = {
menu: {
cluster: all.sort((a, b) => a.id - b.id),
- service: [main, config, m_status, m_import, actions],
+ service: [main, components, config, m_status, m_import, actions],
host: [main, config, m_status, actions],
provider: [main, config, actions],
bundle: [main],
+ servicecomponent: [main, config, m_status, actions],
},
};
diff --git a/web/src/app/shared/details/top/top.component.spec.ts b/web/src/app/shared/details/top/top.component.spec.ts
index b76eb05552..eafb9674cb 100644
--- a/web/src/app/shared/details/top/top.component.spec.ts
+++ b/web/src/app/shared/details/top/top.component.spec.ts
@@ -13,7 +13,7 @@ import { ComponentFixture, TestBed } from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing';
import { ApiService } from '@app/core/api';
import { Cluster } from '@app/core/types';
-import { MaterialModule } from '@app/shared';
+import { MaterialModule } from '@app/shared/material.module';
import { StuffModule } from '@app/shared/stuff.module';
import { IDetails, NavigationService } from '../navigation.service';
diff --git a/web/src/app/shared/details/top/top.component.ts b/web/src/app/shared/details/top/top.component.ts
index 74fd4daa23..6e1b02b396 100644
--- a/web/src/app/shared/details/top/top.component.ts
+++ b/web/src/app/shared/details/top/top.component.ts
@@ -18,11 +18,11 @@ import { IDetails, INavItem, NavigationService } from '../navigation.service';
@Component({
selector: 'app-details-top',
template: `
-
-
-
-
-
+
+
+
+
+
`,
styles: [':host {display: flex;width: 100%;padding-right: 10px;}', 'app-action-list {display: block; margin-top: 2px;}'],
})
diff --git a/web/src/app/shared/directives/base.directive.ts b/web/src/app/shared/directives/base.directive.ts
index 273a41a2ed..54b1a40de0 100644
--- a/web/src/app/shared/directives/base.directive.ts
+++ b/web/src/app/shared/directives/base.directive.ts
@@ -11,7 +11,7 @@
// limitations under the License.
import { Directive, OnDestroy } from '@angular/core';
import { EventMessage } from '@app/core/store';
-import { Subject } from 'rxjs';
+import { MonoTypeOperatorFunction, Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
@Directive({
@@ -25,7 +25,7 @@ export class BaseDirective implements OnDestroy {
this.destroy$.complete();
}
- takeUntil() {
+ takeUntil(): MonoTypeOperatorFunction {
return takeUntil(this.destroy$);
}
}
diff --git a/web/src/app/shared/form-elements/bundles.component.ts b/web/src/app/shared/form-elements/bundles.component.ts
index 89abaf72c3..d1e59abf40 100644
--- a/web/src/app/shared/form-elements/bundles.component.ts
+++ b/web/src/app/shared/form-elements/bundles.component.ts
@@ -14,6 +14,7 @@ import { FormControl } from '@angular/forms';
import { Prototype, StackBase } from '@app/core/types';
import { of } from 'rxjs';
import { filter, map, switchMap } from 'rxjs/operators';
+import { EventHelper } from '@adwp-ui/widgets';
import { AddService } from '../add-component/add.service';
import { ButtonUploaderComponent } from './button-uploader.component';
@@ -42,7 +43,7 @@ import { InputComponent } from './input.component';
[color]="'accent'"
[asIcon]="true"
[label]="'Upload bundles'"
- (click)="$event.stopPropagation()"
+ (click)="EventHelper.stopPropagation($event)"
(output)="upload($event)"
>
@@ -50,15 +51,17 @@ import { InputComponent } from './input.component';
styles: ['.row { align-items: center;display:flex; }', 'mat-form-field {flex: 1}'],
})
export class BundlesComponent extends InputComponent implements OnInit {
+ EventHelper = EventHelper;
+
@Input() typeName: 'cluster' | 'provider';
@ViewChild('uploadBtn', { static: true }) uploadBtn: ButtonUploaderComponent;
- loadedBundle: { bundle_id: number; display_name: string };
+ loadedBundle: { bundle_id: number; display_name: string };
bundles: StackBase[] = [];
versions: StackBase[];
page = 1;
limit = 50;
disabledVersion = true;
-
+
constructor(private service: AddService) {
super();
}
diff --git a/web/src/app/shared/form-elements/form-elements.module.ts b/web/src/app/shared/form-elements/form-elements.module.ts
index 67de388abb..cddd4f8d39 100644
--- a/web/src/app/shared/form-elements/form-elements.module.ts
+++ b/web/src/app/shared/form-elements/form-elements.module.ts
@@ -25,7 +25,7 @@ import { FieldDirective } from './field.directive';
import { InputComponent } from './input.component';
import { JsonComponent } from './json.component';
import { BaseMapListDirective, FieldListComponent, FieldMapComponent } from './map.component';
-import { PasswordComponent } from './password.component';
+import { PasswordComponent } from './password/password.component';
import { TextBoxComponent } from './text-box.component';
import { TextareaComponent } from './textarea.component';
import { VariantComponent } from './variant.component';
diff --git a/web/src/app/shared/form-elements/password.component.ts b/web/src/app/shared/form-elements/password.component.ts
deleted file mode 100644
index ee9bb799f2..0000000000
--- a/web/src/app/shared/form-elements/password.component.ts
+++ /dev/null
@@ -1,72 +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 { Component, OnInit } from '@angular/core';
-import { FormControl } from '@angular/forms';
-
-import { FieldService } from './../configuration/field.service';
-import { FieldDirective } from './field.directive';
-
-@Component({
- selector: 'app-fields-password',
- template: `
-
-
-
- Field [{{ field.display_name }}] is required!
-
-
-
- Confirm [{{ field.display_name }}] is required!
- Field [{{ field.display_name }}] and confirm [{{ field.display_name }}] does not match!
-
-
- `,
- styleUrls: ['./password.component.scss'],
-})
-export class PasswordComponent extends FieldDirective implements OnInit {
- constructor(private service: FieldService) {
- super();
- }
-
- ngOnInit() {
- if (!this.field.ui_options?.no_confirm) {
- this.form.addControl(`confirm_${this.field.name}`, new FormControl(this.field.value, this.field.activatable ? [] : this.service.setValidator(this.field, this.control)));
- }
-
- super.ngOnInit();
-
- const confirm = this.getConfirmPasswordField();
- if (confirm) confirm.markAllAsTouched();
- }
-
- getConfirmPasswordField() {
- return this.form.controls['confirm_' + this.field.name];
- }
-
- hasErrorConfirm(name: string) {
- const c = this.getConfirmPasswordField();
- return this.getConfirmPasswordFieldErrors(name) && (c.touched || c.dirty);
- }
-
- confirmPasswordFieldUpdate() {
- const confirm = this.getConfirmPasswordField();
- return confirm ? confirm.updateValueAndValidity() : '';
- }
-
- getConfirmPasswordFieldErrors(error: string) {
- const confirm = this.getConfirmPasswordField();
- if (confirm && confirm.errors) {
- return confirm.errors[error];
- }
- return null;
- }
-}
diff --git a/web/src/app/shared/form-elements/password/password.component.html b/web/src/app/shared/form-elements/password/password.component.html
new file mode 100644
index 0000000000..6e8aa22a46
--- /dev/null
+++ b/web/src/app/shared/form-elements/password/password.component.html
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ Field [{{ field.display_name }}] is required!
+
+
+
+ Confirm [{{ field.display_name }}] is required!
+
+ Field [{{ field.display_name }}] and confirm [{{ field.display_name }}] does not match!
+
+
+
diff --git a/web/src/app/shared/form-elements/password.component.scss b/web/src/app/shared/form-elements/password/password.component.scss
similarity index 81%
rename from web/src/app/shared/form-elements/password.component.scss
rename to web/src/app/shared/form-elements/password/password.component.scss
index c8cc92b9a7..b3ddad770b 100644
--- a/web/src/app/shared/form-elements/password.component.scss
+++ b/web/src/app/shared/form-elements/password/password.component.scss
@@ -1,5 +1,5 @@
-:host {
- display: flex;
+div {
+ flex: 1;
& mat-form-field {
flex-basis: 50%;
@@ -8,5 +8,4 @@
& mat-form-field:first-child {
margin-right: 10px;
}
-
}
diff --git a/web/src/app/shared/form-elements/password/password.component.ts b/web/src/app/shared/form-elements/password/password.component.ts
new file mode 100644
index 0000000000..458a552388
--- /dev/null
+++ b/web/src/app/shared/form-elements/password/password.component.ts
@@ -0,0 +1,170 @@
+// 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 {
+ AfterViewInit,
+ ChangeDetectorRef,
+ Component,
+ ElementRef,
+ OnChanges,
+ OnInit,
+ SimpleChanges,
+ ViewChild,
+} from '@angular/core';
+import { AbstractControl, FormControl } from '@angular/forms';
+import { fromEvent, merge } from 'rxjs';
+import { debounceTime, pluck, tap } from 'rxjs/operators';
+
+import { FieldDirective } from '../field.directive';
+import { FieldService } from './../../configuration/field.service';
+
+@Component({
+ selector: 'app-fields-password',
+ templateUrl: './password.component.html',
+ styleUrls: ['./password.component.scss'],
+})
+export class PasswordComponent extends FieldDirective implements OnInit, AfterViewInit, OnChanges {
+ dummy = '********';
+ isHideDummy = false;
+ value: string;
+
+ constructor(private service: FieldService, private cd: ChangeDetectorRef) {
+ super();
+ }
+
+ @ViewChild('input', { read: ElementRef }) input: ElementRef;
+ @ViewChild('conf', { read: ElementRef }) conf: ElementRef;
+
+ ngOnChanges(changes: SimpleChanges) {
+ if (!changes.field.firstChange) {
+ this.initConfirm();
+ }
+ }
+
+ ngOnInit() {
+ this.initConfirm();
+ super.ngOnInit();
+
+ if (!this.control.value) this.dummy = '';
+ this.value = this.control.value;
+ }
+
+ initConfirm(): void {
+ if (!this.field.ui_options?.no_confirm) {
+ this.form.addControl(
+ `confirm_${this.field.name}`,
+ new FormControl(
+ this.field.value,
+ this.field.activatable ? [] : this.service.setValidator(this.field, this.control)
+ )
+ );
+ }
+
+ if (this.field.required && !this.field.value) {
+ this.hideDummy(false);
+ }
+
+ if (this.ConfirmPasswordField) this.ConfirmPasswordField.markAllAsTouched();
+ }
+
+ ngAfterViewInit(): void {
+ if (this.ConfirmPasswordField) {
+ const a = fromEvent(this.input.nativeElement, 'blur');
+ const c = fromEvent(this.input.nativeElement, 'focus');
+ const b = fromEvent(this.conf.nativeElement, 'blur');
+ const d = fromEvent(this.conf.nativeElement, 'focus');
+
+ merge(a, b, c, d)
+ .pipe(
+ debounceTime(100),
+ pluck('type'),
+ tap((res: 'focus' | 'blur') => {
+ if (res === 'blur' && (this.isValidField() || this.isCleared())) {
+ if ((this.isValidField() && this.isCleared()) || this.isCleared()) {
+ this.control.setValue(this.value);
+ this.ConfirmPasswordField.setValue(this.value);
+ }
+ this.isHideDummy = false;
+ this.cd.detectChanges();
+ }
+ })
+ )
+ .subscribe();
+ } else {
+ fromEvent(this.input.nativeElement, 'blur')
+ .pipe(
+ tap(_ => {
+ if (this.control.valid || this.value !== '' && this.control.value === '') {
+ if ((this.control.valid && this.value !== '' && this.control.value === '') || this.value !== '' && this.control.value === '') {
+ this.control.setValue(this.value);
+ }
+ this.isHideDummy = false;
+ this.cd.detectChanges();
+ }
+ })
+ ).subscribe();
+ }
+ }
+
+ isValidField(): boolean {
+ return this.control.valid && this.ConfirmPasswordField.valid;
+ }
+
+ isCleared(): boolean {
+ return this.value !== '' && this.control.value === '' && this.ConfirmPasswordField.value === '';
+ }
+
+ hideDummy(isConfirmField: boolean): void {
+ if (this.field.read_only) return null;
+ this.isHideDummy = true;
+ this.cd.detectChanges();
+
+ if (isConfirmField) {
+ this.conf.nativeElement.focus();
+ } else {
+ this.input.nativeElement.focus();
+ }
+
+ this.control.setValue('');
+ if (this.ConfirmPasswordField) this.ConfirmPasswordField.setValue('');
+ }
+
+ get ConfirmPasswordField(): AbstractControl {
+ return this.form.controls['confirm_' + this.field.name];
+ }
+
+ hasErrorConfirm(name: string) {
+ const c = this.ConfirmPasswordField;
+ return this.getConfirmPasswordFieldErrors(name) && (c.touched || c.dirty);
+ }
+
+ confirmPasswordFieldUpdate() {
+ this.dummy = this.control.value;
+ this.value = this.control.value;
+ const confirm = this.ConfirmPasswordField;
+ return confirm ? confirm.updateValueAndValidity() : '';
+ }
+
+ getConfirmPasswordFieldErrors(error: string) {
+ const confirm = this.ConfirmPasswordField;
+ if (confirm && confirm.errors) {
+ return confirm.errors[error];
+ }
+ return null;
+ }
+
+ change(value: string) {
+ if (value === null) {
+ this.hideDummy(false);
+ this.cd.detectChanges();
+ }
+ }
+}
diff --git a/web/src/app/shared/index.ts b/web/src/app/shared/index.ts
deleted file mode 100644
index 98c4409cd1..0000000000
--- a/web/src/app/shared/index.ts
+++ /dev/null
@@ -1,19 +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.
-export * from './shared.module';
-export * from './material.module';
-export * from './components';
-export * from './directives';
-export * from './pipes';
-export * from './configuration/main/main.component';
-export * from './host-components-map/services2hosts/service-host.component';
-export * from './details/detail.component';
diff --git a/web/src/app/shared/material.module.ts b/web/src/app/shared/material.module.ts
index c452ba0d53..27f387ae98 100644
--- a/web/src/app/shared/material.module.ts
+++ b/web/src/app/shared/material.module.ts
@@ -18,7 +18,6 @@ import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatDialogModule } from '@angular/material/dialog';
import { MatExpansionModule } from '@angular/material/expansion';
import { MatFormFieldModule } from '@angular/material/form-field';
-//import { MatGridListModule } from '@angular/material/grid-list';
import { MatIconModule } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input';
import { MatListModule } from '@angular/material/list';
@@ -26,7 +25,6 @@ import { MatMenuModule } from '@angular/material/menu';
import { MatPaginatorModule } from '@angular/material/paginator';
import { MatProgressBarModule } from '@angular/material/progress-bar';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
-//import { MatRadioModule } from '@angular/material/radio';
import { MatSelectModule } from '@angular/material/select';
import { MatSidenavModule } from '@angular/material/sidenav';
import { MatSlideToggleModule } from '@angular/material/slide-toggle';
@@ -35,15 +33,13 @@ import { MatSnackBarModule } from '@angular/material/snack-bar';
import { MatSortModule } from '@angular/material/sort';
import { MatStepperModule } from '@angular/material/stepper';
import { MatTableModule } from '@angular/material/table';
-//import { MatTabsModule } from '@angular/material/tabs';
import { MatToolbarModule } from '@angular/material/toolbar';
import { MatTooltipModule } from '@angular/material/tooltip';
-// import { MatTreeModule } from '@angular/material/tree';
+import { MatButtonToggleModule } from '@angular/material/button-toggle';
@NgModule({
exports: [
MatStepperModule,
- //MatGridListModule,
CdkTableModule,
MatSlideToggleModule,
MatToolbarModule,
@@ -51,14 +47,12 @@ import { MatTooltipModule } from '@angular/material/tooltip';
MatMenuModule,
MatCardModule,
MatExpansionModule,
- //MatTabsModule,
MatFormFieldModule,
MatSelectModule,
MatInputModule,
MatCheckboxModule,
MatButtonModule,
MatAutocompleteModule,
- //MatRadioModule,
MatDialogModule,
MatTooltipModule,
MatSnackBarModule,
@@ -70,7 +64,7 @@ import { MatTooltipModule } from '@angular/material/tooltip';
MatPaginatorModule,
MatSortModule,
MatSliderModule,
- // MatTreeModule,
+ MatButtonToggleModule,
],
})
export class MaterialModule {}
diff --git a/web/src/app/shared/shared.module.ts b/web/src/app/shared/shared.module.ts
index 2f59e4d1de..d8199b0b08 100644
--- a/web/src/app/shared/shared.module.ts
+++ b/web/src/app/shared/shared.module.ts
@@ -13,6 +13,7 @@ import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { RouterModule } from '@angular/router';
+import { AdwpListModule } from '@adwp-ui/widgets';
import { AddingModule } from './add-component/adding.module';
import {
@@ -29,7 +30,6 @@ import {
} from './components';
import { ActionCardComponent } from './components/actions/action-card/action-card.component';
import { ActionMasterConfigComponent } from './components/actions/master/action-master-config.component';
-import { BaseListDirective } from './components/list/base-list.directive';
import { ListComponent } from './components/list/list.component';
import { MultiSortDirective } from './components/list/multi-sort.directive';
import { SimpleTextComponent } from './components/tooltip';
@@ -41,6 +41,11 @@ import { HostComponentsMapModule } from './host-components-map/host-components-m
import { MaterialModule } from './material.module';
import { BreakRowPipe, TagEscPipe } from './pipes';
import { StuffModule } from './stuff.module';
+import { StatusColumnComponent } from '@app/components/columns/status-column/status-column.component';
+import { StateColumnComponent } from '@app/components/columns/state-column/state-column.component';
+import { EditionColumnComponent } from '@app/components/columns/edition-column/edition-column.component';
+import { ClusterColumnComponent } from '@app/components/columns/cluster-column/cluster-column.component';
+import { ServiceComponentsComponent } from '@app/components/service-components.component';
@NgModule({
imports: [
@@ -55,6 +60,9 @@ import { StuffModule } from './stuff.module';
AddingModule,
HostComponentsMapModule,
DetailsModule,
+ AdwpListModule.forRoot({
+ itemsPerPage: [10, 25, 50, 100],
+ }),
],
declarations: [
DialogComponent,
@@ -66,7 +74,6 @@ import { StuffModule } from './stuff.module';
TagEscPipe,
IssueInfoComponent,
SimpleTextComponent,
- BaseListDirective,
StatusComponent,
StatusInfoComponent,
MainInfoComponent,
@@ -76,6 +83,11 @@ import { StuffModule } from './stuff.module';
ActionMasterComponent,
ActionMasterConfigComponent,
ActionCardComponent,
+ StatusColumnComponent,
+ StateColumnComponent,
+ EditionColumnComponent,
+ ClusterColumnComponent,
+ ServiceComponentsComponent,
],
// entryComponents: [DialogComponent, IssueInfoComponent, IssueInfoComponent, StatusInfoComponent, SimpleTextComponent, ActionMasterComponent],
exports: [
@@ -96,13 +108,18 @@ import { StuffModule } from './stuff.module';
ButtonSpinnerComponent,
UpgradeComponent,
TagEscPipe,
- BaseListDirective,
StatusComponent,
StatusInfoComponent,
MainInfoComponent,
ImportComponent,
ExportComponent,
ActionCardComponent,
+ StatusColumnComponent,
+ StateColumnComponent,
+ EditionColumnComponent,
+ ClusterColumnComponent,
+ ServiceComponentsComponent,
+ AdwpListModule,
],
})
export class SharedModule {}
diff --git a/web/src/app/shared/stuff.module.ts b/web/src/app/shared/stuff.module.ts
index 03a54bcc18..aac4bd70ed 100644
--- a/web/src/app/shared/stuff.module.ts
+++ b/web/src/app/shared/stuff.module.ts
@@ -18,10 +18,16 @@ import { ActionListComponent } from './components/actions/action-list/action-lis
import { ActionsDirective } from './components/actions/actions.directive';
import { TooltipComponent } from './components/tooltip/tooltip.component';
import { TooltipDirective } from './components/tooltip/tooltip.directive';
+import { PopoverDirective } from '@app/directives/popover.directive';
import { BaseDirective, ForTestDirective, InfinityScrollDirective, MTextareaDirective, ScrollDirective, SocketListenerDirective } from './directives';
import { MaterialModule } from './material.module';
import { MenuItemComponent } from './components/actions/action-list/menu-item/menu-item.component';
import { CardItemComponent } from './components/actions/action-card/card-item/card-item.component';
+import { PopoverComponent } from '@app/components/popover/popover.component';
+import { IssuesComponent } from '@app/components/issues/issues.component';
+import { KeysPipe } from '@app/pipes/keys.pipe';
+import { IsArrayPipe } from '@app/pipes/is-array.pipe';
+import { IssuePathPipe } from '@app/pipes/issue-path.pipe';
@NgModule({
declarations: [
@@ -40,6 +46,12 @@ import { CardItemComponent } from './components/actions/action-card/card-item/ca
ActionListComponent,
MenuItemComponent,
CardItemComponent,
+ PopoverDirective,
+ PopoverComponent,
+ IssuesComponent,
+ KeysPipe,
+ IsArrayPipe,
+ IssuePathPipe,
],
imports: [CommonModule, MaterialModule, RouterModule],
exports: [
@@ -58,6 +70,12 @@ import { CardItemComponent } from './components/actions/action-card/card-item/ca
ActionListComponent,
MenuItemComponent,
CardItemComponent,
+ PopoverDirective,
+ PopoverComponent,
+ IssuesComponent,
+ KeysPipe,
+ IsArrayPipe,
+ IssuePathPipe,
],
})
export class StuffModule {}
diff --git a/web/src/app/store/navigation/navigation.store.ts b/web/src/app/store/navigation/navigation.store.ts
new file mode 100644
index 0000000000..5cf0cc5fe9
--- /dev/null
+++ b/web/src/app/store/navigation/navigation.store.ts
@@ -0,0 +1,135 @@
+import {
+ Action,
+ createAction,
+ createFeatureSelector,
+ createReducer,
+ createSelector,
+ on,
+ props,
+ Store,
+} from '@ngrx/store';
+import { ParamMap } from '@angular/router';
+import { Injectable } from '@angular/core';
+import { Actions, createEffect, ofType } from '@ngrx/effects';
+import { concatMap, filter, map, switchMap, take } from 'rxjs/operators';
+import { Observable, zip } from 'rxjs';
+
+import { AdcmEntity, AdcmTypedEntity } from '@app/models/entity';
+import { TypeName } from '@app/core/types';
+import { ApiService } from '@app/core/api';
+import { ServiceComponentService } from '@app/services/service-component.service';
+import { EntityNames } from '@app/models/entity-names';
+import { EventMessage, socketResponse } from '@app/core/store/sockets/socket.reducer';
+
+export const setPath = createAction('[Navigation] Set path', props<{ path: AdcmTypedEntity[] }>());
+export const setPathOfRoute = createAction('[Navigation] Set path', props<{ params: ParamMap }>());
+
+export interface NavigationState {
+ path: AdcmTypedEntity[];
+}
+
+const initialState: NavigationState = {
+ path: [],
+};
+
+const reducer = createReducer(
+ initialState,
+ on(setPath, (state, { path }) => ({ path })),
+);
+
+export function navigationReducer(state: NavigationState, action: Action) {
+ return reducer(state, action);
+}
+
+export const getNavigationState = createFeatureSelector('navigation');
+export const getNavigationPath = createSelector(
+ getNavigationState,
+ state => state.path
+);
+
+export function getEventEntityType(type: string): TypeName {
+ return type === 'component' ? 'servicecomponent' : type;
+}
+
+export function getPath(getters: Observable[]): Observable {
+ return zip(...getters).pipe(
+ map((path: AdcmTypedEntity[]) => setPath({ path })),
+ );
+}
+
+@Injectable()
+export class NavigationEffects {
+
+ setPathOfRoute$ = createEffect(
+ () =>
+ this.actions$.pipe(
+ ofType(setPathOfRoute),
+ filter(action => !!action.params),
+ switchMap(action => {
+ const getters: Observable[] = action.params.keys.reduce((acc, param) => {
+ const getter = this.entityGetter(param as TypeName, +action.params.get(param));
+ if (getter) {
+ acc.push(getter);
+ }
+
+ return acc;
+ }, []);
+
+ return getPath(getters);
+ }),
+ ),
+ );
+
+ changePathOfEvent$ = createEffect(() => this.actions$.pipe(
+ ofType(socketResponse),
+ filter(action => ['raise_issue', 'clear_issue'].includes(action.message.event)),
+ concatMap((event: { message: EventMessage }) => {
+ return new Observable(subscriber => {
+ this.store.select(getNavigationPath).pipe(take(1)).subscribe((path) => {
+ if (path.some(item => item.typeName === getEventEntityType(event.message.object.type) && event.message.object.id === item.id)) {
+ this.entityGetter(getEventEntityType(event.message.object.type), event.message.object.id)
+ .subscribe((entity) => {
+ subscriber.next(setPath({
+ path: path.reduce((acc, item) =>
+ acc.concat(getEventEntityType(event.message.object.type) === item.typeName && item.id === event.message.object.id ? entity : item), []),
+ }));
+ subscriber.complete();
+ }, () => subscriber.complete());
+ } else {
+ subscriber.complete();
+ }
+ }, () => subscriber.complete());
+ });
+ }),
+ ));
+
+ constructor(
+ private actions$: Actions,
+ private api: ApiService,
+ private serviceComponentService: ServiceComponentService,
+ private store: Store,
+ ) {}
+
+ entityGetter(type: TypeName, id: number): Observable {
+ const entityToTypedEntity = (getter: Observable, typeName: TypeName) => getter.pipe(
+ map(entity => ({
+ ...entity,
+ typeName,
+ } as AdcmTypedEntity))
+ );
+ if (EntityNames.includes(type)) {
+ if (type === 'servicecomponent') {
+ return entityToTypedEntity(
+ this.serviceComponentService.get(id),
+ type,
+ );
+ } else {
+ return entityToTypedEntity(
+ this.api.getOne(type, id),
+ type,
+ );
+ }
+ }
+ }
+
+}
diff --git a/web/src/styles.scss b/web/src/styles.scss
index 3fe9254d7b..364644b6b3 100644
--- a/web/src/styles.scss
+++ b/web/src/styles.scss
@@ -162,6 +162,8 @@ mat-checkbox.advanced>label>span {
.mat-dialog-content {
margin: 0 -18px !important;
max-height: 80vh !important;
+ padding-top: 2px !important;
+ padding-bottom: 2px !important;
}
.mat-dialog-actions {
@@ -356,3 +358,25 @@ mat-cell.control {
-webkit-font-feature-settings: 'liga';
-webkit-font-smoothing: antialiased;
}
+
+* {
+ scrollbar-width: thin;
+ scrollbar-color: #B5C6CA #FFF;
+}
+
+*::-webkit-scrollbar,
+*::-webkit-scrollbar {
+ width: 4px;
+}
+
+*::-webkit-scrollbar-track,
+*::-webkit-scrollbar-track {
+ background: inherit;
+}
+
+*::-webkit-scrollbar-thumb,
+*::-webkit-scrollbar-thumb {
+ background-color: #B5C6CA;
+ border-radius: 2.5px;
+ border: 3px solid inherit;
+}