]> git.apps.os.sepia.ceph.com Git - ceph-ci.git/commitdiff
mgr/cephadm: add 'orch upgrade ls' to list available versions
authorSage Weil <sage@newdream.net>
Tue, 12 Oct 2021 19:22:14 +0000 (15:22 -0400)
committerSage Weil <sage@newdream.net>
Wed, 13 Oct 2021 16:57:35 +0000 (12:57 -0400)
Optionally pass image name to query an alternative image/registry, or
--tags to list tags instead of ceph versions.

Signed-off-by: Sage Weil <sage@newdream.net>
src/pybind/mgr/cephadm/module.py
src/pybind/mgr/cephadm/registry.py [new file with mode: 0644]
src/pybind/mgr/cephadm/upgrade.py
src/pybind/mgr/orchestrator/_interface.py
src/pybind/mgr/orchestrator/module.py

index 41476c1c99207f1ddf9c75b039d754fb5d2cd206..720422902cc0776785aab99d0371978e04c6efd0 100644 (file)
@@ -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 (file)
index 0000000..7b293a4
--- /dev/null
@@ -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
index d7162941985d3fcdb799f52458651dc5f46cfee3..e71c9c8bf3a1cdf11a506a8e2a0e75f0edc7412b 100644 (file)
@@ -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' % (
index 158299885b1cb84de5f7cc7305c7803e9006f723..ffc77784c3013b8c5682e5859fd924ae79a0e758 100644 (file)
@@ -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()
 
index a9130c7f044f5377d43aba4a4599807a7c9fec8d..6332f70449244e1eb18fe0601bfa2ebf19563839 100644 (file)
@@ -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"""