Skip to content
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
3 changes: 3 additions & 0 deletions extras/python-sambacc.spec
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ Requires: python3-pyxattr
Recommends: %{name}+toml
Recommends: %{name}+yaml
Recommends: %{name}+rados
Recommends: %{name}+grpc
%endif
%if 0%{?fedora} >= 37
Recommends: %{name}+validation
Expand Down Expand Up @@ -78,13 +79,15 @@ Recommends: %{name}+validation
%doc README.*
%{_bindir}/samba-container
%{_bindir}/samba-dc-container
%{_bindir}/samba-remote-control
%{_datadir}/%{bname}/examples/


%pyproject_extras_subpkg -n python3-%{bname} validation
%pyproject_extras_subpkg -n python3-%{bname} toml
%pyproject_extras_subpkg -n python3-%{bname} yaml
%pyproject_extras_subpkg -n python3-%{bname} rados
%pyproject_extras_subpkg -n python3-%{bname} grpc


%changelog
Expand Down
5 changes: 5 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,8 @@ disallow_untyped_defs = false
[[tool.mypy.overrides]]
module = "sambacc.schema.*"
disallow_untyped_defs = false

[[tool.mypy.overrides]]
module = "sambacc.grpc.generated.*"
disallow_untyped_defs = false
ignore_errors = true
Empty file.
61 changes: 61 additions & 0 deletions sambacc/commands/remotecontrol/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
#
# sambacc: a samba container configuration tool
# Copyright (C) 2025 John Mulligan
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>
#

import sys
import typing


from .. import skips
from ..cli import Context, Fail, commands
from ..common import (
CommandContext,
enable_logging,
env_to_cli,
global_args,
pre_action,
)


def _default(ctx: Context) -> None:
sys.stdout.write(f"{sys.argv[0]} requires a subcommand, like 'serve'.\n")
sys.exit(1)


def main(args: typing.Optional[typing.Sequence[str]] = None) -> None:
pkg = "sambacc.commands.remotecontrol"
commands.include(".server", package=pkg)

cli = commands.assemble(arg_func=global_args).parse_args(args)
env_to_cli(cli)
enable_logging(cli)
if not cli.identity:
raise Fail("missing container identity")

pre_action(cli)
ctx = CommandContext(cli)
skip = skips.test(ctx)
if skip:
print(f"Command Skipped: {skip}")
return
cfunc = getattr(cli, "cfunc", _default)
cfunc(ctx)
return


if __name__ == "__main__":
main()
129 changes: 129 additions & 0 deletions sambacc/commands/remotecontrol/server.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
#
# sambacc: a samba container configuration tool
# Copyright (C) 2025 John Mulligan
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>
#

import argparse
import logging
import signal
import sys
import typing

from ..cli import Context, Fail, commands

_logger = logging.getLogger(__name__)
_MTLS = "mtls"
_FORCE = "force"


def _serve_args(parser: argparse.ArgumentParser) -> None:
parser.add_argument(
"--address",
"-a",
help="Specify an {address:port} value to bind to.",
)
# Force an explicit choice of (the only) rpc type in order to clearly
# prepare the space for possible alternatives
egroup = parser.add_mutually_exclusive_group(required=True)
egroup.add_argument(
"--grpc",
dest="rpc_type",
action="store_const",
default="grpc",
const="grpc",
help="Use gRPC",
)
# security settings
parser.add_argument(
"--insecure",
action="store_true",
help="Disable TLS",
)
parser.add_argument(
"--allow-modify",
choices=(_MTLS, _FORCE),
default=_MTLS,
help="Control modification mode",
)
parser.add_argument(
"--tls-key",
help="Server TLS Key",
)
parser.add_argument(
"--tls-cert",
help="Server TLS Certificate",
)
parser.add_argument(
"--tls-ca-cert",
help="CA Certificate",
)


class Restart(Exception):
pass


@commands.command(name="serve", arg_func=_serve_args)
def serve(ctx: Context) -> None:
"""Start an RPC server."""

def _handler(*args: typing.Any) -> None:
raise Restart()

signal.signal(signal.SIGHUP, _handler)
while True:
try:
_serve(ctx)
return
except KeyboardInterrupt:
_logger.info("Exiting")
sys.exit(0)
except Restart:
_logger.info("Re-starting server")
continue


def _serve(ctx: Context) -> None:
import sambacc.grpc.backend
import sambacc.grpc.server

