Skip to content

ctdb: nodes list command, wait for ctdb option #128

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 11 additions & 3 deletions sambacc/commands/ctdb.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import logging
import os
import socket
import sys
import typing

from sambacc import ctdb
Expand Down Expand Up @@ -394,9 +395,7 @@ def ctdb_must_have_node(ctx: Context) -> None:
waiter.wait()
if ctx.cli.write_nodes:
_logger.info("Writing nodes file")
ctdb.cluster_meta_to_nodes(
np.cluster_meta(), real_path=np.persistent_path
)
ctdb.cluster_meta_to_nodes(np.cluster_meta(), dest=np.persistent_path)


def _ctdb_rados_mutex_args(parser: argparse.ArgumentParser) -> None:
Expand Down Expand Up @@ -443,6 +442,15 @@ def ctdb_rados_mutex(ctx: Context) -> None:
samba_cmds.execute(cmd) # replaces process


@commands.command(name="ctdb-list-nodes", arg_func=_ctdb_general_node_args)
def ctdb_list_nodes(ctx: Context) -> None:
"""Write nodes content to stdout based on current cluster meta."""
_ctdb_ok()
np = NodeParams(ctx)

ctdb.cluster_meta_to_nodes(np.cluster_meta(), sys.stdout)


class ErrorLimiter:
def __init__(
self,
Expand Down
68 changes: 67 additions & 1 deletion sambacc/commands/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,12 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>
#

import contextlib
import logging
import signal
import time
import typing

from sambacc import samba_cmds
import sambacc.paths as paths

Expand All @@ -24,7 +30,34 @@
from .join import join


_logger = logging.getLogger(__name__)

INIT_ALL = "init-all"
SMBD = "smbd"
WINBINDD = "winbindd"
CTDBD = "ctdbd"
TARGETS = [SMBD, WINBINDD, CTDBD]


class WaitForCTDBCondition:
def met(self, ctx: Context) -> bool:
target = getattr(ctx.cli, "target", None)
if target == CTDBD:
raise Fail(f"Can not start and wait for {CTDBD}")
_logger.debug("Condition required: ctdb pnn available")
import sambacc.ctdb

pnn = sambacc.ctdb.current_pnn()
ok = pnn is not None
_logger.debug(
"Condition %s: ctdb pnn available: %s",
"met" if ok else "not met",
pnn,
)
return ok


_wait_for_conditions = {"ctdb": WaitForCTDBCondition}


def _run_container_args(parser):
Expand All @@ -47,6 +80,17 @@ def _run_container_args(parser):
" The special 'init-all' name will perform all known setup steps."
),
)
_wait_for_choices = _wait_for_conditions.keys()
parser.add_argument(
"--wait-for",
action="append",
choices=_wait_for_choices,
help=(
"Specify a condition to wait for prior to starting the server"
" process. Available conditions: `ctdb` - wait for ctdb"
" to run and provide a pnn."
),
)
parser.add_argument(
"--insecure-auto-join",
action="store_true",
Expand All @@ -57,16 +101,38 @@ def _run_container_args(parser):
)
parser.add_argument(
"target",
choices=["smbd", "winbindd", "ctdbd"],
choices=TARGETS,
help="Which process to run",
)


_COND_TIMEOUT = 5 * 60


@contextlib.contextmanager
def _timeout(timeout: int) -> typing.Iterator[None]:
def _handler(sig: int, frame: typing.Any) -> None:
raise RuntimeError("timed out waiting for conditions")

signal.signal(signal.SIGALRM, _handler)
signal.alarm(timeout)
yield
signal.alarm(0)
signal.signal(signal.SIGALRM, signal.SIG_DFL)


@commands.command(name="run", arg_func=_run_container_args)
def run_container(ctx: Context) -> None:
"""Run a specified server process."""
if ctx.cli.no_init and ctx.cli.setup:
raise Fail("can not specify both --no-init and --setup")

if ctx.cli.wait_for:
with _timeout(_COND_TIMEOUT):
conditions = [_wait_for_conditions[n]() for n in ctx.cli.wait_for]
while not all(c.met(ctx) for c in conditions):
time.sleep(1)

