From 34ba255024ad14f9915c99d8b0ba1c2bf4208c8a Mon Sep 17 00:00:00 2001 From: Fredrick Amnehagen Date: Thu, 5 Feb 2026 11:37:29 +0100 Subject: [PATCH] feat: complete professional cli with full lifecycle and tests --- README.md | 46 ++++++++++++++++++ infra_cli/__pycache__/config.cpython-313.pyc | Bin 0 -> 2262 bytes infra_cli/__pycache__/dns.cpython-313.pyc | Bin 0 -> 4142 bytes infra_cli/__pycache__/ingress.cpython-313.pyc | Bin 0 -> 2250 bytes infra_cli/__pycache__/main.cpython-313.pyc | Bin 0 -> 6863 bytes infra_cli/__pycache__/router.cpython-313.pyc | Bin 0 -> 4078 bytes infra_cli/__pycache__/ssh.cpython-313.pyc | Bin 0 -> 2158 bytes infra_cli/dns.py | 11 +++-- infra_cli/main.py | 34 +++++++++++++ infra_cli/router.py | 11 +++-- .../test_cli.cpython-313-pytest-9.0.2.pyc | Bin 0 -> 7993 bytes tests/test_cli.py | 19 ++++++++ 12 files changed, 112 insertions(+), 9 deletions(-) create mode 100644 README.md create mode 100644 infra_cli/__pycache__/config.cpython-313.pyc create mode 100644 infra_cli/__pycache__/dns.cpython-313.pyc create mode 100644 infra_cli/__pycache__/ingress.cpython-313.pyc create mode 100644 infra_cli/__pycache__/main.cpython-313.pyc create mode 100644 infra_cli/__pycache__/router.cpython-313.pyc create mode 100644 infra_cli/__pycache__/ssh.cpython-313.pyc create mode 100644 tests/__pycache__/test_cli.cpython-313-pytest-9.0.2.pyc diff --git a/README.md b/README.md new file mode 100644 index 0000000..9a0022f --- /dev/null +++ b/README.md @@ -0,0 +1,46 @@ +# LoopAware Infrastructure CLI + +Professional CLI tooling for managing dynamic infrastructure resources (DNS, DHCP, Ingress, and Port Forwarding). + +## Features +- **DNS/DHCP:** Manage `dnsmasq` reservations and custom records on `la-dnsmasq-01`. +- **Ingress:** Manage HAProxy subdomains and backend routing. +- **Router:** Manage OpenWrt firewall redirects. +- **Agent-Friendly:** Designed for use by AI agents and developers. + +## Installation + +```bash +cd external/dynamic-infra-tooling +pip install -e . +``` + +## Configuration + +Copy `config.yaml.example` to `config.yaml` and update with your infrastructure details. + +```bash +cp config.yaml.example config.yaml +``` + +## Usage + +Use the `infra` command: + +```bash +# List DNS entries +infra dns list + +# Add an ingress +infra ingress add my-app.loopaware.com 10.32.70.50 80 + +# Add a port forward (requires ROUTER_PASS env var) +export ROUTER_PASS='...' +infra router add "My-Service" tcp 5000 10.32.70.50 5000 +``` + +## Testing + +```bash +pytest tests/test_cli.py +``` diff --git a/infra_cli/__pycache__/config.cpython-313.pyc b/infra_cli/__pycache__/config.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..13a16f478a0c44f34651b9f3ddd02619cbaf23c0 GIT binary patch literal 2262 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 ICRVZae}zeU4*&oF literal 0 HcmV?d00001 diff --git a/infra_cli/__pycache__/router.cpython-313.pyc b/infra_cli/__pycache__/router.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..777ba63e3f39d17acc635c55d09203409cb856a2 GIT binary patch literal 4078 zcmbtXU2NOd6}}Wn{adb0MRpy>;q0cK+F5Snx>@ZwS(`sMhUH8QyI?6sa3bw6p-K0W zsuQ6F`Y;R%(84o@z{yj+r@2pcpX$D4PkWU>3^KTE2(YKTxi*Hr?p%tbf8qc$aseHk zzx#jgcMf;j+L8!b4*x+b0QVmv1dZ{TonHX+00~H-rqERiQ#=wP0zDPMk#5wF1f~ZG z>}aIrjp;c$AALrFl&9Q;AWUA+HPx`65p%KJa%xfHL)y-1AP>;T2vY*W5rM+A5a~gf z5on++86QUl7HCX}0*wnXoS2K}6K-3{tk^1^mJNAM#m~rs%l7o4vzOe6Lr{EpV^#X5Z&&XsQUb`6&xRQ7FfzDZE%CTN}>%%*j0lzy(yPEOsmUTmLxZ~FTe_co`o5$J3zib zo7tX~tIJm(e(q$4YRs33P+psLvZJ1+>%>ZRxmxc(?R1TRBK@Sh=j-I7W1F?k|CX z=td>ta-fyMxZ5>|VZ<8|F>+Xx2-MgjLUK1kH)BCRf%t}T5NHv~ppi%rpDHCrUPF1h zh{@urS@FV$eA%3zmkouVl{H;e#`rvoJHhDExMH|TtlAZ9lubo-)1``GYxC+Qj7{uD zEn87B&eND9-b{I3v7nD?S!72%&*Ni6AA?b(N?CJ{?Y}BtO`_U53j}T|)46o#{+%WB zzWGpbG6OYwGnH97cmLeOUpuM3C!KxupN+1UzrX$6?e&lACqHmHFVxc)HrqRENet(J z%6p&92~c=P{}h-P0m~@ZNIUEb1WJemKys)ZATY2SQGpSo830+2Mnwwvn8(M(NXREV zP12(&kq-4~kqP-Wkq!CG$O$6`K|_N1KoeuZh4KgC zpbQ5)<*}-pvXZA=RyP*`>ta}4(B-mrZ)eIY!%rJi>5ua2({Z6WHdfz5%!>d@Nay1hON@`Nwn z-C)-jK|bG-cRhExwa+#tH^xA|O!=Lv8?SG4gW`iganNH6Mcx|P>PWs)s4-jJDABf* zzMuZn^?$^BT2E|m79-Egdt$BXK%l&^kBrcOS?I7VDTXNI1ub2yW=S+Es;=7Vo^U!;pbUtHHVD)oU&4tKf9O*gpdCMPsF!QsT( z#m#K@%GC1I%FOai{q-{&vGse8qxILWIN8Z&cBYY?X=cSnR&=tzsa-l`)M|a;s>4k+ zxl)5GHMtuN?uNtt7Dj!30e9;IlMeS`le^a7t~I%j8r(+?cLQA0+*0v=ar1cpW_R!A zt9^ua_x_lQ_GW%U(YE#<+fX;Z)q`H?s%5+c;+K|~RDHOEkPIOJI;faWlDeKGTd3=k zk~D8B6`k;DN&2KB>%OI3k|5t(HhdhYhABxnM@HZYDG)-k5k3#ZJtj$(E!$dIl5L2L z-^15~B}v~PZzW#(#-wEQ2m_4O3*?VzJHoO8wVh#DZadx19^dYdu*d%!&$3;uj-*eX zacTG>!X&F;@|o8f=`!I+E!rv$;%8fXIEM+;5(KtLNxVf<6!kCE^G}rci8(`cZ6hFF GxBmeXIdZW8 literal 0 HcmV?d00001 diff --git a/infra_cli/__pycache__/ssh.cpython-313.pyc b/infra_cli/__pycache__/ssh.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f9ad6092beb5f9c5d6e2a2948c1aabc656d7b791 GIT binary patch literal 2158 zcmd6o-)kI29KdIP-(BvKTugG;q=}bUX}4{1h>2}9Cn>F(61>uERt_sH+r3S)c5nBb z*)t77X+_E@m8cM!g3$Qnw&0r>t#*xAfA%&Y}Lr=`E zFf#v$DtRL`g|an{_9j8!MDHQYCV~xFPgDgLx&Q@Ig;dgdv1QMa0qyD@XmP zqRgMSEz|KH(Jkc-*l4G5(4)`Z#~?OQq1WyV6*4x{R#c9D1a12B2wiD^8d?-X>tVSV zJ*t?3(Qfoug4FcF z(QTqE@N=1QI2A3nJr2D)=}97_atix&uPi$O#}UM6Gj%9NH?RovI>|j4>P9bf4i96U zR2__UR!$+L@QUzsuPmo>6ELZ84)y?VN4O*wrc|b_U7-mJ+G22{+FJM?N2;K*il|64 zoFl5@F(gOwkZ=yNvCmGp`)S3)mf_6XRCy;41Ox)DEJed}R=`tHRq zFMjdbmcGq@8~rA_^-(=}sw%c;q{p_WYW?4h?2Ob?XMQ-|NS)h#XZP(!>hh=a4^yMH z7q&+lsmbd6ugT%NqhF0S|8K}uc$Ecy zg@47bpoq`lx4%s+@cWX=x3y>U3za%D!veX{kX)l2u!#rZu)-c(AcWIY90B1oui_!9 z41-vb8GMu~qab84gezvdhHi&tunU_d*E2&!B1V}~g^$rGFM=TS;)YxWSotD#@m#<6 zfUM90o_9)lP}yb9=V=+w=VfL|j?iihRojGW9MiOtTddf$AJ?=GD!Sbo z>Cv=h3lk6Ox#_r?hI`>7gX@LqZ`B{rG~(%=WoWtw#pG(mGl{0{pkN!}U8I!}Q$ZgB zl@{GbPgq_UdlK&x2LCt_7jmID9hW&w&l1zWVK525LVNVGgei#3hC6*7p8*^FJrY{1 Y9&-%CJV40@DEcRVoXP!;Km^nN0@8NEw*UYD literal 0 HcmV?d00001 diff --git a/infra_cli/dns.py b/infra_cli/dns.py index 11d0a3d..d824ca2 100644 --- a/infra_cli/dns.py +++ b/infra_cli/dns.py @@ -21,7 +21,8 @@ class DNSManager: if res.returncode == 0: raise ValueError(f"MAC {mac} already exists") - self.client.run(f"pct exec {self.lxc_id} -- sh -c \"echo 'dhcp-host={mac},{hostname},{ip}' >> {self.hosts_file}\") + cmd = f"sh -c \"echo 'dhcp-host={mac},{hostname},{ip}' >> {self.hosts_file}\"" + self.exec_lxc(cmd) self.reload() def remove_host(self, mac): @@ -30,11 +31,13 @@ class DNSManager: def add_dns(self, domain, ip): self.exec_lxc(f"touch {self.dns_file}") - self.client.run(f"pct exec {self.lxc_id} -- sh -c \"echo 'address=/{domain}/{ip}' >> {self.dns_file}\") + cmd = f"sh -c \"echo 'address=/{domain}/{ip}' >> {self.dns_file}\"" + self.exec_lxc(cmd) self.reload() def remove_dns(self, domain): - self.client.run(f"pct exec {self.lxc_id} -- sh -c \"sed -i '\#address=/{domain}/#d' {self.dns_file}\") + cmd = f"sh -c \"sed -i '\#address=/{domain}/#d' {self.dns_file}\"" + self.exec_lxc(cmd) self.reload() def reload(self): @@ -47,4 +50,4 @@ class DNSManager: def list(self): hosts = self.exec_lxc(f"cat {self.hosts_file}").stdout dns = self.exec_lxc(f"cat {self.dns_file}").stdout - return {"hosts": hosts, "dns": dns} + return {"hosts": hosts, "dns": dns} \ No newline at end of file diff --git a/infra_cli/main.py b/infra_cli/main.py index 4dcbdb3..f71a9e1 100644 --- a/infra_cli/main.py +++ b/infra_cli/main.py @@ -63,11 +63,45 @@ def ingress_add(config, domain, ip, port, https): mgr.add(domain, ip, port, https) click.echo(f"Added ingress for {domain}") +@ingress.command(name='remove') +@click.argument('domain') +@click.pass_obj +def ingress_remove(config, domain): + mgr = IngressManager(config) + mgr.remove(domain) + click.echo(f"Removed ingress for {domain}") + +@ingress.command(name='list') +@click.pass_obj +def ingress_list(config): + mgr = IngressManager(config) + click.echo(mgr.list()) + @cli.group() def router(): """Manage Router Port Forwards""" pass +@router.command(name='add') +@click.argument('name') +@click.argument('proto') +@click.argument('ext_port') +@click.argument('int_ip') +@click.argument('int_port') +@click.pass_obj +def router_add(config, name, proto, ext_port, int_ip, int_port): + mgr = RouterManager(config) + mgr.add_forward(name, proto, ext_port, int_ip, int_port) + click.echo(f"Added port forward {name}") + +@router.command(name='remove') +@click.argument('section') +@click.pass_obj +def router_remove(config, section): + mgr = RouterManager(config) + mgr.remove_forward(section) + click.echo(f"Removed port forward {section}") + @router.command(name='list') @click.pass_obj def router_list(config): diff --git a/infra_cli/router.py b/infra_cli/router.py index 988f415..6488db8 100644 --- a/infra_cli/router.py +++ b/infra_cli/router.py @@ -38,16 +38,17 @@ class RouterManager: def list(self): # Get sections + # Use -n to prevent SSH from consuming stdin if we were in a loop (here we use split) res = self.client.run("uci show firewall | grep '=redirect' | cut -d. -f2 | cut -d= -f1 | sort | uniq") sections = res.stdout.strip().split('\n') results = [] for section in sections: if not section: continue - name = self.client.run(f"uci get firewall.{section}.name", capture=True).stdout.strip() - proto = self.client.run(f"uci get firewall.{section}.proto", capture=True).stdout.strip() - port = self.client.run(f"uci get firewall.{section}.src_dport", capture=True).stdout.strip() - dest = self.client.run(f"uci get firewall.{section}.dest_ip", capture=True).stdout.strip() + name = self.client.run(f"uci get firewall.{section}.name").stdout.strip() + proto = self.client.run(f"uci get firewall.{section}.proto").stdout.strip() + port = self.client.run(f"uci get firewall.{section}.src_dport").stdout.strip() + dest = self.client.run(f"uci get firewall.{section}.dest_ip").stdout.strip() results.append({ "section": section, "name": name, @@ -55,4 +56,4 @@ class RouterManager: "port": port, "dest": dest }) - return results + return results \ No newline at end of file diff --git a/tests/__pycache__/test_cli.cpython-313-pytest-9.0.2.pyc b/tests/__pycache__/test_cli.cpython-313-pytest-9.0.2.pyc new file mode 100644 index 0000000000000000000000000000000000000000..98a72fed2a53fa6ca25400ac7deca0b9a89bb1ea GIT binary patch literal 7993 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 Y?qlpbcYxmiT{lnf-|A?h_Z##0FBGpDOaK4? literal 0 HcmV?d00001 diff --git a/tests/test_cli.py b/tests/test_cli.py index 2319748..d4d1faa 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -43,3 +43,22 @@ def test_ingress_cli(unique_id): # Remove res = run_infra(["ingress", "remove", domain]) assert res.returncode == 0 + +def test_router_cli(unique_id): + name = f"Test-Cli-{unique_id}" + section = name.lower().replace("-", "_") + + # Add + # Use environment variable for router password + env = {"ROUTER_PASS": "kpvoh58zhq2sq6ms"} + res = run_infra(["router", "add", name, "tcp", "17000", "10.32.70.212", "17000"], env=env) + assert res.returncode == 0 + + # List + res = run_infra(["router", "list"], env=env) + assert section in res.stdout + + # Remove + res = run_infra(["router", "remove", section], env=env) + assert res.returncode == 0 +