Skip to content

Commit 3f21557

Browse files
committed
mgr/dashboard: multi-cluster management in ceph dashboard
Fixes: https://tracker.ceph.com/issues/64530 Signed-off-by: Nizamudeen A <[email protected]> Signed-off-by: Aashish Sharma <[email protected]>
1 parent 3e76b89 commit 3f21557

28 files changed

+1291
-75
lines changed

src/pybind/mgr/dashboard/controllers/auth.py

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
# -*- coding: utf-8 -*-
22

33
import http.cookies
4+
import json
45
import logging
56
import sys
67

8+
import cherrypy
9+
710
from .. import mgr
811
from ..exceptions import InvalidCredentialsError, UserDoesNotExist
912
from ..services.auth import AuthManager, JwtManager
@@ -34,17 +37,66 @@ class Auth(RESTController, ControllerAuthMixin):
3437
"""
3538
Provide authenticates and returns JWT token.
3639
"""
37-
40+
# pylint: disable=R0912
3841
def create(self, username, password):
3942
user_data = AuthManager.authenticate(username, password)
4043
user_perms, pwd_expiration_date, pwd_update_required = None, None, None
4144
max_attempt = Settings.ACCOUNT_LOCKOUT_ATTEMPTS
45+
origin = cherrypy.request.headers.get('Origin', None)
46+
try:
47+
fsid = mgr.get('config')['fsid']
48+
except KeyError:
49+
fsid = ''
4250
if max_attempt == 0 or mgr.ACCESS_CTRL_DB.get_attempt(username) < max_attempt:
4351
if user_data:
4452
user_perms = user_data.get('permissions')
4553
pwd_expiration_date = user_data.get('pwdExpirationDate', None)
4654
pwd_update_required = user_data.get('pwdUpdateRequired', False)
4755

