]> git.apps.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard: expose cluster upgrade orch API endpoints
authoravanthakkar <avanjohn@gmail.com>
Wed, 28 Jun 2023 10:38:05 +0000 (16:08 +0530)
committerNizamudeen A <nia@redhat.com>
Thu, 17 Aug 2023 17:31:18 +0000 (23:01 +0530)
Add Cluster Upgrade Management support from REST API endpoints.

Fixes: https://tracker.ceph.com/issues/61847
Signed-off-by: avanthakkar <avanjohn@gmail.com>
(cherry picked from commit aa9e00437cff8838603e3b32a1a7fb11b1b00fc0)

src/pybind/mgr/dashboard/controllers/cluster.py
src/pybind/mgr/dashboard/openapi.yaml
src/pybind/mgr/dashboard/services/orchestrator.py
src/pybind/mgr/dashboard/tests/test_cluster_upgrade.py [new file with mode: 0644]
src/pybind/mgr/orchestrator/_interface.py

index d8170e672e9929fc7a80b381f352cc603cf4eb56..5091457ec9805950ac050068cea54ae5c4ae3536 100644 (file)
@@ -1,9 +1,16 @@
 # -*- coding: utf-8 -*-
 
+from typing import Dict, List, Optional
+
 from ..security import Scope
 from ..services.cluster import ClusterModel
-from . import APIDoc, APIRouter, EndpointDoc, RESTController
+from ..services.exception import handle_orchestrator_error
+from ..services.orchestrator import OrchClient, OrchFeature
+from ..tools import str_to_bool
+from . import APIDoc, APIRouter, CreatePermission, Endpoint, EndpointDoc, \
+    ReadPermission, RESTController, UpdatePermission, allow_empty_body
 from ._version import APIVersion
+from .orchestrator import raise_if_no_orchestrator
 
 
 @APIRouter('/cluster', Scope.CONFIG_OPT)
@@ -18,4 +25,77 @@ class Cluster(RESTController):
     @EndpointDoc("Update the cluster status",
                  parameters={'status': (str, 'Cluster Status')})
     def singleton_set(self, status: str):
-        ClusterModel(status).to_db()
+        ClusterModel(status).to_db()  # -*- coding: utf-8 -*-
+
+
+@APIRouter('/cluster/upgrade', Scope.CONFIG_OPT)
+@APIDoc("Upgrade Management API", "Upgrade")
+class ClusterUpgrade(RESTController):
+    @RESTController.MethodMap()
+    @raise_if_no_orchestrator([OrchFeature.UPGRADE_LIST])
+    @handle_orchestrator_error('upgrade')
+    @EndpointDoc("Get the available versions to upgrade",
+                 parameters={
+                     'image': (str, 'Ceph Image'),
+                     'tags': (bool, 'Show all image tags'),
+                     'show_all_versions': (bool, 'Show all available versions')
+                 })
+    @ReadPermission
+    def list(self, tags: bool = False, image: Optional[str] = None,
+             show_all_versions: Optional[bool] = False) -> Dict:
+        orch = OrchClient.instance()
+        available_upgrades = orch.upgrades.list(image, str_to_bool(tags),
+                                                str_to_bool(show_all_versions))
+        return available_upgrades
+
+    @Endpoint()
+    @raise_if_no_orchestrator([OrchFeature.UPGRADE_STATUS])
+    @handle_orchestrator_error('upgrade')
+    @EndpointDoc("Get the cluster upgrade status")
+    @ReadPermission
+    def status(self) -> Dict:
+        orch = OrchClient.instance()
+        status = orch.upgrades.status().to_json()
+        return status
+
+    @Endpoint('POST')
+    @raise_if_no_orchestrator([OrchFeature.UPGRADE_START])
+    @handle_orchestrator_error('upgrade')
+    @EndpointDoc("Start the cluster upgrade")
+    @CreatePermission
+    def start(self, image: Optional[str] = None, version: Optional[str] = None,
+              daemon_types: Optional[List[str]] = None, host_placement: Optional[str] = None,
+              services: Optional[List[str]] = None, limit: Optional[int] = None) -> str:
+        orch = OrchClient.instance()
+        start = orch.upgrades.start(image, version, daemon_types, host_placement, services, limit)
+        return start
+
+    @Endpoint('PUT')
+    @raise_if_no_orchestrator([OrchFeature.UPGRADE_PAUSE])
+    @handle_orchestrator_error('upgrade')
+    @EndpointDoc("Pause the cluster upgrade")
+    @UpdatePermission
+    @allow_empty_body
+    def pause(self) -> str:
+        orch = OrchClient.instance()
+        return orch.upgrades.pause()
+
+    @Endpoint('PUT')
+    @raise_if_no_orchestrator([OrchFeature.UPGRADE_RESUME])
+    @handle_orchestrator_error('upgrade')
+    @EndpointDoc("Resume the cluster upgrade")
+    @UpdatePermission
+    @allow_empty_body
+    def resume(self) -> str:
+        orch = OrchClient.instance()
+        return orch.upgrades.resume()
+
+    @Endpoint('PUT')
+    @raise_if_no_orchestrator([OrchFeature.UPGRADE_STOP])
+    @handle_orchestrator_error('upgrade')
+    @EndpointDoc("Stop the cluster upgrade")
+    @UpdatePermission
+    @allow_empty_body
+    def stop(self) -> str:
+        orch = OrchClient.instance()
+        return orch.upgrades.stop()
index 75d7fa4edc415c38d02643702e4da50877b48895..52a47c26c3389c3416326342dfd8da60aae734bd 100644 (file)
@@ -2625,6 +2625,200 @@ paths:
       summary: Update the cluster status
       tags:
       - Cluster
