Skip to content

Commit

Permalink
Merge pull request ceph#53621 from phlogistonjohn/jjm-cephadm-dtypes-…
Browse files Browse the repository at this point in the history
…common

cephadm: introduce Daemon Forms

Reviewed-by: Adam King <[email protected]>
  • Loading branch information
adk3798 authored Oct 5, 2023
2 parents 3844ff2 + ed1bdff commit ab07057
Show file tree
Hide file tree
Showing 9 changed files with 964 additions and 339 deletions.
642 changes: 308 additions & 334 deletions src/cephadm/cephadm.py

Large diffs are not rendered by default.

82 changes: 82 additions & 0 deletions src/cephadm/cephadmlib/container_daemon_form.py
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
125 changes: 125 additions & 0 deletions src/cephadm/cephadmlib/daemon_form.py
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)
14 changes: 14 additions & 0 deletions src/cephadm/cephadmlib/deploy.py
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'
134 changes: 134 additions & 0 deletions src/cephadm/cephadmlib/firewalld.py
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()
Loading

0 comments on commit ab07057

Please sign in to comment.