Skip to content
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

add support for custom global options in AD DC confgurations #85

Merged
merged 14 commits into from
Aug 23, 2023
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
28 changes: 24 additions & 4 deletions sambacc/addc.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ def provision(
admin_password: str,
dns_backend: typing.Optional[str] = None,
domain: typing.Optional[str] = None,
options: typing.Optional[typing.Iterable[tuple[str, str]]] = None,
) -> None:
# this function is a direct translation of a previous shell script
# as samba-tool is based on python libs, this function could possibly
Expand All @@ -43,6 +44,7 @@ def provision(
admin_password=admin_password,
dns_backend=dns_backend,
domain=domain,
options=options,
)
)
return
Expand All @@ -54,6 +56,7 @@ def join(
admin_password: str,
dns_backend: typing.Optional[str] = None,
domain: typing.Optional[str] = None,
options: typing.Optional[typing.Iterable[tuple[str, str]]] = None,
) -> None:
_logger.info(f"Joining AD domain: realm={realm}")
subprocess.check_call(
Expand All @@ -62,6 +65,7 @@ def join(
dcname,
admin_password=admin_password,
dns_backend=dns_backend,
options=options,
)
)

Expand Down Expand Up @@ -89,12 +93,21 @@ def add_group_members(group_name: str, members: list[str]) -> None:
subprocess.check_call(cmd)


def _filter_opts(
options: typing.Optional[typing.Iterable[tuple[str, str]]]
) -> list[tuple[str, str]]:
_skip_keys = ["netbios name"]
anoopcs9 marked this conversation as resolved.
Show resolved Hide resolved
options = options or []
return [(k, v) for (k, v) in options if k not in _skip_keys]


def _provision_cmd(
realm: str,
dcname: str,
admin_password: str,
dns_backend: typing.Optional[str] = None,
domain: typing.Optional[str] = None,
options: typing.Optional[typing.Iterable[tuple[str, str]]] = None,
) -> list[str]:
if not dns_backend:
dns_backend = "SAMBA_INTERNAL"
Expand All @@ -110,8 +123,11 @@ def _provision_cmd(
f"--realm={realm}",
f"--domain={domain}",
f"--adminpass={admin_password}",
].argv()
return cmd
]
cmd = cmd[
[f"--option={okey}={oval}" for okey, oval in _filter_opts(options)]
]
return cmd.argv()


def _join_cmd(
Expand All @@ -120,6 +136,7 @@ def _join_cmd(
admin_password: str,
dns_backend: typing.Optional[str] = None,
domain: typing.Optional[str] = None,
options: typing.Optional[typing.Iterable[tuple[str, str]]] = None,
) -> list[str]:
if not dns_backend:
dns_backend = "SAMBA_INTERNAL"
Expand All @@ -134,8 +151,11 @@ def _join_cmd(
f"--option=netbios name={dcname}",
f"--dns-backend={dns_backend}",
f"--password={admin_password}",
].argv()
return cmd
]
cmd = cmd[
[f"--option={okey}={oval}" for okey, oval in _filter_opts(options)]
]
return cmd.argv()


def _user_create_cmd(
Expand Down
27 changes: 27 additions & 0 deletions sambacc/commands/addc.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,12 @@
import logging
import os
import shutil
import typing

from sambacc import addc
from sambacc import samba_cmds
from sambacc import smbconf_api
from sambacc import smbconf_samba

from .cli import best_waiter, CommandBuilder, Context, Fail

Expand Down Expand Up @@ -86,7 +89,9 @@ def _prep_provision(ctx: Context) -> None:
domain=domconfig.short_domain,
dcname=dcname,
admin_password=domconfig.admin_password,
options=ctx.instance_config.global_options(),
)
_merge_config(_provisioned, ctx.instance_config.global_options())


def _prep_join(ctx: Context) -> None:
Expand All @@ -102,7 +107,29 @@ def _prep_join(ctx: Context) -> None:
domain=domconfig.short_domain,
dcname=dcname,
admin_password=domconfig.admin_password,
options=ctx.instance_config.global_options(),
)
_merge_config(_provisioned, ctx.instance_config.global_options())


