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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: zboxapi
3
- Version: 0.0.4
3
+ Version: 0.0.6
4
4
  Summary:
5
5
  Author: Kelby Valenti
6
6
  Author-email: kelby.valenti@gmail.com
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "zboxapi"
3
- version = "0.0.4"
3
+ version = "0.0.6"
4
4
  description = ""
5
5
  authors = ["Kelby Valenti <kelby.valenti@gmail.com>", "Timo Sugliani <timo.sugliani@gmail.com>"]
6
6
  readme = "README.md"
@@ -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, *line_fqdns = line.split()
52
+ line_ip, *line_hostnames = line.split()
53
53
  lines.extend(
54
- {"ip": IPv4Address(line_ip), "fqdn": line_fqdn} for line_fqdn in line_fqdns
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
- fqdn: str | None = None,
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"]) and (not fqdn or fqdn == line["fqdn"])
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['fqdn']}\n" for line in lines])
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, fqdn third"""
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["fqdn"],
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 make_list(obj):
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.HTTP_406_NOT_ACCEPTABLE,
138
- detail=f"Record not found: {ip_fqdn_str(obj, ip_key, fqdn_key)}",
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(obj, ip_key="ip", fqdn_key="fqdn"):
132
+ def RecordAlreadyPresent(ip, hostname):
143
133
  return HTTPException(
144
134
  status_code=status.HTTP_406_NOT_ACCEPTABLE,
145
- detail=f"Record already present: {ip_fqdn_str(obj, ip_key, fqdn_key)}",
135
+ detail=f"DNS record already present: ip={ip}, hostname={hostname}",
146
136
  )
147
137
 
148
138
 
149
- def validate_fqdn(value: str):
150
- """
151
- https://en.m.wikipedia.org/wiki/Fully_qualified_domain_name
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
- fqdn = re.compile(r"^[a-z0-9]([a-z-0-9-]{0,61}[a-z0-9])?$", re.IGNORECASE)
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 all(fqdn.match(label) for label in labels):
172
- raise PydanticCustomError("value_error", "Invalid fqdn")
150
+ if not hostname_re.match(value):
151
+ raise PydanticCustomError("value_error", "Invalid hostname")
173
152
  return value
174
153
 
175
154
 
176
- FQDN = Annotated[str, AfterValidator(validate_fqdn)]
155
+ HOSTNAME = Annotated[str, AfterValidator(validate_hostname)]
177
156
 
178
157
 
179
- class HostsCreate(BaseModel):
158
+ class DnsCreate(BaseModel):
180
159
  ip: IPv4Address
181
- fqdn: FQDN
160
+ hostname: HOSTNAME
182
161
 
183
162
 
184
- class HostsDelete(BaseModel):
163
+ class DnsDelete(BaseModel):
185
164
  ip: IPv4Address
186
- fqdn: FQDN
165
+ hostname: HOSTNAME
187
166
 
188
167
 
189
- class HostsUpdate(BaseModel):
168
+ class DnsUpdate(BaseModel):
190
169
  ip: IPv4Address
191
- fqdn: FQDN
192
- new_ip: IPv4Address
193
- new_fqdn: FQDN
170
+ hostname: HOSTNAME
194
171
 
195
172
 
196
- class HostsView(BaseModel):
173
+ class DnsView(BaseModel):
197
174
  ip: IPv4Address
198
- fqdn: str
175
+ hostname: str
199
176
 
200
177
 
201
- hosts_router = APIRouter(prefix="/hosts", tags=["hosts"])
178
+ dns_router = APIRouter(prefix="/dns", tags=["dns"])
202
179
 
203
180
 
204
- @hosts_router.get("")
205
- def get_hosts(
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
- @hosts_router.post("")
216
- def add_hosts(lines_in: list[HostsCreate] | HostsCreate) -> list[HostsView]:
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
- for line in make_list(lines_in):
220
- if filter_hosts_file(hosts_lines, line.ip, line.fqdn):
221
- raise RecordAlreadyPresent(line)
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
- @hosts_router.put("")
229
- def update_hosts(lines_in: list[HostsUpdate] | HostsUpdate) -> list[HostsView]:
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
- for line in make_list(lines_in):
233
- key = ip_fqdn_dict(line)
234
- if key not in hosts_lines:
235
- raise RecordNotFound(line)
236
- ix = hosts_lines.index(key)
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
- @hosts_router.delete("")
244
- def delete_hosts(lines_in: list[HostsDelete] | HostsDelete) -> list[HostsView]:
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
- for line in make_list(lines_in):
248
- key = ip_fqdn_dict(line)
249
- if key not in hosts_lines:
250
- raise RecordNotFound(line)
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(hosts_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