zboxapi 0.0.4__tar.gz → 0.0.6__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.0.6"
|
|
@@ -3,8 +3,6 @@ import fcntl
|
|
|
3
3
|
import os
|
|
4
4
|
import re
|
|
5
5
|
import socket
|
|
6
|
-
|
|
7
|
-
# import ssl
|
|
8
6
|
import subprocess
|
|
9
7
|
import time
|
|
10
8
|
from ipaddress import IPv4Address
|
|
@@ -18,6 +16,8 @@ from fastapi.security.api_key import APIKey, APIKeyHeader
|
|
|
18
16
|
from pydantic import AfterValidator, BaseModel
|
|
19
17
|
from pydantic_core import PydanticCustomError
|
|
20
18
|
|
|
19
|
+
from zboxapi import __version__
|
|
20
|
+
|
|
21
21
|
api_key_header = APIKeyHeader(name="access_token", auto_error=False)
|
|
22
22
|
|
|
23
23
|
|
|
@@ -49,9 +49,10 @@ def get_hosts_lines(hosts_fo: IO) -> list[dict[str, str]]:
|
|
|
49
49
|
line = line.strip()
|
|
50
50
|
if not line or line.startswith("#"):
|
|
51
51
|
continue
|
|
52
|
-
line_ip, *
|
|
52
|
+
line_ip, *line_hostnames = line.split()
|
|
53
53
|
lines.extend(
|
|
54
|
-
{"ip": IPv4Address(line_ip), "
|
|
54
|
+
{"ip": IPv4Address(line_ip), "hostname": line_hostname}
|
|
55
|
+
for line_hostname in line_hostnames
|
|
55
56
|
)
|
|
56
57
|
return sorted(lines, key=sort_hosts_lines)
|
|
57
58
|
|
|
@@ -59,12 +60,13 @@ def get_hosts_lines(hosts_fo: IO) -> list[dict[str, str]]:
|
|
|
59
60
|
def filter_hosts_file(
|
|
60
61
|
lines: list[dict[str, str]],
|
|
61
62
|
ip: IPv4Address | None = None,
|
|
62
|
-
|
|
63
|
+
hostname: str | None = None,
|
|
63
64
|
):
|
|
64
65
|
return [
|
|
65
66
|
line
|
|
66
67
|
for line in lines
|
|
67
|
-
if (not ip or ip == line["ip"])
|
|
68
|
+
if (not ip or ip == line["ip"])
|
|
69
|
+
and (not hostname or hostname == line["hostname"])
|
|
68
70
|
]
|
|
69
71
|
|
|
70
72
|
|
|
@@ -72,7 +74,7 @@ def write_hosts_file(hosts_fo: IO, lines: list[dict[str, str]]):
|
|
|
72
74
|
lines.sort(key=sort_hosts_lines)
|
|
73
75
|
hosts_fo.seek(0)
|
|
74
76
|
hosts_fo.truncate()
|
|
75
|
-
hosts_fo.writelines([f"{line['ip']}\t{line['
|
|
77
|
+
hosts_fo.writelines([f"{line['ip']}\t{line['hostname']}\n" for line in lines])
|
|
76
78
|
|
|
77
79
|
|
|
78
80
|
def validate_api_key(api_key: Annotated[APIKey, Security(api_key_header)]):
|
|
@@ -101,11 +103,11 @@ def dnsmasq_sighup():
|
|
|
101
103
|
|
|
102
104
|
|
|
103
105
|
def sort_hosts_lines(item: dict):
|
|
104
|
-
"""Sort list by loopback first, ip second,
|
|
106
|
+
"""Sort list by loopback first, ip second, hostname third"""
|
|
105
107
|
return (
|
|
106
108
|
not item["ip"].is_loopback,
|
|
107
109
|
socket.inet_aton(str(item["ip"])),
|
|
108
|
-
item["
|
|
110
|
+
item["hostname"],
|
|
109
111
|
)
|
|
110
112
|
|
|
111
113
|
|
|
@@ -120,135 +122,125 @@ def simplify_operation_ids(api: FastAPI) -> None:
|
|
|
120
122
|
route.operation_id = f"{tag}_{route.name}"
|
|
121
123
|
|
|
122
124
|
|
|
123
|
-
def
|
|
124
|
-
return obj if isinstance(obj, list) else [obj]
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
def ip_fqdn_str(obj, ip_key="ip", fqdn_key="fqdn"):
|
|
128
|
-
return f"ip={getattr(obj, ip_key)}, fqdn={getattr(obj, fqdn_key)}"
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
def ip_fqdn_dict(obj, ip_key="ip", fqdn_key="fqdn"):
|
|
132
|
-
return {"ip": getattr(obj, ip_key), "fqdn": getattr(obj, fqdn_key)}
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
def RecordNotFound(obj, ip_key="ip", fqdn_key="fqdn"):
|
|
125
|
+
def RecordNotFound(ip, hostname):
|
|
136
126
|
return HTTPException(
|
|
137
|
-
status_code=status.
|
|
138
|
-
detail=f"
|
|
127
|
+
status_code=status.HTTP_404_NOT_FOUND,
|
|
128
|
+
detail=f"DNS record not found: ip={ip}, hostname={hostname}",
|
|
139
129
|
)
|
|
140
130
|
|
|
141
131
|
|
|
142
|
-
def RecordAlreadyPresent(
|
|
132
|
+
def RecordAlreadyPresent(ip, hostname):
|
|
143
133
|
return HTTPException(
|
|
144
134
|
status_code=status.HTTP_406_NOT_ACCEPTABLE,
|
|
145
|
-
detail=f"
|
|
135
|
+
detail=f"DNS record already present: ip={ip}, hostname={hostname}",
|
|
146
136
|
)
|
|
147
137
|
|
|
148
138
|
|
|
149
|
-
def
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
"""
|
|
153
|
-
|
|
154
|
-
if not 1 < len(value) < 253:
|
|
155
|
-
raise PydanticCustomError("value_error", "Invalid fqdn length")
|
|
156
|
-
|
|
157
|
-
# Remove trailing dot
|
|
158
|
-
if value[-1] == ".":
|
|
159
|
-
value = value[:-1]
|
|
160
|
-
|
|
161
|
-
# Split hostname into list of DNS labels
|
|
162
|
-
labels = value.split(".")
|
|
139
|
+
def validate_hostname(value: str):
|
|
140
|
+
if not 1 < len(value) < 64:
|
|
141
|
+
raise PydanticCustomError("value_error", "Invalid hostname length")
|
|
163
142
|
|
|
164
143
|
# Define pattern of DNS label
|
|
165
144
|
# Can begin and end with a number or letter only
|
|
166
145
|
# Can contain hyphens, a-z, A-Z, 0-9
|
|
167
146
|
# 1 - 63 chars allowed
|
|
168
|
-
|
|
147
|
+
hostname_re = re.compile(r"^[a-z0-9]([a-z-0-9-]{0,61}[a-z0-9])?$", re.IGNORECASE)
|
|
169
148
|
|
|
170
149
|
# Check that all labels match that pattern.
|
|
171
|
-
if not
|
|
172
|
-
raise PydanticCustomError("value_error", "Invalid
|
|
150
|
+
if not hostname_re.match(value):
|
|
151
|
+
raise PydanticCustomError("value_error", "Invalid hostname")
|
|
173
152
|
return value
|
|
174
153
|
|
|
175
154
|
|
|
176
|
-
|
|
155
|
+
HOSTNAME = Annotated[str, AfterValidator(validate_hostname)]
|
|
177
156
|
|
|
178
157
|
|
|
179
|
-
class
|
|
158
|
+
class DnsCreate(BaseModel):
|
|
180
159
|
ip: IPv4Address
|
|
181
|
-
|
|
160
|
+
hostname: HOSTNAME
|
|
182
161
|
|
|
183
162
|
|
|
184
|
-
class
|
|
163
|
+
class DnsDelete(BaseModel):
|
|
185
164
|
ip: IPv4Address
|
|
186
|
-
|
|
165
|
+
hostname: HOSTNAME
|
|
187
166
|
|
|
188
167
|
|
|
189
|
-
class
|
|
168
|
+
class DnsUpdate(BaseModel):
|
|
190
169
|
ip: IPv4Address
|
|
191
|
-
|
|
192
|
-
new_ip: IPv4Address
|
|
193
|
-
new_fqdn: FQDN
|
|
170
|
+
hostname: HOSTNAME
|
|
194
171
|
|
|
195
172
|
|
|
196
|
-
class
|
|
173
|
+
class DnsView(BaseModel):
|
|
197
174
|
ip: IPv4Address
|
|
198
|
-
|
|
175
|
+
hostname: str
|
|
199
176
|
|
|
200
177
|
|
|
201
|
-
|
|
178
|
+
dns_router = APIRouter(prefix="/dns", tags=["dns"])
|
|
202
179
|
|
|
203
180
|
|
|
204
|
-
@
|
|
205
|
-
def
|
|
206
|
-
ip: IPv4Address | None = None,
|
|
207
|
-
fqdn: str | None = None,
|
|
208
|
-
) -> list[HostsView]:
|
|
181
|
+
@dns_router.get("")
|
|
182
|
+
def dns_get_all() -> list[DnsView]:
|
|
209
183
|
with get_hosts_file_object() as hosts_fo:
|
|
210
184
|
hosts_lines = get_hosts_lines(hosts_fo)
|
|
211
|
-
hosts_lines = filter_hosts_file(hosts_lines, ip, fqdn)
|
|
212
185
|
return hosts_lines
|
|
213
186
|
|
|
214
187
|
|
|
215
|
-
@
|
|
216
|
-
def
|
|
188
|
+
@dns_router.get("/{ip}/{hostname}")
|
|
189
|
+
def dns_get(
|
|
190
|
+
ip: IPv4Address,
|
|
191
|
+
hostname: HOSTNAME,
|
|
192
|
+
) -> DnsView:
|
|
193
|
+
with get_hosts_file_object() as hosts_fo:
|
|
194
|
+
hosts_lines = get_hosts_lines(hosts_fo)
|
|
195
|
+
hosts_lines = filter_hosts_file(hosts_lines, ip, hostname)
|
|
196
|
+
if not hosts_lines:
|
|
197
|
+
raise RecordNotFound(ip, hostname)
|
|
198
|
+
return hosts_lines[0]
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
@dns_router.post("")
|
|
202
|
+
def dns_add(
|
|
203
|
+
dns_in: DnsCreate,
|
|
204
|
+
) -> list[DnsView]:
|
|
217
205
|
with get_hosts_file_object() as hosts_fo:
|
|
218
206
|
hosts_lines = get_hosts_lines(hosts_fo)
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
hosts_lines.append(ip_fqdn_dict(line))
|
|
207
|
+
if filter_hosts_file(hosts_lines, dns_in.ip, dns_in.hostname):
|
|
208
|
+
raise RecordAlreadyPresent(dns_in.ip, dns_in.hostname)
|
|
209
|
+
hosts_lines.append({"ip": dns_in.ip, "hostname": dns_in.hostname})
|
|
223
210
|
write_hosts_file(hosts_fo, hosts_lines)
|
|
224
211
|
dnsmasq_sighup()
|
|
225
212
|
return hosts_lines
|
|
226
213
|
|
|
227
214
|
|
|
228
|
-
@
|
|
229
|
-
def
|
|
215
|
+
@dns_router.put("/{ip}/{hostname}")
|
|
216
|
+
def dns_update(
|
|
217
|
+
ip: IPv4Address,
|
|
218
|
+
hostname: HOSTNAME,
|
|
219
|
+
dns_in: DnsUpdate,
|
|
220
|
+
) -> list[DnsView]:
|
|
230
221
|
with get_hosts_file_object() as hosts_fo:
|
|
231
222
|
hosts_lines = get_hosts_lines(hosts_fo)
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
hosts_lines[ix] = ip_fqdn_dict(line, "new_ip", "new_fqdn")
|
|
223
|
+
key = {"ip": ip, "hostname": hostname}
|
|
224
|
+
if key not in hosts_lines:
|
|
225
|
+
raise RecordNotFound(ip, hostname)
|
|
226
|
+
ix = hosts_lines.index(key)
|
|
227
|
+
hosts_lines[ix] = {"ip": dns_in.ip, "hostname": dns_in.hostname}
|
|
238
228
|
write_hosts_file(hosts_fo, hosts_lines)
|
|
239
229
|
dnsmasq_sighup()
|
|
240
230
|
return hosts_lines
|
|
241
231
|
|
|
242
232
|
|
|
243
|
-
@
|
|
244
|
-
def
|
|
233
|
+
@dns_router.delete("/{ip}/{hostname}")
|
|
234
|
+
def dns_delete(
|
|
235
|
+
ip: IPv4Address,
|
|
236
|
+
hostname: HOSTNAME,
|
|
237
|
+
) -> list[DnsView]:
|
|
245
238
|
with get_hosts_file_object() as hosts_fo:
|
|
246
239
|
hosts_lines = get_hosts_lines(hosts_fo)
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
hosts_lines.remove(key)
|
|
240
|
+
key = {"ip": ip, "hostname": hostname}
|
|
241
|
+
if key not in hosts_lines:
|
|
242
|
+
raise RecordNotFound(ip, hostname)
|
|
243
|
+
hosts_lines.remove(key)
|
|
252
244
|
write_hosts_file(hosts_fo, hosts_lines)
|
|
253
245
|
dnsmasq_sighup()
|
|
254
246
|
return hosts_lines
|
|
@@ -260,8 +252,9 @@ app = FastAPI(
|
|
|
260
252
|
title="zBox API",
|
|
261
253
|
root_path=zboxapi_root_path,
|
|
262
254
|
dependencies=[Depends(validate_api_key)],
|
|
255
|
+
version=__version__,
|
|
263
256
|
)
|
|
264
|
-
app.include_router(
|
|
257
|
+
app.include_router(dns_router)
|
|
265
258
|
simplify_operation_ids(app)
|
|
266
259
|
|
|
267
260
|
ZPOD_PASSWORD = get_zpod_password()
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.0.4"
|
|
File without changes
|