def _merge_config(
smb_conf_path: str,
phlogistonjohn marked this conversation as resolved.
Show resolved Hide resolved
options: typing.Optional[typing.Iterable[tuple[str, str]]] = None,
) -> None:
if not options:
return
txt_conf = smbconf_samba.SMBConf.from_file(smb_conf_path)
tmp_conf = smbconf_api.SimpleConfigStore()
tmp_conf.import_smbconf(txt_conf)
global_section = dict(tmp_conf["global"])
global_section.update(options)
tmp_conf["global"] = list(global_section.items())
try:
os.rename(smb_conf_path, f"{smb_conf_path}.orig")
except OSError:
pass
with open(smb_conf_path, "w") as fh:
smbconf_api.write_store_as_smb_conf(fh, tmp_conf)


def _prep_wait_on_domain(ctx: Context) -> None:
Expand Down
3 changes: 0 additions & 3 deletions sambacc/commands/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,6 @@ class Fail(ValueError):


class Parser(typing.Protocol):
def frank(self, x: str) -> str:
... # pragma: no cover

def set_defaults(self, **kwargs: typing.Any) -> None:
... # pragma: no cover

Expand Down
6 changes: 5 additions & 1 deletion sambacc/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -237,7 +237,11 @@ def __init__(self, conf: GlobalConfig, iconfig: dict):
def global_options(self) -> typing.Iterable[typing.Tuple[str, str]]:
"""Iterate over global options."""
# Pull in all global sections that apply
gnames = self.iconfig["globals"]
try:
gnames = self.iconfig["globals"]
except KeyError:
# no globals section in the instance means no global options
return
synarete marked this conversation as resolved.
Show resolved Hide resolved
for gname in gnames:
global_section = self.gconfig.data["globals"][gname]
for k, v in global_section.get("options", {}).items():
Expand Down
73 changes: 73 additions & 0 deletions sambacc/smbconf_api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
#
# sambacc: a samba container configuration tool
# Copyright (C) 2023 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 typing


class ConfigStore(typing.Protocol):
def __getitem__(self, name: str) -> list[tuple[str, str]]:
... # pragma: no cover

def __setitem__(self, name: str, value: list[tuple[str, str]]) -> None:
... # pragma: no cover

def __iter__(self) -> typing.Iterator[str]:
... # pragma: no cover


class SimpleConfigStore:
def __init__(self) -> None:
self._data: dict[str, list[tuple[str, str]]] = {}

@property
def writeable(self) -> bool:
"""True if using a read-write backend."""
return True

def __getitem__(self, name: str) -> list[tuple[str, str]]:
return self._data[name]

def __setitem__(self, name: str, value: list[tuple[str, str]]) -> None:
self._data[name] = value

def __iter__(self) -> typing.Iterator[str]:
return iter(self._data.keys())

def import_smbconf(
self, src: ConfigStore, batch_size: typing.Optional[int] = None
) -> None:
"""Import content from one SMBConf configuration object into the
current SMBConf configuration object.

batch_size is ignored.
"""
for sname in src:
self[sname] = src[sname]


def write_store_as_smb_conf(out: typing.IO, conf: ConfigStore) -> None:
"""Write the configuration store in smb.conf format to `out`."""
# unfortunately, AFAIK, there's no way for an smbconf to write
# into a an smb.conf/ini style file. We have to do it on our own.
# ---
# Make sure global section comes first.
sections = sorted(conf, key=lambda v: 0 if v == "global" else 1)
for sname in sections:
out.write(str("\n[{}]\n".format(sname)))
for skey, sval in conf[sname]:
out.write(str(f"\t{skey} = {sval}\n"))
148 changes: 148 additions & 0 deletions sambacc/smbconf_samba.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
#
# sambacc: a samba container configuration tool
# Copyright (C) 2023 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 types
import importlib
import typing
import itertools

from sambacc.smbconf_api import ConfigStore


def _smbconf() -> types.ModuleType:
return importlib.import_module("samba.smbconf")