56+
if isinstance(Settings.MULTICLUSTER_CONFIG, str):
57+
try:
58+
item_to_dict = json.loads(Settings.MULTICLUSTER_CONFIG)
59+
except json.JSONDecodeError:
60+
item_to_dict = {}
61+
multicluster_config = item_to_dict.copy()
62+
else:
63+
multicluster_config = Settings.MULTICLUSTER_CONFIG.copy()
64+
try:
65+
if fsid in multicluster_config['config']:
66+
existing_entries = multicluster_config['config'][fsid]
67+
if not any(entry['user'] == username for entry in existing_entries):
68+
existing_entries.append({
69+
"name": fsid,
70+
"url": origin,
71+
"cluster_alias": "local-cluster",
72+
"user": username
73+
})
74+
else:
75+
multicluster_config['config'][fsid] = [{
76+
"name": fsid,
77+
"url": origin,
78+
"cluster_alias": "local-cluster",
79+
"user": username
80+
}]
81+
82+
except KeyError:
83+
multicluster_config = {
84+
'current_url': origin,
85+
'current_user': username,
86+
'hub_url': origin,
87+
'config': {
88+
fsid: [
89+
{
90+
"name": fsid,
91+
"url": origin,
92+
"cluster_alias": "local-cluster",
93+
"user": username
94+
}
95+
]
96+
}
97+
}
98+
Settings.MULTICLUSTER_CONFIG = multicluster_config
99+
48100
if user_perms is not None:
49101
url_prefix = 'https' if mgr.get_localized_module_option('ssl') else 'http'
50102

Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
# -*- coding: utf-8 -*-
2+
3+
import json
4+
5+
import requests
6+
7+
from ..exceptions import DashboardException
8+
from ..security import Scope
9+
from ..settings import Settings
10+
from ..tools import configure_cors
11+
from . import APIDoc, APIRouter, CreatePermission, Endpoint, EndpointDoc, \
12+
ReadPermission, RESTController, UIRouter, UpdatePermission
13+
14+
15+
@APIRouter('/multi-cluster', Scope.CONFIG_OPT)
16+
@APIDoc('Multi-cluster Management API', 'Multi-cluster')
17+
class MultiCluster(RESTController):
18+
def _proxy(self, method, base_url, path, params=None, payload=None, verify=False,
19+
token=None):
20+
try:
21+
if token:
22+
headers = {
23+
'Accept': 'application/vnd.ceph.api.v1.0+json',
24+
'Authorization': 'Bearer ' + token,
25+
}
26+
else:
27+
headers = {
28+
'Accept': 'application/vnd.ceph.api.v1.0+json',
29+
'Content-Type': 'application/json',
30+
}
31+
response = requests.request(method, base_url + path, params=params,
32+
json=payload, verify=verify, headers=headers)
33+
except Exception as e:
34+
raise DashboardException(
35+
"Could not reach {}, {}".format(base_url+path, e),
36+
http_status_code=404,
37+
component='dashboard')
38+
39+
try:
40+
content = json.loads(response.content, strict=False)
41+
except json.JSONDecodeError as e:
42+
raise DashboardException(
43+
"Error parsing Dashboard API response: {}".format(e.msg),
44+
component='dashboard')
45+
return content
46+
47+
@Endpoint('POST')
48+
@CreatePermission
49+
@EndpointDoc("Authenticate to a remote cluster")
50+
def auth(self, url: str, cluster_alias: str, username=None,
51+
password=None, token=None, hub_url=None):
52+
53+
multi_cluster_config = self.load_multi_cluster_config()
54+
55+
if not url.endswith('/'):
56+
url = url + '/'
57+
58+
if username and password:
59+
payload = {
60+
'username': username,
61+
'password': password
62+
}
63+
content = self._proxy('POST', url, 'api/auth', payload=payload)
64+
if 'token' not in content:
65+
raise DashboardException(
66+
"Could not authenticate to remote cluster",
67+
http_status_code=400,
68+
component='dashboard')
69+
70+
token = content['token']
71+
72+
if token:
73+
self._proxy('PUT', url, 'ui-api/multi-cluster/set_cors_endpoint',
74+
payload={'url': hub_url}, token=token)
75+
fsid = self._proxy('GET', url, 'api/health/get_cluster_fsid', token=token)
76+
content = self._proxy('POST', url, 'api/auth/check', payload={'token': token},
77+
token=token)
78+
if 'username' in content:
79+
username = content['username']
80+
81+
if 'config' not in multi_cluster_config:
82+
multi_cluster_config['config'] = {}
83+
84+
if fsid in multi_cluster_config['config']:
85+
existing_entries = multi_cluster_config['config'][fsid]
86+
if not any(entry['user'] == username for entry in existing_entries):
87+
existing_entries.append({
88+
"name": fsid,
89+
"url": url,
90+
"cluster_alias": cluster_alias,
91+
"user": username,
92+
"token": token,
93+
})
94+
else:
95+
multi_cluster_config['current_user'] = username
96+
multi_cluster_config['config'][fsid] = [{
97+
"name": fsid,
98+
"url": url,
99+
"cluster_alias": cluster_alias,
100+
"user": username,
101+
"token": token,
102+
}]
103+
104+
Settings.MULTICLUSTER_CONFIG = multi_cluster_config
105+
106+
def load_multi_cluster_config(self):
107+
if isinstance(Settings.MULTICLUSTER_CONFIG, str):
108+
try:
109+
itemw_to_dict = json.loads(Settings.MULTICLUSTER_CONFIG)
110+
except json.JSONDecodeError:
111+
itemw_to_dict = {}
112+
multi_cluster_config = itemw_to_dict.copy()
113+
else:
114+
multi_cluster_config = Settings.MULTICLUSTER_CONFIG.copy()
115+
116+
return multi_cluster_config
117+
118+
@Endpoint('PUT')
119+
@UpdatePermission
120+
def set_config(self, config: object):
121+
multicluster_config = self.load_multi_cluster_config()
122+
multicluster_config.update({'current_url': config['url']})
123+
multicluster_config.update({'current_user': config['user']})
124+
Settings.MULTICLUSTER_CONFIG = multicluster_config
125+
return Settings.MULTICLUSTER_CONFIG
126+
127+
@Endpoint('POST')
128+
@CreatePermission
129+
# pylint: disable=R0911
130+
def verify_connection(self, url: str, username=None, password=None, token=None):
131+
if not url.endswith('/'):
132+
url = url + '/'
133+
134+
if token:
135+
try:
136+
payload = {
137+
'token': token
138+
}
139+
content = self._proxy('POST', url, 'api/auth/check', payload=payload)
140+
if 'permissions' not in content:
141+
return content['detail']
142+
user_content = self._proxy('GET', url, f'api/user/{username}',
143+
token=content['token'])
144+
if 'status' in user_content and user_content['status'] == '403 Forbidden':
145+
return 'User is not an administrator'
146+
except Exception as e: # pylint: disable=broad-except
147+
if '[Errno 111] Connection refused' in str(e):
148+
return 'Connection refused'
149+
return 'Connection failed'
150+
151+
if username and password:
152+
try:
153+
payload = {
154+
'username': username,
155+
'password': password
156+
}
157+
content = self._proxy('POST', url, 'api/auth', payload=payload)
158+
if 'token' not in content:
159+
return content['detail']
160+
user_content = self._proxy('GET', url, f'api/user/{username}',
161+
token=content['token'])
162+
if 'status' in user_content and user_content['status'] == '403 Forbidden':
163+
return 'User is not an administrator'
164+
except Exception as e: # pylint: disable=broad-except
165+
if '[Errno 111] Connection refused' in str(e):
166+
return 'Connection refused'
167+
return 'Connection failed'
168+
return 'Connection successful'
169+
170+
@Endpoint()
171+
@ReadPermission
172+
def get_config(self):
173+
return Settings.MULTICLUSTER_CONFIG
174+
175+
176+
@UIRouter('/multi-cluster', Scope.CONFIG_OPT)
177+
class MultiClusterUi(RESTController):
178+
@Endpoint('PUT')
179+
@UpdatePermission
180+
def set_cors_endpoint(self, url: str):
181+
configure_cors(url)

