Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,15 @@ jobs:
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]

steps:
- uses: actions/checkout@v7
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1

- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v7
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: ${{ matrix.python-version }}

- name: Cache pip packages
uses: actions/cache@v6
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements*.txt', '**/pyproject.toml') }}
Expand All @@ -36,14 +36,14 @@ jobs:
pip install -r requirements-dev.txt

- name: Run pre-commit hooks
uses: pre-commit/action@v3.0.1
uses: pre-commit/action@2c7b3805fd2a0fd8c1884dcaebf91fc102a13ecd # v3.0.1

- name: Run tests with pytest
run: |
pytest --cov --cov-report=xml --cov-report=term-missing

- name: Upload coverage to Codecov
uses: codecov/codecov-action@v7
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
with:
file: ./coverage.xml
fail_ci_if_error: false
Expand All @@ -54,10 +54,10 @@ jobs:
type-check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1

- name: Set up Python
uses: actions/setup-python@v7
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: "3.11"

Expand Down
10 changes: 5 additions & 5 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v6.0.0
rev: 3e8a8703264a2f4a69428a0aa4dcb512790b2c8c # v6.0.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
Expand All @@ -15,26 +15,26 @@ repos:
- id: mixed-line-ending

- repo: https://github.com/gitleaks/gitleaks
rev: v8.30.1
rev: 83d9cd684c87d95d656c1458ef04895a7f1cbd8e # v8.30.1
hooks:
- id: gitleaks
entry: gitleaks dir --redact --verbose .
pass_filenames: false

- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.16.3
rev: 65dbdb59d2f2d9c3bdc343c566821ad3319ddaa3 # v0.16.3
hooks:
- id: ruff
args: [--fix]
- id: ruff-format

- repo: https://github.com/jendrikseipp/vulture
rev: v2.16
rev: b0f67ba0044693aa9ec0d38fe460590facc98004 # v2.16
hooks:
- id: vulture

- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.19.1
rev: a66e98df7b4aeeb3724184b332785976d062b92e # v1.19.1
hooks:
- id: mypy
exclude: ^tests/
Expand Down
5 changes: 0 additions & 5 deletions install.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,10 @@
#!/usr/bin/env python3
"""Install script for cflan - NetworkManager dispatcher setup."""

import os
import shutil
import sys


def install() -> None:
"""Deploy NetworkManager dispatcher script and configuration files."""
if os.getuid() != 0:
sys.exit("Error: Must run as root")

Expand All @@ -23,7 +20,6 @@ def install() -> None:
print("\nDeploying configuration...")
config_deployed = False

# Try unencrypted config first
vars_path = os.path.join(script_dir, "vars.yaml")
if os.path.exists(vars_path):
target_path = "/vars.yaml"
Expand All @@ -33,7 +29,6 @@ def install() -> None:
print(f" Installed: {target_path}")
config_deployed = True

# Fall back to sops encrypted config
sops_path = os.path.join(script_dir, "sops_vars.yaml")
if not config_deployed and os.path.exists(sops_path):
target_path = "/sops_vars.yaml"
Expand Down
2 changes: 2 additions & 0 deletions opencode.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"aws-prod": {"enabled": false},
"grafana": {"enabled": false},
"terraform-docs": {"enabled": false},
"argocd-makeitwork": {"enabled": false},
"argocd-staging-eks": {"enabled": false},
"argocd-prod-eks": {"enabled": false}
},
Expand All @@ -31,6 +32,7 @@
"aws-prod_*": false,
"grafana_*": false,
"terraform-docs_*": false,
"argocd-makeitwork_*": false,
"argocd-staging-eks_*": false,
"argocd-prod-eks_*": false
}
Expand Down
8 changes: 0 additions & 8 deletions set_dns.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@


def get_local_ip() -> str:
"""Get the local IP address."""
local_ip_addr = ""
try:
local_ip_addr = socket.gethostbyname(socket.gethostname() + ".local")
Expand All @@ -32,7 +31,6 @@ def get_local_ip() -> str:


