Skip to content

Commit ab07057

Browse files
authored
Merge pull request ceph#53621 from phlogistonjohn/jjm-cephadm-dtypes-common
cephadm: introduce Daemon Forms Reviewed-by: Adam King <[email protected]>
2 parents 3844ff2 + ed1bdff commit ab07057

File tree

9 files changed

+964
-339
lines changed

9 files changed

+964
-339
lines changed

src/cephadm/cephadm.py

Lines changed: 308 additions & 334 deletions
Large diffs are not rendered by default.
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
# container_deamon_form.py - base class for container based daemon forms
2+
3+
import abc
4+
5+
from typing import List, Tuple, Optional
6+
7+
from .container_types import CephContainer, InitContainer
8+
from .context import CephadmContext
9+
from .daemon_form import DaemonForm
10+
from .deploy import DeploymentType
11+
from .net_utils import EndPoint
12+
13+
14+
class ContainerDaemonForm(DaemonForm):
15+
"""A ContainerDaemonForm is a variety of DaemonForm that runs a
16+
single primary daemon process under as a container.
17+
It requires that the `container` method be implemented by subclasses.
18+
A number of other optional methods may also be overridden.
19+
"""
20+
21+
@abc.abstractmethod
22+
def container(self, ctx: CephadmContext) -> CephContainer:
23+
"""Return the CephContainer instance that will be used to build and run
24+
the daemon.
25+
"""
26+
raise NotImplementedError() # pragma: no cover
27+
28+
@abc.abstractmethod
29+
def uid_gid(self, ctx: CephadmContext) -> Tuple[int, int]:
30+
"""Return a (uid, gid) tuple indicating what UID and GID the daemon is
31+
expected to run as. This function is permitted to take complex actions
32+
such as running a container to get the needed information.
33+
"""
34+
raise NotImplementedError() # pragma: no cover
35+
36+
def init_containers(self, ctx: CephadmContext) -> List[InitContainer]:
37+
"""Returns a list of init containers to execute prior to the primary
38+
container running. By default, returns an empty list.
39+
"""
40+
return []
41+
42+
def customize_container_binds(self, binds: List[List[str]]) -> None:
43+
"""Given a list of container binds this function can update, delete,
44+
or otherwise mutate the binds that the container will use.
45+
"""
46+
pass
47+
48+
def customize_container_mounts(self, mounts: List[str]) -> None:
49+
"""Given a list of container mounts this function can update, delete,
50+
or otherwise mutate the mounts that the container will use.
51+
"""
52+
pass
53+
54+
def customize_container_args(self, args: List[str]) -> None:
55+
"""Given a list of container arguments this function can update,
56+
delete, or otherwise mutate the arguments that the container engine
57+
will use.
58+
"""
59+
pass
60+
61+
def customize_container_endpoints(
62+
self, endpoints: List[EndPoint], deployment_type: DeploymentType
63+
) -> None:
64+
"""Given a list of entrypoints this function can update, delete,
65+
or otherwise mutate the entrypoints that the container will use.
66+
"""
67+
pass
68+
69+
def config_and_keyring(
70+
self, ctx: CephadmContext
71+
) -> Tuple[Optional[str], Optional[str]]:
72+
"""Return a tuple of strings containing the ceph confguration
73+
and keyring for the daemon. Returns (None, None) by default.
74+
"""
75+
return None, None
76+
77+
@property
78+
def osd_fsid(self) -> Optional[str]:
79+
"""Return the OSD FSID or None. Pretty specific to OSDs. You are not
80+
expected to understand this.
81+
"""
82+
return None
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
# deamon_form.py - base class for creating and managing daemons
2+
3+
import abc
4+
5+
from typing import Type, TypeVar, List
6+
7+
from .context import CephadmContext
8+
from .daemon_identity import DaemonIdentity
9+
10+
11+
class DaemonForm(abc.ABC):
12+
"""Base class for all types used to build, customize, or otherwise give
13+
form to a deaemon managed by cephadm.
14+
"""
15+
16+
@classmethod
17+
@abc.abstractmethod
18+
def for_daemon_type(cls, daemon_type: str) -> bool:
19+
"""The for_daemon_type class method accepts a string identifying a
20+
daemon type and should return true if the class can form a daemon of
21+
the named type. Using a method allows supporting arbitrary daemon names
22+
and multiple names for a single class.
23+
"""
24+
raise NotImplementedError() # pragma: no cover
25+
26+
@classmethod
27+
@abc.abstractmethod
28+
def create(
29+
cls, ctx: CephadmContext, ident: DaemonIdentity
30+
) -> 'DaemonForm':
31+
"""The create class method acts as a common interface for creating
32+
any DaemonForm instance. This means that each class implementing a
33+
DaemonForm can have an __init__ tuned to it's specific needs but
34+
this common interface can be used to intantiate any DaemonForm.
35+
"""
36+
raise NotImplementedError() # pragma: no cover
37+
38+
@property
39+
@abc.abstractmethod
40+
def identity(self) -> DaemonIdentity:
41+
"""All DaemonForm instances must be able to identify themselves.
42+
The identity property returns a DaemonIdentity tied to the form
43+
being created or manged.
44+
"""
45+
raise NotImplementedError() # pragma: no cover
46+
47+
48+
DF = TypeVar('DF', bound=DaemonForm)
49+
50+
51+
# Optional daemon form subtypes follow:
52+
# These optional subtypes use the abc modules __subclasshook__ feature.
53+
# Classes that implement these "interfaces" do not need to inherit
54+
# directly from these classes, but can simply implment the optional
55+
# methods. If these methods are avilable then `isinstance` and
56+
# `issubclass` will return true for that class and you can
57+
# safely use the desired method(s).
58+
# Example:
59+
# >>> # daemon1 implements get_sysctl_settings
60+
# >>> assert isinstance(daemon1, SysctlDaemonForm)
61+
# >>> daemon1.get_sysctl_settings()
62+
# >>> # daemon2 doesn't implement get_sysctl_settings
63+
# >>> assert not isinstance(daemon2, SysctlDaemonForm)
64+
65+
66+
class SysctlDaemonForm(DaemonForm, metaclass=abc.ABCMeta):
67+
"""The SysctlDaemonForm is an optional subclass that some DaemonForm
68+
types may choose to implement. A SysctlDaemonForm must implement
69+
get_sysctl_settings.
70+
"""
71+
72+
@abc.abstractmethod
73+
def get_sysctl_settings(self) -> List[str]:
74+
"""Return a list of sysctl settings for the deamon."""
75+
raise NotImplementedError() # pragma: no cover
76+
77+
@classmethod
78+
def __subclasshook__(cls, other: Type[DF]) -> bool:
79+
return callable(getattr(other, 'get_sysctl_settings', None))
80+
81+
82+
class FirewalledServiceDaemonForm(DaemonForm, metaclass=abc.ABCMeta):
83+
"""The FirewalledServiceDaemonForm is an optional subclass that some
84+
DaemonForm types may choose to implement. A FirewalledServiceDaemonForm
85+
must implement firewall_service_name.
86+
"""
87+
88+
@abc.abstractmethod
89+
def firewall_service_name(self) -> str:
90+
"""Return the name of the service known to the firewalld system."""
91+
raise NotImplementedError() # pragma: no cover
92+
93+
@classmethod
94+
def __subclasshook__(cls, other: Type[DF]) -> bool:
95+
return callable(getattr(other, 'firewall_service_name', None))
96+
97+
98+
_DAEMON_FORMERS = []
99+
100+
101+
class UnexpectedDaemonTypeError(KeyError):
102+
pass
103+
104+
105+
def register(cls: Type[DF]) -> Type[DF]:
106+
"""Decorator to be placed on DaemonForm types if the type is to be added to
107+
the daemon form registry.
108+
"""
109+
_DAEMON_FORMERS.append(cls)
110+
return cls
111+
112+
113+
def choose(daemon_type: str) -> Type[DF]:
114+
"""Return a daemon form *class* that is compatible with the given daemon
115+
type name.
116+
"""
117+
for dftype in _DAEMON_FORMERS:
118+
if dftype.for_daemon_type(daemon_type):
119+
return dftype
120+
raise UnexpectedDaemonTypeError(daemon_type)
121+
122+
123+
def create(ctx: CephadmContext, ident: DaemonIdentity) -> DaemonForm:
124+
cls: Type[DaemonForm] = choose(ident.daemon_type)
125+
return cls.create(ctx, ident)