+  /api/cluster/upgrade:
+    get:
+      parameters:
+      - default: false
+        description: Show all image tags
+        in: query
+        name: tags
+        schema:
+          type: boolean
+      - allowEmptyValue: true
+        description: Ceph Image
+        in: query
+        name: image
+        schema:
+          type: string
+      - default: false
+        description: Show all available versions
+        in: query
+        name: show_all_versions
+        schema:
+          type: boolean
+      responses:
+        '200':
+          content:
+            application/vnd.ceph.api.v1.0+json:
+              type: object
+          description: OK
+        '400':
+          description: Operation exception. Please check the response body for details.
+        '401':
+          description: Unauthenticated access. Please login first.
+        '403':
+          description: Unauthorized access. Please check your permissions.
+        '500':
+          description: Unexpected error. Please check the response body for the stack
+            trace.
+      security:
+      - jwt: []
+      summary: Get the available versions to upgrade
+      tags:
+      - Upgrade
+  /api/cluster/upgrade/pause:
+    put:
+      parameters: []
+      responses:
+        '200':
+          content:
+            application/vnd.ceph.api.v1.0+json:
+              type: object
+          description: Resource updated.
+        '202':
+          content:
+            application/vnd.ceph.api.v1.0+json:
+              type: object
+          description: Operation is still executing. Please check the task queue.
+        '400':
+          description: Operation exception. Please check the response body for details.
+        '401':
+          description: Unauthenticated access. Please login first.
+        '403':
+          description: Unauthorized access. Please check your permissions.
+        '500':
+          description: Unexpected error. Please check the response body for the stack
+            trace.
+      security:
+      - jwt: []
+      summary: Pause the cluster upgrade
+      tags:
+      - Upgrade
+  /api/cluster/upgrade/resume:
+    put:
+      parameters: []
+      responses:
+        '200':
+          content:
+            application/vnd.ceph.api.v1.0+json:
+              type: object
+          description: Resource updated.
+        '202':
+          content:
+            application/vnd.ceph.api.v1.0+json:
+              type: object
+          description: Operation is still executing. Please check the task queue.
+        '400':
+          description: Operation exception. Please check the response body for details.
+        '401':
+          description: Unauthenticated access. Please login first.
+        '403':
+          description: Unauthorized access. Please check your permissions.
+        '500':
+          description: Unexpected error. Please check the response body for the stack
+            trace.
+      security:
+      - jwt: []
+      summary: Resume the cluster upgrade
+      tags:
+      - Upgrade
+  /api/cluster/upgrade/start:
+    post:
+      parameters: []
+      requestBody:
+        content:
+          application/json:
+            schema:
+              properties:
+                daemon_types:
+                  type: string
+                host_placement:
+                  type: string
+                image:
+                  type: string
+                limit:
+                  type: string
+                services:
+                  type: string
+                version:
+                  type: string
+              type: object
+      responses:
+        '201':
+          content:
+            application/vnd.ceph.api.v1.0+json:
+              type: object
+          description: Resource created.
+        '202':
+          content:
+            application/vnd.ceph.api.v1.0+json:
+              type: object
+          description: Operation is still executing. Please check the task queue.
+        '400':
+          description: Operation exception. Please check the response body for details.
+        '401':
+          description: Unauthenticated access. Please login first.
+        '403':
+          description: Unauthorized access. Please check your permissions.
+        '500':
+          description: Unexpected error. Please check the response body for the stack
+            trace.
+      security:
+      - jwt: []
+      summary: Start the cluster upgrade
+      tags:
+      - Upgrade
+  /api/cluster/upgrade/status:
+    get:
+      parameters: []
+      responses:
+        '200':
+          content:
+            application/vnd.ceph.api.v1.0+json:
+              type: object
+          description: OK
+        '400':
+          description: Operation exception. Please check the response body for details.
+        '401':
+          description: Unauthenticated access. Please login first.
+        '403':
+          description: Unauthorized access. Please check your permissions.
+        '500':
+          description: Unexpected error. Please check the response body for the stack
+            trace.
+      security:
+      - jwt: []
+      summary: Get the cluster upgrade status
+      tags:
+      - Upgrade
+  /api/cluster/upgrade/stop:
+    put:
+      parameters: []
+      responses:
+        '200':
+          content:
+            application/vnd.ceph.api.v1.0+json:
+              type: object
+          description: Resource updated.
+        '202':
+          content:
+            application/vnd.ceph.api.v1.0+json:
+              type: object
+          description: Operation is still executing. Please check the task queue.
+        '400':
+          description: Operation exception. Please check the response body for details.
+        '401':
+          description: Unauthenticated access. Please login first.
+        '403':
+          description: Unauthorized access. Please check your permissions.
+        '500':
+          description: Unexpected error. Please check the response body for the stack
+            trace.
+      security:
+      - jwt: []
+      summary: Stop the cluster upgrade
+      tags:
+      - Upgrade
   /api/cluster/user:
     get:
       description: "\n        Get list of ceph users and its respective data\n   \
@@ -12441,6 +12635,8 @@ tags:
   name: TcmuRunnerPerfCounter
 - description: Display Telemetry Report
   name: Telemetry