# running servers expect to make use of ctdb whenever it is configured
ctx.expects_ctdb = True
if not ctx.cli.no_init and not ctx.cli.setup:
Expand Down
75 changes: 50 additions & 25 deletions sambacc/ctdb.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,9 @@ def _write_param(name: str, key: str) -> None:
fh.write(enc("\n"))
fh.write(enc("[cluster]\n"))
_write_param("recovery lock", "recovery_lock")
if ctdb_params.get("nodes_cmd"):
nodes_cmd = ctdb_params["nodes_cmd"]
fh.write(enc(f"nodes list = !{nodes_cmd}"))
fh.write(enc("\n"))
fh.write(enc("[legacy]\n"))
_write_param("realtime scheduling", "realtime_scheduling")
Expand Down Expand Up @@ -481,16 +484,21 @@ def _node_update(cmeta: ClusterMeta, real_path: str) -> bool:
return True


def cluster_meta_to_nodes(cmeta: ClusterMeta, real_path: str) -> None:
def cluster_meta_to_nodes(
cmeta: ClusterMeta, dest: typing.Union[str, typing.IO]
) -> None:
"""Write a nodes file based on the current content of the cluster
metadata."""
with cmeta.open(locked=True) as cmo:
json_data = cmo.load()
nodes = json_data.get("nodes", [])
_logger.info("Found node metadata: %r", nodes)
ctdb_nodes = _cluster_meta_to_ctdb_nodes(nodes)
_logger.info("Will write nodes: %s", ctdb_nodes)
_save_nodes(real_path, ctdb_nodes)
if isinstance(dest, str):
_logger.info("Will write nodes: %s", ctdb_nodes)
_save_nodes(dest, ctdb_nodes)
else:
write_nodes_file(dest, ctdb_nodes)


def _cluster_meta_to_ctdb_nodes(nodes: list[dict]) -> list[str]:
Expand Down Expand Up @@ -696,6 +704,42 @@ def check_nodestatus(cmd: samba_cmds.SambaCommand = samba_cmds.ctdb) -> None:
samba_cmds.execute(cmd_ctdb_check)


def _read_command_pnn(cmd: samba_cmds.SambaCommand) -> typing.Optional[int]:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not a big fan of this mixed int-or-None return value. I would prefer a more explicit typing; something like tuple[int, bool]. But I guess its just a matter of style.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, sorry but I find your suggestion less idiomatic. Hope you don't mind.

"""Run a ctdb command assuming it returns a pnn value. Return the pnn as an
int on success, None on command failure.
"""
try:
out = subprocess.check_output(list(cmd))
pnntxt = out.decode("utf8").strip()
except subprocess.CalledProcessError as err:
_logger.error(f"command {cmd!r} failed: {err!r}")
return None
except FileNotFoundError:
_logger.error(f"ctdb command ({cmd!r}) not found")
return None
try:
return int(pnntxt)
except ValueError:
_logger.debug(f"ctdb command wrote invalid pnn: {pnntxt!r}")
return None


def current_pnn() -> typing.Optional[int]:
"""Run the `ctdb pnn` command. Returns the pnn value or None if the command
fails.
"""
return _read_command_pnn(samba_cmds.ctdb["pnn"])


def leader_pnn() -> typing.Optional[int]:
"""Run the `ctdb leader` (or equivalent) command. Returns the pnn value or
None if the command fails.
"""
# recmaster command: <ctdb recmaster|leader>
admin_cmd = samba_cmds.ctdb_leader_admin_cmd()
return _read_command_pnn(samba_cmds.ctdb[admin_cmd])


class CLILeaderStatus:
_isleader = False

Expand All @@ -709,29 +753,10 @@ class CLILeaderLocator:
"""

def __enter__(self) -> CLILeaderStatus:
mypnn = recmaster = ""
# mypnn = <ctdb pnn>
pnn_cmd = samba_cmds.ctdb["pnn"]
try:
out = subprocess.check_output(list(pnn_cmd))
mypnn = out.decode("utf8").strip()
except subprocess.CalledProcessError as err:
_logger.error(f"command {pnn_cmd!r} failed: {err!r}")
except FileNotFoundError:
_logger.error(f"ctdb command ({pnn_cmd!r}) not found")
# recmaster = <ctdb recmaster|leader>
admin_cmd = samba_cmds.ctdb_leader_admin_cmd()
recmaster_cmd = samba_cmds.ctdb[admin_cmd]
try:
out = subprocess.check_output(list(recmaster_cmd))
recmaster = out.decode("utf8").strip()
except subprocess.CalledProcessError as err:
_logger.error(f"command {recmaster_cmd!r} failed: {err!r}")
except FileNotFoundError:
_logger.error(f"ctdb command ({recmaster_cmd!r}) not found")

mypnn = current_pnn()
leader = leader_pnn()
sts = CLILeaderStatus()
sts._isleader = bool(mypnn) and mypnn == recmaster
sts._isleader = mypnn is not None and mypnn == leader
return sts

def __exit__(
Expand Down
Loading