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 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.6"
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"
@@ -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 uvicorn
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
- if not 1 < len(value) < 64:
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
- ip: IPv4Address
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"