pactown 0.1.4__py3-none-any.whl → 0.1.47__py3-none-any.whl
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.
- pactown/__init__.py +178 -4
- pactown/cli.py +539 -37
- pactown/config.py +12 -11
- pactown/deploy/__init__.py +17 -3
- pactown/deploy/base.py +35 -33
- pactown/deploy/compose.py +59 -58
- pactown/deploy/docker.py +40 -41
- pactown/deploy/kubernetes.py +43 -42
- pactown/deploy/podman.py +55 -56
- pactown/deploy/quadlet.py +1021 -0
- pactown/deploy/quadlet_api.py +533 -0
- pactown/deploy/quadlet_shell.py +557 -0
- pactown/events.py +1066 -0
- pactown/fast_start.py +514 -0
- pactown/generator.py +31 -30
- pactown/llm.py +450 -0
- pactown/markpact_blocks.py +50 -0
- pactown/network.py +59 -38
- pactown/orchestrator.py +90 -93
- pactown/parallel.py +40 -40
- pactown/platform.py +146 -0
- pactown/registry/__init__.py +1 -1
- pactown/registry/client.py +45 -46
- pactown/registry/models.py +25 -25
- pactown/registry/server.py +24 -24
- pactown/resolver.py +30 -30
- pactown/runner_api.py +458 -0
- pactown/sandbox_manager.py +480 -79
- pactown/security.py +682 -0
- pactown/service_runner.py +1201 -0
- pactown/user_isolation.py +458 -0
- {pactown-0.1.4.dist-info → pactown-0.1.47.dist-info}/METADATA +65 -9
- pactown-0.1.47.dist-info/RECORD +36 -0
- pactown-0.1.47.dist-info/entry_points.txt +5 -0
- pactown-0.1.4.dist-info/RECORD +0 -24
- pactown-0.1.4.dist-info/entry_points.txt +0 -3
- {pactown-0.1.4.dist-info → pactown-0.1.47.dist-info}/WHEEL +0 -0
- {pactown-0.1.4.dist-info → pactown-0.1.47.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,557 @@
|
|
|
1
|
+
"""Interactive shell for Podman Quadlet deployment management.
|
|
2
|
+
|
|
3
|
+
Provides a REPL-style interface for managing Quadlet deployments,
|
|
4
|
+
generating unit files, and deploying Markdown services.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import cmd
|
|
10
|
+
import shutil
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
from rich.console import Console
|
|
14
|
+
from rich.panel import Panel
|
|
15
|
+
from rich.prompt import Confirm
|
|
16
|
+
from rich.syntax import Syntax
|
|
17
|
+
from rich.table import Table
|
|
18
|
+
|
|
19
|
+
from .base import DeploymentConfig
|
|
20
|
+
from .quadlet import (
|
|
21
|
+
QuadletBackend,
|
|
22
|
+
QuadletConfig,
|
|
23
|
+
QuadletTemplates,
|
|
24
|
+
generate_markdown_service_quadlet,
|
|
25
|
+
generate_traefik_quadlet,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
console = Console()
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class QuadletShell(cmd.Cmd):
|
|
32
|
+
"""Interactive shell for Quadlet deployment management."""
|
|
33
|
+
|
|
34
|
+
intro = """
|
|
35
|
+
╔══════════════════════════════════════════════════════════════════╗
|
|
36
|
+
║ 🚀 Pactown Quadlet Deployment Shell ║
|
|
37
|
+
║ ║
|
|
38
|
+
║ Deploy Markdown services on VPS with Podman Quadlet ║
|
|
39
|
+
║ Type 'help' for available commands ║
|
|
40
|
+
╚══════════════════════════════════════════════════════════════════╝
|
|
41
|
+
"""
|
|
42
|
+
prompt = "pactown-quadlet> "
|
|
43
|
+
|
|
44
|
+
def __init__(
|
|
45
|
+
self,
|
|
46
|
+
tenant_id: str = "default",
|
|
47
|
+
domain: str = "localhost",
|
|
48
|
+
user_mode: bool = True,
|
|
49
|
+
):
|
|
50
|
+
super().__init__()
|
|
51
|
+
self.quadlet_config = QuadletConfig(
|
|
52
|
+
tenant_id=tenant_id,
|
|
53
|
+
domain=domain,
|
|
54
|
+
user_mode=user_mode,
|
|
55
|
+
)
|
|
56
|
+
self.deploy_config = DeploymentConfig.for_production()
|
|
57
|
+
self.backend = QuadletBackend(self.deploy_config, self.quadlet_config)
|
|
58
|
+
|
|
59
|
+
# Check availability
|
|
60
|
+
if not self.backend.is_available():
|
|
61
|
+
console.print("[yellow]⚠ Warning: Podman 4.4+ with Quadlet support not detected[/yellow]")
|
|
62
|
+
console.print("[dim]Some features may not work. Install Podman 4.4+ for full functionality.[/dim]")
|
|
63
|
+
|
|
64
|
+
def do_status(self, arg: str):
|
|
65
|
+
"""Show current configuration and status.
|
|
66
|
+
|
|
67
|
+
Usage: status
|
|
68
|
+
"""
|
|
69
|
+
table = Table(title="Quadlet Configuration")
|
|
70
|
+
table.add_column("Setting", style="cyan")
|
|
71
|
+
table.add_column("Value", style="green")
|
|
72
|
+
|
|
73
|
+
table.add_row("Tenant ID", self.quadlet_config.tenant_id)
|
|
74
|
+
table.add_row("Domain", self.quadlet_config.domain)
|
|
75
|
+
table.add_row("Full Domain", self.quadlet_config.full_domain)
|
|
76
|
+
table.add_row("TLS Enabled", str(self.quadlet_config.tls_enabled))
|
|
77
|
+
table.add_row("User Mode", str(self.quadlet_config.user_mode))
|
|
78
|
+
table.add_row("Systemd Path", str(self.quadlet_config.systemd_path))
|
|
79
|
+
table.add_row("Tenant Path", str(self.quadlet_config.tenant_path))
|
|
80
|
+
table.add_row("Traefik Enabled", str(self.quadlet_config.traefik_enabled))
|
|
81
|
+
table.add_row("CPU Limit", self.quadlet_config.cpus)
|
|
82
|
+
table.add_row("Memory Limit", self.quadlet_config.memory)
|
|
83
|
+
|
|
84
|
+
console.print(table)
|
|
85
|
+
|
|
86
|
+
# Podman version
|
|
87
|
+
version = self.backend.get_quadlet_version()
|
|
88
|
+
if version:
|
|
89
|
+
console.print(f"\n[green]✓ Podman {version} available[/green]")
|
|
90
|
+
else:
|
|
91
|
+
console.print("\n[red]✗ Podman not available[/red]")
|
|
92
|
+
|
|
93
|
+
def do_config(self, arg: str):
|
|
94
|
+
"""Configure deployment settings.
|
|
95
|
+
|
|
96
|
+
Usage: config <setting> <value>
|
|
97
|
+
|
|
98
|
+
Settings:
|
|
99
|
+
tenant - Tenant ID
|
|
100
|
+
domain - Base domain
|
|
101
|
+
subdomain - Subdomain for service
|
|
102
|
+
tls - Enable TLS (true/false)
|
|
103
|
+
cpus - CPU limit (e.g., 0.5)
|
|
104
|
+
memory - Memory limit (e.g., 256M)
|
|
105
|
+
|
|
106
|
+
Example:
|
|
107
|
+
config domain pactown.com
|
|
108
|
+
config subdomain api
|
|
109
|
+
config tls true
|
|
110
|
+
"""
|
|
111
|
+
parts = arg.split(maxsplit=1)
|
|
112
|
+
if len(parts) < 2:
|
|
113
|
+
console.print("[yellow]Usage: config <setting> <value>[/yellow]")
|
|
114
|
+
return
|
|
115
|
+
|
|
116
|
+
setting, value = parts
|
|
117
|
+
|
|
118
|
+
if setting == "tenant":
|
|
119
|
+
self.quadlet_config.tenant_id = value
|
|
120
|
+
elif setting == "domain":
|
|
121
|
+
self.quadlet_config.domain = value
|
|
122
|
+
elif setting == "subdomain":
|
|
123
|
+
self.quadlet_config.subdomain = value if value != "none" else None
|
|
124
|
+
elif setting == "tls":
|
|
125
|
+
self.quadlet_config.tls_enabled = value.lower() in ("true", "1", "yes")
|
|
126
|
+
elif setting == "cpus":
|
|
127
|
+
self.quadlet_config.cpus = value
|
|
128
|
+
elif setting == "memory":
|
|
129
|
+
self.quadlet_config.memory = value
|
|
130
|
+
else:
|
|
131
|
+
console.print(f"[red]Unknown setting: {setting}[/red]")
|
|
132
|
+
return
|
|
133
|
+
|
|
134
|
+
console.print(f"[green]✓ Set {setting} = {value}[/green]")
|
|
135
|
+
|
|
136
|
+
def do_generate(self, arg: str):
|
|
137
|
+
"""Generate Quadlet unit files for a Markdown file.
|
|
138
|
+
|
|
139
|
+
Usage: generate <markdown_path> [image]
|
|
140
|
+
|
|
141
|
+
Example:
|
|
142
|
+
generate ./README.md
|
|
143
|
+
generate ./docs/API.md ghcr.io/pactown/markdown-server:latest
|
|
144
|
+
"""
|
|
145
|
+
parts = arg.split()
|
|
146
|
+
if not parts:
|
|
147
|
+
console.print("[yellow]Usage: generate <markdown_path> [image][/yellow]")
|
|
148
|
+
return
|
|
149
|
+
|
|
150
|
+
markdown_path = Path(parts[0]).resolve()
|
|
151
|
+
image = parts[1] if len(parts) > 1 else "ghcr.io/pactown/markdown-server:latest"
|
|
152
|
+
|
|
153
|
+
if not markdown_path.exists():
|
|
154
|
+
console.print(f"[red]File not found: {markdown_path}[/red]")
|
|
155
|
+
return
|
|
156
|
+
|
|
157
|
+
units = generate_markdown_service_quadlet(
|
|
158
|
+
markdown_path=markdown_path,
|
|
159
|
+
config=self.quadlet_config,
|
|
160
|
+
image=image,
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
console.print(f"\n[bold]Generated {len(units)} unit file(s):[/bold]\n")
|
|
164
|
+
|
|
165
|
+
for unit in units:
|
|
166
|
+
console.print(Panel(
|
|
167
|
+
Syntax(unit.content, "ini", theme="monokai"),
|
|
168
|
+
title=f"📄 {unit.filename}",
|
|
169
|
+
border_style="blue",
|
|
170
|
+
))
|
|
171
|
+
|
|
172
|
+
# Ask to save
|
|
173
|
+
if Confirm.ask("\nSave to systemd directory?"):
|
|
174
|
+
tenant_path = self.quadlet_config.tenant_path
|
|
175
|
+
tenant_path.mkdir(parents=True, exist_ok=True)
|
|
176
|
+
|
|
177
|
+
for unit in units:
|
|
178
|
+
path = unit.save(tenant_path)
|
|
179
|
+
console.print(f"[green]✓ Saved: {path}[/green]")
|
|
180
|
+
|
|
181
|
+
console.print("\n[dim]Run 'reload' to apply changes[/dim]")
|
|
182
|
+
|
|
183
|
+
def do_generate_container(self, arg: str):
|
|
184
|
+
"""Generate a custom container Quadlet file.
|
|
185
|
+
|
|
186
|
+
Usage: generate_container <name> <image> <port>
|
|
187
|
+
|
|
188
|
+
Example:
|
|
189
|
+
generate_container api nginx:latest 8080
|
|
190
|
+
generate_container web python:3.12-slim 5000
|
|
191
|
+
"""
|
|
192
|
+
parts = arg.split()
|
|
193
|
+
if len(parts) < 3:
|
|
194
|
+
console.print("[yellow]Usage: generate_container <name> <image> <port>[/yellow]")
|
|
195
|
+
return
|
|
196
|
+
|
|
197
|
+
name, image, port = parts[0], parts[1], int(parts[2])
|
|
198
|
+
|
|
199
|
+
unit = QuadletTemplates.container(
|
|
200
|
+
name=name,
|
|
201
|
+
image=image,
|
|
202
|
+
port=port,
|
|
203
|
+
config=self.quadlet_config,
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
console.print(Panel(
|
|
207
|
+
Syntax(unit.content, "ini", theme="monokai"),
|
|
208
|
+
title=f"📄 {unit.filename}",
|
|
209
|
+
border_style="blue",
|
|
210
|
+
))
|
|
211
|
+
|
|
212
|
+
if Confirm.ask("\nSave to systemd directory?"):
|
|
213
|
+
tenant_path = self.quadlet_config.tenant_path
|
|
214
|
+
path = unit.save(tenant_path)
|
|
215
|
+
console.print(f"[green]✓ Saved: {path}[/green]")
|
|
216
|
+
|
|
217
|
+
def do_generate_traefik(self, arg: str):
|
|
218
|
+
"""Generate Traefik reverse proxy Quadlet files.
|
|
219
|
+
|
|
220
|
+
Usage: generate_traefik
|
|
221
|
+
|
|
222
|
+
This generates Traefik container and volume unit files
|
|
223
|
+
for automatic HTTPS with Let's Encrypt.
|
|
224
|
+
"""
|
|
225
|
+
units = generate_traefik_quadlet(self.quadlet_config)
|
|
226
|
+
|
|
227
|
+
console.print(f"\n[bold]Generated {len(units)} unit file(s):[/bold]\n")
|
|
228
|
+
|
|
229
|
+
for unit in units:
|
|
230
|
+
console.print(Panel(
|
|
231
|
+
Syntax(unit.content, "ini", theme="monokai"),
|
|
232
|
+
title=f"📄 {unit.filename}",
|
|
233
|
+
border_style="blue",
|
|
234
|
+
))
|
|
235
|
+
|
|
236
|
+
if Confirm.ask("\nSave to systemd directory?"):
|
|
237
|
+
systemd_path = self.quadlet_config.systemd_path
|
|
238
|
+
systemd_path.mkdir(parents=True, exist_ok=True)
|
|
239
|
+
|
|
240
|
+
for unit in units:
|
|
241
|
+
path = unit.save(systemd_path)
|
|
242
|
+
console.print(f"[green]✓ Saved: {path}[/green]")
|
|
243
|
+
|
|
244
|
+
def do_list(self, arg: str):
|
|
245
|
+
"""List all Quadlet services for current tenant.
|
|
246
|
+
|
|
247
|
+
Usage: list
|
|
248
|
+
"""
|
|
249
|
+
services = self.backend.list_services()
|
|
250
|
+
|
|
251
|
+
if not services:
|
|
252
|
+
console.print("[yellow]No services found for tenant: {self.quadlet_config.tenant_id}[/yellow]")
|
|
253
|
+
return
|
|
254
|
+
|
|
255
|
+
table = Table(title=f"Services (tenant: {self.quadlet_config.tenant_id})")
|
|
256
|
+
table.add_column("Name", style="cyan")
|
|
257
|
+
table.add_column("Status", style="green")
|
|
258
|
+
table.add_column("PID", style="dim")
|
|
259
|
+
table.add_column("Unit File", style="dim")
|
|
260
|
+
|
|
261
|
+
for svc in services:
|
|
262
|
+
status = svc["status"]
|
|
263
|
+
status_str = "🟢 running" if status.get("running") else "🔴 stopped"
|
|
264
|
+
table.add_row(
|
|
265
|
+
svc["name"],
|
|
266
|
+
status_str,
|
|
267
|
+
status.get("pid", "-"),
|
|
268
|
+
svc["unit_file"],
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
console.print(table)
|
|
272
|
+
|
|
273
|
+
def do_start(self, arg: str):
|
|
274
|
+
"""Start a Quadlet service.
|
|
275
|
+
|
|
276
|
+
Usage: start <service_name>
|
|
277
|
+
"""
|
|
278
|
+
if not arg:
|
|
279
|
+
console.print("[yellow]Usage: start <service_name>[/yellow]")
|
|
280
|
+
return
|
|
281
|
+
|
|
282
|
+
console.print(f"Starting {arg}...")
|
|
283
|
+
self.backend._systemctl("daemon-reload")
|
|
284
|
+
result = self.backend._systemctl("start", f"{arg}.service")
|
|
285
|
+
|
|
286
|
+
if result.returncode == 0:
|
|
287
|
+
console.print(f"[green]✓ Started {arg}[/green]")
|
|
288
|
+
else:
|
|
289
|
+
console.print(f"[red]✗ Failed to start {arg}: {result.stderr}[/red]")
|
|
290
|
+
|
|
291
|
+
def do_stop(self, arg: str):
|
|
292
|
+
"""Stop a Quadlet service.
|
|
293
|
+
|
|
294
|
+
Usage: stop <service_name>
|
|
295
|
+
"""
|
|
296
|
+
if not arg:
|
|
297
|
+
console.print("[yellow]Usage: stop <service_name>[/yellow]")
|
|
298
|
+
return
|
|
299
|
+
|
|
300
|
+
result = self.backend._systemctl("stop", f"{arg}.service")
|
|
301
|
+
|
|
302
|
+
if result.returncode == 0:
|
|
303
|
+
console.print(f"[green]✓ Stopped {arg}[/green]")
|
|
304
|
+
else:
|
|
305
|
+
console.print(f"[red]✗ Failed to stop {arg}: {result.stderr}[/red]")
|
|
306
|
+
|
|
307
|
+
def do_restart(self, arg: str):
|
|
308
|
+
"""Restart a Quadlet service.
|
|
309
|
+
|
|
310
|
+
Usage: restart <service_name>
|
|
311
|
+
"""
|
|
312
|
+
if not arg:
|
|
313
|
+
console.print("[yellow]Usage: restart <service_name>[/yellow]")
|
|
314
|
+
return
|
|
315
|
+
|
|
316
|
+
result = self.backend._systemctl("restart", f"{arg}.service")
|
|
317
|
+
|
|
318
|
+
if result.returncode == 0:
|
|
319
|
+
console.print(f"[green]✓ Restarted {arg}[/green]")
|
|
320
|
+
else:
|
|
321
|
+
console.print(f"[red]✗ Failed to restart {arg}: {result.stderr}[/red]")
|
|
322
|
+
|
|
323
|
+
def do_logs(self, arg: str):
|
|
324
|
+
"""Show logs for a Quadlet service.
|
|
325
|
+
|
|
326
|
+
Usage: logs <service_name> [lines]
|
|
327
|
+
|
|
328
|
+
Example:
|
|
329
|
+
logs api
|
|
330
|
+
logs api 50
|
|
331
|
+
"""
|
|
332
|
+
parts = arg.split()
|
|
333
|
+
if not parts:
|
|
334
|
+
console.print("[yellow]Usage: logs <service_name> [lines][/yellow]")
|
|
335
|
+
return
|
|
336
|
+
|
|
337
|
+
service = parts[0]
|
|
338
|
+
lines = int(parts[1]) if len(parts) > 1 else 50
|
|
339
|
+
|
|
340
|
+
output = self.backend.logs(service, tail=lines)
|
|
341
|
+
console.print(Panel(output or "[dim]No logs available[/dim]", title=f"Logs: {service}"))
|
|
342
|
+
|
|
343
|
+
def do_reload(self, arg: str):
|
|
344
|
+
"""Reload systemd daemon to apply Quadlet changes.
|
|
345
|
+
|
|
346
|
+
Usage: reload
|
|
347
|
+
"""
|
|
348
|
+
console.print("Reloading systemd daemon...")
|
|
349
|
+
result = self.backend._systemctl("daemon-reload")
|
|
350
|
+
|
|
351
|
+
if result.returncode == 0:
|
|
352
|
+
console.print("[green]✓ Daemon reloaded[/green]")
|
|
353
|
+
else:
|
|
354
|
+
console.print(f"[red]✗ Failed to reload: {result.stderr}[/red]")
|
|
355
|
+
|
|
356
|
+
def do_deploy(self, arg: str):
|
|
357
|
+
"""Deploy a Markdown file as a web service.
|
|
358
|
+
|
|
359
|
+
Usage: deploy <markdown_path> [subdomain]
|
|
360
|
+
|
|
361
|
+
Example:
|
|
362
|
+
deploy ./README.md docs
|
|
363
|
+
deploy ./API.md api
|
|
364
|
+
"""
|
|
365
|
+
parts = arg.split()
|
|
366
|
+
if not parts:
|
|
367
|
+
console.print("[yellow]Usage: deploy <markdown_path> [subdomain][/yellow]")
|
|
368
|
+
return
|
|
369
|
+
|
|
370
|
+
markdown_path = Path(parts[0]).resolve()
|
|
371
|
+
if len(parts) > 1:
|
|
372
|
+
self.quadlet_config.subdomain = parts[1]
|
|
373
|
+
|
|
374
|
+
if not markdown_path.exists():
|
|
375
|
+
console.print(f"[red]File not found: {markdown_path}[/red]")
|
|
376
|
+
return
|
|
377
|
+
|
|
378
|
+
console.print(f"\n[bold]Deploying: {markdown_path.name}[/bold]")
|
|
379
|
+
console.print(f" Domain: {self.quadlet_config.full_domain}")
|
|
380
|
+
console.print(f" Tenant: {self.quadlet_config.tenant_id}")
|
|
381
|
+
console.print()
|
|
382
|
+
|
|
383
|
+
# Generate units
|
|
384
|
+
units = generate_markdown_service_quadlet(
|
|
385
|
+
markdown_path=markdown_path,
|
|
386
|
+
config=self.quadlet_config,
|
|
387
|
+
)
|
|
388
|
+
|
|
389
|
+
# Save units
|
|
390
|
+
tenant_path = self.quadlet_config.tenant_path
|
|
391
|
+
for unit in units:
|
|
392
|
+
unit.save(tenant_path)
|
|
393
|
+
console.print(f"[dim]Generated: {unit.filename}[/dim]")
|
|
394
|
+
|
|
395
|
+
# Reload and start
|
|
396
|
+
self.backend._systemctl("daemon-reload")
|
|
397
|
+
|
|
398
|
+
service_name = units[0].name
|
|
399
|
+
self.backend._systemctl("enable", f"{service_name}.service")
|
|
400
|
+
result = self.backend._systemctl("start", f"{service_name}.service")
|
|
401
|
+
|
|
402
|
+
if result.returncode == 0:
|
|
403
|
+
url = f"https://{self.quadlet_config.full_domain}" if self.quadlet_config.tls_enabled else f"http://{self.quadlet_config.full_domain}"
|
|
404
|
+
console.print("\n[green]✓ Deployed successfully![/green]")
|
|
405
|
+
console.print(f" URL: {url}")
|
|
406
|
+
else:
|
|
407
|
+
console.print(f"\n[red]✗ Deployment failed: {result.stderr}[/red]")
|
|
408
|
+
|
|
409
|
+
def do_undeploy(self, arg: str):
|
|
410
|
+
"""Remove a deployed service.
|
|
411
|
+
|
|
412
|
+
Usage: undeploy <service_name>
|
|
413
|
+
"""
|
|
414
|
+
if not arg:
|
|
415
|
+
console.print("[yellow]Usage: undeploy <service_name>[/yellow]")
|
|
416
|
+
return
|
|
417
|
+
|
|
418
|
+
if not Confirm.ask(f"Remove service '{arg}'?"):
|
|
419
|
+
return
|
|
420
|
+
|
|
421
|
+
result = self.backend.stop(arg)
|
|
422
|
+
|
|
423
|
+
if result.success:
|
|
424
|
+
console.print(f"[green]✓ Removed {arg}[/green]")
|
|
425
|
+
else:
|
|
426
|
+
console.print(f"[red]✗ Failed to remove {arg}: {result.error}[/red]")
|
|
427
|
+
|
|
428
|
+
def do_init(self, arg: str):
|
|
429
|
+
"""Initialize Quadlet directories and Traefik proxy.
|
|
430
|
+
|
|
431
|
+
Usage: init
|
|
432
|
+
|
|
433
|
+
This creates the systemd directories and optionally
|
|
434
|
+
sets up Traefik as a reverse proxy.
|
|
435
|
+
"""
|
|
436
|
+
console.print("[bold]Initializing Quadlet deployment environment...[/bold]\n")
|
|
437
|
+
|
|
438
|
+
# Create directories
|
|
439
|
+
systemd_path = self.quadlet_config.systemd_path
|
|
440
|
+
tenant_path = self.quadlet_config.tenant_path
|
|
441
|
+
|
|
442
|
+
systemd_path.mkdir(parents=True, exist_ok=True)
|
|
443
|
+
tenant_path.mkdir(parents=True, exist_ok=True)
|
|
444
|
+
|
|
445
|
+
console.print(f"[green]✓ Created: {systemd_path}[/green]")
|
|
446
|
+
console.print(f"[green]✓ Created: {tenant_path}[/green]")
|
|
447
|
+
|
|
448
|
+
# Setup Traefik
|
|
449
|
+
if Confirm.ask("\nSetup Traefik reverse proxy?"):
|
|
450
|
+
units = generate_traefik_quadlet(self.quadlet_config)
|
|
451
|
+
for unit in units:
|
|
452
|
+
unit.save(systemd_path)
|
|
453
|
+
console.print(f"[green]✓ Created: {unit.filename}[/green]")
|
|
454
|
+
|
|
455
|
+
self.backend._systemctl("daemon-reload")
|
|
456
|
+
self.backend._systemctl("enable", "traefik.service")
|
|
457
|
+
self.backend._systemctl("start", "traefik.service")
|
|
458
|
+
console.print("[green]✓ Traefik started[/green]")
|
|
459
|
+
|
|
460
|
+
console.print("\n[bold green]Initialization complete![/bold green]")
|
|
461
|
+
|
|
462
|
+
def do_export(self, arg: str):
|
|
463
|
+
"""Export all unit files to a directory.
|
|
464
|
+
|
|
465
|
+
Usage: export <output_dir>
|
|
466
|
+
"""
|
|
467
|
+
if not arg:
|
|
468
|
+
console.print("[yellow]Usage: export <output_dir>[/yellow]")
|
|
469
|
+
return
|
|
470
|
+
|
|
471
|
+
output_dir = Path(arg)
|
|
472
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
473
|
+
|
|
474
|
+
tenant_path = self.quadlet_config.tenant_path
|
|
475
|
+
if tenant_path.exists():
|
|
476
|
+
for f in tenant_path.glob("*"):
|
|
477
|
+
if f.is_file():
|
|
478
|
+
shutil.copy(f, output_dir)
|
|
479
|
+
console.print(f"[dim]Exported: {f.name}[/dim]")
|
|
480
|
+
|
|
481
|
+
console.print(f"\n[green]✓ Exported to: {output_dir}[/green]")
|
|
482
|
+
|
|
483
|
+
def do_help(self, arg: str):
|
|
484
|
+
"""Show help for commands."""
|
|
485
|
+
if arg:
|
|
486
|
+
super().do_help(arg)
|
|
487
|
+
else:
|
|
488
|
+
console.print(Panel("""
|
|
489
|
+
[bold cyan]Deployment Commands:[/bold cyan]
|
|
490
|
+
deploy - Deploy a Markdown file as a web service
|
|
491
|
+
undeploy - Remove a deployed service
|
|
492
|
+
start - Start a service
|
|
493
|
+
stop - Stop a service
|
|
494
|
+
restart - Restart a service
|
|
495
|
+
|
|
496
|
+
[bold cyan]Generation Commands:[/bold cyan]
|
|
497
|
+
generate - Generate Quadlet files for Markdown
|
|
498
|
+
generate_container - Generate custom container Quadlet
|
|
499
|
+
generate_traefik - Generate Traefik reverse proxy
|
|
500
|
+
|
|
501
|
+
[bold cyan]Management Commands:[/bold cyan]
|
|
502
|
+
status - Show configuration and status
|
|
503
|
+
config - Configure deployment settings
|
|
504
|
+
list - List all services
|
|
505
|
+
logs - Show service logs
|
|
506
|
+
reload - Reload systemd daemon
|
|
507
|
+
init - Initialize Quadlet environment
|
|
508
|
+
export - Export unit files
|
|
509
|
+
|
|
510
|
+
[bold cyan]Other:[/bold cyan]
|
|
511
|
+
help - Show this help
|
|
512
|
+
quit - Exit the shell
|
|
513
|
+
""", title="Available Commands"))
|
|
514
|
+
|
|
515
|
+
def do_quit(self, arg: str):
|
|
516
|
+
"""Exit the shell."""
|
|
517
|
+
console.print("[dim]Goodbye![/dim]")
|
|
518
|
+
return True
|
|
519
|
+
|
|
520
|
+
def do_exit(self, arg: str):
|
|
521
|
+
"""Exit the shell."""
|
|
522
|
+
return self.do_quit(arg)
|
|
523
|
+
|
|
524
|
+
def do_EOF(self, arg: str):
|
|
525
|
+
"""Exit on Ctrl+D."""
|
|
526
|
+
console.print()
|
|
527
|
+
return self.do_quit(arg)
|
|
528
|
+
|
|
529
|
+
def default(self, line: str):
|
|
530
|
+
"""Handle unknown commands."""
|
|
531
|
+
console.print(f"[red]Unknown command: {line}[/red]")
|
|
532
|
+
console.print("[dim]Type 'help' for available commands[/dim]")
|
|
533
|
+
|
|
534
|
+
def emptyline(self):
|
|
535
|
+
"""Do nothing on empty line."""
|
|
536
|
+
pass
|
|
537
|
+
|
|
538
|
+
|
|
539
|
+
def run_shell(
|
|
540
|
+
tenant_id: str = "default",
|
|
541
|
+
domain: str = "localhost",
|
|
542
|
+
user_mode: bool = True,
|
|
543
|
+
):
|
|
544
|
+
"""Run the interactive Quadlet shell."""
|
|
545
|
+
shell = QuadletShell(
|
|
546
|
+
tenant_id=tenant_id,
|
|
547
|
+
domain=domain,
|
|
548
|
+
user_mode=user_mode,
|
|
549
|
+
)
|
|
550
|
+
try:
|
|
551
|
+
shell.cmdloop()
|
|
552
|
+
except KeyboardInterrupt:
|
|
553
|
+
console.print("\n[dim]Goodbye![/dim]")
|
|
554
|
+
|
|
555
|
+
|
|
556
|
+
if __name__ == "__main__":
|
|
557
|
+
run_shell()
|