Skip to content

Commit 62741c9

Browse files
phlogistonjohnmergify[bot]
authored andcommitted
sambacc: add smbconf_samba module to wrap samba smbconf module
Add smbconf_samba.py a module that builds a somewhat more pythonic and convenient interface around the SMBConf class and functions provided by samba python libraries. Note that the import of samba modules is deferred until the class needs them so that we can import smbconf_samba without raising an ImportError even if samba modules are unavailable. Signed-off-by: John Mulligan <[email protected]>
1 parent d97636e commit 62741c9

File tree

1 file changed

+148
-0
lines changed

1 file changed

+148
-0
lines changed

sambacc/smbconf_samba.py

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
#
2+
# sambacc: a samba container configuration tool
3+
# Copyright (C) 2023 John Mulligan
4+
#
5+
# This program is free software: you can redistribute it and/or modify
6+
# it under the terms of the GNU General Public License as published by
7+
# the Free Software Foundation, either version 3 of the License, or
8+
# (at your option) any later version.
9+
#
10+
# This program is distributed in the hope that it will be useful,
11+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
# GNU General Public License for more details.
14+
#
15+
# You should have received a copy of the GNU General Public License
16+
# along with this program. If not, see <http://www.gnu.org/licenses/>
17+
#
18+
19+
import sys
20+
import types
21+
import importlib
22+
import typing
23+
import itertools
24+
25+
from sambacc.smbconf_api import ConfigStore
26+
27+
28+
def _smbconf() -> types.ModuleType:
29+
return importlib.import_module("samba.smbconf")
30+
31+
32+
def _s3smbconf() -> types.ModuleType:
33+
return importlib.import_module("samba.samba3.smbconf")
34+
35+
36+
def _s3param() -> types.ModuleType:
37+
return importlib.import_module("samba.samba3.param")
38+
39+
40+
if sys.version_info >= (3, 11):
41+
from typing import Self as _Self
42+
else:
43+
_Self = typing.TypeVar("_Self", bound="SMBConf")
44+
45+
46+
class SMBConf:
47+
"""SMBConf wraps the samba smbconf library, supporting reading from and,
48+
when possible, writing to samba configuration backends. The SMBConf type
49+
supports transactions using the context managager interface. The SMBConf
50+
type can read and write configuration based on dictionary-like access,
51+
using shares as the keys. The global configuration is treated like a
52+
special "share".
53+
"""
54+
55+
def __init__(self, smbconf: typing.Any) -> None:
56+
self._smbconf = smbconf
57+
58+
@classmethod
59+
def from_file(cls: typing.Type[_Self], path: str) -> _Self:
60+
"""Open a smb.conf style configuration from the specified path."""
61+
return cls(_smbconf().init_txt(path))
62+
63+
@classmethod
64+
def from_registry(
65+
cls: typing.Type[_Self],
66+
configfile: str = "/etc/samba/smb.conf",
67+
key: typing.Optional[str] = None,
68+
) -> _Self:
69+
"""Open samba's registry backend for configuration parameters."""
70+
s3_lp = _s3param().get_context()
71+
s3_lp.load(configfile)
72+
return cls(_s3smbconf().init_reg(key))
73+
74+
@property
75+
def writeable(self) -> bool:
76+
"""True if using a read-write backend."""
77+
return self._smbconf.is_writeable()
78+
79+
# the extraneous `self: _Self` type makes mypy on python <3.11 happy.
80+
# otherwise it complains: `A function returning TypeVar should receive at
81+
# least one argument containing the same TypeVar`
82+
def __enter__(self: _Self) -> _Self:
83+
self._smbconf.transaction_start()
84+
return self
85+
86+
def __exit__(
87+
self, exc_type: typing.Any, exc_value: typing.Any, tb: typing.Any
88+
) -> None:
89+
if exc_type is None:
90+
self._smbconf.transaction_commit()
91+
return
92+
return self._smbconf.transaction_cancel()
93+
94+
def __getitem__(self, name: str) -> list[tuple[str, str]]:
95+
try:
96+
n2, values = self._smbconf.get_share(name)
97+
except _smbconf().SMBConfError as err:
98+
if err.error_code == _smbconf().SBC_ERR_NO_SUCH_SERVICE:
99+
raise KeyError(name)
100+
raise
101+
if name != n2:
102+
raise ValueError(f"section name invalid: {name!r} != {n2!r}")
103+
return values
104+
105+
def __setitem__(self, name: str, value: list[tuple[str, str]]) -> None:
106+
try:
107+
self._smbconf.delete_share(name)
108+
except _smbconf().SMBConfError as err:
109+
if err.error_code != _smbconf().SBC_ERR_NO_SUCH_SERVICE:
110+
raise
111+
self._smbconf.create_set_share(name, value)
112+
113+
def __iter__(self) -> typing.Iterator[str]:
114+
return iter(self._smbconf.share_names())
115+
116+
def import_smbconf(
117+
self, src: ConfigStore, batch_size: typing.Optional[int] = 100
118+
) -> None:
119+
"""Import content from one SMBConf configuration object into the
120+
current SMBConf configuration object.
121+
122+
Set batch_size to the maximum number of "shares" to import in one
123+
transaction. Set batch_size to None to use only one transaction.
124+
"""
125+
if not self.writeable:
126+
raise ValueError("SMBConf is not writable")
127+
if batch_size is None:
128+
return self._import_smbconf_all(src)
129+
return self._import_smbconf_batched(src, batch_size)
130+
131+
def _import_smbconf_all(self, src: ConfigStore) -> None:
132+
with self:
133+
for sname in src:
134+
self[sname] = src[sname]
135+
136+
def _import_smbconf_batched(
137+
self, src: ConfigStore, batch_size: int
138+
) -> None:
139+
# based on a comment in samba's source code for the net command
140+
# only import N 'shares' at a time so that the transaction does
141+
# not exceed talloc memory limits
142+
def _batch_keyfunc(item: tuple[int, str]) -> int:
143+
return item[0] // batch_size
144+
145+
for _, snames in itertools.groupby(enumerate(src), _batch_keyfunc):
146+
with self:
147+
for _, sname in snames:
148+
self[sname] = src[sname]

0 commit comments

Comments
 (0)