src/cephadm/cephadmlib/deploy.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# deploy.py - fundamental deployment types
2+
3+
from enum import Enum
4+
5+
6+
class DeploymentType(Enum):
7+
# Fresh deployment of a daemon.
8+
DEFAULT = 'Deploy'
9+
# Redeploying a daemon. Works the same as fresh
10+
# deployment minus port checking.
11+
REDEPLOY = 'Redeploy'
12+
# Reconfiguring a daemon. Rewrites config
13+
# files and potentially restarts daemon.
14+
RECONFIG = 'Reconfig'
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
# firewalld.py - functions and types for working with firewalld
2+
3+
import logging
4+
5+
from typing import List, Dict
6+
7+
from .call_wrappers import call, call_throws, CallVerbosity
8+
from .context import CephadmContext
9+
from .daemon_form import DaemonForm, FirewalledServiceDaemonForm
10+
from .exe_utils import find_executable
11+
from .systemd import check_unit
12+
13+
logger = logging.getLogger()
14+
15+
16+
class Firewalld(object):
17+
18+
# for specifying ports we should always open when opening
19+
# ports for a daemon of that type. Main use case is for ports
20+
# that we should open when deploying the daemon type but that
21+
# the daemon itself may not necessarily need to bind to the port.
22+
# This needs to be handed differently as we don't want to fail
23+
# deployment if the port cannot be bound to but we still want to
24+
# open the port in the firewall.
25+
external_ports: Dict[str, List[int]] = {
26+
'iscsi': [3260] # 3260 is the well known iSCSI port
27+
}
28+
29+
def __init__(self, ctx):
30+
# type: (CephadmContext) -> None
31+
self.ctx = ctx
32+
self.available = self.check()
33+
34+
def check(self):
35+
# type: () -> bool
36+
self.cmd = find_executable('firewall-cmd')
37+
if not self.cmd:
38+
logger.debug('firewalld does not appear to be present')
39+
return False
40+
(enabled, state, _) = check_unit(self.ctx, 'firewalld.service')
41+
if not enabled:
42+
logger.debug('firewalld.service is not enabled')
43+
return False
44+
if state != 'running':
45+
logger.debug('firewalld.service is not running')
46+
return False
47+
48+
logger.info('firewalld ready')
49+
return True
50+
51+
def enable_service_for(self, svc: str) -> None:
52+
assert svc, 'service name not provided'
53+
if not self.available:
54+
logger.debug('Not possible to enable service <%s>. firewalld.service is not available' % svc)
55+
return
56+
57+
if not self.cmd:
58+
raise RuntimeError('command not defined')
59+
60+
out, err, ret = call(self.ctx, [self.cmd, '--permanent', '--query-service', svc], verbosity=CallVerbosity.DEBUG)
61+
if ret:
62+
logger.info('Enabling firewalld service %s in current zone...' % svc)
63+
out, err, ret = call(self.ctx, [self.cmd, '--permanent', '--add-service', svc])
64+
if ret:
65+
raise RuntimeError(
66+
'unable to add service %s to current zone: %s' % (svc, err))
67+
else:
68+
logger.debug('firewalld service %s is enabled in current zone' % svc)
69+
70+
def open_ports(self, fw_ports):
71+
# type: (List[int]) -> None
72+
if not self.available:
73+
logger.debug('Not possible to open ports <%s>. firewalld.service is not available' % fw_ports)
74+
return
75+
76+
if not self.cmd:
77+
raise RuntimeError('command not defined')
78+
79+
for port in fw_ports:
80+
tcp_port = str(port) + '/tcp'
81+
out, err, ret = call(self.ctx, [self.cmd, '--permanent', '--query-port', tcp_port], verbosity=CallVerbosity.DEBUG)
82+
if ret:
83+
logger.info('Enabling firewalld port %s in current zone...' % tcp_port)
84+
out, err, ret = call(self.ctx, [self.cmd, '--permanent', '--add-port', tcp_port])
85+
if ret:
86+
raise RuntimeError('unable to add port %s to current zone: %s' %
87+
(tcp_port, err))
88+
else:
89+
logger.debug('firewalld port %s is enabled in current zone' % tcp_port)
90+
91+
def close_ports(self, fw_ports):
92+
# type: (List[int]) -> None
93+
if not self.available:
94+
logger.debug('Not possible to close ports <%s>. firewalld.service is not available' % fw_ports)
95+
return
96+
97+
if not self.cmd:
98+
raise RuntimeError('command not defined')
99+
100+
for port in fw_ports:
101+
tcp_port = str(port) + '/tcp'
102+
out, err, ret = call(self.ctx, [self.cmd, '--permanent', '--query-port', tcp_port], verbosity=CallVerbosity.DEBUG)
103+
if not ret:
104+
logger.info('Disabling port %s in current zone...' % tcp_port)
105+
out, err, ret = call(self.ctx, [self.cmd, '--permanent', '--remove-port', tcp_port])
106+
if ret:
107+
raise RuntimeError('unable to remove port %s from current zone: %s' %
108+
(tcp_port, err))
109+
else:
110+
logger.info(f'Port {tcp_port} disabled')
111+
else:
112+
logger.info(f'firewalld port {tcp_port} already closed')
113+
114+
def apply_rules(self):
115+
# type: () -> None
116+
if not self.available:
117+
return
118+
119+
if not self.cmd:
120+
raise RuntimeError('command not defined')
121+
122+
call_throws(self.ctx, [self.cmd, '--reload'])
123+
124+
125+
def update_firewalld(ctx: CephadmContext, daemon: DaemonForm) -> None:
126+
if not ('skip_firewalld' in ctx and ctx.skip_firewalld) and isinstance(
127+
daemon, FirewalledServiceDaemonForm
128+
):
129+
svc = daemon.firewall_service_name()
130+
if not svc:
131+
return
132+
firewall = Firewalld(ctx)
133+
firewall.enable_service_for(svc)
134+
firewall.apply_rules()

0 commit comments

Comments
 (0)