checkpoint commit
All checks were successful
Podman DDNS Image / build-and-push-ddns (push) Successful in 1m3s
All checks were successful
Podman DDNS Image / build-and-push-ddns (push) Successful in 1m3s
This commit is contained in:
13
active/device_unifi/config.yaml
Normal file
13
active/device_unifi/config.yaml
Normal file
@@ -0,0 +1,13 @@
|
||||
rabbitmq:
|
||||
host: "rabbitmq.reeselink.com"
|
||||
port: 5672
|
||||
virtual_host: "/"
|
||||
username: "user"
|
||||
password: "password"
|
||||
|
||||
# Which *exchange* (topic) you actually want to listen to.
|
||||
# The program will create a temporary queue, bind it to this exchange
|
||||
# with the routing key supplied in `routing_key`.
|
||||
subscriber:
|
||||
exchange: "nic" # ← change to “reese” or any other exchange
|
||||
routing_key: "add" # ← could be “add”, “delete”, or any pattern
|
||||
136
active/device_unifi/rmq_dns.py
Normal file
136
active/device_unifi/rmq_dns.py
Normal file
@@ -0,0 +1,136 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
RabbitMQ setup & consumer using pika.
|
||||
|
||||
- Creates two **topic exchanges**: nic, reese
|
||||
- For each exchange creates two queues: add, delete
|
||||
- Binds the queues with routing keys “add” and “delete”
|
||||
- Subscribes (consumes) from a **single** exchange/queue pair that is
|
||||
supplied via a tiny config file (config.yaml).
|
||||
|
||||
Run:
|
||||
python3 rabbit_demo.py
|
||||
"""
|
||||
|
||||
import logging
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pika # type: ignore
|
||||
import yaml
|
||||
|
||||
# ----------------------------------------------------------------------
|
||||
# 1️⃣ Load configuration
|
||||
# ----------------------------------------------------------------------
|
||||
DEFAULT_CFG = """
|
||||
rabbitmq:
|
||||
host: "localhost"
|
||||
port: 5672
|
||||
virtual_host: "/"
|
||||
username: "guest"
|
||||
password: "guest"
|
||||
|
||||
# Which *exchange* (topic) you actually want to listen to.
|
||||
# The program will create a temporary queue, bind it to this exchange
|
||||
# with the routing key supplied in `routing_key`.
|
||||
subscriber:
|
||||
exchange: "nic" # ← change to “reese” or any other exchange
|
||||
routing_key: "add" # ← could be “add”, “delete”, or any pattern
|
||||
"""
|
||||
|
||||
CONFIG_PATH = Path("active/device_unifi/config.yaml")
|
||||
if not CONFIG_PATH.exists():
|
||||
CONFIG_PATH.write_text(DEFAULT_CFG)
|
||||
|
||||
with CONFIG_PATH.open() as f:
|
||||
cfg = yaml.safe_load(f)
|
||||
|
||||
# ----------------------------------------------------------------------
|
||||
# 2️⃣ Build connection parameters
|
||||
# ----------------------------------------------------------------------
|
||||
cred = pika.PlainCredentials(cfg["rabbitmq"]["username"], cfg["rabbitmq"]["password"])
|
||||
params = pika.ConnectionParameters(
|
||||
host=cfg["rabbitmq"]["host"],
|
||||
port=cfg["rabbitmq"]["port"],
|
||||
virtual_host=cfg["rabbitmq"]["virtual_host"],
|
||||
credentials=cred,
|
||||
)
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------
|
||||
# 3️⃣ Helper to declare exchanges / queues
|
||||
# ----------------------------------------------------------------------
|
||||
def declare_topology(channel):
|
||||
"""
|
||||
Create the two topic exchanges and the four queues,
|
||||
then bind each queue to its exchange with the appropriate routing key.
|
||||
"""
|
||||
exchanges = ["nic", "reese"]
|
||||
routing_keys = ["add", "delete"]
|
||||
|
||||
for exch in exchanges:
|
||||
channel.exchange_declare(exchange=exch, exchange_type="topic", durable=True)
|
||||
for key in routing_keys:
|
||||
queue_name = f"{exch}_{key}" # e.g. nic_add, reese_delete
|
||||
channel.queue_declare(queue=queue_name, durable=True)
|
||||
# bind queue to the exchange with the same routing key
|
||||
channel.queue_bind(queue=queue_name, exchange=exch, routing_key=key)
|
||||
logging.info(
|
||||
f"Declared queue {queue_name} bound to {exch} with key '{key}'"
|
||||
)
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------
|
||||
# 4️⃣ Consumer callback
|
||||
# ----------------------------------------------------------------------
|
||||
def on_message(ch, method, properties, body):
|
||||
logging.info(
|
||||
f"Received from exchange '{method.exchange}' "
|
||||
f"routing_key='{method.routing_key}': {body!r}"
|
||||
)
|
||||
# Acknowledge the message
|
||||
ch.basic_ack(delivery_tag=method.delivery_tag)
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------
|
||||
# 5️⃣ Main routine
|
||||
# ----------------------------------------------------------------------
|
||||
def main():
|
||||
logging.basicConfig(
|
||||
level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s"
|
||||
)
|
||||
|
||||
with pika.BlockingConnection(params) as conn:
|
||||
channel = conn.channel()
|
||||
|
||||
# 1️⃣ Declare the static topology (exchanges + queues)
|
||||
declare_topology(channel)
|
||||
|
||||
# 2️⃣ Set up a *temporary* queue for the subscriber defined in config
|
||||
result = channel.queue_declare(
|
||||
queue="", exclusive=True
|
||||
) # server‑generated name
|
||||
tmp_queue = result.method.queue
|
||||
|
||||
exch = cfg["subscriber"]["exchange"]
|
||||
rkey = cfg["subscriber"]["routing_key"]
|
||||
channel.queue_bind(queue=tmp_queue, exchange=exch, routing_key=rkey)
|
||||
|
||||
logging.info(
|
||||
f"Subscribed to exchange '{exch}' with routing_key '{rkey}' "
|
||||
f"using temporary queue '{tmp_queue}'"
|
||||
)
|
||||
|
||||
# 3️⃣ Start consuming
|
||||
channel.basic_consume(queue=tmp_queue, on_message_callback=on_message)
|
||||
|
||||
try:
|
||||
logging.info("Waiting for messages. Press Ctrl+C to exit.")
|
||||
channel.start_consuming()
|
||||
except KeyboardInterrupt:
|
||||
logging.info("Interrupted – closing connection.")
|
||||
channel.stop_consuming()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
46
active/device_unifi/rmq_dns_test.py
Normal file
46
active/device_unifi/rmq_dns_test.py
Normal file
@@ -0,0 +1,46 @@
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
import pika
|
||||
import yaml
|
||||
|
||||
DEFAULT_CFG = """
|
||||
rabbitmq:
|
||||
host: "localhost"
|
||||
port: 5672
|
||||
virtual_host: "/"
|
||||
username: "guest"
|
||||
password: "guest"
|
||||
|
||||
# Which *exchange* (topic) you actually want to listen to.
|
||||
# The program will create a temporary queue, bind it to this exchange
|
||||
# with the routing key supplied in `routing_key`.
|
||||
subscriber:
|
||||
exchange: "nic" # ← change to “reese” or any other exchange
|
||||
routing_key: "add" # ← could be “add”, “delete”, or any pattern
|
||||
"""
|
||||
|
||||
CONFIG_PATH = Path("active/device_unifi/config.yaml")
|
||||
if not CONFIG_PATH.exists():
|
||||
CONFIG_PATH.write_text(DEFAULT_CFG)
|
||||
|
||||
with CONFIG_PATH.open() as f:
|
||||
cfg = yaml.safe_load(f)
|
||||
|
||||
|
||||
cred = pika.PlainCredentials(cfg["rabbitmq"]["username"], cfg["rabbitmq"]["password"])
|
||||
params = pika.ConnectionParameters(
|
||||
host=cfg["rabbitmq"]["host"],
|
||||
port=cfg["rabbitmq"]["port"],
|
||||
virtual_host=cfg["rabbitmq"]["virtual_host"],
|
||||
credentials=cred,
|
||||
)
|
||||
|
||||
with pika.BlockingConnection(params) as c:
|
||||
ch = c.channel()
|
||||
ch.basic_publish(
|
||||
exchange="reese",
|
||||
routing_key="add",
|
||||
body=json.dumps({"msg": "hello nic add"}),
|
||||
properties=pika.BasicProperties(delivery_mode=2), # make it persistent
|
||||
)
|
||||
15
active/device_unifi/test_update_dns.py
Normal file
15
active/device_unifi/test_update_dns.py
Normal file
@@ -0,0 +1,15 @@
|
||||
from update_dns import ApiHelperMethods, ApiPaths, ApiWrapper
|
||||
|
||||
|
||||
def test_api_get():
|
||||
assert ApiWrapper.api_get(ApiPaths.list_sites()) is not None
|
||||
|
||||
|
||||
def test_site_name_to_id():
|
||||
assert ApiHelperMethods.site_name_to_id("Default") is not None
|
||||
|
||||
|
||||
def test_dns_record_exists():
|
||||
assert ApiHelperMethods.dns_record_exists("test.reeselink.com")
|
||||
|
||||
assert not ApiHelperMethods.dns_record_exists("idontexist.reeselink.com")
|
||||
58
active/device_unifi/unifi.md
Normal file
58
active/device_unifi/unifi.md
Normal file
@@ -0,0 +1,58 @@
|
||||
# Unifi
|
||||
|
||||
## Update DNS Records via API
|
||||
|
||||
```bash
|
||||
export API_KEY=$(cat active/device_unifi/secrets/api-key)
|
||||
|
||||
# List site IDs
|
||||
curl -L -g -k -s "https://192.168.1.1/proxy/network/integration/v1/sites" \
|
||||
-H "Accept: application/json" \
|
||||
-H "X-API-Key: ${API_KEY}" | jq -rc '.data[0].id'
|
||||
|
||||
# List domains
|
||||
curl -L -g -k -s "https://192.168.1.1/proxy/network/integration/v1/sites/88f7af54-98f8-306a-a1c7-c9349722b1f6/dns/policies" \
|
||||
-H "Accept: application/json" \
|
||||
-H "X-API-Key: ${API_KEY}" | jq -r '.data[] | {domain, id}'
|
||||
|
||||
# List device domains
|
||||
curl -L -g -k -s "https://10.1.0.1/proxy/network/v2/api/site/default/static-dns/devices" \
|
||||
-H "Accept: application/json" \
|
||||
-H "X-API-Key: ${API_KEY}" | jq -r
|
||||
|
||||
# List clients
|
||||
curl -L -g -k -s "https://192.168.1.1/proxy/network/integration/v1/sites/88f7af54-98f8-306a-a1c7-c9349722b1f6/clients" \
|
||||
-H "Accept: application/json" \
|
||||
-H "X-API-Key: ${API_KEY}"
|
||||
|
||||
# List firewall policies
|
||||
curl -L -g -k -s "https://192.168.1.1/proxy/network/integration/v1/sites/88f7af54-98f8-306a-a1c7-c9349722b1f6/firewall/policies" \
|
||||
-H "Accept: application/json" \
|
||||
-H "X-API-Key: ${API_KEY}"
|
||||
|
||||
# Create a record
|
||||
curl -L -g -k -s "https://192.168.1.1/proxy/network/integration/v1/sites/88f7af54-98f8-306a-a1c7-c9349722b1f6/dns/policies" \
|
||||
-H "Accept: application/json" \
|
||||
-H "X-API-Key: ${API_KEY}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{
|
||||
\"type\": \"A_RECORD\",
|
||||
\"enabled\": true,
|
||||
\"domain\": \"test.reeselink.com\",
|
||||
\"ipv4Address\": \"10.1.0.100\",
|
||||
\"ttlSeconds\": 300
|
||||
}"
|
||||
|
||||
# Update a record
|
||||
curl -L -g -k -s -X PUT "https://192.168.1.1/proxy/network/integration/v1/sites/88f7af54-98f8-306a-a1c7-c9349722b1f6/dns/policies/a5689d61-811a-48b0-a47c-2ece038e4356" \
|
||||
-H "Accept: application/json" \
|
||||
-H "X-API-Key: ${API_KEY}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{
|
||||
\"type\": \"A_RECORD\",
|
||||
\"enabled\": true,
|
||||
\"domain\": \"test.reeselink.com\",
|
||||
\"ipv4Address\": \"10.1.0.100\",
|
||||
\"ttlSeconds\": 300
|
||||
}"
|
||||
```
|
||||
103
active/device_unifi/update_dns.py
Normal file
103
active/device_unifi/update_dns.py
Normal file
@@ -0,0 +1,103 @@
|
||||
import json
|
||||
import os
|
||||
from typing import TypedDict
|
||||
|
||||
import requests
|
||||
|
||||
UNIFI_API_ENDPOINT = os.getenv("UNIFI_API_ENDPOINT", "https://192.168.1.1")
|
||||
UNIFI_SITE_NAME = os.getenv("UNIFI_SITE_NAME", "Default")
|
||||
UNIFI_API_KEY = os.getenv("UNIFI_API_KEY")
|
||||
|
||||
if not UNIFI_API_KEY:
|
||||
try:
|
||||
with open("active/device_unifi/secrets/api-key", "r") as f:
|
||||
UNIFI_API_KEY = f.read()
|
||||
except (FileNotFoundError, PermissionError) as e:
|
||||
print(e)
|
||||
print("UNIFI_API_KEY required.")
|
||||
|
||||
type uuid_type = str
|
||||
|
||||
|
||||
class UnifiSite(TypedDict):
|
||||
id: uuid_type
|
||||
internalReference: str
|
||||
name: str
|
||||
|
||||
|
||||
class ApiPaths:
|
||||
@classmethod
|
||||
def list_sites(cls) -> str:
|
||||
return "/proxy/network/integration/v1/sites"
|
||||
|
||||
@classmethod
|
||||
def list_records(cls, site_id: uuid_type) -> str:
|
||||
return f"/proxy/network/integration/v1/sites/{site_id}/dns/policies"
|
||||
|
||||
@classmethod
|
||||
def create_record(cls, site_id: uuid_type) -> str:
|
||||
return f"/proxy/network/integration/v1/sites/{site_id}/dns/policies"
|
||||
|
||||
@classmethod
|
||||
def update_record(cls, site_id: uuid_type, record_id: uuid_type) -> str:
|
||||
return f"/proxy/network/integration/v1/sites/{site_id}/dns/policies/{record_id}"
|
||||
|
||||
|
||||
class ApiWrapper:
|
||||
@classmethod
|
||||
def api_get(cls, path: str):
|
||||
return requests.get(
|
||||
f"{UNIFI_API_ENDPOINT}{path}",
|
||||
headers={"X-API-Key": UNIFI_API_KEY},
|
||||
verify=False,
|
||||
).json()
|
||||
|
||||
@classmethod
|
||||
def api_put(cls, path: str, body: dict):
|
||||
return requests.put(
|
||||
f"{UNIFI_API_ENDPOINT}{path}",
|
||||
headers={"X-API-Key": UNIFI_API_KEY},
|
||||
verify=False,
|
||||
json=json.dumps(body),
|
||||
).json()
|
||||
|
||||
|
||||
class ApiHelperMethods:
|
||||
@classmethod
|
||||
def site_name_to_id(cls, site_name: str) -> uuid_type:
|
||||
results = ApiWrapper.api_get(ApiPaths.list_sites())
|
||||
data: list[UnifiSite] = results.get("data")
|
||||
if not data:
|
||||
print("No sites found")
|
||||
exit(1)
|
||||
filtered_sites: list[UnifiSite] = list(
|
||||
filter(lambda data_item: data_item["name"] == site_name, data)
|
||||
)
|
||||
if not filtered_sites:
|
||||
print("Site with that name not found")
|
||||
exit(1)
|
||||
site_id = filtered_sites[0]["id"]
|
||||
return site_id
|
||||
|
||||
# @classmethod
|
||||
# def upsert_dns_ipv4_record(cls, record_name: str, ipv4_addr: str) -> uuid_type:
|
||||
|
||||
@classmethod
|
||||
def dns_record_exists(cls, record_name: str) -> bool:
|
||||
site_id = ApiHelperMethods.site_name_to_id(UNIFI_SITE_NAME)
|
||||
record_id = ApiWrapper.api_get(path=ApiPaths.list_records(site_id))
|
||||
matched_dns_records = list(
|
||||
filter(
|
||||
lambda record_item: record_item["domain"] == record_name,
|
||||
record_id["data"],
|
||||
)
|
||||
)
|
||||
if matched_dns_records:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
|
||||
site_name = sys.argv[1]
|
||||
Reference in New Issue
Block a user