Skip to content

Commit 8c2dec5

Browse files
authored
feat: add devices management with config generator (#4)
2 parents dfd2d88 + 4f3cbf6 commit 8c2dec5

File tree

10 files changed

+131
-14
lines changed

10 files changed

+131
-14
lines changed

labctl/commands/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
from .config import app as config_app
2+
from .devices import app as devices_app

labctl/commands/config.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ def show():
1515
config = Config()
1616
api_token = config.api_token
1717
if api_token:
18-
me = APIDriver().get("/me")
18+
me = APIDriver().get("/me").json()
1919
# todo handle old token and valid token
2020
if me.get("email"):
2121
api_token = "Logged in as " + me["email"]
@@ -27,6 +27,7 @@ def show():
2727
table.add_column("Key", style="cyan")
2828
table.add_column("Value", style="magenta")
2929
table.add_row("API URL", config.api_endpoint)
30+
table.add_row("API User", config.username)
3031
table.add_row("API Token", api_token)
3132
console.print(table)
3233

labctl/commands/devices.py

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
from os import getcwd
2+
3+
import typer
4+
5+
from rich.table import Table
6+
7+
from labctl.core import Config, APIDriver, console
8+
from labctl.core import cli_ready, wireguard
9+
10+
app = typer.Typer()
11+
12+
@app.command(name="list")
13+
@cli_ready
14+
def list_devices():
15+
"""
16+
List devices
17+
"""
18+
config = Config()
19+
devices = APIDriver().get("/devices/" + config.username).json()
20+
table = Table(title=":computer: Devices")
21+
table.add_column("ID", style="cyan")
22+
table.add_column("Name", style="magenta")
23+
table.add_column("IPv4", style="green")
24+
table.add_column("RX Bytes", style="blue")
25+
table.add_column("TX Bytes", style="yellow")
26+
table.add_column("Remote IP", style="red")
27+
28+
for device in devices:
29+
table.add_row(
30+
device["id"],
31+
device["name"],
32+
device["ipv4"],
33+
device["rx_bytes"],
34+
device["tx_bytes"],
35+
device["remote_ip"],
36+
)
37+
console.print(table)
38+
39+
@app.command(name="create")
40+
@cli_ready
41+
def create_device(name: str = typer.Argument(..., help="The device name")):
42+
"""
43+
Create a device
44+
"""
45+
rsp = APIDriver().post(
46+
f"/devices/{Config().username}",
47+
json={"name": name},
48+
additional_headers={"Content-Type": "application/json"}
49+
)
50+
if rsp.status_code >= 200 < 300:
51+
console.print(f"Device {name} created :tada:")
52+
data = rsp.json()
53+
config_path = f"/{getcwd()}/{name}.conf"
54+
wireguard.generate_config(data["device"], data["private_key"], config_path)
55+
console.print(f"Configuration file saved to {config_path}")
56+
return
57+
console.print(f"Error creating device {name} ({rsp.status_code})")
58+
59+
@app.command(name="delete")
60+
@cli_ready
61+
def delete_device(
62+
device_id: str = typer.Argument(..., help="The device ID")
63+
):
64+
"""
65+
Delete a device
66+
"""
67+
rsp = APIDriver().delete(f"/devices/{Config().username}/{device_id}")
68+
if rsp.status_code == 200:
69+
console.print(f"Device {device_id} deleted :fire:")
70+
return
71+
console.print(f"Error deleting device {device_id} ({rsp.status_code})")

labctl/core/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@
22
from .api import APIDriver
33
from .console import console
44
from .decorators import cli_ready
5+
from . import wireguard

labctl/core/api.py

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,19 @@ def __init__(self):
2222
}
2323

2424
def validate_token(self):
25-
return self.get("/token/verify").get("valid", False)
25+
return self.get("/token/verify").json().get("valid", False)
2626

27-
def get(self, path: str):
28-
return requests.get(self.api_url + path, headers=self.headers).json()
27+
def get(self, path: str) -> requests.Response:
28+
return requests.get(self.api_url + path, headers=self.headers)
2929

30-
def post(self, path: str, data: dict = {}, additional_headers: dict = {}):
30+
def post(self, path: str, data: dict = {}, json: dict = {}, additional_headers: dict = {}) -> requests.Response:
3131
headers = self.headers
3232
headers.update(additional_headers)
33-
return requests.post(self.api_url + path, headers=headers, data=data).json()
33+
if data:
34+
return requests.post(self.api_url + path, headers=headers, data=data)
35+
if json:
36+
return requests.post(self.api_url + path, headers=headers, json=json)
37+
return requests.post(self.api_url + path, headers=headers)
38+
39+
def delete(self, path: str) -> requests.Response:
40+
return requests.delete(self.api_url + path, headers=self.headers)

labctl/core/config.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
class Config:
99

