From 7f750fe31aa62f0055e43e86d4461d48baaaa7d3 Mon Sep 17 00:00:00 2001 From: Sage Weil Date: Tue, 12 Oct 2021 15:22:14 -0400 Subject: [PATCH] mgr/cephadm: add 'orch upgrade ls' to list available versions Optionally pass image name to query an alternative image/registry, or --tags to list tags instead of ceph versions. Signed-off-by: Sage Weil --- src/pybind/mgr/cephadm/module.py | 4 ++ src/pybind/mgr/cephadm/registry.py | 61 +++++++++++++++++++++++ src/pybind/mgr/cephadm/upgrade.py | 29 +++++++++++ src/pybind/mgr/orchestrator/_interface.py | 3 ++ src/pybind/mgr/orchestrator/module.py | 10 ++++ 5 files changed, 107 insertions(+) create mode 100644 src/pybind/mgr/cephadm/registry.py diff --git a/src/pybind/mgr/cephadm/module.py b/src/pybind/mgr/cephadm/module.py index 41476c1c992..720422902cc 100644 --- a/src/pybind/mgr/cephadm/module.py +++ b/src/pybind/mgr/cephadm/module.py @@ -2469,6 +2469,10 @@ Then run the following: def upgrade_status(self) -> orchestrator.UpgradeStatusSpec: return self.upgrade.upgrade_status() + @handle_orch_error + def upgrade_ls(self, image: Optional[str], tags: bool) -> List[str]: + return self.upgrade.upgrade_ls(image, tags) + @handle_orch_error def upgrade_start(self, image: str, version: str) -> str: if self.inventory.get_host_with_state("maintenance"): diff --git a/src/pybind/mgr/cephadm/registry.py b/src/pybind/mgr/cephadm/registry.py new file mode 100644 index 00000000000..7b293a4c1a5 --- /dev/null +++ b/src/pybind/mgr/cephadm/registry.py @@ -0,0 +1,61 @@ +import requests +from typing import List, Dict, Tuple +from requests import Response + + +class Registry: + + def __init__(self, url: str): + self._url: str = url + + @property + def api_domain(self) -> str: + if self._url == 'docker.io': + return 'registry-1.docker.io' + return self._url + + def get_token(self, response: Response) -> str: + realm, params = self.parse_www_authenticate(response.headers['Www-Authenticate']) + r = requests.get(realm, params=params) + r.raise_for_status() + ret = r.json() + if 'access_token' in ret: + return ret['access_token'] + if 'token' in ret: + return ret['token'] + raise ValueError(f'Unknown token reply {ret}') + + def parse_www_authenticate(self, text: str) -> Tuple[str, Dict[str, str]]: + # 'Www-Authenticate': 'Bearer realm="https://auth.docker.io/token",service="registry.docker.io",scope="repository:ceph/ceph:pull"' + r: Dict[str, str] = {} + for token in text.split(','): + key, value = token.split('=', 1) + r[key] = value.strip('"') + realm = r.pop('Bearer realm') + return realm, r + + def get_tags(self, image: str) -> List[str]: + tags = [] + headers = {'Accept': 'application/json'} + url = f'https://{self.api_domain}/v2/{image}/tags/list' + while True: + r = requests.get(url, headers=headers) + if r.status_code == 401: + if 'Authorization' in headers: + raise ValueError('failed authentication') + token = self.get_token(r) + headers['Authorization'] = f'Bearer {token}' + continue + r.raise_for_status() + + new_tags = r.json()['tags'] + tags.extend(new_tags) + + if 'Link' not in r.headers: + break + + # strip < > brackets off and prepend the domain + url = f'https://{self.api_domain}' + r.headers['Link'].split(';')[0][1:-1] + continue + + return tags diff --git a/src/pybind/mgr/cephadm/upgrade.py b/src/pybind/mgr/cephadm/upgrade.py index d7162941985..e71c9c8bf3a 100644 --- a/src/pybind/mgr/cephadm/upgrade.py +++ b/src/pybind/mgr/cephadm/upgrade.py @@ -5,6 +5,7 @@ import uuid from typing import TYPE_CHECKING, Optional, Dict, List, Tuple import orchestrator +from cephadm.registry import Registry from cephadm.serve import CephadmServe from cephadm.services.cephadmservice import CephadmDaemonDeploySpec from cephadm.utils import ceph_release_to_major, name_to_config_section, CEPH_UPGRADE_ORDER, MONITORING_STACK_TYPES @@ -176,6 +177,34 @@ class CephadmUpgrade: return None + def upgrade_ls(self, image: Optional[str], tags: bool) -> List[str]: + if image: + reg_name, image = image.split('/', 1) + else: + reg_name, image = self.mgr.container_image_base.split('/', 1) + self.mgr.log.info(f'reg_name {reg_name} image {image}') + reg = Registry(reg_name) + versions = [] + ls = reg.get_tags(image) + if not tags: + for t in ls: + if t[0] != 'v': + continue + v = t[1:].split('.') + if len(v) != 3: + continue + if '-' in v[2]: + continue + versions.append('.'.join(v)) + ls = sorted( + versions, + key=lambda k: list(map(int, k.split('.'))), + reverse=True + ) + else: + ls = sorted(ls) + return ls + def upgrade_start(self, image: str, version: str) -> str: if self.mgr.mode != 'root': raise OrchestratorError('upgrade is not supported in %s mode' % ( diff --git a/src/pybind/mgr/orchestrator/_interface.py b/src/pybind/mgr/orchestrator/_interface.py index 158299885b1..ffc77784c30 100644 --- a/src/pybind/mgr/orchestrator/_interface.py +++ b/src/pybind/mgr/orchestrator/_interface.py @@ -642,6 +642,9 @@ class Orchestrator(object): def upgrade_check(self, image: Optional[str], version: Optional[str]) -> OrchResult[str]: raise NotImplementedError() + def upgrade_ls(self, image: Optional[str], tags: bool) -> OrchResult[List[str]]: + raise NotImplementedError() + def upgrade_start(self, image: Optional[str], version: Optional[str]) -> OrchResult[str]: raise NotImplementedError() diff --git a/src/pybind/mgr/orchestrator/module.py b/src/pybind/mgr/orchestrator/module.py index a9130c7f044..6332f704492 100644 --- a/src/pybind/mgr/orchestrator/module.py +++ b/src/pybind/mgr/orchestrator/module.py @@ -1327,6 +1327,16 @@ Usage: raise_if_exception(completion) return HandleCommandResult(stdout=completion.result_str()) + @_cli_read_command('orch upgrade ls') + def _upgrade_ls(self, + image: Optional[str] = None, + tags: bool = False) -> HandleCommandResult: + """Check for available versions (or tags) we can upgrade to""" + completion = self.upgrade_ls(image, tags) + r = raise_if_exception(completion) + out = json.dumps(r, indent=4) + return HandleCommandResult(stdout=out) + @_cli_write_command('orch upgrade status') def _upgrade_status(self) -> HandleCommandResult: """Check service versions vs available and target containers""" -- 2.39.5