Skip to content

Commit b8e6e76

Browse files
phlogistonjohnmergify[bot]
authored andcommitted
sambacc: extend rados opener to support passing client name
Add an intermediate type, _RADOSInterface, to act as a shim between the opener type and rados module api. This way we are able to bundle values like the client name so we don't need to pass everything through all the intermediate layers. Signed-off-by: John Mulligan <[email protected]>
1 parent 736833f commit b8e6e76

File tree

2 files changed

+92
-16
lines changed

2 files changed

+92
-16
lines changed

sambacc/rados_opener.py

Lines changed: 41 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
from .typelets import ExcType, ExcValue, ExcTraceback
2828

2929
_RADOSModule = typing.Any
30+
_RADOSObject = typing.Any
3031

3132
_CHUNK_SIZE = 4 * 1024
3233

@@ -35,43 +36,60 @@ class RADOSUnsupported(Exception):
3536
pass
3637

3738

39+
class _RADOSInterface:
40+
api: _RADOSModule
41+
client_name: str
42+
full_name: bool
43+
44+
def Rados(self) -> _RADOSObject:
45+
name = rados_id = ""
46+
if self.full_name:
47+
name = self.client_name
48+
else:
49+
rados_id = self.client_name
50+
return self.api.Rados(
51+
name=name,
52+
rados_id=rados_id,
53+
conffile=self.api.Rados.DEFAULT_CONF_FILES,
54+
)
55+
56+
3857
class _RADOSHandler(urllib.request.BaseHandler):
39-
_rados_api: typing.Optional[_RADOSModule] = None
58+
_interface: typing.Optional[_RADOSInterface] = None
4059

4160
def rados_open(self, req: urllib.request.Request) -> typing.IO:
42-
if self._rados_api is None:
61+
if self._interface is None:
4362
raise RADOSUnsupported()
4463
if req.selector.startswith("mon-config-key:"):
4564
return _get_mon_config_key(
46-
self._rados_api, req.selector.split(":", 1)[1]
65+
self._interface, req.selector.split(":", 1)[1]
4766
)
4867
sel = req.selector.lstrip("/")
4968
if req.host:
5069
pool = req.host
5170
ns, key = sel.split("/", 1)
5271
else:
5372
pool, ns, key = sel.split("/", 2)
54-
return _RADOSResponse(self._rados_api, pool, ns, key)
73+
return _RADOSResponse(self._interface, pool, ns, key)
5574

5675

5776
# it's quite annoying to have a read-only typing.IO we're forced to
5877
# have so many stub methods. Go's much more granular io interfaces for
5978
# readers/writers is much nicer for this.
6079
class _RADOSResponse(typing.IO):
6180
def __init__(
62-
self, rados_api: _RADOSModule, pool: str, ns: str, key: str
81+
self, interface: _RADOSInterface, pool: str, ns: str, key: str
6382
) -> None:
6483
self._pool = pool
6584
self._ns = ns
6685
self._key = key
6786

68-
self._open(rados_api)
87+
self._open(interface)
6988
self._test()
7089

71-
def _open(self, rados_api: _RADOSModule) -> None:
90+
def _open(self, interface: _RADOSInterface) -> None:
7291
# TODO: connection caching
73-
self._conn = rados_api.Rados()
74-
self._conn.conf_read_file()
92+
self._conn = interface.Rados()
7593
self._conn.connect()
7694
self._connected = True
7795
self._ioctx = self._conn.open_ioctx(self._pool)
@@ -177,14 +195,14 @@ def writelines(self, ls: typing.Iterable[typing.Any]) -> None:
177195
raise NotImplementedError()
178196

179197

180-
def _get_mon_config_key(rados_api: _RADOSModule, key: str) -> io.BytesIO:
198+
def _get_mon_config_key(interface: _RADOSInterface, key: str) -> io.BytesIO:
181199
mcmd = json.dumps(
182200
{
183201
"prefix": "config-key get",
184202
"key": str(key),
185203
}
186204
)
187-
with rados_api.Rados(conffile=rados_api.Rados.DEFAULT_CONF_FILES) as rc:
205+
with interface.Rados() as rc:
188206
ret, out, err = rc.mon_command(mcmd, b"")
189207
if ret == 0:
190208
# We need to return a file like object. Since we are handed just
@@ -196,7 +214,12 @@ def _get_mon_config_key(rados_api: _RADOSModule, key: str) -> io.BytesIO:
196214
raise OSError(ret, msg)
197215

198216