+- description: Upgrade Management API
+  name: Upgrade
 - description: Display User Details
   name: User
 - description: Change User Password
index 1818164d6c96cbe3d30379e8783b03212b4548be..e49ab80bfc5dc359b94138cb581f7d6a63e7bceb 100644 (file)
@@ -170,6 +170,36 @@ class DaemonManager(ResourceManager):
         return self.api.daemon_action(daemon_name=daemon_name, action=action, image=image)
 
 
+class UpgradeManager(ResourceManager):
+    @wait_api_result
+    def list(self, image: Optional[str], tags: bool,
+             show_all_versions: Optional[bool]) -> Dict[Any, Any]:
+        return self.api.upgrade_ls(image, tags, show_all_versions)
+
+    @wait_api_result
+    def status(self):
+        return self.api.upgrade_status()
+
+    @wait_api_result
+    def start(self, image: str, version: str, daemon_types: Optional[List[str]] = None,
+              host_placement: Optional[str] = None, services: Optional[List[str]] = None,
+              limit: Optional[int] = None) -> str:
+        return self.api.upgrade_start(image, version, daemon_types, host_placement, services,
+                                      limit)
+
+    @wait_api_result
+    def pause(self) -> str:
+        return self.api.upgrade_pause()
+
+    @wait_api_result
+    def resume(self) -> str:
+        return self.api.upgrade_resume()
+
+    @wait_api_result
+    def stop(self) -> str:
+        return self.api.upgrade_stop()
+
+
 class OrchClient(object):
 
     _instance = None
@@ -189,6 +219,7 @@ class OrchClient(object):
         self.services = ServiceManager(self.api)
         self.osds = OsdManager(self.api)
         self.daemons = DaemonManager(self.api)
+        self.upgrades = UpgradeManager(self.api)
 
     def available(self, features: Optional[List[str]] = None) -> bool:
         available = self.status()['available']
