zboxapi 0.0.6__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 +135 -0
- zboxapi-0.0.7/README.md +118 -0
- {zboxapi-0.0.6 → zboxapi-0.0.7}/pyproject.toml +1 -1
- zboxapi-0.0.7/src/zboxapi/__init__.py +1 -0
- zboxapi-0.0.6/src/zboxapi/main.py → zboxapi-0.0.7/src/zboxapi/dns.py +26 -76
- zboxapi-0.0.7/src/zboxapi/main.py +83 -0
- zboxapi-0.0.7/src/zboxapi/vlan.py +709 -0
- zboxapi-0.0.6/PKG-INFO +0 -51
- zboxapi-0.0.6/README.md +0 -34
- zboxapi-0.0.6/src/zboxapi/__init__.py +0 -1
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
|
+
|
zboxapi-0.0.7/README.md
ADDED
|
@@ -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
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.0.7"
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import contextlib
|
|
2
2
|
import fcntl
|
|
3
|
-
import os
|
|
4
3
|
import re
|
|
5
4
|
import socket
|
|
6
5
|
import subprocess
|
|
@@ -9,20 +8,14 @@ from ipaddress import IPv4Address
|
|
|
9
8
|
from pathlib import Path
|
|
10
9
|
from typing import IO, Annotated
|
|
11
10
|
|
|
12
|
-
import
|
|
13
|
-
from fastapi import APIRouter, Depends, FastAPI, HTTPException, Security, status
|
|
14
|
-
from fastapi.routing import APIRoute
|
|
15
|
-
from fastapi.security.api_key import APIKey, APIKeyHeader
|
|
11
|
+
from fastapi import APIRouter, HTTPException, status
|
|
16
12
|
from pydantic import AfterValidator, BaseModel
|
|
17
13
|
from pydantic_core import PydanticCustomError
|
|
18
14
|
|
|
19
|
-
from zboxapi import __version__
|
|
20
|
-
|
|
21
|
-
api_key_header = APIKeyHeader(name="access_token", auto_error=False)
|
|
22
|
-
|
|
23
15
|
|
|
24
16
|
@contextlib.contextmanager
|
|
25
17
|
def get_hosts_file_object():
|
|
18
|
+
"""Context manager for safely handling /etc/hosts file"""
|
|
26
19
|
pfile = Path("/etc/hosts")
|
|
27
20
|
if not pfile.is_file():
|
|
28
21
|
pfile.write_text("")
|
|
@@ -44,6 +37,7 @@ def get_hosts_file_object():
|
|
|
44
37
|
|
|
45
38
|
|
|
46
39
|
def get_hosts_lines(hosts_fo: IO) -> list[dict[str, str]]:
|
|
40
|
+
"""Parse hosts file and return list of IP-hostname mappings"""
|
|
47
41
|
lines = []
|
|
48
42
|
for line in hosts_fo.read().splitlines():
|
|
49
43
|
line = line.strip()
|
|
@@ -62,6 +56,7 @@ def filter_hosts_file(
|
|
|
62
56
|
ip: IPv4Address | None = None,
|
|
63
57
|
hostname: str | None = None,
|
|
64
58
|
):
|
|
59
|
+
"""Filter hosts file lines by IP and/or hostname"""
|
|
65
60
|
return [
|
|
66
61
|
line
|
|
67
62
|
for line in lines
|
|
@@ -71,33 +66,15 @@ def filter_hosts_file(
|
|
|
71
66
|
|
|
72
67
|
|
|
73
68
|
def write_hosts_file(hosts_fo: IO, lines: list[dict[str, str]]):
|
|
69
|
+
"""Write hosts file lines back to file"""
|
|
74
70
|
lines.sort(key=sort_hosts_lines)
|
|
75
71
|
hosts_fo.seek(0)
|
|
76
72
|
hosts_fo.truncate()
|
|
77
73
|
hosts_fo.writelines([f"{line['ip']}\t{line['hostname']}\n" for line in lines])
|
|
78
74
|
|
|
79
75
|
|
|
80
|
-
def validate_api_key(api_key: Annotated[APIKey, Security(api_key_header)]):
|
|
81
|
-
if api_key != ZPOD_PASSWORD:
|
|
82
|
-
raise HTTPException(
|
|
83
|
-
status_code=status.HTTP_403_FORBIDDEN,
|
|
84
|
-
detail="Invalid access_token",
|
|
85
|
-
)
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
def get_zpod_password():
|
|
89
|
-
ovfenv = subprocess.run(
|
|
90
|
-
["vmtoolsd", "--cmd", "info-get guestinfo.ovfenv"],
|
|
91
|
-
capture_output=True,
|
|
92
|
-
text=True,
|
|
93
|
-
)
|
|
94
|
-
pw_re = re.compile(r'<Property oe:key="guestinfo.password" oe:value="([^"]*)"/>')
|
|
95
|
-
if item := re.search(pw_re, ovfenv.stdout):
|
|
96
|
-
return item[1]
|
|
97
|
-
raise Exception("Unable to retrieve zpod password")
|
|
98
|
-
|
|
99
|
-
|
|
100
76
|
def dnsmasq_sighup():
|
|
77
|
+
"""Send SIGHUP to dnsmasq to reload configuration"""
|
|
101
78
|
print("Send SIGHUP to dnsmasq...")
|
|
102
79
|
subprocess.call(["pkill", "-SIGHUP", "dnsmasq"])
|
|
103
80
|
|
|
@@ -111,18 +88,8 @@ def sort_hosts_lines(item: dict):
|
|
|
111
88
|
)
|
|
112
89
|
|
|
113
90
|
|
|
114
|
-
def simplify_operation_ids(api: FastAPI) -> None:
|
|
115
|
-
"""
|
|
116
|
-
Update operation IDs so that generated API clients have simpler function
|
|
117
|
-
names.
|
|
118
|
-
"""
|
|
119
|
-
for route in api.routes:
|
|
120
|
-
if isinstance(route, APIRoute) and not route.operation_id:
|
|
121
|
-
tag = route.tags[0] if route.tags else "default"
|
|
122
|
-
route.operation_id = f"{tag}_{route.name}"
|
|
123
|
-
|
|
124
|
-
|
|
125
91
|
def RecordNotFound(ip, hostname):
|
|
92
|
+
"""Return HTTPException for DNS record not found"""
|
|
126
93
|
return HTTPException(
|
|
127
94
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
128
95
|
detail=f"DNS record not found: ip={ip}, hostname={hostname}",
|
|
@@ -130,6 +97,7 @@ def RecordNotFound(ip, hostname):
|
|
|
130
97
|
|
|
131
98
|
|
|
132
99
|
def RecordAlreadyPresent(ip, hostname):
|
|
100
|
+
"""Return HTTPException for DNS record already present"""
|
|
133
101
|
return HTTPException(
|
|
134
102
|
status_code=status.HTTP_406_NOT_ACCEPTABLE,
|
|
135
103
|
detail=f"DNS record already present: ip={ip}, hostname={hostname}",
|
|
@@ -137,7 +105,8 @@ def RecordAlreadyPresent(ip, hostname):
|
|
|
137
105
|
|
|
138
106
|
|
|
139
107
|
def validate_hostname(value: str):
|
|
140
|
-
|
|
108
|
+
"""Validate hostname format"""
|
|
109
|
+
if not 1 <= len(value) < 64:
|
|
141
110
|
raise PydanticCustomError("value_error", "Invalid hostname length")
|
|
142
111
|
|
|
143
112
|
# Define pattern of DNS label
|
|
@@ -156,40 +125,44 @@ HOSTNAME = Annotated[str, AfterValidator(validate_hostname)]
|
|
|
156
125
|
|
|
157
126
|
|
|
158
127
|
class DnsCreate(BaseModel):
|
|
159
|
-
|
|
160
|
-
hostname: HOSTNAME
|
|
128
|
+
"""Model for creating DNS records"""
|
|
161
129
|
|
|
162
|
-
|
|
163
|
-
class DnsDelete(BaseModel):
|
|
164
130
|
ip: IPv4Address
|
|
165
131
|
hostname: HOSTNAME
|
|
166
132
|
|
|
167
133
|
|
|
168
134
|
class DnsUpdate(BaseModel):
|
|
135
|
+
"""Model for updating DNS records"""
|
|
136
|
+
|
|
169
137
|
ip: IPv4Address
|
|
170
138
|
hostname: HOSTNAME
|
|
171
139
|
|
|
172
140
|
|
|
173
141
|
class DnsView(BaseModel):
|
|
142
|
+
"""Model for viewing DNS records"""
|
|
143
|
+
|
|
174
144
|
ip: IPv4Address
|
|
175
145
|
hostname: str
|
|
176
146
|
|
|
177
147
|
|
|
148
|
+
# API Router
|
|
178
149
|
dns_router = APIRouter(prefix="/dns", tags=["dns"])
|
|
179
150
|
|
|
180
151
|
|
|
181
|
-
@dns_router.get("")
|
|
152
|
+
@dns_router.get("", response_model=list[DnsView])
|
|
182
153
|
def dns_get_all() -> list[DnsView]:
|
|
154
|
+
"""Get all DNS records"""
|
|
183
155
|
with get_hosts_file_object() as hosts_fo:
|
|
184
156
|
hosts_lines = get_hosts_lines(hosts_fo)
|
|
185
157
|
return hosts_lines
|
|
186
158
|
|
|
187
159
|
|
|
188
|
-
@dns_router.get("/{ip}/{hostname}")
|
|
160
|
+
@dns_router.get("/{ip}/{hostname}", response_model=DnsView)
|
|
189
161
|
def dns_get(
|
|
190
162
|
ip: IPv4Address,
|
|
191
163
|
hostname: HOSTNAME,
|
|
192
164
|
) -> DnsView:
|
|
165
|
+
"""Get specific DNS record"""
|
|
193
166
|
with get_hosts_file_object() as hosts_fo:
|
|
194
167
|
hosts_lines = get_hosts_lines(hosts_fo)
|
|
195
168
|
hosts_lines = filter_hosts_file(hosts_lines, ip, hostname)
|
|
@@ -198,10 +171,11 @@ def dns_get(
|
|
|
198
171
|
return hosts_lines[0]
|
|
199
172
|
|
|
200
173
|
|
|
201
|
-
@dns_router.post("")
|
|
174
|
+
@dns_router.post("", response_model=list[DnsView])
|
|
202
175
|
def dns_add(
|
|
203
176
|
dns_in: DnsCreate,
|
|
204
177
|
) -> list[DnsView]:
|
|
178
|
+
"""Add DNS record"""
|
|
205
179
|
with get_hosts_file_object() as hosts_fo:
|
|
206
180
|
hosts_lines = get_hosts_lines(hosts_fo)
|
|
207
181
|
if filter_hosts_file(hosts_lines, dns_in.ip, dns_in.hostname):
|
|
@@ -212,12 +186,13 @@ def dns_add(
|
|
|
212
186
|
return hosts_lines
|
|
213
187
|
|
|
214
188
|
|
|
215
|
-
@dns_router.put("/{ip}/{hostname}")
|
|
189
|
+
@dns_router.put("/{ip}/{hostname}", response_model=list[DnsView])
|
|
216
190
|
def dns_update(
|
|
217
191
|
ip: IPv4Address,
|
|
218
192
|
hostname: HOSTNAME,
|
|
219
193
|
dns_in: DnsUpdate,
|
|
220
194
|
) -> list[DnsView]:
|
|
195
|
+
"""Update DNS record"""
|
|
221
196
|
with get_hosts_file_object() as hosts_fo:
|
|
222
197
|
hosts_lines = get_hosts_lines(hosts_fo)
|
|
223
198
|
key = {"ip": ip, "hostname": hostname}
|
|
@@ -230,11 +205,12 @@ def dns_update(
|
|
|
230
205
|
return hosts_lines
|
|
231
206
|
|
|
232
207
|
|
|
233
|
-
@dns_router.delete("/{ip}/{hostname}")
|
|
208
|
+
@dns_router.delete("/{ip}/{hostname}", response_model=list[DnsView])
|
|
234
209
|
def dns_delete(
|
|
235
210
|
ip: IPv4Address,
|
|
236
211
|
hostname: HOSTNAME,
|
|
237
212
|
) -> list[DnsView]:
|
|
213
|
+
"""Delete DNS record"""
|
|
238
214
|
with get_hosts_file_object() as hosts_fo:
|
|
239
215
|
hosts_lines = get_hosts_lines(hosts_fo)
|
|
240
216
|
key = {"ip": ip, "hostname": hostname}
|
|
@@ -244,29 +220,3 @@ def dns_delete(
|
|
|
244
220
|
write_hosts_file(hosts_fo, hosts_lines)
|
|
245
221
|
dnsmasq_sighup()
|
|
246
222
|
return hosts_lines
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
zboxapi_root_path = os.getenv("ZBOXAPI_ROOT_PATH", None)
|
|
250
|
-
|
|
251
|
-
app = FastAPI(
|
|
252
|
-
title="zBox API",
|
|
253
|
-
root_path=zboxapi_root_path,
|
|
254
|
-
dependencies=[Depends(validate_api_key)],
|
|
255
|
-
version=__version__,
|
|
256
|
-
)
|
|
257
|
-
app.include_router(dns_router)
|
|
258
|
-
simplify_operation_ids(app)
|
|
259
|
-
|
|
260
|
-
ZPOD_PASSWORD = get_zpod_password()
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
def launch():
|
|
264
|
-
uvicorn.run(
|
|
265
|
-
app,
|
|
266
|
-
host="127.0.0.1",
|
|
267
|
-
port=8000,
|
|
268
|
-
)
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
if __name__ == "__main__":
|
|
272
|
-
launch()
|
|
@@ -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()
|
|
@@ -0,0 +1,709 @@
|
|
|
1
|
+
import configparser
|
|
2
|
+
import contextlib
|
|
3
|
+
import fcntl
|
|
4
|
+
import ipaddress
|
|
5
|
+
import re
|
|
6
|
+
import subprocess
|
|
7
|
+
import time
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Annotated
|
|
10
|
+
|
|
11
|
+
from fastapi import APIRouter, HTTPException, status
|
|
12
|
+
from pydantic import AfterValidator, BaseModel, Field
|
|
13
|
+
from pydantic_core import PydanticCustomError
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ConfigError(Exception):
|
|
17
|
+
"""Configuration error"""
|
|
18
|
+
|
|
19
|
+
pass
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class NetworkError(Exception):
|
|
23
|
+
"""Network configuration error"""
|
|
24
|
+
|
|
25
|
+
pass
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def load_config() -> configparser.ConfigParser:
|
|
29
|
+
"""Load configuration from /etc/zboxapi.conf"""
|
|
30
|
+
config = configparser.ConfigParser()
|
|
31
|
+
config_path = Path("/etc/zboxapi.conf")
|
|
32
|
+
|
|
33
|
+
if not config_path.exists():
|
|
34
|
+
raise ConfigError("Configuration file /etc/zboxapi.conf not found")
|
|
35
|
+
|
|
36
|
+
config.read(config_path)
|
|
37
|
+
return config
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def get_config_value(
|
|
41
|
+
config: configparser.ConfigParser, section: str, key: str, default: str = None
|
|
42
|
+
) -> str:
|
|
43
|
+
"""Get configuration value with optional default"""
|
|
44
|
+
try:
|
|
45
|
+
return config.get(section, key)
|
|
46
|
+
except (configparser.NoSectionError, configparser.NoOptionError):
|
|
47
|
+
if default is not None:
|
|
48
|
+
return default
|
|
49
|
+
raise ConfigError(f"Configuration missing: [{section}] {key}") from None
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def get_system_vlans_default() -> list[int]:
|
|
53
|
+
"""Get system default VLANs from configuration"""
|
|
54
|
+
config = load_config()
|
|
55
|
+
system_vlans_str = get_config_value(
|
|
56
|
+
config, "DEFAULT", "system_vlans_default", "10,20,30"
|
|
57
|
+
)
|
|
58
|
+
try:
|
|
59
|
+
return [int(v.strip()) for v in system_vlans_str.split(",")]
|
|
60
|
+
except ValueError as e:
|
|
61
|
+
raise ConfigError("Invalid system_vlans_default configuration") from e
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def get_system_vlans_zpod() -> list[int]:
|
|
65
|
+
"""Get system zPod VLANs from configuration"""
|
|
66
|
+
config = load_config()
|
|
67
|
+
system_vlans_str = get_config_value(
|
|
68
|
+
config, "DEFAULT", "system_vlans_zpod", "64,128,192"
|
|
69
|
+
)
|
|
70
|
+
try:
|
|
71
|
+
return [int(v.strip()) for v in system_vlans_str.split(",")]
|
|
72
|
+
except ValueError as e:
|
|
73
|
+
raise ConfigError("Invalid system_vlans_zpod configuration") from e
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def get_interface_name() -> str:
|
|
77
|
+
"""Get interface name from configuration"""
|
|
78
|
+
config = load_config()
|
|
79
|
+
return get_config_value(config, "DEFAULT", "interface", "eth1")
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def get_mtu() -> int:
|
|
83
|
+
"""Get MTU from configuration"""
|
|
84
|
+
config = load_config()
|
|
85
|
+
try:
|
|
86
|
+
return int(get_config_value(config, "DEFAULT", "mtu", "1700"))
|
|
87
|
+
except ValueError as e:
|
|
88
|
+
raise ConfigError("Invalid MTU configuration") from e
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def validate_vlan_id(vlan_id: int) -> int:
|
|
92
|
+
"""Validate VLAN ID is in valid range and not a system VLAN"""
|
|
93
|
+
if not 1 <= vlan_id <= 4094:
|
|
94
|
+
raise PydanticCustomError("value_error", "VLAN ID must be between 1 and 4094")
|
|
95
|
+
|
|
96
|
+
system_vlans_default = get_system_vlans_default()
|
|
97
|
+
system_vlans_zpod = get_system_vlans_zpod()
|
|
98
|
+
|
|
99
|
+
if vlan_id in system_vlans_default:
|
|
100
|
+
raise PydanticCustomError(
|
|
101
|
+
"value_error",
|
|
102
|
+
f"VLAN {vlan_id} is a system default VLAN and cannot be modified",
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
if vlan_id in system_vlans_zpod:
|
|
106
|
+
raise PydanticCustomError(
|
|
107
|
+
"value_error",
|
|
108
|
+
f"VLAN {vlan_id} is a system zPod VLAN and cannot be modified",
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
return vlan_id
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def validate_cidr(cidr: str) -> str:
|
|
115
|
+
"""Validate CIDR notation, but preserve the original input."""
|
|
116
|
+
try:
|
|
117
|
+
# Parse the CIDR to validate it's correct
|
|
118
|
+
network = ipaddress.IPv4Network(cidr, strict=False)
|
|
119
|
+
# But return the original input, not the normalized network
|
|
120
|
+
return cidr
|
|
121
|
+
except ValueError as e:
|
|
122
|
+
raise PydanticCustomError("value_error", f"Invalid CIDR: {e}") from e
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def check_no_overlap(cidrs):
|
|
126
|
+
"""
|
|
127
|
+
Check a list of CIDR networks for overlaps.
|
|
128
|
+
Raises ValueError if any two networks overlap.
|
|
129
|
+
|
|
130
|
+
Args:
|
|
131
|
+
cidrs (list[str]): List of CIDR notation strings (e.g., "192.168.0.0/24").
|
|
132
|
+
|
|
133
|
+
Returns:
|
|
134
|
+
bool: True if no overlaps are found.
|
|
135
|
+
|
|
136
|
+
Raises:
|
|
137
|
+
ValueError: If any two networks overlap.
|
|
138
|
+
"""
|
|
139
|
+
# Parse strings into IPv4Network/IPv6Network objects with strict=False
|
|
140
|
+
# This allows host addresses like 172.16.10.1/24 to be converted to 172.16.10.0/24
|
|
141
|
+
networks = [ipaddress.ip_network(cidr, strict=False) for cidr in cidrs]
|
|
142
|
+
|
|
143
|
+
# Compare each pair for overlap
|
|
144
|
+
for i, net1 in enumerate(networks):
|
|
145
|
+
for net2 in networks[i + 1 :]:
|
|
146
|
+
if net1.overlaps(net2):
|
|
147
|
+
raise ValueError(f"Networks {net1} and {net2} overlap")
|
|
148
|
+
|
|
149
|
+
# No overlaps detected
|
|
150
|
+
return True
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def validate_vlan_networks(new_gateway: str, exclude_vlan_id: int = None) -> None:
|
|
154
|
+
"""Validate that a new gateway doesn't overlap with existing VLAN networks"""
|
|
155
|
+
# Get all existing VLAN gateways
|
|
156
|
+
existing_vlans = get_existing_vlans()
|
|
157
|
+
all_gateways = [new_gateway] # Include the new gateway
|
|
158
|
+
|
|
159
|
+
for vlan in existing_vlans:
|
|
160
|
+
# Skip the VLAN we're updating (if this is an update operation)
|
|
161
|
+
if exclude_vlan_id is not None and vlan.vlan == exclude_vlan_id:
|
|
162
|
+
continue
|
|
163
|
+
all_gateways.append(vlan.gateway)
|
|
164
|
+
|
|
165
|
+
# Check for overlaps
|
|
166
|
+
try:
|
|
167
|
+
check_no_overlap(all_gateways)
|
|
168
|
+
except ValueError as e:
|
|
169
|
+
raise NetworkError(f"Network overlap detected: {e}") from e
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
VLAN_ID = Annotated[int, AfterValidator(validate_vlan_id)]
|
|
173
|
+
CIDR = Annotated[str, AfterValidator(validate_cidr)]
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
class VlanCreate(BaseModel):
|
|
177
|
+
vlan: VLAN_ID = Field(..., description="VLAN ID (1-4094, excluding system VLANs)")
|
|
178
|
+
gateway: CIDR = Field(..., description="Gateway IP address in CIDR notation")
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
class VlanUpdate(BaseModel):
|
|
182
|
+
gateway: CIDR = Field(..., description="Gateway IP address in CIDR notation")
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
class VlanView(BaseModel):
|
|
186
|
+
vlan: int
|
|
187
|
+
gateway: str
|
|
188
|
+
interface: str
|
|
189
|
+
status: str
|
|
190
|
+
owner: str
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
@contextlib.contextmanager
|
|
194
|
+
def get_vlan_config_file_object(vlan_id: int):
|
|
195
|
+
"""Context manager for safely handling VLAN configuration files in /etc/network/interfaces.d/"""
|
|
196
|
+
interface_name = get_interface_name()
|
|
197
|
+
config_dir = Path("/etc/network/interfaces.d")
|
|
198
|
+
config_file = config_dir / f"{interface_name}.{vlan_id}.cfg"
|
|
199
|
+
|
|
200
|
+
# Ensure the directory exists
|
|
201
|
+
config_dir.mkdir(mode=0o755, exist_ok=True)
|
|
202
|
+
|
|
203
|
+
while True:
|
|
204
|
+
try:
|
|
205
|
+
file_handle = config_file.open("w") # Use write mode for individual files
|
|
206
|
+
fcntl.flock(file_handle, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
|
207
|
+
break
|
|
208
|
+
except IOError:
|
|
209
|
+
time.sleep(0.1)
|
|
210
|
+
|
|
211
|
+
try:
|
|
212
|
+
yield file_handle
|
|
213
|
+
finally:
|
|
214
|
+
fcntl.flock(file_handle, fcntl.LOCK_UN)
|
|
215
|
+
file_handle.close()
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def get_existing_vlans() -> list[VlanView]:
|
|
219
|
+
"""Get all existing VLAN configurations from /etc/network/interfaces.d/ and system VLANs"""
|
|
220
|
+
interface_name = get_interface_name()
|
|
221
|
+
config_dir = Path("/etc/network/interfaces.d")
|
|
222
|
+
|
|
223
|
+
vlans = []
|
|
224
|
+
|
|
225
|
+
# Add system default VLANs
|
|
226
|
+
system_vlans_default = get_system_vlans_default()
|
|
227
|
+
for vlan_id in system_vlans_default:
|
|
228
|
+
# Try to get actual gateway from ip addr command
|
|
229
|
+
gateway = "system-default"
|
|
230
|
+
interface_name_full = f"{interface_name}.{vlan_id}"
|
|
231
|
+
|
|
232
|
+
# Check if interface is currently up
|
|
233
|
+
status = "down"
|
|
234
|
+
try:
|
|
235
|
+
result = subprocess.run(
|
|
236
|
+
["ip", "link", "show", interface_name_full],
|
|
237
|
+
capture_output=True,
|
|
238
|
+
text=True,
|
|
239
|
+
check=False,
|
|
240
|
+
)
|
|
241
|
+
if result.returncode == 0 and "UP" in result.stdout:
|
|
242
|
+
status = "up"
|
|
243
|
+
except Exception:
|
|
244
|
+
pass
|
|
245
|
+
|
|
246
|
+
try:
|
|
247
|
+
result = subprocess.run(
|
|
248
|
+
["ip", "addr", "show", interface_name_full],
|
|
249
|
+
capture_output=True,
|
|
250
|
+
text=True,
|
|
251
|
+
check=False,
|
|
252
|
+
)
|
|
253
|
+
if result.returncode == 0:
|
|
254
|
+
# Extract IP address from ip addr output
|
|
255
|
+
ip_match = re.search(r"inet\s+(\d+\.\d+\.\d+\.\d+/\d+)", result.stdout)
|
|
256
|
+
if ip_match:
|
|
257
|
+
gateway = ip_match.group(1)
|
|
258
|
+
except Exception:
|
|
259
|
+
pass
|
|
260
|
+
|
|
261
|
+
vlans.append(
|
|
262
|
+
VlanView(
|
|
263
|
+
vlan=vlan_id,
|
|
264
|
+
gateway=gateway,
|
|
265
|
+
interface=interface_name_full,
|
|
266
|
+
status=status,
|
|
267
|
+
owner="system-default",
|
|
268
|
+
)
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
# Add system zPod VLANs
|
|
272
|
+
system_vlans_zpod = get_system_vlans_zpod()
|
|
273
|
+
for vlan_id in system_vlans_zpod:
|
|
274
|
+
# Try to get actual gateway from ip addr command
|
|
275
|
+
gateway = "system-zpod"
|
|
276
|
+
interface_name_full = f"{interface_name}.{vlan_id}"
|
|
277
|
+
|
|
278
|
+
# Check if interface is currently up
|
|
279
|
+
status = "down"
|
|
280
|
+
try:
|
|
281
|
+
result = subprocess.run(
|
|
282
|
+
["ip", "link", "show", interface_name_full],
|
|
283
|
+
capture_output=True,
|
|
284
|
+
text=True,
|
|
285
|
+
check=False,
|
|
286
|
+
)
|
|
287
|
+
if result.returncode == 0 and "UP" in result.stdout:
|
|
288
|
+
status = "up"
|
|
289
|
+
except Exception:
|
|
290
|
+
pass
|
|
291
|
+
|
|
292
|
+
try:
|
|
293
|
+
result = subprocess.run(
|
|
294
|
+
["ip", "addr", "show", interface_name_full],
|
|
295
|
+
capture_output=True,
|
|
296
|
+
text=True,
|
|
297
|
+
check=False,
|
|
298
|
+
)
|
|
299
|
+
if result.returncode == 0:
|
|
300
|
+
# Extract IP address from ip addr output
|
|
301
|
+
ip_match = re.search(r"inet\s+(\d+\.\d+\.\d+\.\d+/\d+)", result.stdout)
|
|
302
|
+
if ip_match:
|
|
303
|
+
gateway = ip_match.group(1)
|
|
304
|
+
except Exception:
|
|
305
|
+
pass
|
|
306
|
+
|
|
307
|
+
vlans.append(
|
|
308
|
+
VlanView(
|
|
309
|
+
vlan=vlan_id,
|
|
310
|
+
gateway=gateway,
|
|
311
|
+
interface=interface_name_full,
|
|
312
|
+
status=status,
|
|
313
|
+
owner="system-zpod",
|
|
314
|
+
)
|
|
315
|
+
)
|
|
316
|
+
|
|
317
|
+
# Add user-configured VLANs from /etc/network/interfaces.d/
|
|
318
|
+
if config_dir.exists():
|
|
319
|
+
# Find all VLAN configuration files
|
|
320
|
+
vlan_pattern = rf"{re.escape(interface_name)}\.(\d+)\.cfg$"
|
|
321
|
+
|
|
322
|
+
for config_file in config_dir.glob(f"{interface_name}.*.cfg"):
|
|
323
|
+
match = re.match(vlan_pattern, config_file.name)
|
|
324
|
+
if not match:
|
|
325
|
+
continue
|
|
326
|
+
|
|
327
|
+
vlan_id = int(match.group(1))
|
|
328
|
+
|
|
329
|
+
# Skip if this is a system VLAN (already added above)
|
|
330
|
+
if vlan_id in system_vlans_default or vlan_id in system_vlans_zpod:
|
|
331
|
+
continue
|
|
332
|
+
|
|
333
|
+
try:
|
|
334
|
+
with open(config_file, "r") as f:
|
|
335
|
+
content = f.read()
|
|
336
|
+
|
|
337
|
+
# Extract gateway from the configuration
|
|
338
|
+
gateway_match = re.search(
|
|
339
|
+
r"address\s+(\d+\.\d+\.\d+\.\d+/\d+)", content
|
|
340
|
+
)
|
|
341
|
+
if not gateway_match:
|
|
342
|
+
continue
|
|
343
|
+
|
|
344
|
+
gateway = gateway_match.group(1)
|
|
345
|
+
|
|
346
|
+
# Check if interface is currently up
|
|
347
|
+
status = "down"
|
|
348
|
+
try:
|
|
349
|
+
result = subprocess.run(
|
|
350
|
+
["ip", "link", "show", f"{interface_name}.{vlan_id}"],
|
|
351
|
+
capture_output=True,
|
|
352
|
+
text=True,
|
|
353
|
+
check=False,
|
|
354
|
+
)
|
|
355
|
+
if result.returncode == 0 and "UP" in result.stdout:
|
|
356
|
+
status = "up"
|
|
357
|
+
except Exception:
|
|
358
|
+
pass
|
|
359
|
+
|
|
360
|
+
vlans.append(
|
|
361
|
+
VlanView(
|
|
362
|
+
vlan=vlan_id,
|
|
363
|
+
gateway=gateway,
|
|
364
|
+
interface=f"{interface_name}.{vlan_id}",
|
|
365
|
+
status=status,
|
|
366
|
+
owner="user-defined",
|
|
367
|
+
)
|
|
368
|
+
)
|
|
369
|
+
except Exception:
|
|
370
|
+
continue # Skip files that can't be read or parsed
|
|
371
|
+
|
|
372
|
+
return sorted(vlans, key=lambda x: x.vlan)
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
def add_vlan_interface(vlan_id: int, gateway: str) -> None:
|
|
376
|
+
"""Add VLAN interface configuration to /etc/network/interfaces.d/"""
|
|
377
|
+
interface_name = get_interface_name()
|
|
378
|
+
mtu = get_mtu()
|
|
379
|
+
|
|
380
|
+
# Validate inputs - check for overlaps and gateway uniqueness
|
|
381
|
+
validate_vlan_networks(gateway)
|
|
382
|
+
|
|
383
|
+
# Check if VLAN configuration file already exists
|
|
384
|
+
config_dir = Path("/etc/network/interfaces.d")
|
|
385
|
+
config_file = config_dir / f"{interface_name}.{vlan_id}.cfg"
|
|
386
|
+
|
|
387
|
+
if config_file.exists():
|
|
388
|
+
raise NetworkError(f"VLAN interface {interface_name}.{vlan_id} already exists")
|
|
389
|
+
|
|
390
|
+
# Create VLAN configuration
|
|
391
|
+
vlan_config = f"""auto {interface_name}.{vlan_id}
|
|
392
|
+
iface {interface_name}.{vlan_id} inet static
|
|
393
|
+
address {gateway}
|
|
394
|
+
mtu {mtu}
|
|
395
|
+
"""
|
|
396
|
+
|
|
397
|
+
# Write to dedicated configuration file
|
|
398
|
+
with get_vlan_config_file_object(vlan_id) as f:
|
|
399
|
+
f.write(vlan_config)
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
def update_vlan_interface(vlan_id: int, gateway: str) -> None:
|
|
403
|
+
"""Update VLAN interface configuration in /etc/network/interfaces.d/"""
|
|
404
|
+
interface_name = get_interface_name()
|
|
405
|
+
mtu = get_mtu()
|
|
406
|
+
|
|
407
|
+
# Validate inputs - check for overlaps and gateway uniqueness, excluding current VLAN
|
|
408
|
+
validate_vlan_networks(gateway, exclude_vlan_id=vlan_id)
|
|
409
|
+
|
|
410
|
+
# Check if VLAN configuration file exists
|
|
411
|
+
config_dir = Path("/etc/network/interfaces.d")
|
|
412
|
+
config_file = config_dir / f"{interface_name}.{vlan_id}.cfg"
|
|
413
|
+
|
|
414
|
+
if not config_file.exists():
|
|
415
|
+
raise NetworkError(f"VLAN interface {interface_name}.{vlan_id} does not exist")
|
|
416
|
+
|
|
417
|
+
# Create updated VLAN configuration
|
|
418
|
+
vlan_config = f"""auto {interface_name}.{vlan_id}
|
|
419
|
+
iface {interface_name}.{vlan_id} inet static
|
|
420
|
+
address {gateway}
|
|
421
|
+
mtu {mtu}
|
|
422
|
+
"""
|
|
423
|
+
|
|
424
|
+
# Write updated configuration to file
|
|
425
|
+
with get_vlan_config_file_object(vlan_id) as f:
|
|
426
|
+
f.write(vlan_config)
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
def delete_vlan_interface(vlan_id: int) -> None:
|
|
430
|
+
"""Delete VLAN interface configuration from /etc/network/interfaces.d/"""
|
|
431
|
+
interface_name = get_interface_name()
|
|
432
|
+
config_dir = Path("/etc/network/interfaces.d")
|
|
433
|
+
config_file = config_dir / f"{interface_name}.{vlan_id}.cfg"
|
|
434
|
+
|
|
435
|
+
if not config_file.exists():
|
|
436
|
+
raise NetworkError(f"VLAN interface {interface_name}.{vlan_id} does not exist")
|
|
437
|
+
|
|
438
|
+
interface_name_full = f"{interface_name}.{vlan_id}"
|
|
439
|
+
|
|
440
|
+
# Step 1: Bring interface down
|
|
441
|
+
try:
|
|
442
|
+
bring_interface_down(interface_name_full)
|
|
443
|
+
print(f"Interface {interface_name_full} brought down")
|
|
444
|
+
except Exception as e:
|
|
445
|
+
print(f"Warning: Could not bring down interface {interface_name_full}: {e}")
|
|
446
|
+
# Continue with deletion even if interface is already down
|
|
447
|
+
|
|
448
|
+
# Step 2: Delete the configuration file
|
|
449
|
+
try:
|
|
450
|
+
config_file.unlink()
|
|
451
|
+
print(f"Configuration file {config_file} deleted")
|
|
452
|
+
except Exception as e:
|
|
453
|
+
raise NetworkError(f"Failed to delete VLAN configuration file: {e}") from e
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
def bring_interface_up(interface_name: str) -> None:
|
|
457
|
+
"""Bring up a network interface"""
|
|
458
|
+
try:
|
|
459
|
+
subprocess.run(
|
|
460
|
+
["ifup", interface_name], check=True, capture_output=True, text=True
|
|
461
|
+
)
|
|
462
|
+
except subprocess.CalledProcessError as e:
|
|
463
|
+
raise NetworkError(
|
|
464
|
+
f"Failed to bring up interface {interface_name}: {e.stderr}"
|
|
465
|
+
) from e
|
|
466
|
+
|
|
467
|
+
|
|
468
|
+
def bring_interface_down(interface_name: str) -> None:
|
|
469
|
+
"""Bring down a network interface"""
|
|
470
|
+
try:
|
|
471
|
+
subprocess.run(
|
|
472
|
+
["ifdown", interface_name], check=True, capture_output=True, text=True
|
|
473
|
+
)
|
|
474
|
+
except subprocess.CalledProcessError as e:
|
|
475
|
+
raise NetworkError(
|
|
476
|
+
f"Failed to bring down interface {interface_name}: {e.stderr}"
|
|
477
|
+
) from e
|
|
478
|
+
|
|
479
|
+
|
|
480
|
+
# API Router
|
|
481
|
+
vlan_router = APIRouter(prefix="/vlan", tags=["vlan"])
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
@vlan_router.get("", response_model=list[VlanView])
|
|
485
|
+
def vlan_get_all() -> list[VlanView]:
|
|
486
|
+
"""Get all VLAN interfaces"""
|
|
487
|
+
try:
|
|
488
|
+
return get_existing_vlans()
|
|
489
|
+
except Exception as e:
|
|
490
|
+
raise HTTPException(
|
|
491
|
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
492
|
+
detail=f"Failed to get VLAN interfaces: {str(e)}",
|
|
493
|
+
) from e
|
|
494
|
+
|
|
495
|
+
|
|
496
|
+
@vlan_router.get("/{vlan_id}", response_model=VlanView)
|
|
497
|
+
def vlan_get(vlan_id: int) -> VlanView:
|
|
498
|
+
"""Get specific VLAN interface"""
|
|
499
|
+
try:
|
|
500
|
+
vlans = get_existing_vlans()
|
|
501
|
+
for vlan in vlans:
|
|
502
|
+
if vlan.vlan == vlan_id:
|
|
503
|
+
return vlan
|
|
504
|
+
|
|
505
|
+
raise HTTPException(
|
|
506
|
+
status_code=status.HTTP_404_NOT_FOUND, detail=f"VLAN {vlan_id} not found"
|
|
507
|
+
)
|
|
508
|
+
except HTTPException:
|
|
509
|
+
raise
|
|
510
|
+
except Exception as e:
|
|
511
|
+
raise HTTPException(
|
|
512
|
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
513
|
+
detail=f"Failed to get VLAN {vlan_id}: {str(e)}",
|
|
514
|
+
) from e
|
|
515
|
+
|
|
516
|
+
|
|
517
|
+
@vlan_router.post("", response_model=VlanView)
|
|
518
|
+
def vlan_create(vlan_in: VlanCreate) -> VlanView:
|
|
519
|
+
"""Create a new VLAN interface"""
|
|
520
|
+
try:
|
|
521
|
+
# Validate VLAN ID
|
|
522
|
+
validate_vlan_id(vlan_in.vlan)
|
|
523
|
+
|
|
524
|
+
# Add VLAN interface configuration
|
|
525
|
+
add_vlan_interface(vlan_in.vlan, vlan_in.gateway)
|
|
526
|
+
|
|
527
|
+
# Bring up the interface
|
|
528
|
+
interface_name = get_interface_name()
|
|
529
|
+
vlan_interface = f"{interface_name}.{vlan_in.vlan}"
|
|
530
|
+
bring_interface_up(vlan_interface)
|
|
531
|
+
|
|
532
|
+
# Return the created VLAN
|
|
533
|
+
return VlanView(
|
|
534
|
+
vlan=vlan_in.vlan,
|
|
535
|
+
gateway=vlan_in.gateway,
|
|
536
|
+
interface=vlan_interface,
|
|
537
|
+
status="up",
|
|
538
|
+
owner="user-defined",
|
|
539
|
+
)
|
|
540
|
+
except (ConfigError, NetworkError) as e:
|
|
541
|
+
raise HTTPException(
|
|
542
|
+
status_code=status.HTTP_400_BAD_REQUEST, detail=str(e)
|
|
543
|
+
) from e
|
|
544
|
+
except Exception as e:
|
|
545
|
+
raise HTTPException(
|
|
546
|
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
547
|
+
detail=f"Failed to create VLAN: {str(e)}",
|
|
548
|
+
) from e
|
|
549
|
+
|
|
550
|
+
|
|
551
|
+
@vlan_router.put("/{vlan_id}", response_model=VlanView)
|
|
552
|
+
def vlan_update(vlan_id: int, vlan_in: VlanUpdate) -> VlanView:
|
|
553
|
+
"""Update a VLAN interface"""
|
|
554
|
+
try:
|
|
555
|
+
# Validate VLAN ID
|
|
556
|
+
validate_vlan_id(vlan_id)
|
|
557
|
+
|
|
558
|
+
# Update VLAN interface configuration
|
|
559
|
+
update_vlan_interface(vlan_id, vlan_in.gateway)
|
|
560
|
+
|
|
561
|
+
# Restart the interface
|
|
562
|
+
interface_name = get_interface_name()
|
|
563
|
+
vlan_interface = f"{interface_name}.{vlan_id}"
|
|
564
|
+
bring_interface_down(vlan_interface)
|
|
565
|
+
bring_interface_up(vlan_interface)
|
|
566
|
+
|
|
567
|
+
# Return the updated VLAN
|
|
568
|
+
return VlanView(
|
|
569
|
+
vlan=vlan_id,
|
|
570
|
+
gateway=vlan_in.gateway,
|
|
571
|
+
interface=vlan_interface,
|
|
572
|
+
status="up",
|
|
573
|
+
owner="user-defined",
|
|
574
|
+
)
|
|
575
|
+
except (ConfigError, NetworkError) as e:
|
|
576
|
+
raise HTTPException(
|
|
577
|
+
status_code=status.HTTP_400_BAD_REQUEST, detail=str(e)
|
|
578
|
+
) from e
|
|
579
|
+
except Exception as e:
|
|
580
|
+
raise HTTPException(
|
|
581
|
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
582
|
+
detail=f"Failed to update VLAN {vlan_id}: {str(e)}",
|
|
583
|
+
) from e
|
|
584
|
+
|
|
585
|
+
|
|
586
|
+
@vlan_router.delete("/{vlan_id}")
|
|
587
|
+
def vlan_delete(vlan_id: int) -> dict:
|
|
588
|
+
"""Delete a VLAN interface"""
|
|
589
|
+
try:
|
|
590
|
+
# Validate VLAN ID
|
|
591
|
+
validate_vlan_id(vlan_id)
|
|
592
|
+
|
|
593
|
+
# Delete VLAN interface configuration (includes bringing down and deleting interface)
|
|
594
|
+
delete_vlan_interface(vlan_id)
|
|
595
|
+
|
|
596
|
+
return {"message": f"VLAN {vlan_id} deleted successfully"}
|
|
597
|
+
except (ConfigError, NetworkError) as e:
|
|
598
|
+
raise HTTPException(
|
|
599
|
+
status_code=status.HTTP_400_BAD_REQUEST, detail=str(e)
|
|
600
|
+
) from e
|
|
601
|
+
except Exception as e:
|
|
602
|
+
raise HTTPException(
|
|
603
|
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
604
|
+
detail=f"Failed to delete VLAN {vlan_id}: {str(e)}",
|
|
605
|
+
) from e
|
|
606
|
+
|
|
607
|
+
|
|
608
|
+
@vlan_router.put("/{vlan_id}/enable")
|
|
609
|
+
def vlan_enable(vlan_id: int) -> dict:
|
|
610
|
+
"""Enable a VLAN interface (bring it up)"""
|
|
611
|
+
try:
|
|
612
|
+
# Validate VLAN ID
|
|
613
|
+
validate_vlan_id(vlan_id)
|
|
614
|
+
|
|
615
|
+
interface_name = get_interface_name()
|
|
616
|
+
vlan_interface = f"{interface_name}.{vlan_id}"
|
|
617
|
+
|
|
618
|
+
# Check if interface exists
|
|
619
|
+
result = subprocess.run(
|
|
620
|
+
["ip", "link", "show", vlan_interface],
|
|
621
|
+
capture_output=True,
|
|
622
|
+
text=True,
|
|
623
|
+
check=False,
|
|
624
|
+
)
|
|
625
|
+
if result.returncode != 0:
|
|
626
|
+
raise HTTPException(
|
|
627
|
+
status_code=status.HTTP_404_NOT_FOUND,
|
|
628
|
+
detail=f"VLAN interface {vlan_interface} does not exist",
|
|
629
|
+
)
|
|
630
|
+
|
|
631
|
+
# Bring interface up
|
|
632
|
+
result = subprocess.run(
|
|
633
|
+
["ip", "link", "set", vlan_interface, "up"],
|
|
634
|
+
capture_output=True,
|
|
635
|
+
text=True,
|
|
636
|
+
check=False,
|
|
637
|
+
)
|
|
638
|
+
if result.returncode != 0:
|
|
639
|
+
raise NetworkError(
|
|
640
|
+
f"Failed to enable interface {vlan_interface}: {result.stderr}"
|
|
641
|
+
)
|
|
642
|
+
|
|
643
|
+
return {"message": f"VLAN {vlan_id} enabled successfully"}
|
|
644
|
+
except PydanticCustomError as e:
|
|
645
|
+
# System VLANs are forbidden from modification
|
|
646
|
+
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=str(e)) from e
|
|
647
|
+
except HTTPException:
|
|
648
|
+
raise
|
|
649
|
+
except (ConfigError, NetworkError) as e:
|
|
650
|
+
raise HTTPException(
|
|
651
|
+
status_code=status.HTTP_400_BAD_REQUEST, detail=str(e)
|
|
652
|
+
) from e
|
|
653
|
+
except Exception as e:
|
|
654
|
+
raise HTTPException(
|
|
655
|
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
656
|
+
detail=f"Failed to enable VLAN {vlan_id}: {str(e)}",
|
|
657
|
+
) from e
|
|
658
|
+
|
|
659
|
+
|
|
660
|
+
@vlan_router.put("/{vlan_id}/disable")
|
|
661
|
+
def vlan_disable(vlan_id: int) -> dict:
|
|
662
|
+
"""Disable a VLAN interface (bring it down)"""
|
|
663
|
+
try:
|
|
664
|
+
# Validate VLAN ID
|
|
665
|
+
validate_vlan_id(vlan_id)
|
|
666
|
+
|
|
667
|
+
interface_name = get_interface_name()
|
|
668
|
+
vlan_interface = f"{interface_name}.{vlan_id}"
|
|
669
|
+
|
|
670
|
+
# Check if interface exists
|
|
671
|
+
result = subprocess.run(
|
|
672
|
+
["ip", "link", "show", vlan_interface],
|
|
673
|
+
capture_output=True,
|
|
674
|
+
text=True,
|
|
675
|
+
check=False,
|
|
676
|
+
)
|
|
677
|
+
if result.returncode != 0:
|
|
678
|
+
raise HTTPException(
|
|
679
|
+
status_code=status.HTTP_404_NOT_FOUND,
|
|
680
|
+
detail=f"VLAN interface {vlan_interface} does not exist",
|
|
681
|
+
)
|
|
682
|
+
|
|
683
|
+
# Bring interface down
|
|
684
|
+
result = subprocess.run(
|
|
685
|
+
["ip", "link", "set", vlan_interface, "down"],
|
|
686
|
+
capture_output=True,
|
|
687
|
+
text=True,
|
|
688
|
+
check=False,
|
|
689
|
+
)
|
|
690
|
+
if result.returncode != 0:
|
|
691
|
+
raise NetworkError(
|
|
692
|
+
f"Failed to disable interface {vlan_interface}: {result.stderr}"
|
|
693
|
+
)
|
|
694
|
+
|
|
695
|
+
return {"message": f"VLAN {vlan_id} disabled successfully"}
|
|
696
|
+
except PydanticCustomError as e:
|
|
697
|
+
# System VLANs are forbidden from modification
|
|
698
|
+
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=str(e)) from e
|
|
699
|
+
except HTTPException:
|
|
700
|
+
raise
|
|
701
|
+
except (ConfigError, NetworkError) as e:
|
|
702
|
+
raise HTTPException(
|
|
703
|
+
status_code=status.HTTP_400_BAD_REQUEST, detail=str(e)
|
|
704
|
+
) from e
|
|
705
|
+
except Exception as e:
|
|
706
|
+
raise HTTPException(
|
|
707
|
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
708
|
+
detail=f"Failed to disable VLAN {vlan_id}: {str(e)}",
|
|
709
|
+
) from e
|
zboxapi-0.0.6/PKG-INFO
DELETED
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.1
|
|
2
|
-
Name: zboxapi
|
|
3
|
-
Version: 0.0.6
|
|
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
|
-
## Installation
|
|
22
|
-
|
|
23
|
-
Complete the following steps to set up zBox Api:
|
|
24
|
-
|
|
25
|
-
1. Install pipx
|
|
26
|
-
|
|
27
|
-
```bash
|
|
28
|
-
# Install and configure pipx
|
|
29
|
-
apt update
|
|
30
|
-
apt install -y pipx
|
|
31
|
-
pipx ensurepath
|
|
32
|
-
|
|
33
|
-
# Reload your profile
|
|
34
|
-
source ~/.zshrc
|
|
35
|
-
```
|
|
36
|
-
|
|
37
|
-
1. Install zBoxApi:
|
|
38
|
-
|
|
39
|
-
```bash
|
|
40
|
-
pipx install zboxapi
|
|
41
|
-
```
|
|
42
|
-
|
|
43
|
-
1. Set up and start zboxapi.service
|
|
44
|
-
|
|
45
|
-
```bash
|
|
46
|
-
cp zboxapi.service /etc/systemd/system
|
|
47
|
-
systemctl daemon-reload
|
|
48
|
-
systemctl enable zboxapi.service
|
|
49
|
-
systemctl start zboxapi.service
|
|
50
|
-
```
|
|
51
|
-
|
zboxapi-0.0.6/README.md
DELETED
|
@@ -1,34 +0,0 @@
|
|
|
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 zBoxApi:
|
|
22
|
-
|
|
23
|
-
```bash
|
|
24
|
-
pipx install zboxapi
|
|
25
|
-
```
|
|
26
|
-
|
|
27
|
-
1. Set up and start zboxapi.service
|
|
28
|
-
|
|
29
|
-
```bash
|
|
30
|
-
cp zboxapi.service /etc/systemd/system
|
|
31
|
-
systemctl daemon-reload
|
|
32
|
-
systemctl enable zboxapi.service
|
|
33
|
-
systemctl start zboxapi.service
|
|
34
|
-
```
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.0.6"
|