zboxapi 0.0.5__tar.gz → 0.0.7__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.7/PKG-INFO ADDED
@@ -0,0 +1,135 @@
1
+ Metadata-Version: 2.1
2
+ Name: zboxapi
3
+ Version: 0.0.7
4
+ Summary:
5
+ Author: Kelby Valenti
6
+ Author-email: kelby.valenti@gmail.com
7
+ Requires-Python: >=3.10
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Programming Language :: Python :: 3.10
10
+ Classifier: Programming Language :: Python :: 3.11
11
+ Classifier: Programming Language :: Python :: 3.12
12
+ Requires-Dist: fastapi (==0.111.0)
13
+ Requires-Dist: ipython (>=8.24.0,<9.0.0)
14
+ Requires-Dist: uvicorn (==0.29.0)
15
+ Description-Content-Type: text/markdown
16
+
17
+ # zBoxApi
18
+
19
+ zPodFactory zBox Api
20
+
21
+ ## Features
22
+
23
+ - **DNS Management**: Manage DNS records in `/etc/hosts` with automatic dnsmasq integration
24
+ - **VLAN Management**: Manage VLAN interfaces with automatic network configuration
25
+
26
+ ## Installation
27
+
28
+ Complete the following steps to set up zBox Api:
29
+
30
+ 1. Install pipx
31
+
32
+ ```bash
33
+ # Install and configure pipx
34
+ apt update
35
+ apt install -y pipx
36
+ pipx ensurepath
37
+
38
+ # Reload your profile
39
+ source ~/.zshrc
40
+ ```
41
+
42
+ 1. Install zBoxApi:
43
+
44
+ ```bash
45
+ pipx install zboxapi
46
+ ```
47
+
48
+ 1. Set up and start zboxapi.service
49
+
50
+ ```bash
51
+ cp zboxapi.service /etc/systemd/system
52
+ systemctl daemon-reload
53
+ systemctl enable zboxapi.service
54
+ systemctl start zboxapi.service
55
+ ```
56
+
57
+ **Note**: The service runs on `127.0.0.1:8000` and requires root privileges for network configuration operations.
58
+
59
+ ## Configuration
60
+
61
+ ### VLAN Management
62
+
63
+ For VLAN management functionality, create a configuration file at `/etc/zboxapi.conf`:
64
+
65
+ ```ini
66
+ [DEFAULT]
67
+ # Base interface name for VLAN management
68
+ interface = eth1
69
+
70
+ # MTU setting for VLAN interfaces
71
+ mtu = 1700
72
+
73
+ # System default VLANs that cannot be modified (comma-separated)
74
+ system_vlans_default = 10,20,30
75
+
76
+ # System zPod VLANs that cannot be modified (comma-separated)
77
+ system_vlans_zpod = 64,128,192
78
+ ```
79
+
80
+ See [DOC_VLAN.md](DOC_VLAN.md) for detailed documentation on VLAN management features.
81
+
82
+ ## API Usage
83
+
84
+ ### Authentication
85
+
86
+ All API endpoints require authentication using the `access_token` header. The API key is the zPod password which is also the root password of the zbox VM. The password is automatically retrieved from VMware tools:
87
+
88
+ ```bash
89
+ curl -H "access_token: your_zpod_password" http://127.0.0.1:8000/dns
90
+ ```
91
+
92
+ **Note**: The service runs on `127.0.0.1:8000` and requires root privileges for network configuration operations.
93
+
94
+ ### DNS Management
95
+
96
+ Manage DNS records in `/etc/hosts`:
97
+
98
+ ```bash
99
+ # Add DNS record
100
+ curl -X POST "http://127.0.0.1:8000/dns" \
101
+ -H "access_token: your_zpod_password" \
102
+ -H "Content-Type: application/json" \
103
+ -d '{"ip": "192.168.1.100", "hostname": "example.com"}'
104
+
105
+ # List all DNS records
106
+ curl -X GET "http://127.0.0.1:8000/dns" \
107
+ -H "access_token: your_zpod_password"
108
+ ```
109
+
110
+ For complete DNS management documentation, see [DOC_DNS.md](DOC_DNS.md).
111
+
112
+ ### VLAN Management
113
+
114
+ Manage VLAN interfaces:
115
+
116
+ ```bash
117
+ # Create VLAN interface
118
+ curl -X POST "http://127.0.0.1:8000/vlan" \
119
+ -H "access_token: your_zpod_password" \
120
+ -H "Content-Type: application/json" \
121
+ -d '{"vlan": 2000, "gateway": "192.168.42.129/25"}'
122
+
123
+ # List all VLAN interfaces
124
+ curl -X GET "http://127.0.0.1:8000/vlan" \
125
+ -H "access_token: your_zpod_password"
126
+ ```
127
+
128
+ For complete VLAN management documentation, see [DOC_VLAN.md](DOC_VLAN.md).
129
+
130
+
131
+ ## Documentation
132
+
133
+ - [DOC_DNS.md](DOC_DNS.md) - Complete guide to DNS management features
134
+ - [DOC_VLAN.md](DOC_VLAN.md) - Complete guide to VLAN management features
135
+
@@ -0,0 +1,118 @@
1
+ # zBoxApi
2
+
3
+ zPodFactory zBox Api
4
+
5
+ ## Features
6
+
7
+ - **DNS Management**: Manage DNS records in `/etc/hosts` with automatic dnsmasq integration
8
+ - **VLAN Management**: Manage VLAN interfaces with automatic network configuration
9
+
10
+ ## Installation
11
+
12
+ Complete the following steps to set up zBox Api:
13
+
14
+ 1. Install pipx
15
+
16
+ ```bash
17
+ # Install and configure pipx
18
+ apt update
19
+ apt install -y pipx
20
+ pipx ensurepath
21
+
22
+ # Reload your profile
23
+ source ~/.zshrc
24
+ ```
25
+
26
+ 1. Install zBoxApi:
27
+
28
+ ```bash
29
+ pipx install zboxapi
30
+ ```
31
+
32
+ 1. Set up and start zboxapi.service
33
+
34
+ ```bash
35
+ cp zboxapi.service /etc/systemd/system
36
+ systemctl daemon-reload
37
+ systemctl enable zboxapi.service
38
+ systemctl start zboxapi.service
39
+ ```
40
+
41
+ **Note**: The service runs on `127.0.0.1:8000` and requires root privileges for network configuration operations.
42
+
43
+ ## Configuration
44
+
45
+ ### VLAN Management
46
+
47
+ For VLAN management functionality, create a configuration file at `/etc/zboxapi.conf`:
48
+
49
+ ```ini
50
+ [DEFAULT]
51
+ # Base interface name for VLAN management
52
+ interface = eth1
53
+
54
+ # MTU setting for VLAN interfaces
55
+ mtu = 1700
56
+
57
+ # System default VLANs that cannot be modified (comma-separated)
58
+ system_vlans_default = 10,20,30
59
+
60
+ # System zPod VLANs that cannot be modified (comma-separated)
61
+ system_vlans_zpod = 64,128,192
62
+ ```
63
+
64
+ See [DOC_VLAN.md](DOC_VLAN.md) for detailed documentation on VLAN management features.
65
+
66
+ ## API Usage
67
+
68
+ ### Authentication
69
+
70
+ All API endpoints require authentication using the `access_token` header. The API key is the zPod password which is also the root password of the zbox VM. The password is automatically retrieved from VMware tools:
71
+
72
+ ```bash
73
+ curl -H "access_token: your_zpod_password" http://127.0.0.1:8000/dns
74
+ ```
75
+
76
+ **Note**: The service runs on `127.0.0.1:8000` and requires root privileges for network configuration operations.
77
+
78
+ ### DNS Management
79
+
80
+ Manage DNS records in `/etc/hosts`:
81
+
82
+ ```bash
83
+ # Add DNS record
84
+ curl -X POST "http://127.0.0.1:8000/dns" \
85
+ -H "access_token: your_zpod_password" \
86
+ -H "Content-Type: application/json" \
87
+ -d '{"ip": "192.168.1.100", "hostname": "example.com"}'
88
+
89
+ # List all DNS records
90
+ curl -X GET "http://127.0.0.1:8000/dns" \
91
+ -H "access_token: your_zpod_password"
92
+ ```
93
+
94
+ For complete DNS management documentation, see [DOC_DNS.md](DOC_DNS.md).
95
+
96
+ ### VLAN Management
97
+
98
+ Manage VLAN interfaces:
99
+
100
+ ```bash
101
+ # Create VLAN interface
102
+ curl -X POST "http://127.0.0.1:8000/vlan" \
103
+ -H "access_token: your_zpod_password" \
104
+ -H "Content-Type: application/json" \
105
+ -d '{"vlan": 2000, "gateway": "192.168.42.129/25"}'
106
+
107
+ # List all VLAN interfaces
108
+ curl -X GET "http://127.0.0.1:8000/vlan" \
109
+ -H "access_token: your_zpod_password"
110
+ ```
111
+
112
+ For complete VLAN management documentation, see [DOC_VLAN.md](DOC_VLAN.md).
113
+
114
+
115
+ ## Documentation
116
+
117
+ - [DOC_DNS.md](DOC_DNS.md) - Complete guide to DNS management features
118
+ - [DOC_VLAN.md](DOC_VLAN.md) - Complete guide to VLAN management features
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "zboxapi"
3
- version = "0.0.5"
3
+ version = "0.0.7"
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.7"
@@ -0,0 +1,222 @@
1
+ import contextlib
2
+ import fcntl
3
+ import re
4
+ import socket
5
+ import subprocess
6
+ import time
7
+ from ipaddress import IPv4Address
8
+ from pathlib import Path
9
+ from typing import IO, Annotated
10
+
11
+ from fastapi import APIRouter, HTTPException, status
12
+ from pydantic import AfterValidator, BaseModel
13
+ from pydantic_core import PydanticCustomError
14
+
15
+
16
+ @contextlib.contextmanager
17
+ def get_hosts_file_object():
18
+ """Context manager for safely handling /etc/hosts file"""
19
+ pfile = Path("/etc/hosts")
20
+ if not pfile.is_file():
21
+ pfile.write_text("")
22
+
23
+ while True:
24
+ try:
25
+ file_handle = pfile.open("r+")
26
+ fcntl.flock(file_handle, fcntl.LOCK_EX | fcntl.LOCK_NB)
27
+ break
28
+ except IOError: # noqa: UP024
29
+ # File is locked, wait for a while and try again
30
+ time.sleep(0.1)
31
+
32
+ try:
33
+ yield file_handle
34
+ finally:
35
+ fcntl.flock(file_handle, fcntl.LOCK_UN)
36
+ file_handle.close()
37
+
38
+
39
+ def get_hosts_lines(hosts_fo: IO) -> list[dict[str, str]]:
40
+ """Parse hosts file and return list of IP-hostname mappings"""
41
+ lines = []
42
+ for line in hosts_fo.read().splitlines():
43
+ line = line.strip()
44
+ if not line or line.startswith("#"):
45
+ continue
46
+ line_ip, *line_hostnames = line.split()
47
+ lines.extend(
48
+ {"ip": IPv4Address(line_ip), "hostname": line_hostname}
49
+ for line_hostname in line_hostnames
50
+ )
51
+ return sorted(lines, key=sort_hosts_lines)
52
+
53
+
54
+ def filter_hosts_file(
55
+ lines: list[dict[str, str]],
56
+ ip: IPv4Address | None = None,
57
+ hostname: str | None = None,
58
+ ):
59
+ """Filter hosts file lines by IP and/or hostname"""
60
+ return [
61
+ line
62
+ for line in lines
63
+ if (not ip or ip == line["ip"])
64
+ and (not hostname or hostname == line["hostname"])
65
+ ]
66
+
67
+
68
+ def write_hosts_file(hosts_fo: IO, lines: list[dict[str, str]]):
69
+ """Write hosts file lines back to file"""
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['hostname']}\n" for line in lines])
74
+
75
+
76
+ def dnsmasq_sighup():
77
+ """Send SIGHUP to dnsmasq to reload configuration"""
78
+ print("Send SIGHUP to dnsmasq...")
79
+ subprocess.call(["pkill", "-SIGHUP", "dnsmasq"])
80
+
81
+
82
+ def sort_hosts_lines(item: dict):
83
+ """Sort list by loopback first, ip second, hostname third"""
84
+ return (
85
+ not item["ip"].is_loopback,
86
+ socket.inet_aton(str(item["ip"])),
87
+ item["hostname"],
88
+ )
89
+
90
+
91
+ def RecordNotFound(ip, hostname):
92
+ """Return HTTPException for DNS record not found"""
93
+ return HTTPException(
94
+ status_code=status.HTTP_404_NOT_FOUND,
95
+ detail=f"DNS record not found: ip={ip}, hostname={hostname}",
96
+ )
97
+
98
+
99
+ def RecordAlreadyPresent(ip, hostname):
100
+ """Return HTTPException for DNS record already present"""
101
+ return HTTPException(
102
+ status_code=status.HTTP_406_NOT_ACCEPTABLE,
103
+ detail=f"DNS record already present: ip={ip}, hostname={hostname}",
104
+ )
105
+
106
+
107
+ def validate_hostname(value: str):
108
+ """Validate hostname format"""
109
+ if not 1 <= len(value) < 64:
110
+ raise PydanticCustomError("value_error", "Invalid hostname length")
111
+
112
+ # Define pattern of DNS label
113
+ # Can begin and end with a number or letter only
114
+ # Can contain hyphens, a-z, A-Z, 0-9
115
+ # 1 - 63 chars allowed
116
+ hostname_re = re.compile(r"^[a-z0-9]([a-z-0-9-]{0,61}[a-z0-9])?$", re.IGNORECASE)
117
+
118
+ # Check that all labels match that pattern.
119
+ if not hostname_re.match(value):
120
+ raise PydanticCustomError("value_error", "Invalid hostname")
121
+ return value
122
+
123
+
124
+ HOSTNAME = Annotated[str, AfterValidator(validate_hostname)]
125
+
126
+
127
+ class DnsCreate(BaseModel):
128
+ """Model for creating DNS records"""
129
+
130
+ ip: IPv4Address
131
+ hostname: HOSTNAME
132
+
133
+
134
+ class DnsUpdate(BaseModel):
135
+ """Model for updating DNS records"""
136
+
137
+ ip: IPv4Address
138
+ hostname: HOSTNAME
139
+
140
+
141
+ class DnsView(BaseModel):
142
+ """Model for viewing DNS records"""
143
+
144
+ ip: IPv4Address
145
+ hostname: str
146
+
147
+
148
+ # API Router
149
+ dns_router = APIRouter(prefix="/dns", tags=["dns"])
150
+
151
+
152
+ @dns_router.get("", response_model=list[DnsView])
153
+ def dns_get_all() -> list[DnsView]:
154
+ """Get all DNS records"""
155
+ with get_hosts_file_object() as hosts_fo:
156
+ hosts_lines = get_hosts_lines(hosts_fo)
157
+ return hosts_lines
158
+
159
+
160
+ @dns_router.get("/{ip}/{hostname}", response_model=DnsView)
161
+ def dns_get(
162
+ ip: IPv4Address,
163
+ hostname: HOSTNAME,
164
+ ) -> DnsView:
165
+ """Get specific DNS record"""
166
+ with get_hosts_file_object() as hosts_fo:
167
+ hosts_lines = get_hosts_lines(hosts_fo)
168
+ hosts_lines = filter_hosts_file(hosts_lines, ip, hostname)
169
+ if not hosts_lines:
170
+ raise RecordNotFound(ip, hostname)
171
+ return hosts_lines[0]
172
+
173
+
174
+ @dns_router.post("", response_model=list[DnsView])
175
+ def dns_add(
176
+ dns_in: DnsCreate,
177
+ ) -> list[DnsView]:
178
+ """Add DNS record"""
179
+ with get_hosts_file_object() as hosts_fo:
180
+ hosts_lines = get_hosts_lines(hosts_fo)
181
+ if filter_hosts_file(hosts_lines, dns_in.ip, dns_in.hostname):
182
+ raise RecordAlreadyPresent(dns_in.ip, dns_in.hostname)
183
+ hosts_lines.append({"ip": dns_in.ip, "hostname": dns_in.hostname})
184
+ write_hosts_file(hosts_fo, hosts_lines)
185
+ dnsmasq_sighup()
186
+ return hosts_lines
187
+
188
+
189
+ @dns_router.put("/{ip}/{hostname}", response_model=list[DnsView])
190
+ def dns_update(
191
+ ip: IPv4Address,
192
+ hostname: HOSTNAME,
193
+ dns_in: DnsUpdate,
194
+ ) -> list[DnsView]:
195
+ """Update DNS record"""
196
+ with get_hosts_file_object() as hosts_fo:
197
+ hosts_lines = get_hosts_lines(hosts_fo)
198
+ key = {"ip": ip, "hostname": hostname}
199
+ if key not in hosts_lines:
200
+ raise RecordNotFound(ip, hostname)
201
+ ix = hosts_lines.index(key)
202
+ hosts_lines[ix] = {"ip": dns_in.ip, "hostname": dns_in.hostname}
203
+ write_hosts_file(hosts_fo, hosts_lines)
204
+ dnsmasq_sighup()
205
+ return hosts_lines
206
+
207
+
208
+ @dns_router.delete("/{ip}/{hostname}", response_model=list[DnsView])
209
+ def dns_delete(
210
+ ip: IPv4Address,
211
+ hostname: HOSTNAME,
212
+ ) -> list[DnsView]:
213
+ """Delete DNS record"""
214
+ with get_hosts_file_object() as hosts_fo:
215
+ hosts_lines = get_hosts_lines(hosts_fo)
216
+ key = {"ip": ip, "hostname": hostname}
217
+ if key not in hosts_lines:
218
+ raise RecordNotFound(ip, hostname)
219
+ hosts_lines.remove(key)
220
+ write_hosts_file(hosts_fo, hosts_lines)
221
+ dnsmasq_sighup()
222
+ return hosts_lines
@@ -0,0 +1,83 @@
1
+ import os
2
+ import re
3
+ import subprocess
4
+ from typing import Annotated
5
+
6
+ import uvicorn
7
+ from fastapi import Depends, FastAPI, HTTPException, Security, status
8
+ from fastapi.routing import APIRoute
9
+ from fastapi.security.api_key import APIKey, APIKeyHeader
10
+
11
+ from zboxapi import __version__
12
+ from zboxapi.dns import dns_router
13
+ from zboxapi.vlan import vlan_router
14
+
15
+ api_key_header = APIKeyHeader(name="access_token", auto_error=False)
16
+
17
+
18
+ def validate_api_key(api_key: Annotated[APIKey, Security(api_key_header)]):
19
+ """Validate API key for authentication"""
20
+ if api_key != ZPOD_PASSWORD:
21
+ raise HTTPException(
22
+ status_code=status.HTTP_403_FORBIDDEN,
23
+ detail="Invalid access_token",
24
+ )
25
+
26
+
27
+ def get_zpod_password():
28
+ """Retrieve zpod password from VMware tools"""
29
+ ovfenv = subprocess.run(
30
+ ["vmtoolsd", "--cmd", "info-get guestinfo.ovfenv"],
31
+ capture_output=True,
32
+ text=True,
33
+ )
34
+ pw_re = re.compile(r'<Property oe:key="guestinfo.password" oe:value="([^"]*)"/>')
35
+ if item := re.search(pw_re, ovfenv.stdout):
36
+ return item[1]
37
+ raise Exception("Unable to retrieve zpod password")
38
+
39
+
40
+ def simplify_operation_ids(api: FastAPI) -> None:
41
+ """
42
+ Update operation IDs so that generated API clients have simpler function
43
+ names.
44
+ """
45
+ for route in api.routes:
46
+ if isinstance(route, APIRoute) and not route.operation_id:
47
+ tag = route.tags[0] if route.tags else "default"
48
+ route.operation_id = f"{tag}_{route.name}"
49
+
50
+
51
+ # Get zpod password for authentication
52
+ ZPOD_PASSWORD = get_zpod_password()
53
+
54
+ # Get root path from environment
55
+ zboxapi_root_path = os.getenv("ZBOXAPI_ROOT_PATH", None)
56
+
57
+ # Create FastAPI application
58
+ app = FastAPI(
59
+ title="zBox API",
60
+ root_path=zboxapi_root_path,
61
+ dependencies=[Depends(validate_api_key)],
62
+ version=__version__,
63
+ )
64
+
65
+ # Include routers
66
+ app.include_router(dns_router)
67
+ app.include_router(vlan_router)
68
+
69
+ # Simplify operation IDs
70
+ simplify_operation_ids(app)
71
+
72
+
73
+ def launch():
74
+ """Launch the FastAPI application with uvicorn"""
75
+ uvicorn.run(
76
+ app,
77
+ host="127.0.0.1",
78
+ port=8000,
79
+ )
80
+
81
+
82
+ if __name__ == "__main__":
83
+ launch()