forked from ceph/ceph
-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request ceph#53621 from phlogistonjohn/jjm-cephadm-dtypes-…
…common cephadm: introduce Daemon Forms Reviewed-by: Adam King <[email protected]>
- Loading branch information
Showing
9 changed files
with
964 additions
and
339 deletions.
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,82 @@ | ||
# container_deamon_form.py - base class for container based daemon forms | ||
|
||
import abc | ||
|
||
from typing import List, Tuple, Optional | ||
|
||
from .container_types import CephContainer, InitContainer | ||
from .context import CephadmContext | ||
from .daemon_form import DaemonForm | ||
from .deploy import DeploymentType | ||
from .net_utils import EndPoint | ||
|
||
|
||
class ContainerDaemonForm(DaemonForm): | ||
"""A ContainerDaemonForm is a variety of DaemonForm that runs a | ||
single primary daemon process under as a container. | ||
It requires that the `container` method be implemented by subclasses. | ||
A number of other optional methods may also be overridden. | ||
""" | ||
|
||
@abc.abstractmethod | ||
def container(self, ctx: CephadmContext) -> CephContainer: | ||
"""Return the CephContainer instance that will be used to build and run | ||
the daemon. | ||
""" | ||
raise NotImplementedError() # pragma: no cover | ||
|
||
@abc.abstractmethod | ||
def uid_gid(self, ctx: CephadmContext) -> Tuple[int, int]: | ||
"""Return a (uid, gid) tuple indicating what UID and GID the daemon is | ||
expected to run as. This function is permitted to take complex actions | ||
such as running a container to get the needed information. | ||
""" | ||
raise NotImplementedError() # pragma: no cover | ||
|
||
def init_containers(self, ctx: CephadmContext) -> List[InitContainer]: | ||
"""Returns a list of init containers to execute prior to the primary | ||
container running. By default, returns an empty list. | ||
""" | ||
return [] | ||
|
||
def customize_container_binds(self, binds: List[List[str]]) -> None: | ||
"""Given a list of container binds this function can update, delete, | ||
or otherwise mutate the binds that the container will use. | ||
""" | ||
pass | ||
|
||
def customize_container_mounts(self, mounts: List[str]) -> None: | ||
"""Given a list of container mounts this function can update, delete, | ||
or otherwise mutate the mounts that the container will use. | ||
""" | ||
pass | ||
|
||
def customize_container_args(self, args: List[str]) -> None: | ||
"""Given a list of container arguments this function can update, | ||
delete, or otherwise mutate the arguments that the container engine | ||
will use. | ||
""" | ||
pass | ||
|
||
def customize_container_endpoints( | ||
self, endpoints: List[EndPoint], deployment_type: DeploymentType | ||
) -> None: | ||
"""Given a list of entrypoints this function can update, delete, | ||
or otherwise mutate the entrypoints that the container will use. | ||
""" | ||
pass | ||
|
||
def config_and_keyring( | ||
self, ctx: CephadmContext | ||
) -> Tuple[Optional[str], Optional[str]]: | ||
"""Return a tuple of strings containing the ceph confguration | ||
and keyring for the daemon. Returns (None, None) by default. | ||
""" | ||
return None, None | ||
|
||
@property | ||
def osd_fsid(self) -> Optional[str]: | ||
"""Return the OSD FSID or None. Pretty specific to OSDs. You are not | ||
expected to understand this. | ||
""" | ||
return None |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,125 @@ | ||
# deamon_form.py - base class for creating and managing daemons | ||
|
||
import abc | ||
|
||
from typing import Type, TypeVar, List | ||
|
||
from .context import CephadmContext | ||
from .daemon_identity import DaemonIdentity | ||
|
||
|
||
class DaemonForm(abc.ABC): | ||
"""Base class for all types used to build, customize, or otherwise give | ||
form to a deaemon managed by cephadm. | ||
""" | ||
|
||
@classmethod | ||
@abc.abstractmethod | ||
def for_daemon_type(cls, daemon_type: str) -> bool: | ||
"""The for_daemon_type class method accepts a string identifying a | ||
daemon type and should return true if the class can form a daemon of | ||
the named type. Using a method allows supporting arbitrary daemon names | ||
and multiple names for a single class. | ||
""" | ||
raise NotImplementedError() # pragma: no cover | ||
|
||
@classmethod | ||
@abc.abstractmethod | ||
def create( | ||
cls, ctx: CephadmContext, ident: DaemonIdentity | ||
) -> 'DaemonForm': | ||
"""The create class method acts as a common interface for creating | ||
any DaemonForm instance. This means that each class implementing a | ||
DaemonForm can have an __init__ tuned to it's specific needs but | ||
this common interface can be used to intantiate any DaemonForm. | ||
""" | ||
raise NotImplementedError() # pragma: no cover | ||
|
||
@property | ||
@abc.abstractmethod | ||
def identity(self) -> DaemonIdentity: | ||
"""All DaemonForm instances must be able to identify themselves. | ||
The identity property returns a DaemonIdentity tied to the form | ||
being created or manged. | ||
""" | ||
raise NotImplementedError() # pragma: no cover | ||
|
||
|
||
DF = TypeVar('DF', bound=DaemonForm) | ||
|
||
|
||
# Optional daemon form subtypes follow: | ||
# These optional subtypes use the abc modules __subclasshook__ feature. | ||
# Classes that implement these "interfaces" do not need to inherit | ||
# directly from these classes, but can simply implment the optional | ||
# methods. If these methods are avilable then `isinstance` and | ||
# `issubclass` will return true for that class and you can | ||
# safely use the desired method(s). | ||
# Example: | ||
# >>> # daemon1 implements get_sysctl_settings | ||
# >>> assert isinstance(daemon1, SysctlDaemonForm) | ||
# >>> daemon1.get_sysctl_settings() | ||
# >>> # daemon2 doesn't implement get_sysctl_settings | ||
# >>> assert not isinstance(daemon2, SysctlDaemonForm) | ||
|
||
|
||
class SysctlDaemonForm(DaemonForm, metaclass=abc.ABCMeta): | ||
"""The SysctlDaemonForm is an optional subclass that some DaemonForm | ||
types may choose to implement. A SysctlDaemonForm must implement | ||
get_sysctl_settings. | ||
""" | ||
|
||
@abc.abstractmethod | ||
def get_sysctl_settings(self) -> List[str]: | ||
"""Return a list of sysctl settings for the deamon.""" | ||
raise NotImplementedError() # pragma: no cover | ||
|
||
@classmethod | ||
def __subclasshook__(cls, other: Type[DF]) -> bool: | ||
return callable(getattr(other, 'get_sysctl_settings', None)) | ||
|
||
|
||
class FirewalledServiceDaemonForm(DaemonForm, metaclass=abc.ABCMeta): | ||
"""The FirewalledServiceDaemonForm is an optional subclass that some | ||
DaemonForm types may choose to implement. A FirewalledServiceDaemonForm | ||
must implement firewall_service_name. | ||
""" | ||
|
||
@abc.abstractmethod | ||
def firewall_service_name(self) -> str: | ||
"""Return the name of the service known to the firewalld system.""" | ||
raise NotImplementedError() # pragma: no cover | ||
|
||
@classmethod | ||
def __subclasshook__(cls, other: Type[DF]) -> bool: | ||
return callable(getattr(other, 'firewall_service_name', None)) | ||
|
||
|
||
_DAEMON_FORMERS = [] | ||
|
||
|
||
class UnexpectedDaemonTypeError(KeyError): | ||
pass | ||
|
||
|
||
def register(cls: Type[DF]) -> Type[DF]: | ||
"""Decorator to be placed on DaemonForm types if the type is to be added to | ||
the daemon form registry. | ||
""" | ||
_DAEMON_FORMERS.append(cls) | ||
return cls | ||
|
||
|
||
def choose(daemon_type: str) -> Type[DF]: | ||
"""Return a daemon form *class* that is compatible with the given daemon | ||
type name. | ||
""" | ||
for dftype in _DAEMON_FORMERS: | ||
if dftype.for_daemon_type(daemon_type): | ||
return dftype | ||
raise UnexpectedDaemonTypeError(daemon_type) | ||
|
||
|
||
def create(ctx: CephadmContext, ident: DaemonIdentity) -> DaemonForm: | ||
cls: Type[DaemonForm] = choose(ident.daemon_type) | ||
return cls.create(ctx, ident) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
# deploy.py - fundamental deployment types | ||
|
||
from enum import Enum | ||
|
||
|
||
class DeploymentType(Enum): | ||
# Fresh deployment of a daemon. | ||
DEFAULT = 'Deploy' | ||
# Redeploying a daemon. Works the same as fresh | ||
# deployment minus port checking. | ||
REDEPLOY = 'Redeploy' | ||
# Reconfiguring a daemon. Rewrites config | ||
# files and potentially restarts daemon. | ||
RECONFIG = 'Reconfig' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,134 @@ | ||
# firewalld.py - functions and types for working with firewalld | ||
|
||
import logging | ||
|
||
from typing import List, Dict | ||
|
||
from .call_wrappers import call, call_throws, CallVerbosity | ||
from .context import CephadmContext | ||
from .daemon_form import DaemonForm, FirewalledServiceDaemonForm | ||
from .exe_utils import find_executable | ||
from .systemd import check_unit | ||
|
||
logger = logging.getLogger() | ||
|
||
|
||
class Firewalld(object): | ||
|
||
# for specifying ports we should always open when opening | ||
# ports for a daemon of that type. Main use case is for ports | ||
# that we should open when deploying the daemon type but that | ||
# the daemon itself may not necessarily need to bind to the port. | ||
# This needs to be handed differently as we don't want to fail | ||
# deployment if the port cannot be bound to but we still want to | ||
# open the port in the firewall. | ||
external_ports: Dict[str, List[int]] = { | ||
'iscsi': [3260] # 3260 is the well known iSCSI port | ||
} | ||
|
||
def __init__(self, ctx): | ||
# type: (CephadmContext) -> None | ||
self.ctx = ctx | ||
self.available = self.check() | ||
|
||
def check(self): | ||
# type: () -> bool | ||
self.cmd = find_executable('firewall-cmd') | ||
if not self.cmd: | ||
logger.debug('firewalld does not appear to be present') | ||
return False | ||
(enabled, state, _) = check_unit(self.ctx, 'firewalld.service') | ||
if not enabled: | ||
logger.debug('firewalld.service is not enabled') | ||
return False | ||
if state != 'running': | ||
logger.debug('firewalld.service is not running') | ||
return False | ||
|
||
logger.info('firewalld ready') | ||
return True | ||
|
||
def enable_service_for(self, svc: str) -> None: | ||
assert svc, 'service name not provided' | ||
if not self.available: | ||
logger.debug('Not possible to enable service <%s>. firewalld.service is not available' % svc) | ||
return | ||
|
||
if not self.cmd: | ||
raise RuntimeError('command not defined') | ||
|
||
out, err, ret = call(self.ctx, [self.cmd, '--permanent', '--query-service', svc], verbosity=CallVerbosity.DEBUG) | ||
if ret: | ||
logger.info('Enabling firewalld service %s in current zone...' % svc) | ||
out, err, ret = call(self.ctx, [self.cmd, '--permanent', '--add-service', svc]) | ||
if ret: | ||
raise RuntimeError( | ||
'unable to add service %s to current zone: %s' % (svc, err)) | ||
else: | ||
logger.debug('firewalld service %s is enabled in current zone' % svc) | ||
|
||
def open_ports(self, fw_ports): | ||
# type: (List[int]) -> None | ||
if not self.available: | ||
logger.debug('Not possible to open ports <%s>. firewalld.service is not available' % fw_ports) | ||
return | ||
|
||
if not self.cmd: | ||
raise RuntimeError('command not defined') | ||
|
||
for port in fw_ports: | ||
tcp_port = str(port) + '/tcp' | ||
out, err, ret = call(self.ctx, [self.cmd, '--permanent', '--query-port', tcp_port], verbosity=CallVerbosity.DEBUG) | ||
if ret: | ||
logger.info('Enabling firewalld port %s in current zone...' % tcp_port) | ||
out, err, ret = call(self.ctx, [self.cmd, '--permanent', '--add-port', tcp_port]) | ||
if ret: | ||
raise RuntimeError('unable to add port %s to current zone: %s' % | ||
(tcp_port, err)) | ||
else: | ||
logger.debug('firewalld port %s is enabled in current zone' % tcp_port) | ||
|
||
def close_ports(self, fw_ports): | ||
# type: (List[int]) -> None | ||
if not self.available: | ||
logger.debug('Not possible to close ports <%s>. firewalld.service is not available' % fw_ports) | ||
return | ||
|
||
if not self.cmd: | ||
raise RuntimeError('command not defined') | ||
|
||
for port in fw_ports: | ||
tcp_port = str(port) + '/tcp' | ||
out, err, ret = call(self.ctx, [self.cmd, '--permanent', '--query-port', tcp_port], verbosity=CallVerbosity.DEBUG) | ||
if not ret: | ||
logger.info('Disabling port %s in current zone...' % tcp_port) | ||
out, err, ret = call(self.ctx, [self.cmd, '--permanent', '--remove-port', tcp_port]) | ||
if ret: | ||
raise RuntimeError('unable to remove port %s from current zone: %s' % | ||
(tcp_port, err)) | ||
else: | ||
logger.info(f'Port {tcp_port} disabled') | ||
else: | ||
logger.info(f'firewalld port {tcp_port} already closed') | ||
|
||
def apply_rules(self): | ||
# type: () -> None | ||
if not self.available: | ||
return | ||
|
||
if not self.cmd: | ||
raise RuntimeError('command not defined') | ||
|
||
call_throws(self.ctx, [self.cmd, '--reload']) | ||
|
||
|
||
def update_firewalld(ctx: CephadmContext, daemon: DaemonForm) -> None: | ||
if not ('skip_firewalld' in ctx and ctx.skip_firewalld) and isinstance( | ||
daemon, FirewalledServiceDaemonForm | ||
): | ||
svc = daemon.firewall_service_name() | ||
if not svc: | ||
return | ||
firewall = Firewalld(ctx) | ||
firewall.enable_service_for(svc) | ||
firewall.apply_rules() |
Oops, something went wrong.