From 2bf15df143a80a760a557bb78a03b401331fb809 Mon Sep 17 00:00:00 2001 From: Aashish Sharma Date: Thu, 25 Jan 2024 12:44:01 +0530 Subject: [PATCH] mgr/dashboard: Add a manage clusters page to the multi-cluster nav to list/connect/disconnect/edit clusters in multi-cluster setup Signed-off-by: Aashish Sharma --- .../dashboard/controllers/multi_cluster.py | 169 ++++++++++---- .../frontend/src/app/app-routing.module.ts | 18 +- .../src/app/ceph/cluster/cluster.module.ts | 4 +- .../multi-cluster-form.component.html | 72 ++++-- .../multi-cluster-form.component.ts | 138 ++++++++++-- .../multi-cluster-list.component.html | 37 +++ .../multi-cluster-list.component.scss | 0 .../multi-cluster-list.component.spec.ts | 30 +++ .../multi-cluster-list.component.ts | 213 ++++++++++++++++++ .../multi-cluster.component.html | 10 - .../multi-cluster/multi-cluster.component.ts | 5 +- .../workbench-layout.component.ts | 1 + .../navigation/navigation.component.html | 10 +- .../navigation/navigation.component.ts | 43 +++- .../app/shared/api/multi-cluster.service.ts | 94 +++++++- .../src/app/shared/constants/app.constants.ts | 4 + .../src/app/shared/models/multi-cluster.ts | 8 + .../services/api-interceptor.service.ts | 6 +- src/pybind/mgr/dashboard/openapi.yaml | 169 +++++++++++++- 19 files changed, 913 insertions(+), 118 deletions(-) create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/multi-cluster/multi-cluster-list/multi-cluster-list.component.html create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/multi-cluster/multi-cluster-list/multi-cluster-list.component.scss create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/multi-cluster/multi-cluster-list/multi-cluster-list.component.spec.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/multi-cluster/multi-cluster-list/multi-cluster-list.component.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/shared/models/multi-cluster.ts diff --git a/src/pybind/mgr/dashboard/controllers/multi_cluster.py b/src/pybind/mgr/dashboard/controllers/multi_cluster.py index d7acec22bebbf..c918c2ec3c240 100644 --- a/src/pybind/mgr/dashboard/controllers/multi_cluster.py +++ b/src/pybind/mgr/dashboard/controllers/multi_cluster.py @@ -1,6 +1,8 @@ # -*- coding: utf-8 -*- +import base64 import json +import time import requests @@ -8,8 +10,8 @@ from ..exceptions import DashboardException from ..security import Scope from ..settings import Settings from ..tools import configure_cors -from . import APIDoc, APIRouter, CreatePermission, Endpoint, EndpointDoc, \ - ReadPermission, RESTController, UIRouter, UpdatePermission +from . import APIDoc, APIRouter, CreatePermission, DeletePermission, Endpoint, \ + EndpointDoc, ReadPermission, RESTController, UIRouter, UpdatePermission @APIRouter('/multi-cluster', Scope.CONFIG_OPT) @@ -17,6 +19,8 @@ from . import APIDoc, APIRouter, CreatePermission, Endpoint, EndpointDoc, \ class MultiCluster(RESTController): def _proxy(self, method, base_url, path, params=None, payload=None, verify=False, token=None): + if not base_url.endswith('/'): + base_url = base_url + '/' try: if token: headers = { @@ -48,12 +52,7 @@ class MultiCluster(RESTController): @CreatePermission @EndpointDoc("Authenticate to a remote cluster") def auth(self, url: str, cluster_alias: str, username=None, - password=None, token=None, hub_url=None): - - multi_cluster_config = self.load_multi_cluster_config() - - if not url.endswith('/'): - url = url + '/' + password=None, token=None, hub_url=None, cluster_fsid=None): if username and password: payload = { @@ -67,41 +66,40 @@ class MultiCluster(RESTController): http_status_code=400, component='dashboard') - token = content['token'] + cluster_token = content['token'] - if token: self._proxy('PUT', url, 'ui-api/multi-cluster/set_cors_endpoint', - payload={'url': hub_url}, token=token) - fsid = self._proxy('GET', url, 'api/health/get_cluster_fsid', token=token) - content = self._proxy('POST', url, 'api/auth/check', payload={'token': token}, - token=token) - if 'username' in content: - username = content['username'] - - if 'config' not in multi_cluster_config: - multi_cluster_config['config'] = {} - - if fsid in multi_cluster_config['config']: - existing_entries = multi_cluster_config['config'][fsid] - if not any(entry['user'] == username for entry in existing_entries): - existing_entries.append({ - "name": fsid, - "url": url, - "cluster_alias": cluster_alias, - "user": username, - "token": token, - }) - else: - multi_cluster_config['current_user'] = username - multi_cluster_config['config'][fsid] = [{ + payload={'url': hub_url}, token=cluster_token) + + fsid = self._proxy('GET', url, 'api/health/get_cluster_fsid', token=cluster_token) + + self.set_multi_cluster_config(fsid, username, url, cluster_alias, cluster_token) + + if token and cluster_fsid and username: + self.set_multi_cluster_config(cluster_fsid, username, url, cluster_alias, token) + + def set_multi_cluster_config(self, fsid, username, url, cluster_alias, token): + multi_cluster_config = self.load_multi_cluster_config() + if fsid in multi_cluster_config['config']: + existing_entries = multi_cluster_config['config'][fsid] + if not any(entry['user'] == username for entry in existing_entries): + existing_entries.append({ "name": fsid, "url": url, "cluster_alias": cluster_alias, "user": username, "token": token, - }] - - Settings.MULTICLUSTER_CONFIG = multi_cluster_config + }) + else: + multi_cluster_config['current_user'] = username + multi_cluster_config['config'][fsid] = [{ + "name": fsid, + "url": url, + "cluster_alias": cluster_alias, + "user": username, + "token": token, + }] + Settings.MULTICLUSTER_CONFIG = multi_cluster_config def load_multi_cluster_config(self): if isinstance(Settings.MULTICLUSTER_CONFIG, str): @@ -124,13 +122,71 @@ class MultiCluster(RESTController): Settings.MULTICLUSTER_CONFIG = multicluster_config return Settings.MULTICLUSTER_CONFIG - @Endpoint('POST') + @Endpoint('PUT') @CreatePermission - # pylint: disable=R0911 - def verify_connection(self, url: str, username=None, password=None, token=None): - if not url.endswith('/'): - url = url + '/' + # pylint: disable=unused-variable + def reconnect_cluster(self, url: str, username=None, password=None, token=None): + multicluster_config = self.load_multi_cluster_config() + if username and password: + payload = { + 'username': username, + 'password': password + } + content = self._proxy('POST', url, 'api/auth', payload=payload) + if 'token' not in content: + raise DashboardException( + "Could not authenticate to remote cluster", + http_status_code=400, + component='dashboard') + token = content['token'] + + if username and token: + if "config" in multicluster_config: + for key, cluster_details in multicluster_config["config"].items(): + for cluster in cluster_details: + if cluster["url"] == url and cluster["user"] == username: + cluster['token'] = token + Settings.MULTICLUSTER_CONFIG = multicluster_config + return Settings.MULTICLUSTER_CONFIG + + @Endpoint('PUT') + @UpdatePermission + # pylint: disable=unused-variable + def edit_cluster(self, url, cluster_alias, username): + multicluster_config = self.load_multi_cluster_config() + if "config" in multicluster_config: + for key, cluster_details in multicluster_config["config"].items(): + for cluster in cluster_details: + if cluster["url"] == url and cluster["user"] == username: + cluster['cluster_alias'] = cluster_alias + Settings.MULTICLUSTER_CONFIG = multicluster_config + return Settings.MULTICLUSTER_CONFIG + + @Endpoint(method='DELETE') + @DeletePermission + def delete_cluster(self, cluster_name, cluster_user): + multicluster_config = self.load_multi_cluster_config() + if "config" in multicluster_config: + keys_to_remove = [] + for key, cluster_details in multicluster_config["config"].items(): + cluster_details_copy = list(cluster_details) + for cluster in cluster_details_copy: + if cluster["name"] == cluster_name and cluster["user"] == cluster_user: + cluster_details.remove(cluster) + if not cluster_details: + keys_to_remove.append(key) + + for key in keys_to_remove: + del multicluster_config["config"][key] + + Settings.MULTICLUSTER_CONFIG = multicluster_config + return Settings.MULTICLUSTER_CONFIG + + @Endpoint() + @ReadPermission + # pylint: disable=R0911 + def verify_connection(self, url=None, username=None, password=None, token=None): if token: try: payload = { @@ -172,6 +228,37 @@ class MultiCluster(RESTController): def get_config(self): return Settings.MULTICLUSTER_CONFIG + def is_token_expired(self, jwt_token): + split_message = jwt_token.split(".") + base64_message = split_message[1] + decoded_token = json.loads(base64.urlsafe_b64decode(base64_message + "====")) + expiration_time = decoded_token['exp'] + current_time = time.time() + return expiration_time < current_time + + def check_token_status_expiration(self, token): + if self.is_token_expired(token): + return 1 + return 0 + + def check_token_status_array(self, clusters_token_array): + token_status_map = {} + + for item in clusters_token_array: + cluster_name = item['name'] + token = item['token'] + user = item['user'] + status = self.check_token_status_expiration(token) + token_status_map[cluster_name] = {'status': status, 'user': user} + + return token_status_map + + @Endpoint() + @ReadPermission + def check_token_status(self, clustersTokenMap=None): + clusters_token_map = json.loads(clustersTokenMap) + return self.check_token_status_array(clusters_token_map) + @UIRouter('/multi-cluster', Scope.CONFIG_OPT) class MultiClusterUi(RESTController): diff --git a/src/pybind/mgr/dashboard/frontend/src/app/app-routing.module.ts b/src/pybind/mgr/dashboard/frontend/src/app/app-routing.module.ts index 48224c844d47b..c54681b065f27 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/app-routing.module.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/app-routing.module.ts @@ -49,6 +49,7 @@ import { UpgradeComponent } from './ceph/cluster/upgrade/upgrade.component'; import { CephfsVolumeFormComponent } from './ceph/cephfs/cephfs-form/cephfs-form.component'; import { UpgradeProgressComponent } from './ceph/cluster/upgrade/upgrade-progress/upgrade-progress.component'; import { MultiClusterComponent } from './ceph/cluster/multi-cluster/multi-cluster.component'; +import { MultiClusterListComponent } from './ceph/cluster/multi-cluster/multi-cluster-list/multi-cluster-list.component'; @Injectable() export class PerformanceCounterBreadcrumbsResolver extends BreadcrumbsResolver { @@ -187,7 +188,22 @@ const routes: Routes = [ }, { path: 'multi-cluster', - component: MultiClusterComponent + children: [ + { + path: 'overview', + component: MultiClusterComponent, + data: { + breadcrumbs: 'Multi-Cluster/Overview' + } + }, + { + path: 'manage-clusters', + component: MultiClusterListComponent, + data: { + breadcrumbs: 'Multi-Cluster/Manage Clusters' + } + } + ] }, { path: 'inventory', diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/cluster.module.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/cluster.module.ts index b1eb9275a462c..2f0734885d857 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/cluster.module.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/cluster.module.ts @@ -63,6 +63,7 @@ import { UpgradeStartModalComponent } from './upgrade/upgrade-form/upgrade-start import { UpgradeProgressComponent } from './upgrade/upgrade-progress/upgrade-progress.component'; import { MultiClusterComponent } from './multi-cluster/multi-cluster.component'; import { MultiClusterFormComponent } from './multi-cluster/multi-cluster-form/multi-cluster-form.component'; +import { MultiClusterListComponent } from './multi-cluster/multi-cluster-list/multi-cluster-list.component'; @NgModule({ imports: [ @@ -128,7 +129,8 @@ import { MultiClusterFormComponent } from './multi-cluster/multi-cluster-form/mu UpgradeStartModalComponent, UpgradeProgressComponent, MultiClusterComponent, - MultiClusterFormComponent + MultiClusterFormComponent, + MultiClusterListComponent ], providers: [NgbActiveModal] }) diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/multi-cluster/multi-cluster-form/multi-cluster-form.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/multi-cluster/multi-cluster-form/multi-cluster-form.component.html index cc9ed7453fc4d..c875557306a85 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/multi-cluster/multi-cluster-form/multi-cluster-form.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/multi-cluster/multi-cluster-form/multi-cluster-form.component.html @@ -1,6 +1,6 @@ Connect Cluster + class="modal-title">{{ action | titlecase }} Cluster
This field is required. + The chosen alias name is already in use. +
+ *ngIf="action !== 'edit'">
@@ -82,10 +86,32 @@ *ngIf="remoteClusterForm.showError('username', frm, 'required')" i18n>This field is required. + A cluster with the chosen user is already connected. + +
+
+
+ +
+ + This field is required. +
+ *ngIf="!remoteClusterForm.getValue('showToken') && !showCrossOriginError && action !== 'edit'">
-
-
-
- - -
-
-
+ *ngIf="remoteClusterForm.getValue('showToken') && action !== 'edit'">
+ *ngIf="action !== 'edit'"> +
+
+ + +
+
+
+
-
-
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/multi-cluster/multi-cluster.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/multi-cluster/multi-cluster.component.ts index 2630c839a4245..dbbf10e74848b 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/multi-cluster/multi-cluster.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/multi-cluster/multi-cluster.component.ts @@ -50,7 +50,10 @@ export class MultiClusterComponent implements OnInit { } openRemoteClusterInfoModal() { - this.bsModalRef = this.modalService.show(MultiClusterFormComponent, { + const initialState = { + action: 'connect' + }; + this.bsModalRef = this.modalService.show(MultiClusterFormComponent, initialState, { size: 'xl' }); } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/layouts/workbench-layout/workbench-layout.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/core/layouts/workbench-layout/workbench-layout.component.ts index 1d7c4bb751cb6..8ddbddf2fe814 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/core/layouts/workbench-layout/workbench-layout.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/core/layouts/workbench-layout/workbench-layout.component.ts @@ -27,6 +27,7 @@ export class WorkbenchLayoutComponent implements OnInit, OnDestroy { ngOnInit() { this.subs.add(this.multiClusterService.startPolling()); + this.subs.add(this.multiClusterService.startClusterTokenStatusPolling()); this.subs.add(this.summaryService.startPolling()); this.subs.add(this.taskManagerService.init(this.summaryService)); this.faviconService.init(); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/navigation/navigation.component.html b/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/navigation/navigation.component.html index 6af3799b4ef84..8f2633ed0a15a 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/navigation/navigation.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/navigation/navigation.component.html @@ -44,7 +44,8 @@