Skip to content

Commit 1a4b610

Browse files
sambacc: add optional rados support to url opener
Add a function to conditionally enable RADOS support for the URLOpener. In the future, this will allow sambacc configuration to be stored as a RADOS object, similar to how NFS-Ganesha allows configuration to be stored in RADOS, but without any direct changes to Samba. Signed-off-by: John Mulligan <[email protected]>
1 parent 455f342 commit 1a4b610

File tree

1 file changed

+190
-0
lines changed

1 file changed

+190
-0
lines changed

sambacc/rados_opener.py

Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
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+
from __future__ import annotations
20+
21+
import typing
22+
import urllib.request
23+
24+
from . import url_opener
25+
from .typelets import ExcType, ExcValue, ExcTraceback
26+
27+
_RADOSModule = typing.Any
28+
29+
_CHUNK_SIZE = 4 * 1024
30+
31+
32+
class RADOSUnsupported(Exception):
33+
pass
34+
35+
36+
class _RADOSHandler(urllib.request.BaseHandler):
37+
_rados_api: typing.Optional[_RADOSModule] = None
38+
39+
def rados_open(self, req: urllib.request.Request) -> typing.IO:
40+
if self._rados_api is None:
41+
raise RADOSUnsupported()
42+
sel = req.selector.lstrip("/")
43+
if req.host:
44+
pool = req.host
45+
ns, key = sel.split("/", 1)
46+
else:
47+
pool, ns, key = sel.split("/", 2)
48+
return _RADOSResponse(self._rados_api, pool, ns, key)
49+
50+
51+
# it's quite annoying to have a read-only typing.IO we're forced to
52+
# have so many stub methods. Go's much more granular io interfaces for
53+
# readers/writers is much nicer for this.
54+
class _RADOSResponse(typing.IO):
55+
def __init__(
56+
self, rados_api: _RADOSModule, pool: str, ns: str, key: str
57+
) -> None:
58+
self._pool = pool
59+
self._ns = ns
60+
self._key = key
61+
62+
self._open(rados_api)
63+
self._test()
64+
65+
def _open(self, rados_api: _RADOSModule) -> None:
66+
# TODO: connection caching
67+
self._conn = rados_api.Rados()
68+
self._conn.conf_read_file()
69+
self._conn.connect()
70+
self._connected = True
71+
self._ioctx = self._conn.open_ioctx(self._pool)
72+
self._ioctx.set_namespace(self._ns)
73+
self._closed = False
74+
self._offset = 0
75+
76+
def _test(self) -> None:
77+
self._ioctx.stat(self._key)
78+
79+
def read(self, size: typing.Optional[int] = None) -> bytes:
80+
if self._closed:
81+
raise ValueError("can not read from closed response")
82+
return self._read_all() if size is None else self._read(size)
83+
84+
def _read_all(self) -> bytes:
85+
ba = bytearray()
86+
while True:
87+
chunk = self._read(_CHUNK_SIZE)
88+
ba += chunk
89+
if len(chunk) < _CHUNK_SIZE:
90+
break
91+
return bytes(ba)
92+
93+
def _read(self, size: int) -> bytes:
94+
result = self._ioctx.read(self._key, size, self._offset)
95+
self._offset += len(result)
96+
return result
97+
98+
def close(self) -> None:
99+
if not self._closed:
100+
self._ioctx.close()
101+
self._closed = True
102+
if self._connected:
103+
self._conn.shutdown()
104+
105+
@property
106+
def closed(self) -> bool:
107+
return self._closed
108+
109+
@property
110+
def mode(self) -> str:
111+
return "rb"
112+
113+
@property
114+
def name(self) -> str:
115+
return self._key
116+
117+
def __enter__(self) -> _RADOSResponse:
118+
return self
119+
120+
def __exit__(
121+
self, exc_type: ExcType, exc_val: ExcValue, exc_tb: ExcTraceback
122+
) -> None:
123+
self.close()
124+
125+
def __iter__(self) -> _RADOSResponse:
126+
return self
127+
128+
def __next__(self) -> bytes:
129+
res = self.read(_CHUNK_SIZE)
130+
if not res:
131+
raise StopIteration()
132+
return res
133+
134+
def seekable(self) -> bool:
135+
return False
136+
137+
def readable(self) -> bool:
138+
return True
139+
140+
def writable(self) -> bool:
141+
return False
142+
143+
def flush(self) -> None:
144+
pass
145+
146+
def isatty(self) -> bool:
147+
return False
148+
149+
def tell(self) -> int:
150+
return self._offset
151+
152+
def seek(self, offset: int, whence: int = 0) -> int:
153+
raise NotImplementedError()
154+
155+
def fileno(self) -> int:
156+
raise NotImplementedError()
157+
158+
def readline(self, limit: int = -1) -> bytes:
159+
raise NotImplementedError()
160+
161+
def readlines(self, hint: int = -1) -> list[bytes]:
162+
raise NotImplementedError()
163+
164+
def truncate(self, size: typing.Optional[int] = None) -> int:
165+
raise NotImplementedError()
166+
167+
def write(self, s: typing.Any) -> int:
168+
raise NotImplementedError()
169+
170+
def writelines(self, ls: typing.Iterable[typing.Any]) -> None:
171+
raise NotImplementedError()
172+
173+
174+
def enable_rados_url_opener(cls: typing.Type[url_opener.URLOpener]) -> None:
175+
"""Extend the URLOpener type to support pseudo-URLs for rados
176+
object storage. If rados libraries are not found the function
177+
does nothing.
178+
179+
If rados libraries are found than URLOpener can be used like:
180+
>>> uo = url_opener.URLOpener()
181+
>>> res = uo.open("rados://my_pool/namepace/obj_key")
182+
>>> res.read()
183+
"""
184+
try:
185+
import rados # type: ignore[import]
186+
except ImportError:
187+
return
188+
189+
_RADOSHandler._rados_api = rados
190+
cls._handlers.append(_RADOSHandler)

0 commit comments

Comments
 (0)