Skip to content

Commit 17481c1

Browse files
committed
mgr/cephadm: adding new cephadm service mgmt-gateway
adding mgmt-gateway, a new cephadm service based on nginx, to act as the front-end and single entry point to the cluster. This gateway offers unified access to all Ceph applications, including the Ceph dashboard and monitoring tools (Prometheus, Grafana, ..), while enhancing security and simplifying access management through nginx. Fixes: https://tracker.ceph.com/issues/66095 Signed-off-by: Redouane Kachach <[email protected]>
1 parent 2c16096 commit 17481c1

File tree

20 files changed

+1065
-95
lines changed

20 files changed

+1065
-95
lines changed

src/cephadm/cephadm.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,7 @@
176176
NFSGanesha,
177177
SMB,
178178
SNMPGateway,
179+
MgmtGateway,
179180
Tracing,
180181
NodeProxy,
181182
)
@@ -227,6 +228,7 @@ def get_supported_daemons():
227228
supported_daemons.append(Keepalived.daemon_type)
228229
supported_daemons.append(CephadmAgent.daemon_type)
229230
supported_daemons.append(SNMPGateway.daemon_type)
231+
supported_daemons.append(MgmtGateway.daemon_type)
230232
supported_daemons.extend(Tracing.components)
231233
supported_daemons.append(NodeProxy.daemon_type)
232234
supported_daemons.append(SMB.daemon_type)
@@ -463,6 +465,8 @@ def update_default_image(ctx: CephadmContext) -> None:
463465
ctx.image = Keepalived.default_image
464466
if type_ == SNMPGateway.daemon_type:
465467
ctx.image = SNMPGateway.default_image
468+
if type_ == MgmtGateway.daemon_type:
469+
ctx.image = MgmtGateway.default_image
466470
if type_ == CephNvmeof.daemon_type:
467471
ctx.image = CephNvmeof.default_image
468472
if type_ in Tracing.components:
@@ -855,6 +859,10 @@ def create_daemon_dirs(
855859
sg = SNMPGateway.init(ctx, fsid, ident.daemon_id)
856860
sg.create_daemon_conf()
857861

862+
elif daemon_type == MgmtGateway.daemon_type:
863+
cg = MgmtGateway.init(ctx, fsid, ident.daemon_id)
864+
cg.create_daemon_dirs(data_dir, uid, gid)
865+
858866
elif daemon_type == NodeProxy.daemon_type:
859867
node_proxy = NodeProxy.init(ctx, fsid, ident.daemon_id)
860868
node_proxy.create_daemon_dirs(data_dir, uid, gid)
@@ -3571,6 +3579,9 @@ def list_daemons(
35713579
elif daemon_type == SNMPGateway.daemon_type:
35723580
version = SNMPGateway.get_version(ctx, fsid, daemon_id)
35733581
seen_versions[image_id] = version
3582+
elif daemon_type == MgmtGateway.daemon_type:
3583+
version = MgmtGateway.get_version(ctx, container_id)
3584+
seen_versions[image_id] = version
35743585
else:
35753586
logger.warning('version for unknown daemon type %s' % daemon_type)
35763587
else:

src/cephadm/cephadmlib/constants.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
DEFAULT_JAEGER_AGENT_IMAGE = 'quay.io/jaegertracing/jaeger-agent:1.29'
2020
DEFAULT_JAEGER_QUERY_IMAGE = 'quay.io/jaegertracing/jaeger-query:1.29'
2121
DEFAULT_SMB_IMAGE = 'quay.io/samba.org/samba-server:devbuilds-centos-amd64'
22+
DEFAULT_NGINX_IMAGE = 'quay.io/ceph/nginx:1.26.1'
2223
DEFAULT_REGISTRY = 'docker.io' # normalize unqualified digests to this
2324
# ------------------------------------------------------------------------------
2425

src/cephadm/cephadmlib/daemons/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from .snmp import SNMPGateway
1010
from .tracing import Tracing
1111
from .node_proxy import NodeProxy
12+
from .mgmt_gateway import MgmtGateway
1213

1314
__all__ = [
1415
'Ceph',
@@ -25,4 +26,5 @@
2526
'SNMPGateway',
2627
'Tracing',
2728
'NodeProxy',
29+
'MgmtGateway',
2830
]
Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
import logging
2+
import os
3+
from typing import Dict, List, Tuple, Optional
4+
import re
5+
6+
from ..call_wrappers import call, CallVerbosity
7+
from ..container_daemon_form import ContainerDaemonForm, daemon_to_container
8+
from ..container_types import CephContainer
9+
from ..context import CephadmContext
10+
from ..context_getters import fetch_configs
11+
from ..daemon_form import register as register_daemon_form
12+
from ..daemon_identity import DaemonIdentity
13+
from ..deployment_utils import to_deployment_container
14+
from ..constants import DEFAULT_NGINX_IMAGE
15+
from ..data_utils import dict_get, is_fsid
16+
from ..file_utils import populate_files, makedirs, recursive_chown
17+
from ..exceptions import Error
18+
19+
logger = logging.getLogger()
20+
21+
22+
@register_daemon_form
23+
class MgmtGateway(ContainerDaemonForm):
24+
"""Defines an MgmtGateway container"""
25+
26+
daemon_type = 'mgmt-gateway'
27+
required_files = [
28+
'nginx.conf',
29+
'nginx_external_server.conf',
30+
'nginx_internal_server.conf',
31+
'nginx_internal.crt',
32+
'nginx_internal.key',
33+
]
34+
35+
default_image = DEFAULT_NGINX_IMAGE
36+
37+
@classmethod
38+
def for_daemon_type(cls, daemon_type: str) -> bool:
39+
return cls.daemon_type == daemon_type
40+
41+
def __init__(
42+
self,
43+
ctx: CephadmContext,
44+
fsid: str,
45+
daemon_id: str,
46+
config_json: Dict,
47+
image: str = DEFAULT_NGINX_IMAGE,
48+
):
49+
self.ctx = ctx
50+
self.fsid = fsid
51+
self.daemon_id = daemon_id
52+
self.image = image
53+
self.files = dict_get(config_json, 'files', {})
54+
self.validate()
55+
56+
@classmethod
57+
def init(
58+
cls, ctx: CephadmContext, fsid: str, daemon_id: str
59+
) -> 'MgmtGateway':
60+
return cls(ctx, fsid, daemon_id, fetch_configs(ctx), ctx.image)
61+
62+
@classmethod
63+
def create(
64+
cls, ctx: CephadmContext, ident: DaemonIdentity
65+
) -> 'MgmtGateway':
66+
return cls.init(ctx, ident.fsid, ident.daemon_id)
67+
68+
@property
69+
def identity(self) -> DaemonIdentity:
70+
return DaemonIdentity(self.fsid, self.daemon_type, self.daemon_id)
71+
72+
def validate(self) -> None:
73+
if not is_fsid(self.fsid):
74+
raise Error(f'not an fsid: {self.fsid}')
75+
if not self.daemon_id:
76+
raise Error(f'invalid daemon_id: {self.daemon_id}')
77+
if not self.image:
78+
raise Error(f'invalid image: {self.image}')
79+
80+
# check for the required files
81+
if self.required_files:
82+
for fname in self.required_files:
83+
if fname not in self.files:
84+
raise Error(
85+
'required file missing from config-json: %s' % fname
86+
)
87+
88+
def container(self, ctx: CephadmContext) -> CephContainer:
89+
ctr = daemon_to_container(ctx, self)
90+
return to_deployment_container(ctx, ctr)
91+
92+
def uid_gid(self, ctx: CephadmContext) -> Tuple[int, int]:
93+
return 65534, 65534 # nobody, nobody
94+
95+
def get_daemon_args(self) -> List[str]:
96+
return []
97+
98+
def default_entrypoint(self) -> str:
99+
return ''
100+
101+
def create_daemon_dirs(self, data_dir: str, uid: int, gid: int) -> None:
102+
"""Create files under the container data dir"""
103+
if not os.path.isdir(data_dir):
104+
raise OSError('data_dir is not a directory: %s' % (data_dir))
105+
logger.info('Writing mgmt-gateway config...')
106+
config_dir = os.path.join(data_dir, 'etc/')
107+
makedirs(config_dir, uid, gid, 0o755)
108+
recursive_chown(config_dir, uid, gid)
109+
populate_files(config_dir, self.files, uid, gid)
110+
111+
def _get_container_mounts(self, data_dir: str) -> Dict[str, str]:
112+
mounts: Dict[str, str] = {}
113+
mounts[
114+
os.path.join(data_dir, 'nginx.conf')
115+
] = '/etc/nginx/nginx.conf:Z'
116+
return mounts
117+
118+
@staticmethod
119+
def get_version(ctx: CephadmContext, container_id: str) -> Optional[str]:
120+
"""Return the version of the Nginx container"""
121+
version = None
122+
out, err, code = call(
123+
ctx,
124+
[
125+
ctx.container_engine.path,
126+
'exec',
127+
container_id,
128+
'nginx',
129+
'-v',
130+
],
131+
verbosity=CallVerbosity.QUIET,
132+
)
133+
if code == 0:
134+
# nginx is using stderr to print the version!!
135+
match = re.search(r'nginx version:\s*nginx\/(.+)', err)
136+
if match:
137+
version = match.group(1)
138+
return version
139+
140+
def customize_container_mounts(
141+
self, ctx: CephadmContext, mounts: Dict[str, str]
142+
) -> None:
143+
data_dir = self.identity.data_dir(ctx.data_dir)
144+
mounts.update(
145+
{
146+
os.path.join(
147+
data_dir, 'etc/nginx.conf'
148+
): '/etc/nginx/nginx.conf:Z',
149+
os.path.join(
150+
data_dir, 'etc/nginx_internal_server.conf'
151+
): '/etc/nginx_internal_server.conf:Z',
152+
os.path.join(
153+
data_dir, 'etc/nginx_external_server.conf'
154+
): '/etc/nginx_external_server.conf:Z',
155+
os.path.join(
156+
data_dir, 'etc/nginx_internal.crt'
157+
): '/etc/nginx/ssl/nginx_internal.crt:Z',
158+
os.path.join(
159+
data_dir, 'etc/nginx_internal.key'
160+
): '/etc/nginx/ssl/nginx_internal.key:Z',
161+
}
162+
)
163+
164+
if 'nginx.crt' in self.files:
165+
mounts.update(
166+
{
167+
os.path.join(
168+
data_dir, 'etc/nginx.crt'
169+
): '/etc/nginx/ssl/nginx.crt:Z',
170+
os.path.join(
171+
data_dir, 'etc/nginx.key'
172+
): '/etc/nginx/ssl/nginx.key:Z',
173+
}
174+
)

src/cephadm/cephadmlib/daemons/monitoring.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,7 @@ def get_daemon_args(self) -> List[str]:
260260
retention_size = config.get(
261261
'retention_size', '0'
262262
) # default to disabled
263+
use_url_prefix = config.get('use_url_prefix', False)
263264
r += [f'--storage.tsdb.retention.time={retention_time}']
264265
r += [f'--storage.tsdb.retention.size={retention_size}']
265266
scheme = 'http'
@@ -271,10 +272,17 @@ def get_daemon_args(self) -> List[str]:
271272
# use the first ipv4 (if any) otherwise use the first ipv6
272273
addr = next(iter(ipv4_addrs or ipv6_addrs), None)
273274
host = wrap_ipv6(addr) if addr else host
274-
r += [f'--web.external-url={scheme}://{host}:{port}']
275+
if use_url_prefix:
276+
r += [
277+
f'--web.external-url={scheme}://{host}:{port}/prometheus'
278+
]
279+
r += ['--web.route-prefix=/prometheus/']
280+
else:
281+
r += [f'--web.external-url={scheme}://{host}:{port}']
275282
r += [f'--web.listen-address={ip}:{port}']
276283
if daemon_type == 'alertmanager':
277284
config = fetch_configs(ctx)
285+
use_url_prefix = config.get('use_url_prefix', False)
278286
peers = config.get('peers', list()) # type: ignore
279287
for peer in peers:
280288
r += ['--cluster.peer={}'.format(peer)]
@@ -284,6 +292,8 @@ def get_daemon_args(self) -> List[str]:
284292
pass
285293
# some alertmanager, by default, look elsewhere for a config
286294
r += ['--config.file=/etc/alertmanager/alertmanager.yml']
295+
if use_url_prefix:
296+
r += ['--web.route-prefix=/alertmanager']
287297
if daemon_type == 'promtail':
288298
r += ['--config.expand-env']
289299
if daemon_type == 'prometheus':

src/pybind/mgr/cephadm/inventory.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1889,6 +1889,7 @@ def _init_known_cert_key_dicts(self) -> None:
18891889
'iscsi_ssl_cert': {}, # service-name -> cert
18901890
'ingress_ssl_cert': {}, # service-name -> cert
18911891
'agent_endpoint_root_cert': Cert(), # cert
1892+
'mgmt_gw_root_cert': Cert(), # cert
18921893
'service_discovery_root_cert': Cert(), # cert
18931894
'grafana_cert': {}, # host -> cert
18941895
'alertmanager_cert': {}, # host -> cert
@@ -1901,6 +1902,7 @@ def _init_known_cert_key_dicts(self) -> None:
19011902
self.known_keys = {
19021903
'agent_endpoint_key': PrivKey(), # key
19031904
'service_discovery_key': PrivKey(), # key
1905+
'mgmt_gw_root_key': PrivKey(), # cert
19041906
'grafana_key': {}, # host -> key
19051907
'alertmanager_key': {}, # host -> key
19061908
'prometheus_key': {}, # host -> key

0 commit comments

Comments
 (0)