1010
api_endpoint: str
11+
username: str
1112
api_token: str
1213
token_type: str
1314

@@ -53,4 +54,4 @@ def ready(self):
5354
"""
5455
Check if the configuration is ready to be used
5556
"""
56-
return all([self.api_endpoint, self.api_token, self.token_type])
57+
return all([self.api_endpoint, self.api_token, self.token_type, self.username])

labctl/core/wireguard.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import wgconfig
2+
3+
def generate_config(device: dict, private_key: str, config_file: str):
4+
wg = wgconfig.WGConfig(config_file)
5+
wg.add_attr(None, "PrivateKey", private_key)
6+
wg.add_attr(None, "Address", f"{device['ipv4']}/32, {device['ipv6']}/128")
7+
wg.add_attr(None, "DNS", ", ".join(device["dns"]))
8+
wg.add_attr(None, "MTU", device["mtu"])
9+
10+
wg.add_peer(device["server_public_key"], "# LaboInfra WireGuard Server Client")
11+
wg.add_attr(device["server_public_key"], "Endpoint", device["endpoint"])
12+
wg.add_attr(device["server_public_key"], "AllowedIPs", ", ".join(device["allowed_ips"]))
13+
wg.add_attr(device["server_public_key"], "PersistentKeepalive", device["persistent_keepalive"])
14+
wg.add_attr(device["server_public_key"], "PresharedKey", device["preshared_key"])
15+
16+
wg.write_file()

labctl/main.py

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
app = typer.Typer()
1414

1515
app.add_typer(commands.config_app, name="config", help="Manage the configuration")
16+
app.add_typer(commands.devices_app, name="devices", help="Manage vpn devices")
1617

1718
@app.callback()
1819
def callback():
@@ -39,7 +40,7 @@ def me(
3940
Print the current status of the fastonboard-api account
4041
"""
4142
api_driver = APIDriver()
42-
data = api_driver.get("/me")
43+
data = api_driver.get("/me").json()
4344
if json:
4445
print(dumps(data))
4546
return
@@ -66,12 +67,12 @@ def sync():
6667
Ask FastOnBoard-API to sync your account onto the vpn and openstack services
6768
"""
6869
api = APIDriver()
69-
me = api.get("/me")
70-
task_id = api.get("/users/" + me['username'] + "/sync")
70+
me = api.get("/me").json()
71+
task_id = api.get("/users/" + me['username'] + "/sync").json()
7172
typer.echo(f"Syncing account for user {me['username']} this may take a while...")
7273
typer.echo("Task ID: " + task_id.get("id"))
7374
while True:
74-
task = api.get("/users/" + me['username'] + "/sync/" + task_id.get("id"))
75+
task = api.get("/users/" + me['username'] + "/sync/" + task_id.get("id")).json()
7576
if task.get("status") == "SUCCESS":
7677
typer.echo("Sync successful")
7778
break
@@ -81,11 +82,16 @@ def sync():
8182
sleep(1)
8283

8384
@app.command()
84-
def login(username: Annotated[str, typer.Argument(help="The username to authenticate with")]):
85+
def login(username: str = typer.Option(None, help="The username to login with")):
8586
"""
8687
Login to the FastOnBoard-API server
8788
Enter your password when prompted or set LABCTL_API_ENDPOINT_PASSWORD
8889
"""
90+
env_user = environ.get("LABCTL_API_ENDPOINT_USERNAME")
91+
username = Config().username or username or env_user
92+
if not username:
93+
username = typer.prompt("Enter your username")
94+
8995
env_pass = environ.get("LABCTL_API_ENDPOINT_PASSWORD")
9096
if env_pass:
9197
password = env_pass
@@ -103,7 +109,7 @@ def login(username: Annotated[str, typer.Argument(help="The username to authenti
103109
'password': password,
104110
}, additional_headers={
105111
'Content-Type': 'application/x-www-form-urlencoded',
106-
})
112+
}).json()
107113
if 'detail' in data:
108114
if "Method Not Allowed" in data['detail']:
109115
console.print("[red]Error: Invalid endpoint or path to api[/red]")
@@ -112,6 +118,7 @@ def login(username: Annotated[str, typer.Argument(help="The username to authenti
112118
return
113119
if 'access_token' in data:
114120
config = Config()
121+
config.username=username
115122
config.api_token=data['access_token']
116123
config.token_type=data["token_type"]
117124
config.save()

poetry.lock

Lines changed: 12 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ requests = "^2.32.3"
2525
pyyaml = "^6.0.2"
2626
typer = {extras = ["all"], version = "^0.12.5"}
2727
colorama = "^0.4.6"
28+
wgconfig = "^1.0.4"
2829

2930

3031
[tool.poetry.dev-dependencies]

0 commit comments

Comments
 (0)