-
Notifications
You must be signed in to change notification settings - Fork 17
remote control: add initial remote control gRPC server #154
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
mergify
merged 16 commits into
samba-in-kubernetes:master
from
phlogistonjohn:jjm-grpc-begin
May 29, 2025
Merged
Changes from all commits
Commits
Show all changes
16 commits
Select commit
Hold shift + click to select a range
5904cad
sambacc: add smbstatus command to samba_cmds.py
phlogistonjohn aaf9fe0
tox.ini: exclude files that grpc toolchain will generate
phlogistonjohn e4da352
pyproject.toml: exclude generated files from mypy check
phlogistonjohn 2d76c7f
tox.ini: support mypy checking for files using grpc libs
phlogistonjohn fa25587
grpc: establish a location for grpc related code and files
phlogistonjohn af6c916
grpc/protobufs: add initial .proto file
phlogistonjohn 0938579
tox.ini: add new environments for generating grpc related files
phlogistonjohn fb51e26
grpc/generated: add generated grpc files
phlogistonjohn ee1fe84
grpc: add backend.py for interfacing with samba commands
phlogistonjohn dd4b62e
grpc: add server.py for managing the grpc server
phlogistonjohn 95d45b3
tests: add test_grpc_server.py to test grpc server behavior
phlogistonjohn cfdc905
tests: add test_grpc_backend.py to test server backend module
phlogistonjohn ecd693e
commands/remotecontrol: create remote control command
phlogistonjohn 369c259
setup.cfg: add new modules, entrypoint, extras
phlogistonjohn f631214
tox.ini: include grpc libs for unit tests
phlogistonjohn a5a7bcc
extras: update python-sambacc.spec for new grpc extra
phlogistonjohn File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or 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
This file contains hidden or 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
Empty file.
This file contains hidden or 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,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() |
This file contains hidden or 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,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.
This file contains hidden or 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,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.
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.