diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000..79a16af --- /dev/null +++ b/.flake8 @@ -0,0 +1,2 @@ +[flake8] +max-line-length = 120 \ No newline at end of file diff --git a/services/mysql-mgmt/docker_compose.yml b/services/mysql-mgmt/docker_compose.yml index 01a2209..3c12bcb 100644 --- a/services/mysql-mgmt/docker_compose.yml +++ b/services/mysql-mgmt/docker_compose.yml @@ -1,8 +1,7 @@ --- services: initcontainer: - build: "." - image: "mysql-mgmt" + image: "ghcr.io/szachovy/superset-cluster-mysql-mgmt:latest" user: "root" container_name: "mysql-mgmt-initcontainer" network_mode: "host" @@ -12,7 +11,7 @@ services: - "/bin/bash" - "-c" - | - set -euxo pipefail + set -euo pipefail export MYSQL_SUPERSET_PASSWORD=$( str | StopIteration: if clib.getifaddrs(ctypes.pointer(network_interfaces)) == 0: current_interface: network_interface_structure = network_interfaces.contents while True: - if current_interface.network_interface_name == network_interface.encode() and current_interface.network_interface_data: + if current_interface.network_interface_name == network_interface.encode() \ + and current_interface.network_interface_data: if current_interface.network_interface_address.contents.socket_address_family == 2: - return socket.inet_ntop(socket.AF_INET, - ctypes.cast(ctypes.pointer(current_interface.network_interface_address.contents), - ctypes.POINTER(socket_interface) - ).contents.socket_interface_address - ) + return socket.inet_ntop( + socket.AF_INET, + ctypes.cast( + ctypes.pointer(current_interface.network_interface_address.contents), + ctypes.POINTER(socket_interface) + ).contents.socket_interface_address + ) if not current_interface.next_network_interface: clib.freeifaddrs(network_interfaces) raise StopIteration(f'Provided network interface {network_interface} not found.') diff --git a/services/mysql-mgmt/keepalived.conf.tpl b/services/mysql-mgmt/keepalived.conf.tpl index 6001055..210e536 100644 --- a/services/mysql-mgmt/keepalived.conf.tpl +++ b/services/mysql-mgmt/keepalived.conf.tpl @@ -1,28 +1,34 @@ global_defs { vrrp_startup_delay 15 + log_file "/opt/default/mysql_router/log/keepalived.log" + enable_traps } vrrp_script status { script "/bin/killall -0 mysqlrouter" - interval 2 + interval 1 weight 2 } vrrp_instance virtual_instance { - state ${STATE} - interface ${VIRTUAL_NETWORK_INTERFACE} + state "${STATE}" + interface "${VIRTUAL_NETWORK_INTERFACE}" virtual_router_id 51 priority ${PRIORITY} advert_int 1 nopreempt + garp_master_delay 2 track_script { status } + track_interface { + "${VIRTUAL_NETWORK_INTERFACE}" + } virtual_ipaddress { - ${VIRTUAL_IP_ADDRESS}/${VIRTUAL_NETWORK_MASK} + "${VIRTUAL_IP_ADDRESS}/${VIRTUAL_NETWORK_MASK}" } virtual_routes { - ${VIRTUAL_NETWORK} src ${VIRTUAL_IP_ADDRESS} metric 1 dev ${VIRTUAL_NETWORK_INTERFACE} scope link + "${VIRTUAL_NETWORK}" src "${VIRTUAL_IP_ADDRESS}" metric 1 dev "${VIRTUAL_NETWORK_INTERFACE}" scope link } } diff --git a/services/mysql-server/mysql_config.cnf.tpl b/services/mysql-server/mysql_config.cnf.tpl index 9d1cc22..e3c8114 100644 --- a/services/mysql-server/mysql_config.cnf.tpl +++ b/services/mysql-server/mysql_config.cnf.tpl @@ -12,3 +12,5 @@ ssl-ca = "/etc/mysql/ssl/superset_cluster_ca_certificate.pem" ssl-cert = "/etc/mysql/ssl/mysql_server_certificate.pem" ssl-key = "/etc/mysql/ssl/mysql_server_key.pem" require_secure_transport = "ON" +max_connections = "50" +max_connect_errors = "50" diff --git a/services/mysql-server/seccomp.json b/services/mysql-server/seccomp.json index 45a0598..f79acd4 100644 --- a/services/mysql-server/seccomp.json +++ b/services/mysql-server/seccomp.json @@ -1,14 +1,13 @@ { "defaultAction": "SCMP_ACT_ALLOW", "architectures": [ - "SCMP_ARCH_X86_64" + "SCMP_ARCH_X86_64" ], "syscalls": [ - { - "names": [ - "kill" - ], - "action": "SCMP_ACT_ERRNO" - } - ] + { + "names": [ + "kill" + ], + "action": "SCMP_ACT_ERRNO" + }] } diff --git a/services/mysql-server/store_credentials.exp b/services/mysql-server/store_credentials.exp index 80c4d4b..a02f961 100755 --- a/services/mysql-server/store_credentials.exp +++ b/services/mysql-server/store_credentials.exp @@ -2,41 +2,21 @@ log_user 0 -set node1 [lindex $argv 0] -set node2 [lindex $argv 1] -set node3 [lindex $argv 2] +set timeout 10 set mysql_root_password [exec cat "/var/run/mysqld/mysql_root_password"] set filepath $env(MYSQL_TEST_LOGIN_FILE) -spawn mysql_config_editor set \ - --login-path=$node1 \ - --host=$node1 \ - --user=root \ - --skip-warn \ - --password - -expect "Enter password:" -send "$mysql_root_password\r" - -spawn mysql_config_editor set \ - --login-path=$node2 \ - --host=$node2 \ - --user=root \ - --skip-warn \ - --password - -expect "Enter password:" -send "$mysql_root_password\r" - -spawn mysql_config_editor set \ - --login-path=$node3 \ - --host=$node3 \ - --user=root \ - --skip-warn \ - --password - -expect "Enter password:" -send "$mysql_root_password\r" +foreach node $argv { + spawn mysql_config_editor set \ + --login-path=$node \ + --host=$node \ + --user=root \ + --skip-warn \ + --password + + expect "Enter password:" + send "$mysql_root_password\r" +} expect eof diff --git a/services/superset/Dockerfile b/services/superset/Dockerfile index 3c8a16d..200b540 100644 --- a/services/superset/Dockerfile +++ b/services/superset/Dockerfile @@ -41,7 +41,6 @@ RUN \ superset:superset \ "/var/lib/nginx" \ "/var/log/nginx" \ - "/app" \ && \ apt-get \ clean \ @@ -58,6 +57,7 @@ ENTRYPOINT [ "/bin/bash", "-c", " \ --recursive \ superset:superset \ /etc/ssl/certs \ + /app \ && \ gosu \ superset \ @@ -67,5 +67,10 @@ ENTRYPOINT [ "/bin/bash", "-c", " \ gosu \ superset \ /app/entrypoint.sh \ + && \ + chown \ + --recursive \ + root:root \ + /app \ " \ ] diff --git a/services/superset/entrypoint.sh b/services/superset/entrypoint.sh index f87a41c..81c0c45 100755 --- a/services/superset/entrypoint.sh +++ b/services/superset/entrypoint.sh @@ -1,7 +1,9 @@ #!/bin/bash +set -euxo pipefail + if superset test_db \ - "mysql+mysqlconnector://superset:$(cat /run/secrets/mysql_superset_password)@${VIRTUAL_IP_ADDRESS}:6446/superset" \ + "mysql+mysqlconnector://superset:$(< /run/secrets/mysql_superset_password)@${VIRTUAL_IP_ADDRESS}:6446/superset" \ --connect-args {}; then superset fab create-admin \ @@ -21,7 +23,9 @@ if superset test_db \ --app superset.tasks.celery_app:app worker \ --pool prefork \ --concurrency 4 \ - -O fair + -O fair & + + wait else echo "Could not connect to the MySQL database" exit 1 diff --git a/services/superset/set_database_uri.exp b/services/superset/set_database_uri.exp index abe0e2d..de3167e 100755 --- a/services/superset/set_database_uri.exp +++ b/services/superset/set_database_uri.exp @@ -1,9 +1,38 @@ #!/usr/bin/expect -f -spawn superset shell -expect ">>>" -send "import mysql_connect; mysql_connect.create_mysql_connection();\n" -expect ">>>" -send "exit()\n" +set timeout 10 + +if {[catch {spawn superset shell} result]} { + puts "Failed to start the Superset shell: $result" + exit 1 +} + +expect { + ">>>" { + send "import mysql_connect; mysql_connect.create_mysql_connection()\n" + } + timeout { + puts "Timeout while waiting for Superset shell prompt to create MySQL database connection" + exit 1 + } + eof { + puts "Superset shell prompt to create MySQL database connection terminated unexpectedly" + exit 1 + } +} + +expect { + ">>>" { + send "exit()\n" + } + timeout { + puts "Timeout while waiting for Superset shell prompt to create MySQL database connection" + exit 1 + } + eof { + puts "Superset shell prompt to create MySQL database connection terminated unexpectedly" + exit 1 + } +} expect eof diff --git a/services/superset/superset_config.py b/services/superset/superset_config.py index ae8dbfa..b91bd61 100644 --- a/services/superset/superset_config.py +++ b/services/superset/superset_config.py @@ -2,6 +2,7 @@ import flask_caching.backends.rediscache import os + class CeleryConfig(object): broker_url = "redis://redis:6379/0" imports = ( @@ -17,17 +18,19 @@ class CeleryConfig(object): }, } -with open('/run/secrets/superset_secret_key', 'r') as superset_secret_key: + +with open("/run/secrets/superset_secret_key", "r") as superset_secret_key: SECRET_KEY = superset_secret_key.read().strip() -with open('/run/secrets/mysql_superset_password', 'r') as mysql_superset_password: + +with open("/run/secrets/mysql_superset_password", "r") as mysql_superset_password: SQLALCHEMY_DATABASE_URI = f"mysql+mysqlconnector://superset:{mysql_superset_password.read().strip()}@{os.environ.get('VIRTUAL_IP_ADDRESS')}:6446/superset" CELERY_CONFIG = CeleryConfig -RESULTS_BACKEND = flask_caching.backends.rediscache.RedisCache(host="redis", port=6379, key_prefix='superset_results') +RESULTS_BACKEND = flask_caching.backends.rediscache.RedisCache(host="redis", port=6379, key_prefix="superset_results") FILTER_STATE_CACHE_CONFIG = { - 'CACHE_TYPE': 'RedisCache', - 'CACHE_DEFAULT_TIMEOUT': 86400, - 'CACHE_KEY_PREFIX': 'superset_filter_cache', - 'CACHE_REDIS_URL': "redis://redis:6379/0" + "CACHE_TYPE": "RedisCache", + "CACHE_DEFAULT_TIMEOUT": 86400, + "CACHE_KEY_PREFIX": "superset_filter_cache", + "CACHE_REDIS_URL": "redis://redis:6379/0" } diff --git a/src/container.py b/src/container.py index 381bffd..5bc99e2 100644 --- a/src/container.py +++ b/src/container.py @@ -17,6 +17,7 @@ import abc + class ContainerInstance(abc.ABC): @abc.abstractmethod def run(self): @@ -80,7 +81,15 @@ def decode_command_output(command: bytes) -> dict | ValueError: def get_logs(self): if self.container == 'superset': - return self.client.containers.get(self.client.containers.list(filters={"name": "superset"})[0].name).logs().decode('utf-8') + try: + return self.client.containers.get(self.client.containers.list(filters={"name": "superset"})[0].name).logs().decode('utf-8') + except IndexError: + return 'Container superset has not been spawned by the service.' + if self.container == 'mysql-mgmt': + container_log: str = self.client.containers.get('mysql-mgmt-initcontainer').logs().decode('utf-8') + '\n\n' + if 'mysql-mgmt' in self.client.containers.list(all=True): + container_log += self.client.containers.get(self.container).logs().decode('utf-8') + return container_log return self.client.containers.get(self.container).logs().decode('utf-8') def wait_until_healthy(self, cls: typing.Type[ContainerInstance]) -> str: @@ -94,7 +103,7 @@ def wait_until_healthy(self, cls: typing.Type[ContainerInstance]) -> str: if self.client.containers.get(self.container).attrs['State']['Health']['Status'] == 'healthy': return f'{self.get_logs()}\nContainer {self.container} is healthy' time.sleep(cls.healthcheck_interval) - return f'{self.get_logs()}\nTimeout while waiting for {self.container} healthcheck' + return f'{self.get_logs()}\nTimeout while waiting for {self.container} healthcheck to be healthy' def run_mysql_server(self) -> None: class MySQL_Server(ContainerInstance): @@ -298,7 +307,7 @@ def create_mysql_superset_password_secret(self)-> str: def run(self) -> None: self.client.services.create( name="superset", - image="ghcr.io/szachovy/superset-cluster-service:latest", + image="ghcr.io/szachovy/superset-cluster-superset-service:latest", networks=["superset-network"], secrets = [ docker.types.SecretReference(secret_id=self.create_superset_secret_key_secret(), secret_name="superset_secret_key"), diff --git a/src/remote.py b/src/remote.py index d574dbb..9cbcd4b 100644 --- a/src/remote.py +++ b/src/remote.py @@ -23,7 +23,10 @@ def __init__(self, node: str) -> None: except (paramiko.ssh_exception.SSHException, socket.gaierror): self.ssh_config = paramiko.SSHConfig() self.ssh_config.parse(open(f'{pathlib.Path.home()}/.ssh/config')) - self.ssh_client.connect(hostname=self.node_hostname(), username='superset', key_filename=self.identity_path()) + try: + self.ssh_client.connect(hostname=self.node_hostname(), username='superset', key_filename=self.identity_path()) + except KeyError: + logger.error(f'Unable to connect to {self.node} from the localhost') self.sftp_client = self.ssh_client.open_sftp() def log_remote_command_execution(func): diff --git a/tests/setup/main.tf b/tests/setup/main.tf index 6119a32..7ba5953 100644 --- a/tests/setup/main.tf +++ b/tests/setup/main.tf @@ -1,5 +1,10 @@ terraform { + required_version = "1.0.10" required_providers { + null = { + source = "hashicorp/null" + version = "3.2.2" + } docker = { source = "kreuzwerker/docker" version = "3.0.2" @@ -13,7 +18,7 @@ provider "docker" { resource "null_resource" "manage_ssh" { triggers = { - always_run = "${timestamp()}" + always_run = timestamp() } provisioner "local-exec" { @@ -56,7 +61,7 @@ resource "docker_image" "node_image" { name = "${var.node_prefix}:${var.node_version}" triggers = { - always_run = "${timestamp()}" + always_run = timestamp() } build { @@ -78,7 +83,7 @@ resource "docker_container" "nodes" { name = "${var.node_prefix}-${count.index}" hostname = "${var.node_prefix}-${count.index}" image = docker_image.node_image.name - privileged = true # nodes containers are treated as standalone virtual machines + privileged = true # nodes containers are treated as standalone virtual machines ulimit { name = "nofile" @@ -88,7 +93,7 @@ resource "docker_container" "nodes" { networks_advanced { name = docker_network.nodes_network.name - ipv4_address = cidrhost("${var.subnet}", "${2 + count.index}") + ipv4_address = cidrhost(var.subnet, 2 + count.index) } labels { @@ -120,7 +125,7 @@ resource "docker_container" "nodes" { environment = { HOSTNAME = "${var.node_prefix}-${count.index}" - IP_ADDRESS = cidrhost("${var.subnet}", "${2 + count.index}") + IP_ADDRESS = cidrhost(var.subnet, 2 + count.index) } } @@ -133,7 +138,7 @@ resource "docker_container" "nodes" { resource "null_resource" "generate_ansible_group_vars" { triggers = { - always_run = "${timestamp()}" + always_run = timestamp() } provisioner "local-exec" { @@ -150,9 +155,9 @@ resource "null_resource" "generate_ansible_group_vars" { environment = { GROUP_VARS_FILE = "../testsuite/group_vars/testing.yml" - NODE_PREFIX = "${var.node_prefix}" - VIRTUAL_IP_ADDRESS = cidrhost("${var.subnet}", "${10}") - VIRTUAL_NETWORK_MASK = cidrnetmask("${var.subnet}") + NODE_PREFIX = var.node_prefix + VIRTUAL_IP_ADDRESS = cidrhost(var.subnet, 10) + VIRTUAL_NETWORK_MASK = cidrnetmask(var.subnet) } } @@ -168,7 +173,7 @@ resource "null_resource" "generate_ansible_group_vars" { resource "null_resource" "finish_configuration" { triggers = { - always_run = "${timestamp()}" + always_run = timestamp() } provisioner "local-exec" { diff --git a/tests/testsuite/deploy.yml b/tests/testsuite/deploy.yml index 9804e66..c56d3ac 100644 --- a/tests/testsuite/deploy.yml +++ b/tests/testsuite/deploy.yml @@ -4,18 +4,21 @@ hosts: "testing" any_errors_fatal: true tasks: - - ansible.builtin.include_role: {name: "testing", tasks_from: "sanity"} + - name: "Run sanity tasks from testing role" + ansible.builtin.include_role: {name: "testing", tasks_from: "sanity"} - name: "System testing" connection: "local" hosts: "testing" any_errors_fatal: true tasks: - - ansible.builtin.include_role: {name: "testing", tasks_from: "system"} + - name: "Run system tasks from testing role" + ansible.builtin.include_role: {name: "testing", tasks_from: "system"} - name: "Functional testing" connection: "local" hosts: "testing" any_errors_fatal: true tasks: - - ansible.builtin.include_role: {name: "testing", tasks_from: "functional"} + - name: "Run functional tasks from testing role" + ansible.builtin.include_role: {name: "testing", tasks_from: "functional"} diff --git a/tests/testsuite/roles/testing/files/functional_superset.py b/tests/testsuite/roles/testing/files/functional_superset.py index 2ccbbe0..e01dc21 100644 --- a/tests/testsuite/roles/testing/files/functional_superset.py +++ b/tests/testsuite/roles/testing/files/functional_superset.py @@ -15,14 +15,22 @@ def __init__(self, container: str) -> None: @decorators.Overlay.run_selected_methods_once def status(self) -> None | AssertionError: - command: str = "python3 -c 'import redis; print(redis.StrictRedis(host=\"redis\", port=6379).ping())'" + command: str = """python3 -c \ + 'import redis; print(redis.StrictRedis(host=\"redis\", port=6379).ping())' + """ test_connection: bytes = self.run_command_on_the_container(command) - assert self.find_in_the_output(test_connection, b'True'), f'The redis container is not responding\nCommand: {command}\nReturned: {test_connection}' + assert \ + self.find_in_the_output(test_connection, b'True'), \ + f"The redis container is not responding\nCommand: {command}\nReturned: {test_connection}" def fetch_query_result(self, results_key: float) -> bool | AssertionError: - command: str = f"python3 -c 'import redis; print(redis.StrictRedis(host=\"redis\", port=6379).get(\"{results_key}\"))'" + command: str = f"""python3 -c + 'import redis; print(redis.StrictRedis(host=\"redis\", port=6379).get(\"{results_key}\"))' + """ query_result: bytes = self.run_command_on_the_container(command) - assert not self.find_in_the_output(query_result, b"None"), f'Query results given key {results_key} not found in Redis\nCommand: {command}\nReturned: {query_result}' + assert \ + not self.find_in_the_output(query_result, b"None"), \ + f"Query results given key {results_key} not found in Redis\nCommand: {command}\nReturned: {query_result}" return True @@ -30,31 +38,53 @@ class Celery(container.ContainerConnection, metaclass=decorators.Overlay): def __init__(self, container: str) -> None: super().__init__(container=container) self.superset_container: str = container - self.celery_broker: str = f"redis://redis:6379/0" + self.celery_broker: str = "redis://redis:6379/0" self.celery_sql_lab_task_annotations: str = "sql_lab.get_sql_results" @decorators.Overlay.run_selected_methods_once def status(self) -> None | AssertionError: - command: str = f"python3 -c 'import celery; print(celery.Celery(\"tasks\", broker=\"{self.celery_broker}\").control.inspect().ping())'" + command: str = f"""python3 -c + 'import celery; print(celery.Celery(\"tasks\", broker=\"{self.celery_broker}\").control.inspect().ping()) + """ test_connection: bytes = self.run_command_on_the_container(command) - assert self.find_in_the_output(test_connection, b"{'ok': 'pong'}"), f'The Celery process in the {self.superset_container} container on is not responding\nCommand: {command}\nReturned: {test_connection}' + assert \ + self.find_in_the_output(test_connection, b"{'ok': 'pong'}"), \ + f"""The Celery process in the {self.superset_container} container on is not responding + \nCommand: {command}\nReturned: {test_connection} + """ @decorators.Overlay.run_selected_methods_once def status_cache(self) -> None | AssertionError: - command: str = f"python3 -c 'import celery; print(celery.Celery(\"tasks\", broker=\"{self.celery_broker}\").control.inspect().conf())'" + command: str = f"""python3 -c + 'import celery; print(celery.Celery(\"tasks\", broker=\"{self.celery_broker}\").control.inspect().conf()) + """ celery_workers_configuration: dict = self.decode_command_output( self.run_command_on_the_container(command) ) celery_worker_id: str = next(iter(celery_workers_configuration)) - assert all(features in celery_workers_configuration[celery_worker_id]['include'] for features in ['superset.tasks.cache', 'superset.tasks.scheduler']), f'Celery cache and scheduler features in the {self.celery_broker} not found\nCommand: {command}\nReturned decoded: {celery_workers_configuration}' + assert \ + all( + features in celery_workers_configuration[celery_worker_id]["include"] + for features in ['superset.tasks.cache', 'superset.tasks.scheduler'] + ), \ + f"""Celery cache and scheduler features in the {self.celery_broker} not found + \nCommand: {command}\nReturned decoded: {celery_workers_configuration} + """ def find_processed_queries(self) -> bool | AssertionError: - command = f"python3 -c 'import celery; print(celery.Celery(\"tasks\", broker=\"{self.celery_broker}\").control.inspect().stats())'" + command = f"""python3 -c + 'import celery; print(celery.Celery(\"tasks\", broker=\"{self.celery_broker}\").control.inspect().stats()) + """ celery_workers_stats: dict = self.decode_command_output( self.run_command_on_the_container(command) ) celery_worker_id: str = next(iter(celery_workers_stats)) - assert celery_workers_stats[celery_worker_id]['total'][self.celery_sql_lab_task_annotations] > 0, f'Executed SQL Lab queries are not processed or registered by Celery, check the Celery worker process on the {self.superset_container}\nCommand: {command}\nReturned decoded: {celery_workers_stats}' + assert \ + celery_workers_stats[celery_worker_id]["total"][self.celery_sql_lab_task_annotations] > 0, \ + f"""Executed SQL Lab queries are not processed or registered by Celery, + check the Celery worker process on the {self.superset_container} + \nCommand: {command}\nReturned decoded: {celery_workers_stats} + """ return True @@ -77,28 +107,60 @@ def load_ssl_server_certificate(self) -> None: with context.wrap_socket(sock, server_hostname=self.virtual_ip_address) as ssl_sock: with open("/opt/superset-testing/server_certificate.pem", "wb") as cert: cert.write(ssl.DER_cert_to_PEM_cert(ssl_sock.getpeercert(binary_form=True)).encode()) - self.copy_file_to_the_container(host_filepath='/opt/superset-testing/server_certificate.pem', container_dirpath='/app') + self.copy_file_to_the_container( + host_filepath="/opt/superset-testing/server_certificate.pem", + container_dirpath="/app" + ) @decorators.Overlay.single_sign_on def login_to_superset_api(self) -> str | AssertionError: self.load_ssl_server_certificate() headers: str = "Content-Type: application/json" - payload: str = f'{{"username": "superset", "password": "cluster", "provider": "db", "refresh": true}}' - command: str = f"curl --cacert /app/server_certificate.pem --silent --url {self.api_default_url}/security/login --header '{headers}' --data '{payload}'" + payload: str = "{{'username': 'superset', 'password': 'cluster', 'provider': 'db', 'refresh': true}}" + command: str = f""" + curl + --cacert /app/server_certificate.pem + --silent + --url {self.api_default_url}/security/login + --header '{headers}' + --data '{payload}' + """ api_login_output: bytes = self.run_command_on_the_container(command) - assert not self.find_in_the_output(api_login_output, b'"message"'), f'Could not log in to the Superset API {api_login_output}\nCommand: {command}\nReturned: {api_login_output}' + assert \ + not self.find_in_the_output(api_login_output, b'"message"'), \ + f"Could not log in to the Superset API {api_login_output}\nCommand: {command}\nReturned: {api_login_output}" return self.decode_command_output(api_login_output).get("access_token") @decorators.Overlay.single_sign_on def login_to_superset(self) -> dict[str, str] | AssertionError: - command: str = f"curl --cacert /app/server_certificate.pem --include --url {self.api_default_url}/security/csrf_token/ --header '{self.api_authorization_header}'" + command: str = f""" + curl + --cacert /app/server_certificate.pem + --include + --url {self.api_default_url}/security/csrf_token/ + --header '{self.api_authorization_header}' + """ csrf_login_request: bytes = self.run_command_on_the_container(command) - assert not self.find_in_the_output(csrf_login_request, b'"msg"'), f'Could not pass login request {csrf_login_request}\nCommand: {command}\nReturned: {csrf_login_request}' + assert \ + not self.find_in_the_output(csrf_login_request, b'"msg"'), \ + f"Could not pass login request {csrf_login_request}\nCommand: {command}\nReturned: {csrf_login_request}" session_request_cookie: str = self.extract_session_cookie(csrf_login_request) csrf_token: str = json.loads(csrf_login_request.decode('utf-8').split('\r\n\r\n')[1]).get("result") - command: str = f"curl --location --cacert /app/server_certificate.pem --include --url https://{self.virtual_ip_address}/login/ --header 'Cookie: session={session_request_cookie}' --data 'csrf_token={csrf_token}'" + command: str = f""" + curl + --location + --cacert /app/server_certificate.pem + --include + --url https://{self.virtual_ip_address}/login/ + --header 'Cookie: session={session_request_cookie}' + --data 'csrf_token={csrf_token}' + """ superset_login_request: bytes = self.run_command_on_the_container(command) - assert not self.find_in_the_output(superset_login_request, b'Redirecting...'), f'Invalid login request to Superset. Could not get a response from the server. Check if it is possible to log in to the server manually\nCommand: {command}\nReturned: {superset_login_request}' + assert \ + not self.find_in_the_output(superset_login_request, b"Redirecting..."), \ + f"""Invalid login request to Superset. Could not get a response from the server. + Check if it is possible to log in to the server manually + \nCommand: {command}\nReturned: {superset_login_request}""" superset_login_session_cookie: str = self.extract_session_cookie(superset_login_request) return { "csrf_token": csrf_token, @@ -107,34 +169,96 @@ def login_to_superset(self) -> dict[str, str] | AssertionError: @decorators.Overlay.run_selected_methods_once def status_database(self) -> None | AssertionError: - with open('/opt/superset-cluster/mysql-mgmt/mysql_superset_password', 'r') as mysql_superset_password: - payload: str = f'{{"database_name": "MySQL", "sqlalchemy_uri": "mysql+mysqlconnector://superset:{mysql_superset_password.read().strip()}@{self.virtual_ip_address}:6446/superset", "impersonate_user": false}}' - command: str = f"curl --location --cacert /app/server_certificate.pem --silent {self.api_default_url}/database/test_connection/ --header 'Content-Type: application/json' --header '{self.api_authorization_header}' --header '{self.api_csrf_header}' --header '{self.api_session_header}' --header 'Referer: https://{self.virtual_ip_address}' --data '{payload}'" - test_database_connection: bytes = self.run_command_on_the_container(f"curl --location --cacert /app/server_certificate.pem --silent {self.api_default_url}/database/test_connection/ --header 'Content-Type: application/json' --header '{self.api_authorization_header}' --header '{self.api_csrf_header}' --header '{self.api_session_header}' --header 'Referer: https://{self.virtual_ip_address}' --data '{payload}'") - assert self.find_in_the_output(test_database_connection, b'{"message":"OK"}'), f'Could not connect to the superset database on {self.virtual_ip_address} port 6446, the database is either down or not configured according to the given SQL Alchemy URI\nCommand: {command}\nReturned: {test_database_connection}' + with open("/opt/superset-cluster/mysql-mgmt/mysql_superset_password", "r") as mysql_superset_password: + payload: str = f""" + { + { + "database_name": "MySQL", + "sqlalchemy_uri": f"mysql+mysqlconnector://superset:{mysql_superset_password.read().strip()}@{self.virtual_ip_address}:6446/superset", + "impersonate_user": "false" + } + } + """ + command: str = f""" + curl + --location + --cacert /app/server_certificate.pem + --silent + {self.api_default_url}/database/test_connection/ + --header 'Content-Type: application/json' + --header '{self.api_authorization_header}' + --header '{self.api_csrf_header}' + --header '{self.api_session_header}' + --header 'Referer: https://{self.virtual_ip_address}' + --data '{payload}' + """ + test_database_connection: bytes = self.run_command_on_the_container(command) + assert \ + self.find_in_the_output(test_database_connection, b'{"message":"OK"}'), \ + f"""Could not connect to the superset database on {self.virtual_ip_address} port 6446, \ + the database is either down or not configured according to the given SQL Alchemy URI \ + \nCommand: {command}\nReturned: {test_database_connection} \ + """ @decorators.Overlay.run_selected_methods_once def status_swarm(self) -> None | AssertionError: - swarm_info = self.info()['Swarm'] - assert swarm_info['LocalNodeState'] == 'active', 'The Swarm node has not been activated' - assert swarm_info['ControlAvailable'] is True, f'The testing localhost is supposed to be a Swarm manager, but it is not' + swarm_info = self.info()["Swarm"] + assert \ + swarm_info["LocalNodeState"] == "active", \ + "The Swarm node has not been activated" + assert \ + swarm_info["ControlAvailable"] is True, \ + "The testing localhost is supposed to be a Swarm manager, but it is not" def run_query(self) -> float | AssertionError: - payload: str = f'{{"database_id": 1, "runAsync": true, "sql": "SELECT * FROM superset.logs;"}}' - command: str = f"curl --location --cacert /app/server_certificate.pem --silent {self.api_default_url}/sqllab/execute/ --header 'Content-Type: application/json' --header '{self.api_authorization_header}' --header '{self.api_session_header}' --header '{self.api_csrf_header}' --header 'Referer: https://{self.virtual_ip_address}' --data '{payload}'" + payload: str = "{{'database_id': 1, 'runAsync': true, 'sql': 'SELECT * FROM superset.logs;'}}" + command: str = f""" + curl + --location + --cacert /app/server_certificate.pem + --silent + {self.api_default_url}/sqllab/execute/ + --header 'Content-Type: application/json' + --header '{self.api_authorization_header}' + --header '{self.api_session_header}' + --header '{self.api_csrf_header}' + --header 'Referer: https://{self.virtual_ip_address}' + --data '{payload}' + """ sqllab_run_query: bytes = self.run_command_on_the_container(command) - assert not self.find_in_the_output(sqllab_run_query, b'"msg"'), f'SQL query execution failed\nCommand: {command}\nReturned: {sqllab_run_query}' - assert not self.find_in_the_output(sqllab_run_query, b'"message"'), f'Could not execute SQL query\nCommand: {command}\nReturned: {sqllab_run_query}' + assert \ + not self.find_in_the_output(sqllab_run_query, b'"msg"'), \ + f"SQL query execution failed\nCommand: {command}\nReturned: {sqllab_run_query}" + assert \ + not self.find_in_the_output(sqllab_run_query, b'"message"'), \ + f"Could not execute SQL query\nCommand: {command}\nReturned: {sqllab_run_query}" dttm_time_query_identifier: float = self.decode_command_output(sqllab_run_query).get("query").get("startDttm") return dttm_time_query_identifier - - def get_query_results(self, dttm_time_query_identifier: float): + + def get_query_results(self, dttm_time_query_identifier: float) -> None | AssertionError: time.sleep(45) # state refreshing - command: str = f"curl --location --cacert /app/server_certificate.pem --silent '{self.api_default_url}/query/updated_since?q=(last_updated_ms:{dttm_time_query_identifier})' --header 'Accept: application/json' --header '{self.api_authorization_header}' --header '{self.api_session_header}' --header 'Referer: https://{self.virtual_ip_address}' --header '{self.api_csrf_header}'" + command: str = f""" + curl + --location + --cacert /app/server_certificate.pem + --silent + '{self.api_default_url}/query/updated_since?q=(last_updated_ms:{dttm_time_query_identifier})' + --header 'Accept: application/json' + --header '{self.api_authorization_header}' + --header '{self.api_session_header}' + --header 'Referer: https://{self.virtual_ip_address}' + --header '{self.api_csrf_header}' + """ query_result: dict = self.decode_command_output( self.run_command_on_the_container(command) - ) - assert query_result.get("result")[0]['state'] == 'success', f'Could not find query state or returned unsuccessful\nCommand: {command}\nReturned decoded: {query_result}' + ) + assert \ + query_result.get("result")[0]["state"] == "success", \ + f"Could not find query state or returned unsuccessful\nCommand: {command}\nReturned decoded: {query_result}" results_key: str = f"superset_results{query_result.get('result')[0]['resultsKey']}" - assert self.redis.fetch_query_result(results_key), f'Query result with the {results_key} key can not be found in Redis after\nCommand: {command}' - assert self.celery.find_processed_queries(), f'Query seems to be processed outside Celery worker after\nCommand: {command}' + assert \ + self.redis.fetch_query_result(results_key), \ + f"Query result with the {results_key} key can not be found in Redis after\nCommand: {command}" + assert \ + self.celery.find_processed_queries(), \ + f"Query seems to be processed outside Celery worker after\nCommand: {command}" diff --git a/tests/testsuite/roles/testing/files/introspect.py b/tests/testsuite/roles/testing/files/introspect.py index 94b01fd..34bc578 100644 --- a/tests/testsuite/roles/testing/files/introspect.py +++ b/tests/testsuite/roles/testing/files/introspect.py @@ -15,14 +15,39 @@ def __init__(self, node_prefix: str, virtual_network_interface: str) -> None: @decorators.Overlay.run_selected_methods_once def status_services(self) -> None | AssertionError: - assert subprocess.run(['service', 'ssh', 'status'], stdout=subprocess.PIPE, stderr=subprocess.PIPE).returncode == 0, f'SSH service is not running in {socket.gethostname()} node' - assert subprocess.run(['service', 'docker', 'status'], stdout=subprocess.PIPE, stderr=subprocess.PIPE).returncode == 0, f'Docker service is not running in {socket.gethostname()} node' + assert \ + subprocess.run( + [ + 'service', + 'ssh', + 'status' + ], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE + ).returncode == 0, \ + f'SSH service is not running in {socket.gethostname()} node' + assert \ + subprocess.run( + [ + 'service', + 'docker', + 'status' + ], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE + ).returncode == 0, \ + f'Docker service is not running in {socket.gethostname()} node' @decorators.Overlay.run_selected_methods_once def status_dns(self) -> None | AssertionError: for node_number in range(self.nodes): - assert bool(ipaddress.IPv4Address(socket.gethostbyname(f"{self.node_prefix}-{node_number}"))), f"{self.node_prefix}-{node_number} node can not be resolved." + assert \ + bool(ipaddress.IPv4Address(socket.gethostbyname(f"{self.node_prefix}-{node_number}"))), \ + f"{self.node_prefix}-{node_number} node can not be resolved." @decorators.Overlay.run_selected_methods_once def status_network_interfaces(self) -> None | AssertionError: - assert socket.gethostbyaddr(interfaces.network_interfaces(network_interface=self.virtual_network_interface))[0] == socket.gethostname(), 'Hostname does not resolve to IPv4 address of the node taken from the configuration' + assert \ + socket.gethostbyaddr(interfaces.network_interfaces(network_interface=self.virtual_network_interface))[0] \ + == socket.gethostname(), \ + 'Hostname does not resolve to IPv4 address of the node taken from the configuration' diff --git a/tests/testsuite/roles/testing/tasks/sanity.yml b/tests/testsuite/roles/testing/tasks/sanity.yml index 6ffc7d6..b95fbbf 100644 --- a/tests/testsuite/roles/testing/tasks/sanity.yml +++ b/tests/testsuite/roles/testing/tasks/sanity.yml @@ -23,10 +23,12 @@ (node.container.Config.ExposedPorts | length != 0) - name: "Check out passwordless ssh to the nodes" - ansible.builtin.shell: "ssh superset@{{ container.value['Name'] }} exit" + ansible.builtin.command: "ssh superset@{{ container.value['Name'] }} exit" loop: "{{ network.network.Containers | dict2items }}" loop_control: loop_var: "container" + register: "ssh_command" + changed_when: "ssh_command.rc != 0" - name: "Check out internal nodes settings" community.docker.docker_container_exec: diff --git a/tests/testsuite/roles/testing/tasks/system.yml b/tests/testsuite/roles/testing/tasks/system.yml index 34401ca..66d0066 100644 --- a/tests/testsuite/roles/testing/tasks/system.yml +++ b/tests/testsuite/roles/testing/tasks/system.yml @@ -15,3 +15,5 @@ chdir: "../../" async: 1800 poll: 60 + register: "run_command" + changed_when: "run_command.rc != 0"