From aa9e00437cff8838603e3b32a1a7fb11b1b00fc0 Mon Sep 17 00:00:00 2001 From: avanthakkar Date: Wed, 28 Jun 2023 16:08:05 +0530 Subject: [PATCH] mgr/dashboard: expose cluster upgrade orch API endpoints Add Cluster Upgrade Management support from REST API endpoints. Fixes: https://tracker.ceph.com/issues/61847 Signed-off-by: avanthakkar --- .../mgr/dashboard/controllers/cluster.py | 84 +++++++- src/pybind/mgr/dashboard/openapi.yaml | 196 ++++++++++++++++++ .../mgr/dashboard/services/orchestrator.py | 38 ++++ .../dashboard/tests/test_cluster_upgrade.py | 61 ++++++ src/pybind/mgr/orchestrator/_interface.py | 11 + 5 files changed, 388 insertions(+), 2 deletions(-) create mode 100644 src/pybind/mgr/dashboard/tests/test_cluster_upgrade.py diff --git a/src/pybind/mgr/dashboard/controllers/cluster.py b/src/pybind/mgr/dashboard/controllers/cluster.py index d8170e672e9..5091457ec98 100644 --- a/src/pybind/mgr/dashboard/controllers/cluster.py +++ b/src/pybind/mgr/dashboard/controllers/cluster.py @@ -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() diff --git a/src/pybind/mgr/dashboard/openapi.yaml b/src/pybind/mgr/dashboard/openapi.yaml index 83c58d9c5ca..c4007317049 100644 --- a/src/pybind/mgr/dashboard/openapi.yaml +++ b/src/pybind/mgr/dashboard/openapi.yaml @@ -2174,6 +2174,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 \ @@ -11947,6 +12141,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 diff --git a/src/pybind/mgr/dashboard/services/orchestrator.py b/src/pybind/mgr/dashboard/services/orchestrator.py index 1818164d6c9..e49ab80bfc5 100644 --- a/src/pybind/mgr/dashboard/services/orchestrator.py +++ b/src/pybind/mgr/dashboard/services/orchestrator.py @@ -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 index 00000000000..9e21587b9ca --- /dev/null +++ b/src/pybind/mgr/dashboard/tests/test_cluster_upgrade.py @@ -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) diff --git a/src/pybind/mgr/orchestrator/_interface.py b/src/pybind/mgr/orchestrator/_interface.py index 733e1cdcb5f..916bbfbd872 100644 --- a/src/pybind/mgr/orchestrator/_interface.py +++ b/src/pybind/mgr/orchestrator/_interface.py @@ -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) -- 2.39.5