config = sambacc.grpc.server.ServerConfig()
config.insecure = bool(ctx.cli.insecure)
if ctx.cli.address:
config.address = ctx.cli.address
if not (ctx.cli.insecure or ctx.cli.tls_key):
raise Fail("Specify --tls-key=... or --insecure")
if not (ctx.cli.insecure or ctx.cli.tls_cert):
raise Fail("Specify --tls-cert=... or --insecure")
if ctx.cli.tls_key:
config.server_key = _read(ctx, ctx.cli.tls_key)
if ctx.cli.tls_cert:
config.server_cert = _read(ctx, ctx.cli.tls_cert)
if ctx.cli.tls_ca_cert:
config.ca_cert = _read(ctx, ctx.cli.tls_ca_cert)
config.read_only = not (
ctx.cli.allow_modify == _FORCE
or (not config.insecure and config.ca_cert)
)

backend = sambacc.grpc.backend.ControlBackend(ctx.instance_config)
sambacc.grpc.server.serve(config, backend)


def _read(ctx: Context, path_or_url: str) -> bytes:
with ctx.opener.open(path_or_url) as fh:
content = fh.read()
return content if isinstance(content, bytes) else content.encode()
Empty file added sambacc/grpc/__init__.py
Empty file.
161 changes: 161 additions & 0 deletions sambacc/grpc/backend.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
#
# sambacc: a samba container configuration tool (and more)
# Copyright (C) 2025 John Mulligan
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>
#

from typing import Any, Union

import dataclasses
import json
import os
import subprocess

from sambacc.typelets import Self
import sambacc.config
import sambacc.samba_cmds


@dataclasses.dataclass
class Versions:
samba_version: str = "foo"
sambacc_version: str = "bar"
container_version: str = "baz"


@dataclasses.dataclass
class Session:
session_id: str
username: str
groupname: str
remote_machine: str
hostname: str
session_dialect: str
uid: int
gid: int

@classmethod
def load(cls, json_object: dict[str, Any]) -> Self:
return cls(
session_id=json_object.get("session_id", ""),
username=json_object.get("username", ""),
groupname=json_object.get("groupname", ""),
remote_machine=json_object.get("remote_machine", ""),
hostname=json_object.get("hostname", ""),
session_dialect=json_object.get("session_dialect", ""),
uid=int(json_object.get("uid", -1)),
gid=int(json_object.get("gid", -1)),
)


@dataclasses.dataclass
class TreeConnection:
tcon_id: str
session_id: str
service_name: str

@classmethod
def load(cls, json_object: dict[str, Any]) -> Self:
return cls(
tcon_id=json_object.get("tcon_id", ""),
session_id=json_object.get("session_id", ""),
service_name=json_object.get("service", ""),
)


@dataclasses.dataclass
class Status:
timestamp: str
version: str
sessions: list[Session]
tcons: list[TreeConnection]

@classmethod
def load(cls, json_object: dict[str, Any]) -> Self:
return cls(
timestamp=json_object.get("timestamp", ""),
version=json_object.get("version", ""),
sessions=[
Session.load(v)
for _, v in json_object.get("sessions", {}).items()
],
tcons=[
TreeConnection.load(v)
for _, v in json_object.get("tcons", {}).items()
],
)

@classmethod
def parse(cls, txt: Union[str, bytes]) -> Self:
return cls.load(json.loads(txt))


class ControlBackend:
def __init__(self, config: sambacc.config.InstanceConfig) -> None:
self._config = config

def _samba_version(self) -> str:
smbd_ver = sambacc.samba_cmds.smbd["--version"]
res = subprocess.run(list(smbd_ver), check=True, capture_output=True)
return res.stdout.decode().strip()

def _sambacc_version(self) -> str:
try:
import sambacc._version

return sambacc._version.version
except ImportError:
return "(unknown)"

def _container_version(self) -> str:
return os.environ.get("SAMBA_CONTAINER_VERSION", "(unknown)")

def get_versions(self) -> Versions:
versions = Versions()
versions.samba_version = self._samba_version()
versions.sambacc_version = self._sambacc_version()
versions.container_version = self._container_version()
return versions

def is_clustered(self) -> bool:
return self._config.with_ctdb

def get_status(self) -> Status:
smbstatus = sambacc.samba_cmds.smbstatus["--json"]
proc = subprocess.Popen(
list(smbstatus),
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)
# TODO: the json output of smbstatus is potentially large
# investigate streaming reads instead of fully buffered read
# later
stdout, stderr = proc.communicate()
if proc.returncode != 0:
raise RuntimeError(
f"smbstatus error: {proc.returncode}: {stderr!r}"
)
return Status.parse(stdout)

def close_share(self, share_name: str, denied_users: bool) -> None:
_close = "close-denied-share" if denied_users else "close-share"
cmd = sambacc.samba_cmds.smbcontrol["smbd", _close, share_name]
subprocess.run(list(cmd), check=True)

def kill_client(self, ip_address: str) -> None:
cmd = sambacc.samba_cmds.smbcontrol[
"smbd", "kill-client-ip", ip_address
]
subprocess.run(list(cmd), check=True)
Empty file.
Loading
Loading