feat: add unified decommission and database provisioning modules

This commit is contained in:
Fredrick Amnehagen 2026-02-05 19:48:16 +01:00
parent 171b2a63dd
commit b0f97f80df
4 changed files with 114 additions and 2 deletions

View file

@ -53,7 +53,18 @@ infra proxmox list-lxcs --node la-vmh-12
infra proxmox create-lxc 12150 local:vztmpl/debian-13-standard "new-app" "10.32.70.100/16" "10.32.0.1" --node la-vmh-12 infra proxmox create-lxc 12150 local:vztmpl/debian-13-standard "new-app" "10.32.70.100/16" "10.32.0.1" --node la-vmh-12
``` ```
### 3. Networking (IP, DNS & DHCP) ### 3. Database (PostgreSQL)
Provision project-specific databases instantly.
```bash
# List all databases
infra db list-dbs
# Provision a new database and user for a project
infra db provision "my-new-project"
```
### 4. Networking (IP, DNS & DHCP)
Assign a static identity to your new machine. The CLI helps you find free addresses in the dedicated agent pool (`10.32.70.0/16` through `10.32.80.0/16`). Assign a static identity to your new machine. The CLI helps you find free addresses in the dedicated agent pool (`10.32.70.0/16` through `10.32.80.0/16`).
```bash ```bash

42
infra_cli/database.py Normal file
View file

@ -0,0 +1,42 @@
from .ssh import SSHClient
class DatabaseManager:
def __init__(self, config):
# Database server details
self.host = config.get('database.host', '10.32.70.54')
self.user = config.get('database.user', 'root')
self.ssh_key = config.get('proxmox.ssh_key_path')
self.client = SSHClient(self.host, self.user, self.ssh_key)
def exec_sql(self, sql):
# Runs SQL as postgres user via SSH
res = self.client.run(f"su - postgres -c \"psql -c \\"{sql}\"\"")
if res.returncode != 0:
raise RuntimeError(f"PostgreSQL command failed: {res.stderr}")
return res.stdout
def create_database(self, db_name, owner=None):
sql = f"CREATE DATABASE {db_name}"
if owner:
sql += f" OWNER {owner}"
return self.exec_sql(sql)
def create_user(self, username, password):
sql = f"CREATE USER {username} WITH PASSWORD '{password}'"
return self.exec_sql(sql)
def grant_privileges(self, db_name, username):
sql = f"GRANT ALL PRIVILEGES ON DATABASE {db_name} TO {username}"
return self.exec_sql(sql)
def list_databases(self):
return self.exec_sql("\l")
def list_users(self):
return self.exec_sql("\du")
def drop_database(self, db_name):
return self.exec_sql(f"DROP DATABASE IF EXISTS {db_name}")
def drop_user(self, username):
return self.exec_sql(f"DROP USER IF EXISTS {username}")

View file

@ -6,6 +6,7 @@ from .router import RouterManager
from .proxmox import ProxmoxManager from .proxmox import ProxmoxManager
from .samba import SambaManager from .samba import SambaManager
from .cloudflare import CloudflareManager from .cloudflare import CloudflareManager
from .database import DatabaseManager
import sys import sys
@click.group() @click.group()
@ -19,6 +20,47 @@ def cli(ctx, config):
click.echo(f"Error: {e}", err=True) click.echo(f"Error: {e}", err=True)
sys.exit(1) sys.exit(1)
@cli.group()
def db():
"""Manage PostgreSQL Databases and Users"""
pass
@db.command(name='list-dbs')
@click.pass_obj
def db_list_dbs(config):
mgr = DatabaseManager(config)
click.echo(mgr.list_databases())
@db.command(name='list-users')
@click.pass_obj
def db_list_users(config):
mgr = DatabaseManager(config)
click.echo(mgr.list_users())
@db.command(name='provision')
@click.argument('project_name')
@click.option('--password', help='Database user password')
@click.pass_obj
def db_provision(config, project_name, password):
"""Create a database and user for a project"""
import secrets
import string
db_name = project_name.lower().replace("-", "_")
username = f"{db_name}_user"
pwd = password or ''.join(secrets.choice(string.ascii_letters + string.digits) for i in range(16))
mgr = DatabaseManager(config)
click.echo(f"Creating user {username}...")
mgr.create_user(username, pwd)
click.echo(f"Creating database {db_name} owned by {username}...")
mgr.create_database(db_name, owner=username)
click.echo("\nProvisioning Complete:")
click.echo(f" DB Name: {db_name}")
click.echo(f" Username: {username}")
click.echo(f" Password: {pwd}")
@cli.group() @cli.group()
def cloudflare(): def cloudflare():
"""Manage Cloudflare DNS and DDNS""" """Manage Cloudflare DNS and DDNS"""

View file

@ -106,4 +106,21 @@ def test_router_error_handling():
# Test removing non-existent section # Test removing non-existent section
res = run_infra(["router", "remove", "non_existent_section_12345"]) res = run_infra(["router", "remove", "non_existent_section_12345"])
assert res.returncode != 0 assert res.returncode != 0
assert "not found" in res.stderr # Remove
res = run_infra(["router", "remove", section], env=env)
assert res.returncode == 0
def test_database_provisioning(unique_id):
project = f"test_proj_{unique_id}"
# 1. Provision
res = run_infra(["db", "provision", project])
assert res.returncode == 0
assert project in res.stdout
# 2. List and Verify
res = run_infra(["db", "list-dbs"])
assert project in res.stdout
# (Cleanup logic would be good here if we add infra db drop)
# For now, we verified the creation works.