zboxapi 0.0.2__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.
zboxapi-0.0.2/PKG-INFO ADDED
@@ -0,0 +1,75 @@
1
+ Metadata-Version: 2.1
2
+ Name: zboxapi
3
+ Version: 0.0.2
4
+ Summary:
5
+ Author: Kelby Valenti
6
+ Author-email: kelby.valenti@gmail.com
7
+ Requires-Python: >=3.12
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Programming Language :: Python :: 3.12
10
+ Requires-Dist: fastapi (==0.111.0)
11
+ Requires-Dist: ipython (>=8.24.0,<9.0.0)
12
+ Requires-Dist: uvicorn (==0.29.0)
13
+ Description-Content-Type: text/markdown
14
+
15
+ # zBoxApi
16
+
17
+ zPodFactory zBox Api
18
+
19
+ ## Installation
20
+
21
+ Complete the following steps to set up zBox Api:
22
+
23
+ 1. Install pipx
24
+
25
+ ```bash
26
+ # Install and configure pipx
27
+ apt update
28
+ apt install -y pipx
29
+ pipx ensurepath
30
+
31
+ # Reload your profile
32
+ source ~/.zshrc
33
+ ```
34
+
35
+ 1. Install pyenv and add 3.12.1
36
+
37
+ ```bash
38
+
39
+ # Install required packages
40
+ apt update
41
+ apt install -y make build-essential libssl-dev zlib1g-dev \
42
+ libbz2-dev libreadline-dev libsqlite3-dev wget curl llvm \
43
+ libncursesw5-dev xz-utils tk-dev libxml2-dev libxmlsec1-dev libffi-dev \
44
+ liblzma-dev
45
+
46
+ # Install pyenv
47
+ curl https://pyenv.run | bash
48
+
49
+ # Configure user profile
50
+ echo 'export PYENV_ROOT="$HOME/.pyenv"' >> ~/.zshrc
51
+ echo '[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"' >> ~/.zshrc
52
+ echo 'eval "$(pyenv init -)"' >> ~/.zshrc
53
+
54
+ # Reload your profile
55
+ source ~/.zshrc
56
+
57
+ # Install 3.12.1
58
+ pyenv install 3.12.1
59
+ ```
60
+
61
+ 1. Install zBoxApi:
62
+
63
+ ```bash
64
+ pipx install zboxapi --python /root/.pyenv/versions/3.12.1/bin/python3
65
+ ```
66
+
67
+ 1. Set up and start zboxapi.service
68
+
69
+ ```bash
70
+ cp zboxapi.service /etc/systemd/system
71
+ systemctl daemon-reload
72
+ systemctl enable zboxapi.service
73
+ systemctl start zboxapi.service
74
+ ```
75
+
@@ -0,0 +1,60 @@
1
+ # zBoxApi
2
+
3
+ zPodFactory zBox Api
4
+
5
+ ## Installation
6
+
7
+ Complete the following steps to set up zBox Api:
8
+
9
+ 1. Install pipx
10
+
11
+ ```bash
12
+ # Install and configure pipx
13
+ apt update
14
+ apt install -y pipx
15
+ pipx ensurepath
16
+
17
+ # Reload your profile
18
+ source ~/.zshrc
19
+ ```
20
+
21
+ 1. Install pyenv and add 3.12.1
22
+
23
+ ```bash
24
+
25
+ # Install required packages
26
+ apt update
27
+ apt install -y make build-essential libssl-dev zlib1g-dev \
28
+ libbz2-dev libreadline-dev libsqlite3-dev wget curl llvm \
29
+ libncursesw5-dev xz-utils tk-dev libxml2-dev libxmlsec1-dev libffi-dev \
30
+ liblzma-dev
31
+
32
+ # Install pyenv
33
+ curl https://pyenv.run | bash
34
+
35
+ # Configure user profile
36
+ echo 'export PYENV_ROOT="$HOME/.pyenv"' >> ~/.zshrc
37
+ echo '[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"' >> ~/.zshrc
38
+ echo 'eval "$(pyenv init -)"' >> ~/.zshrc
39
+
40
+ # Reload your profile
41
+ source ~/.zshrc
42
+
43
+ # Install 3.12.1
44
+ pyenv install 3.12.1
45
+ ```
46
+
47
+ 1. Install zBoxApi:
48
+
49
+ ```bash
50
+ pipx install zboxapi --python /root/.pyenv/versions/3.12.1/bin/python3
51
+ ```
52
+
53
+ 1. Set up and start zboxapi.service
54
+
55
+ ```bash
56
+ cp zboxapi.service /etc/systemd/system
57
+ systemctl daemon-reload
58
+ systemctl enable zboxapi.service
59
+ systemctl start zboxapi.service
60
+ ```
@@ -0,0 +1,44 @@
1
+ [tool.poetry]
2
+ name = "zboxapi"
3
+ version = "0.0.2"
4
+ description = ""
5
+ authors = ["Kelby Valenti <kelby.valenti@gmail.com>", "Timo Sugliani <timo.sugliani@gmail.com>"]
6
+ readme = "README.md"
7
+
8
+ [tool.poetry.scripts]
9
+ zboxapi = "zboxapi.main:launch"
10
+
11
+ [tool.poetry.dependencies]
12
+ fastapi = "0.111.0"
13
+ python = ">=3.12"
14
+ uvicorn = "0.29.0"
15
+ ipython = "^8.24.0"
16
+
17
+ [tool.poetry.group.dev.dependencies]
18
+ ruff = "^0.4"
19
+
20
+ [build-system]
21
+ requires = ["poetry-core"]
22
+ build-backend = "poetry.core.masonry.api"
23
+
24
+ [tool.ruff.lint]
25
+ select = [
26
+ "E", # pycodestyle errors
27
+ "W", # pycodestyle warnings
28
+ "F", # pyflakes
29
+ "I", # isort
30
+ "C", # flake8-comprehensions
31
+ "B", # flake8-bugbear
32
+ "UP", # pyupgrade
33
+ ]
34
+ ignore = [
35
+ "B008",
36
+ ]
37
+
38
+ [[tool.poetry_bumpversion.replacements]]
39
+ files = [
40
+ "src/zboxapi/__init__.py",
41
+ ]
42
+ search = '__version__ = "{current_version}"'
43
+ replace = '__version__ = "{new_version}"'
44
+
@@ -0,0 +1 @@
1
+ __version__ = "0.0.2"
@@ -0,0 +1,258 @@
1
+ import contextlib
2
+ import fcntl
3
+ import re
4
+ import socket
5
+
6
+ # import ssl
7
+ import subprocess
8
+ import time
9
+ from ipaddress import IPv4Address
10
+ from pathlib import Path
11
+ from typing import IO, Annotated
12
+
13
+ import uvicorn
14
+ from fastapi import APIRouter, Depends, FastAPI, HTTPException, Security, status
15
+ from fastapi.security.api_key import APIKey, APIKeyHeader
16
+ from pydantic import AfterValidator, BaseModel
17
+ from pydantic_core import PydanticCustomError
18
+
19
+ api_key_header = APIKeyHeader(name="access_token", auto_error=False)
20
+
21
+
22
+ @contextlib.contextmanager
23
+ def get_hosts_file_object():
24
+ pfile = Path("/etc/hosts")
25
+ if not pfile.is_file():
26
+ pfile.write_text("")
27
+
28
+ while True:
29
+ try:
30
+ file_handle = pfile.open("r+")
31
+ fcntl.flock(file_handle, fcntl.LOCK_EX | fcntl.LOCK_NB)
32
+ break
33
+ except IOError: # noqa: UP024
34
+ # File is locked, wait for a while and try again
35
+ time.sleep(0.1)
36
+
37
+ try:
38
+ yield file_handle
39
+ finally:
40
+ fcntl.flock(file_handle, fcntl.LOCK_UN)
41
+ file_handle.close()
42
+
43
+
44
+ def get_hosts_lines(hosts_fo: IO) -> list[dict[str, str]]:
45
+ lines = []
46
+ for line in hosts_fo.read().splitlines():
47
+ line = line.strip()
48
+ if not line or line.startswith("#"):
49
+ continue
50
+ line_ip, *line_fqdns = line.split()
51
+ lines.extend(
52
+ {"ip": IPv4Address(line_ip), "fqdn": line_fqdn} for line_fqdn in line_fqdns
53
+ )
54
+ return sorted(lines, key=sort_hosts_lines)
55
+
56
+
57
+ def filter_hosts_file(
58
+ lines: list[dict[str, str]],
59
+ ip: IPv4Address | None = None,
60
+ fqdn: str | None = None,
61
+ ):
62
+ return [
63
+ line
64
+ for line in lines
65
+ if (not ip or ip == line["ip"]) and (not fqdn or fqdn == line["fqdn"])
66
+ ]
67
+
68
+
69
+ def write_hosts_file(hosts_fo: IO, lines: list[dict[str, str]]):
70
+ lines.sort(key=sort_hosts_lines)
71
+ hosts_fo.seek(0)
72
+ hosts_fo.truncate()
73
+ hosts_fo.writelines([f"{line['ip']}\t{line['fqdn']}\n" for line in lines])
74
+
75
+
76
+ def validate_api_key(api_key: Annotated[APIKey, Security(api_key_header)]):
77
+ if api_key != ZPOD_PASSWORD:
78
+ raise HTTPException(
79
+ status_code=status.HTTP_403_FORBIDDEN,
80
+ detail="Invalid access_token",
81
+ )
82
+
83
+
84
+ def get_zpod_password():
85
+ ovfenv = subprocess.run(
86
+ ["vmtoolsd", "--cmd", "info-get guestinfo.ovfenv"],
87
+ capture_output=True,
88
+ text=True,
89
+ )
90
+ pw_re = re.compile(r'<Property oe:key="guestinfo.password" oe:value="([^"]*)"/>')
91
+ if item := re.search(pw_re, ovfenv.stdout):
92
+ return item[1]
93
+ raise Exception("Unable to retrieve zpod password")
94
+
95
+
96
+ def dnsmasq_sighup():
97
+ print("Send SIGHUP to dnsmasq...")
98
+ subprocess.call(["pkill", "-SIGHUP", "dnsmasq"])
99
+
100
+
101
+ def sort_hosts_lines(item: dict):
102
+ """Sort list by loopback first, ip second, fqdn third"""
103
+ return (
104
+ not item["ip"].is_loopback,
105
+ socket.inet_aton(str(item["ip"])),
106
+ item["fqdn"],
107
+ )
108
+
109
+
110
+ def make_list(obj):
111
+ return obj if isinstance(obj, list) else [obj]
112
+
113
+
114
+ def ip_fqdn_str(obj, ip_key="ip", fqdn_key="fqdn"):
115
+ return f"ip={getattr(obj, ip_key)}, fqdn={getattr(obj, fqdn_key)}"
116
+
117
+
118
+ def ip_fqdn_dict(obj, ip_key="ip", fqdn_key="fqdn"):
119
+ return {"ip": getattr(obj, ip_key), "fqdn": getattr(obj, fqdn_key)}
120
+
121
+
122
+ def RecordNotFound(obj, ip_key="ip", fqdn_key="fqdn"):
123
+ return HTTPException(
124
+ status_code=status.HTTP_406_NOT_ACCEPTABLE,
125
+ detail=f"Record not found: {ip_fqdn_str(obj, ip_key, fqdn_key)}",
126
+ )
127
+
128
+
129
+ def RecordAlreadyPresent(obj, ip_key="ip", fqdn_key="fqdn"):
130
+ return HTTPException(
131
+ status_code=status.HTTP_406_NOT_ACCEPTABLE,
132
+ detail=f"Record already present: {ip_fqdn_str(obj, ip_key, fqdn_key)}",
133
+ )
134
+
135
+
136
+ def validate_fqdn(value: str):
137
+ """
138
+ https://en.m.wikipedia.org/wiki/Fully_qualified_domain_name
139
+ """
140
+
141
+ if not 1 < len(value) < 253:
142
+ raise PydanticCustomError("value_error", "Invalid fqdn length")
143
+
144
+ # Remove trailing dot
145
+ if value[-1] == ".":
146
+ value = value[:-1]
147
+
148
+ # Split hostname into list of DNS labels
149
+ labels = value.split(".")
150
+
151
+ # Define pattern of DNS label
152
+ # Can begin and end with a number or letter only
153
+ # Can contain hyphens, a-z, A-Z, 0-9
154
+ # 1 - 63 chars allowed
155
+ fqdn = re.compile(r"^[a-z0-9]([a-z-0-9-]{0,61}[a-z0-9])?$", re.IGNORECASE)
156
+
157
+ # Check that all labels match that pattern.
158
+ if not all(fqdn.match(label) for label in labels):
159
+ raise PydanticCustomError("value_error", "Invalid fqdn")
160
+ return value
161
+
162
+
163
+ FQDN = Annotated[str, AfterValidator(validate_fqdn)]
164
+
165
+
166
+ class DnsCreate(BaseModel):
167
+ ip: IPv4Address
168
+ fqdn: FQDN
169
+
170
+
171
+ class DnsDelete(BaseModel):
172
+ ip: IPv4Address
173
+ fqdn: FQDN
174
+
175
+
176
+ class DnsUpdate(BaseModel):
177
+ ip: IPv4Address
178
+ fqdn: FQDN
179
+ new_ip: IPv4Address
180
+ new_fqdn: FQDN
181
+
182
+
183
+ class DnsView(BaseModel):
184
+ ip: IPv4Address
185
+ fqdn: str
186
+
187
+
188
+ hosts_router = APIRouter(prefix="/hosts", tags=["hosts"])
189
+
190
+
191
+ @hosts_router.get("")
192
+ def get_hosts(
193
+ ip: IPv4Address | None = None,
194
+ fqdn: str | None = None,
195
+ ) -> list[DnsView]:
196
+ with get_hosts_file_object() as hosts_fo:
197
+ hosts_lines = get_hosts_lines(hosts_fo)
198
+ hosts_lines = filter_hosts_file(hosts_lines, ip, fqdn)
199
+ return hosts_lines
200
+
201
+
202
+ @hosts_router.post("")
203
+ def add_hosts(lines_in: list[DnsCreate] | DnsCreate) -> list[DnsView]:
204
+ with get_hosts_file_object() as hosts_fo:
205
+ hosts_lines = get_hosts_lines(hosts_fo)
206
+ for line in make_list(lines_in):
207
+ if filter_hosts_file(hosts_lines, line.ip, line.fqdn):
208
+ raise RecordAlreadyPresent(line)
209
+ hosts_lines.append(ip_fqdn_dict(line))
210
+ write_hosts_file(hosts_fo, hosts_lines)
211
+ dnsmasq_sighup()
212
+ return hosts_lines
213
+
214
+
215
+ @hosts_router.put("")
216
+ def update_hosts(lines_in: list[DnsUpdate] | DnsUpdate) -> list[DnsView]:
217
+ with get_hosts_file_object() as hosts_fo:
218
+ hosts_lines = get_hosts_lines(hosts_fo)
219
+ for line in make_list(lines_in):
220
+ key = ip_fqdn_dict(line)
221
+ if key not in hosts_lines:
222
+ raise RecordNotFound(line)
223
+ ix = hosts_lines.index(key)
224
+ hosts_lines[ix] = ip_fqdn_dict(line, "new_ip", "new_fqdn")
225
+ write_hosts_file(hosts_fo, hosts_lines)
226
+ dnsmasq_sighup()
227
+ return hosts_lines
228
+
229
+
230
+ @hosts_router.delete("")
231
+ def delete_hosts(lines_in: list[DnsDelete] | DnsDelete) -> list[DnsView]:
232
+ with get_hosts_file_object() as hosts_fo:
233
+ hosts_lines = get_hosts_lines(hosts_fo)
234
+ for line in make_list(lines_in):
235
+ key = ip_fqdn_dict(line)
236
+ if key not in hosts_lines:
237
+ raise RecordNotFound(line)
238
+ hosts_lines.remove(key)
239
+ write_hosts_file(hosts_fo, hosts_lines)
240
+ dnsmasq_sighup()
241
+ return hosts_lines
242
+
243
+
244
+ app = FastAPI(title="zPod zBox API", dependencies=[Depends(validate_api_key)])
245
+ app.include_router(hosts_router)
246
+ ZPOD_PASSWORD = get_zpod_password()
247
+
248
+
249
+ def launch():
250
+ uvicorn.run(
251
+ app,
252
+ host="zbox",
253
+ port=8000,
254
+ )
255
+
256
+
257
+ if __name__ == "__main__":
258
+ launch()