def _s3smbconf() -> types.ModuleType:
return importlib.import_module("samba.samba3.smbconf")


def _s3param() -> types.ModuleType:
return importlib.import_module("samba.samba3.param")


if sys.version_info >= (3, 11):
from typing import Self as _Self
else:
_Self = typing.TypeVar("_Self", bound="SMBConf")


class SMBConf:
"""SMBConf wraps the samba smbconf library, supporting reading from and,
when possible, writing to samba configuration backends. The SMBConf type
supports transactions using the context managager interface. The SMBConf
type can read and write configuration based on dictionary-like access,
using shares as the keys. The global configuration is treated like a
special "share".
"""

def __init__(self, smbconf: typing.Any) -> None:
self._smbconf = smbconf

@classmethod
def from_file(cls: typing.Type[_Self], path: str) -> _Self:
"""Open a smb.conf style configuration from the specified path."""
return cls(_smbconf().init_txt(path))

@classmethod
def from_registry(
cls: typing.Type[_Self],
configfile: str = "/etc/samba/smb.conf",
key: typing.Optional[str] = None,
) -> _Self:
"""Open samba's registry backend for configuration parameters."""
s3_lp = _s3param().get_context()
s3_lp.load(configfile)
return cls(_s3smbconf().init_reg(key))

@property
def writeable(self) -> bool:
"""True if using a read-write backend."""
return self._smbconf.is_writeable()

# the extraneous `self: _Self` type makes mypy on python <3.11 happy.
# otherwise it complains: `A function returning TypeVar should receive at
# least one argument containing the same TypeVar`
def __enter__(self: _Self) -> _Self:
self._smbconf.transaction_start()
return self

def __exit__(
self, exc_type: typing.Any, exc_value: typing.Any, tb: typing.Any
) -> None:
if exc_type is None:
self._smbconf.transaction_commit()
return
return self._smbconf.transaction_cancel()

def __getitem__(self, name: str) -> list[tuple[str, str]]:
try:
n2, values = self._smbconf.get_share(name)
except _smbconf().SMBConfError as err:
if err.error_code == _smbconf().SBC_ERR_NO_SUCH_SERVICE:
raise KeyError(name)
raise
if name != n2:
raise ValueError(f"section name invalid: {name!r} != {n2!r}")
return values

def __setitem__(self, name: str, value: list[tuple[str, str]]) -> None:
try:
self._smbconf.delete_share(name)
except _smbconf().SMBConfError as err:
if err.error_code != _smbconf().SBC_ERR_NO_SUCH_SERVICE:
raise
self._smbconf.create_set_share(name, value)

def __iter__(self) -> typing.Iterator[str]:
return iter(self._smbconf.share_names())

def import_smbconf(
self, src: ConfigStore, batch_size: typing.Optional[int] = 100
) -> None:
"""Import content from one SMBConf configuration object into the
current SMBConf configuration object.

Set batch_size to the maximum number of "shares" to import in one
transaction. Set batch_size to None to use only one transaction.
"""
if not self.writeable:
raise ValueError("SMBConf is not writable")
if batch_size is None:
return self._import_smbconf_all(src)
return self._import_smbconf_batched(src, batch_size)

def _import_smbconf_all(self, src: ConfigStore) -> None:
with self:
for sname in src:
self[sname] = src[sname]

def _import_smbconf_batched(
self, src: ConfigStore, batch_size: int
) -> None:
# based on a comment in samba's source code for the net command
# only import N 'shares' at a time so that the transaction does
# not exceed talloc memory limits
def _batch_keyfunc(item: tuple[int, str]) -> int:
return item[0] // batch_size

for _, snames in itertools.groupby(enumerate(src), _batch_keyfunc):
with self:
for _, sname in snames:
self[sname] = src[sname]
2 changes: 1 addition & 1 deletion tests/container/Containerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
ARG SAMBACC_BASE_IMAGE='registry.fedoraproject.org/fedora:36'
ARG SAMBACC_BASE_IMAGE='registry.fedoraproject.org/fedora:37'
FROM $SAMBACC_BASE_IMAGE


Expand Down
Loading
Loading