199-
def enable_rados_url_opener(cls: typing.Type[url_opener.URLOpener]) -> None:
217+
def enable_rados_url_opener(
218+
cls: typing.Type[url_opener.URLOpener],
219+
*,
220+
client_name: str = "",
221+
full_name: bool = False,
222+
) -> None:
200223
"""Extend the URLOpener type to support pseudo-URLs for rados
201224
object storage. If rados libraries are not found the function
202225
does nothing.
@@ -211,5 +234,10 @@ def enable_rados_url_opener(cls: typing.Type[url_opener.URLOpener]) -> None:
211234
except ImportError:
212235
return
213236

214-
_RADOSHandler._rados_api = rados
237+
rados_interface = _RADOSInterface()
238+
rados_interface.api = rados
239+
rados_interface.client_name = client_name
240+
rados_interface.full_name = full_name
241+
242+
_RADOSHandler._interface = rados_interface
215243
cls._handlers.append(_RADOSHandler)

tests/test_rados_opener.py

Lines changed: 51 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,54 @@ def test_enable_rados_url_opener_fail(monkeypatch):
4545
assert not cls_mock._handlers.append.called
4646

4747

48+
def test_enable_rados_url_opener_with_args(monkeypatch):
49+
mock = unittest.mock.MagicMock()
50+
monkeypatch.setitem(sys.modules, "rados", mock)
51+
52+
cls_mock = unittest.mock.MagicMock()
53+
cls_mock._handlers = []
54+
sambacc.rados_opener.enable_rados_url_opener(cls_mock, client_name="user1")
55+
assert len(cls_mock._handlers) == 1
56+
assert isinstance(
57+
cls_mock._handlers[0]._interface, sambacc.rados_opener._RADOSInterface
58+
)
59+
assert cls_mock._handlers[0]._interface.api is mock
60+
assert cls_mock._handlers[0]._interface.client_name == "user1"
61+
assert not cls_mock._handlers[0]._interface.full_name
62+
ri = cls_mock._handlers[0]._interface
63+
ri.Rados()
64+
assert ri.api.Rados.call_args[1]["rados_id"] == "user1"
65+
assert ri.api.Rados.call_args[1]["name"] == ""
66+
assert (
67+
ri.api.Rados.call_args[1]["conffile"] == mock.Rados.DEFAULT_CONF_FILES
68+
)
69+
70+
71+
def test_enable_rados_url_opener_with_args2(monkeypatch):
72+
mock = unittest.mock.MagicMock()
73+
monkeypatch.setitem(sys.modules, "rados", mock)
74+
75+
cls_mock = unittest.mock.MagicMock()
76+
cls_mock._handlers = []
77+
sambacc.rados_opener.enable_rados_url_opener(
78+
cls_mock, client_name="client.user1", full_name=True
79+
)
80+
assert len(cls_mock._handlers) == 1
81+
assert isinstance(
82+
cls_mock._handlers[0]._interface, sambacc.rados_opener._RADOSInterface
83+
)
84+
assert cls_mock._handlers[0]._interface.api is mock
85+
assert cls_mock._handlers[0]._interface.client_name == "client.user1"
86+
assert cls_mock._handlers[0]._interface.full_name
87+
ri = cls_mock._handlers[0]._interface
88+
ri.Rados()
89+
assert ri.api.Rados.call_args[1]["rados_id"] == ""
90+
assert ri.api.Rados.call_args[1]["name"] == "client.user1"
91+
assert (
92+
ri.api.Rados.call_args[1]["conffile"] == mock.Rados.DEFAULT_CONF_FILES
93+
)
94+
95+
4896
def test_rados_handler_parse():
4997
class RH(sambacc.rados_opener._RADOSHandler):
5098
_rados_api = unittest.mock.MagicMock()
@@ -67,7 +115,7 @@ def test_rados_handler_norados():
67115
# Generally, this shouldn't happen because the rados handler shouldn't
68116
# be added to the URLOpener if rados module was unavailable.
69117
class RH(sambacc.rados_opener._RADOSHandler):
70-
_rados_api = None
118+
_interface = None
71119

72120
rh = RH()
73121
rq = urllib.request.Request("rados://foo/bar/baz")
@@ -160,9 +208,9 @@ def test_rados_response_not_implemented():
160208

161209
def test_rados_handler_config_key():
162210
class RH(sambacc.rados_opener._RADOSHandler):
163-
_rados_api = unittest.mock.MagicMock()
211+
_interface = unittest.mock.MagicMock()
164212

165-
mc = RH._rados_api.Rados.return_value.__enter__.return_value.mon_command
213+
mc = RH._interface.Rados.return_value.__enter__.return_value.mon_command
166214
mc.return_value = (0, b"rubber baby buggy bumpers", "")
167215

168216
rh = RH()

0 commit comments

Comments
 (0)