src/pybind/mgr/dashboard/frontend/src/app/app-routing.module.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ import { NoSsoGuardService } from './shared/services/no-sso-guard.service';
4848
import { UpgradeComponent } from './ceph/cluster/upgrade/upgrade.component';
4949
import { CephfsVolumeFormComponent } from './ceph/cephfs/cephfs-form/cephfs-form.component';
5050
import { UpgradeProgressComponent } from './ceph/cluster/upgrade/upgrade-progress/upgrade-progress.component';
51+
import { MultiClusterComponent } from './ceph/cluster/multi-cluster/multi-cluster.component';
5152

5253
@Injectable()
5354
export class PerformanceCounterBreadcrumbsResolver extends BreadcrumbsResolver {
@@ -184,6 +185,10 @@ const routes: Routes = [
184185
}
185186
]
186187
},
188+
{
189+
path: 'multi-cluster',
190+
component: MultiClusterComponent
191+
},
187192
{
188193
path: 'inventory',
189194
canActivate: [ModuleStatusGuardService],

src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/cluster.module.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,8 @@ import { TelemetryComponent } from './telemetry/telemetry.component';
6161
import { UpgradeComponent } from './upgrade/upgrade.component';
6262
import { UpgradeStartModalComponent } from './upgrade/upgrade-form/upgrade-start-modal.component';
6363
import { UpgradeProgressComponent } from './upgrade/upgrade-progress/upgrade-progress.component';
64+
import { MultiClusterComponent } from './multi-cluster/multi-cluster.component';
65+
import { MultiClusterFormComponent } from './multi-cluster/multi-cluster-form/multi-cluster-form.component';
6466

6567
@NgModule({
6668
imports: [
@@ -124,7 +126,9 @@ import { UpgradeProgressComponent } from './upgrade/upgrade-progress/upgrade-pro
124126
CreateClusterReviewComponent,
125127
UpgradeComponent,
126128
UpgradeStartModalComponent,
127-
UpgradeProgressComponent
129+
UpgradeProgressComponent,
130+
MultiClusterComponent,
131+
MultiClusterFormComponent
128132
],
129133
providers: [NgbActiveModal]
130134
})

0 commit comments

Comments
 (0)