|
| 1 | +# Copyright 2020 Vectorized, Inc. |
| 2 | +# |
| 3 | +# Use of this software is governed by the Business Source License |
| 4 | +# included in the file licenses/BSL.md |
| 5 | +# |
| 6 | +# As of the Change Date specified in that file, in accordance with |
| 7 | +# the Business Source License, use of this software will be governed |
| 8 | +# by the Apache License, Version 2.0 |
| 9 | + |
| 10 | +import os |
| 11 | +import json |
| 12 | +import collections |
| 13 | +import re |
| 14 | +from typing import Optional, Any |
| 15 | + |
| 16 | +from ducktape.services.service import Service |
| 17 | +from ducktape.utils.util import wait_until |
| 18 | +from ducktape.cluster.cluster import ClusterNode |
| 19 | + |
| 20 | +from polaris.management.api_client import ApiClient |
| 21 | +from polaris.management.configuration import Configuration |
| 22 | +from polaris.management.api.polaris_default_api import PolarisDefaultApi |
| 23 | + |
| 24 | + |
| 25 | +class PolarisCatalog(Service): |
| 26 | + """Polaris Catalog service |
| 27 | + |
| 28 | + The polaris catalog service maintain lifecycle of catalog process on the nodes. |
| 29 | + The service deploys polaris in a test mode with in-memory storage which is intended |
| 30 | + to be used for dev/test purposes. |
| 31 | + """ |
| 32 | + PERSISTENT_ROOT = "/var/lib/polaris" |
| 33 | + INSTALL_PATH = "/opt/polaris" |
| 34 | + JAR = "polaris-service-1.0.0-all.jar" |
| 35 | + JAR_PATH = os.path.join(INSTALL_PATH, "polaris-service/build/libs", JAR) |
| 36 | + LOG_FILE = os.path.join(PERSISTENT_ROOT, "polaris.log") |
| 37 | + POLARIS_CONFIG = os.path.join(PERSISTENT_ROOT, "polaris-server.yml") |
| 38 | + logs = { |
| 39 | + # Includes charts/ and results/ directories along with benchmark.log |
| 40 | + "polaris_logs": { |
| 41 | + "path": LOG_FILE, |
| 42 | + "collect_default": True |
| 43 | + }, |
| 44 | + } |
| 45 | + # the only way to access polaris credentials running with the in-memory |
| 46 | + # storage is to parse them from standard output |
| 47 | + credentials_pattern = re.compile( |
| 48 | + "realm: default-realm root principal credentials: (?P<client_id>.+):(?P<password>.+)" |
| 49 | + ) |
| 50 | + |
| 51 | + nodes: list[ClusterNode] |
| 52 | + |
| 53 | + def _cmd(self, node): |
| 54 | + java = "/opt/java/java-21" |
| 55 | + return f"{java} -jar {PolarisCatalog.JAR_PATH} server {PolarisCatalog.POLARIS_CONFIG} \ |
| 56 | + 1>> {PolarisCatalog.LOG_FILE} 2>> {PolarisCatalog.LOG_FILE} &" |
| 57 | + |
| 58 | + def __init__(self, ctx, node: ClusterNode | None = None): |
| 59 | + super(PolarisCatalog, self).__init__(ctx, num_nodes=0 if node else 1) |
| 60 | + |
| 61 | + if node: |
| 62 | + self.nodes = [node] |
| 63 | + self._ctx = ctx |
| 64 | + # catalog API url |
| 65 | + self.catalog_url = None |
| 66 | + # polaris management api url |
| 67 | + self.management_url = None |
| 68 | + self.client_id = None |
| 69 | + self.password = None |
| 70 | + |
| 71 | + def _parse_credentials(self, node): |
| 72 | + line = node.account.ssh_output( |
| 73 | + f"grep 'root principal credentials' {PolarisCatalog.LOG_FILE}" |
| 74 | + ).decode('utf-8') |
| 75 | + m = PolarisCatalog.credentials_pattern.match(line) |
| 76 | + if m is None: |
| 77 | + raise Exception(f"Unable to find credentials in line: {line}") |
| 78 | + self.client_id = m['client_id'] |
| 79 | + self.password = m['password'] |
| 80 | + |
| 81 | + def start_node(self, node, timeout_sec=60, **kwargs): |
| 82 | + node.account.ssh("mkdir -p %s" % PolarisCatalog.PERSISTENT_ROOT, |
| 83 | + allow_fail=False) |
| 84 | + # polaris server settings |
| 85 | + cfg_yaml = self.render("polaris-server.yml") |
| 86 | + node.account.create_file(PolarisCatalog.POLARIS_CONFIG, cfg_yaml) |
| 87 | + cmd = self._cmd(node) |
| 88 | + self.logger.info( |
| 89 | + f"Starting polaris catalog service on {node.name} with command {cmd}" |
| 90 | + ) |
| 91 | + node.account.ssh(cmd, allow_fail=False) |
| 92 | + |
| 93 | + # wait for the healthcheck to return 200 |
| 94 | + def _polaris_ready(): |
| 95 | + out = node.account.ssh_output( |
| 96 | + "curl -s -o /dev/null -w '%{http_code}' http://localhost:8182/healthcheck" |
| 97 | + ) |
| 98 | + status_code = int(out.decode('utf-8')) |
| 99 | + self.logger.info(f"health check result status code: {status_code}") |
| 100 | + return status_code == 200 |
| 101 | + |
| 102 | + wait_until(_polaris_ready, |
| 103 | + timeout_sec=timeout_sec, |
| 104 | + backoff_sec=0.4, |
| 105 | + err_msg="Error waiting for polaris catalog to start", |
| 106 | + retry_on_exc=True) |
| 107 | + |
| 108 | + # setup urls and credentials |
| 109 | + self.catalog_url = f"http://{node.account.hostname}:8181/api/catalog/v1" |
| 110 | + self.management_url = f'http://{node.account.hostname}:8181/api/management/v1' |
| 111 | + self._parse_credentials(node) |
| 112 | + self.logger.info( |
| 113 | + f"Polaris catalog ready, credentials - client_id: {self.client_id}, password: {self.password}" |
| 114 | + ) |
| 115 | + |
| 116 | + def _get_token(self) -> str: |
| 117 | + client = ApiClient(configuration=Configuration(host=self.catalog_url)) |
| 118 | + response = client.call_api('POST', |
| 119 | + f'{self.catalog_url}/oauth/tokens', |
| 120 | + header_params={ |
| 121 | + 'Content-Type': |
| 122 | + 'application/x-www-form-urlencoded' |
| 123 | + }, |
| 124 | + post_params={ |
| 125 | + 'grant_type': 'client_credentials', |
| 126 | + 'client_id': self.client_id, |
| 127 | + 'client_secret': self.password, |
| 128 | + 'scope': 'PRINCIPAL_ROLE:ALL' |
| 129 | + }).response.data |
| 130 | + |
| 131 | + if 'access_token' not in json.loads(response): |
| 132 | + raise Exception('Failed to get access token') |
| 133 | + return json.loads(response)['access_token'] |
| 134 | + |
| 135 | + def management_client(self) -> ApiClient: |
| 136 | + token = self._get_token() |
| 137 | + return ApiClient(configuration=Configuration(host=self.management_url, |
| 138 | + access_token=token)) |
| 139 | + |
| 140 | + def catalog_client(self) -> ApiClient: |
| 141 | + token = self._get_token() |
| 142 | + return ApiClient(configuration=Configuration(host=self.catalog_url, |
| 143 | + access_token=token)) |
| 144 | + |
| 145 | + def wait_node(self, node, timeout_sec=None): |
| 146 | + ## unused as there is nothing to wait for here |
| 147 | + return False |
| 148 | + |
| 149 | + def stop_node(self, node, allow_fail=False, **_): |
| 150 | + |
| 151 | + node.account.kill_java_processes(PolarisCatalog.JAR, |
| 152 | + allow_fail=allow_fail) |
| 153 | + |
| 154 | + def _stopped(): |
| 155 | + out = node.account.ssh_output("jcmd").decode('utf-8') |
| 156 | + return PolarisCatalog.JAR not in out |
| 157 | + |
| 158 | + wait_until(_stopped, |
| 159 | + timeout_sec=10, |
| 160 | + backoff_sec=1, |
| 161 | + err_msg="Error stopping Polaris") |
| 162 | + |
| 163 | + def clean_node(self, node, **_): |
| 164 | + self.stop_node(node, allow_fail=True) |
| 165 | + node.account.remove(PolarisCatalog.PERSISTENT_ROOT, allow_fail=True) |
0 commit comments