@@ -240,3 +271,10 @@ class OrchFeature(object):
     DEVICE_BLINK_LIGHT = 'blink_device_light'
 
     DAEMON_ACTION = 'daemon_action'
+
+    UPGRADE_LIST = 'upgrade_ls'
+    UPGRADE_STATUS = 'upgrade_status'
+    UPGRADE_START = 'upgrade_start'
+    UPGRADE_PAUSE = 'upgrade_pause'
+    UPGRADE_RESUME = 'upgrade_resume'
+    UPGRADE_STOP = 'upgrade_stop'
diff --git a/src/pybind/mgr/dashboard/tests/test_cluster_upgrade.py b/src/pybind/mgr/dashboard/tests/test_cluster_upgrade.py
new file mode 100644 (file)
index 0000000..9e21587
--- /dev/null
@@ -0,0 +1,61 @@
+from ..controllers.cluster import ClusterUpgrade
+from ..tests import ControllerTestCase, patch_orch
+from ..tools import NotificationQueue, TaskManager
+
+
+class ClusterUpgradeControllerTest(ControllerTestCase):
+    URL_CLUSTER_UPGRADE = '/api/cluster/upgrade'
+
+    @classmethod
+    def setup_server(cls):
+        NotificationQueue.start_queue()
+        TaskManager.init()
+        cls.setup_controllers([ClusterUpgrade])
+
+    @classmethod
+    def tearDownClass(cls):
+        NotificationQueue.stop()
+
+    def test_upgrade_list(self):
+        result = ['17.1.0', '16.2.7', '16.2.6', '16.2.5', '16.1.4', '16.1.3']
+        with patch_orch(True) as fake_client:
+            fake_client.upgrades.list.return_value = result
+            self._get('{}?image=quay.io/ceph/ceph:v16.1.0&tags=False&show_all_versions=False'
+                      .format(self.URL_CLUSTER_UPGRADE))
+            self.assertStatus(200)
+            self.assertJsonBody(result)
+
+    def test_start_upgrade(self):
+        msg = "Initiating upgrade to 17.2.6"
+        with patch_orch(True) as fake_client:
+            fake_client.upgrades.start.return_value = msg
+            payload = {
+                'version': '17.2.6'
+            }
+            self._post('{}/start'.format(self.URL_CLUSTER_UPGRADE), payload)
+            self.assertStatus(200)
+            self.assertJsonBody(msg)
+
+    def test_pause_upgrade(self):
+        msg = "Paused upgrade to 17.2.6"
+        with patch_orch(True) as fake_client:
+            fake_client.upgrades.pause.return_value = msg
+            self._put('{}/pause'.format(self.URL_CLUSTER_UPGRADE))
+            self.assertStatus(200)
+            self.assertJsonBody(msg)
+
+    def test_resume_upgrade(self):
+        msg = "Resumed upgrade to 17.2.6"
+        with patch_orch(True) as fake_client:
+            fake_client.upgrades.resume.return_value = msg
+            self._put('{}/resume'.format(self.URL_CLUSTER_UPGRADE))
+            self.assertStatus(200)
+            self.assertJsonBody(msg)
+
+    def test_stop_upgrade(self):
+        msg = "Stopped upgrade to 17.2.6"
+        with patch_orch(True) as fake_client:
+            fake_client.upgrades.stop.return_value = msg
+            self._put('{}/stop'.format(self.URL_CLUSTER_UPGRADE))
+            self.assertStatus(200)
+            self.assertJsonBody(msg)
index 733e1cdcb5fac499a5912571cc6f22d400ac991f..916bbfbd8728e20bc29c4eff1c88f7ee175a9c05 100644 (file)
@@ -858,6 +858,17 @@ class UpgradeStatusSpec(object):
         self.message = ""  # Freeform description
         self.is_paused: bool = False  # Is the upgrade paused?
 
+    def to_json(self) -> dict:
+        return {
+            'in_progress': self.in_progress,
+            'target_image': self.target_image,
+            'which': self.which,
+            'services_complete': self.services_complete,
+            'progress': self.progress,
+            'message': self.message,
+            'is_paused': self.is_paused,
+        }
+
 
 def handle_type_error(method: FuncT) -> FuncT:
     @wraps(method)