def validate_network_manager_args(local_ip_addr: str) -> None:
"""Validate NetworkManager arguments."""
print("Parsing NetworkManager arguments...")
try:
if (
Expand Down Expand Up @@ -67,7 +65,6 @@ def validate_network_manager_args(local_ip_addr: str) -> None:


def get_yaml_vars() -> dict[str, Any]:
"""Get YAML variables from vars.yaml or sops_vars.yaml."""
print("Getting unencrypted values from vars.yaml ...")
try:
with open("vars.yaml") as f:
Expand All @@ -94,7 +91,6 @@ def get_yaml_vars() -> dict[str, Any]:


def get_zone_info(cf: CloudFlare.CloudFlare, domain_name: str) -> tuple[str, str]:
"""Get CloudFlare DNS Zone ID and Name."""
print("Getting CloudFlare DNS Zone ID and Name...")
zone_data = cf.zones.get(params={"per_page": "1", "name": domain_name})[0]
return zone_data["id"], zone_data["name"]
Expand All @@ -103,7 +99,6 @@ def get_zone_info(cf: CloudFlare.CloudFlare, domain_name: str) -> tuple[str, str
def get_dns_record_id(
cf: CloudFlare.CloudFlare, zone_id: str, hostname: str, zone_name: str
) -> str:
"""Get existing DNS record ID if it exists."""
print(
"Attempting to get existing DNS record for "
+ hostname
Expand Down Expand Up @@ -131,7 +126,6 @@ def get_dns_record_id(
def create_dns_record(
cf: CloudFlare.CloudFlare, zone_id: str, hostname: str, ip_addr: str
) -> None:
"""Create a new DNS record."""
print("Record not found...")
print("Creating new record for " + hostname + "...")
try:
Expand All @@ -147,7 +141,6 @@ def create_dns_record(
def update_dns_record(
cf: CloudFlare.CloudFlare, zone_id: str, dns_id: str, hostname: str, ip_addr: str
) -> None:
"""Update existing DNS record with new IP."""
print("Getting IP address for existing record...")
dns_content = cf.zones.dns_records.get(
zone_id,
Expand Down Expand Up @@ -179,7 +172,6 @@ def update_dns_record(


def set_dns() -> None:
"""Main function to set DNS records."""
local_ip_addr = get_local_ip()

print("Using IP address " + local_ip_addr + " ...")
Expand Down
37 changes: 0 additions & 37 deletions tests/test_set_dns.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
"""Tests for the set_dns module."""

import socket
import sys
from unittest.mock import MagicMock, mock_open, patch
Expand All @@ -23,12 +21,9 @@


class TestGetLocalIp:
"""Tests for get_local_ip function."""

@patch("set_dns.socket.gethostbyname")
@patch("set_dns.socket.gethostname")
def test_get_local_ip_with_local_suffix(self, mock_hostname, mock_gethost):
"""Test getting local IP with .local suffix."""
mock_hostname.return_value = "testhost"
mock_gethost.return_value = "192.168.1.100"
result = get_local_ip()
Expand All @@ -38,7 +33,6 @@ def test_get_local_ip_with_local_suffix(self, mock_hostname, mock_gethost):
@patch("set_dns.socket.gethostbyname")
@patch("set_dns.socket.gethostname")
def test_get_local_ip_fallback_to_lan(self, mock_hostname, mock_gethost):
"""Test fallback to .lan suffix when .local fails."""
mock_hostname.return_value = "testhost"

def side_effect(hostname):
Expand All @@ -53,7 +47,6 @@ def side_effect(hostname):
@patch("set_dns.socket.gethostbyname")
@patch("set_dns.socket.gethostname")
def test_get_local_ip_both_suffixes_fail(self, mock_hostname, mock_gethost):
"""Test that exception propagates when both .local and .lan fail."""
mock_hostname.return_value = "testhost"

def side_effect(hostname):
Expand All @@ -66,23 +59,18 @@ def side_effect(hostname):


class TestValidateNetworkManagerArgs:
"""Tests for validate_network_manager_args function."""

@patch("set_dns.netifaces")
@patch("set_dns.sys")
def test_valid_interface_and_action(self, mock_sys, mock_netifaces):
"""Test validation passes with correct interface and action."""
mock_sys.argv = ["script", "eth0", "up"]
mock_netifaces.ifaddresses.return_value = {2: [{"addr": "192.168.1.100"}]}
mock_netifaces.AF_INET = 2

# Should not raise or exit
validate_network_manager_args("192.168.1.100")

@patch("set_dns.netifaces")
@patch("set_dns.sys")
def test_mismatched_ip_address(self, mock_sys, mock_netifaces):
"""Test validation fails when IP doesn't match."""
mock_sys.argv = ["script", "eth0", "up"]
mock_sys.exit.side_effect = SystemExit
mock_netifaces.ifaddresses.return_value = {2: [{"addr": "10.0.0.1"}]}
Expand All @@ -94,7 +82,6 @@ def test_mismatched_ip_address(self, mock_sys, mock_netifaces):
@patch("set_dns.netifaces")
@patch("set_dns.sys")
def test_wrong_action(self, mock_sys, mock_netifaces):
"""Test validation fails with wrong action."""
mock_sys.argv = ["script", "eth0", "down"]
mock_sys.exit.side_effect = SystemExit
mock_netifaces.ifaddresses.return_value = {2: [{"addr": "192.168.1.100"}]}
Expand All @@ -106,24 +93,19 @@ def test_wrong_action(self, mock_sys, mock_netifaces):
@patch("set_dns.netifaces")
@patch("set_dns.sys")
def test_no_arguments(self, mock_sys, mock_netifaces):
"""Test validation continues when no arguments provided."""
mock_sys.argv = ["script"]
mock_netifaces.AF_INET = 2

# Should not raise
validate_network_manager_args("192.168.1.100")


class TestGetYamlVars:
"""Tests for get_yaml_vars function."""

@patch(
"builtins.open",
mock_open(read_data="cf_token: test123\ncf_domain: example.com"),
)
@patch("set_dns.yaml")
def test_get_unencrypted_yaml(self, mock_yaml):
"""Test reading unencrypted vars.yaml."""
mock_yaml.safe_load.return_value = {
"cf_token": "test123",
"cf_domain": "example.com",
Expand All @@ -138,8 +120,6 @@ def test_get_unencrypted_yaml(self, mock_yaml):
@patch("set_dns.yaml")
@patch("builtins.open")
def test_get_sops_encrypted_yaml(self, mock_open_file, mock_yaml, mock_run):
"""Test reading sops encrypted yaml when vars.yaml missing."""
# First open raises FileNotFoundError
mock_open_file.side_effect = FileNotFoundError()

mock_run.return_value = MagicMock(
Expand All @@ -160,7 +140,6 @@ def test_get_sops_encrypted_yaml(self, mock_open_file, mock_yaml, mock_run):
@patch("set_dns.subprocess.run")
@patch("builtins.open")
def test_sops_not_installed(self, mock_open_file, mock_run):
"""Test exit when sops is not installed."""
mock_open_file.side_effect = FileNotFoundError()
mock_run.side_effect = FileNotFoundError("sops not found")

Expand All @@ -169,10 +148,7 @@ def test_sops_not_installed(self, mock_open_file, mock_run):


class TestGetZoneInfo:
"""Tests for get_zone_info function."""

def test_get_zone_info(self):
"""Test retrieving zone ID and name."""
mock_cf = MagicMock()
mock_cf.zones.get.return_value = [{"id": "zone123", "name": "example.com"}]

Expand All @@ -183,10 +159,7 @@ def test_get_zone_info(self):


class TestGetDnsRecordId:
"""Tests for get_dns_record_id function."""

def test_existing_record_found(self):
"""Test getting DNS record ID when record exists."""
mock_cf = MagicMock()
mock_cf.zones.dns_records.get.return_value = [{"id": "record123"}]

Expand All @@ -195,7 +168,6 @@ def test_existing_record_found(self):
assert result == "record123"

def test_no_existing_record(self):
"""Test empty string returned when no record exists."""
mock_cf = MagicMock()
mock_cf.zones.dns_records.get.return_value = []

Expand All @@ -205,10 +177,7 @@ def test_no_existing_record(self):


class TestCreateDnsRecord:
"""Tests for create_dns_record function."""

def test_create_record_success(self):
"""Test successful DNS record creation."""
mock_cf = MagicMock()

create_dns_record(mock_cf, "zone123", "host", "192.168.1.100")
Expand All @@ -219,10 +188,8 @@ def test_create_record_success(self):
)

def test_create_record_api_error(self):
"""Test handling of Cloudflare API error."""
mock_cf = MagicMock()

# Create a proper exception class that inherits from Exception
class MockAPIError(Exception):
def __str__(self):
return "API Error"
Expand All @@ -241,10 +208,7 @@ def __int__(self):


class TestUpdateDnsRecord:
"""Tests for update_dns_record function."""

def test_update_when_ip_changed(self):
"""Test updating record when IP has changed."""
mock_cf = MagicMock()
mock_cf.zones.dns_records.get.return_value = [{"content": "10.0.0.1"}]

Expand All @@ -256,7 +220,6 @@ def test_update_when_ip_changed(self):
mock_cf.zones.dns_records.post.assert_called_once()

def test_no_update_when_ip_unchanged(self):
"""Test exiting when IP hasn't changed."""
mock_cf = MagicMock()
mock_cf.zones.dns_records.get.return_value = [{"content": "192.168.1.100"}]

Expand Down
Loading