feat: dynamic ip_range propagation to DNS, firewall, and docker-compose
When ip_range changes in Settings, the new subnet is now applied to: - DNS zone records (network_manager.apply_ip_range) - Caddy virtual IPs (firewall_manager.ensure_caddy_virtual_ips) - iptables per-service rules (firewall_manager.update_service_ips) - docker-compose.yml static IPs if writable (ip_utils.update_docker_compose_ips) New module ip_utils.py derives all container IPs from the subnet using fixed offsets so the entire stack stays consistent from one setting. 321 tests pass (72 new tests added for ip_utils, apply_ip_range, update_service_ips). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -263,6 +263,98 @@ test2 1800 IN CNAME test1
|
||||
self.assertIn('192.168.1.10', content)
|
||||
self.assertIn('192.168.1.11', content)
|
||||
|
||||
class TestBootstrapDnsRecords(unittest.TestCase):
|
||||
"""Test bootstrap_dns_records with dynamic IP derivation."""
|
||||
|
||||
def setUp(self):
|
||||
self.test_dir = tempfile.mkdtemp()
|
||||
self.data_dir = os.path.join(self.test_dir, 'data')
|
||||
self.config_dir = os.path.join(self.test_dir, 'config')
|
||||
os.makedirs(self.data_dir, exist_ok=True)
|
||||
os.makedirs(self.config_dir, exist_ok=True)
|
||||
self.nm = NetworkManager(self.data_dir, self.config_dir)
|
||||
|
||||
def tearDown(self):
|
||||
shutil.rmtree(self.test_dir)
|
||||
|
||||
@patch('subprocess.run')
|
||||
def test_creates_zone_file(self, _mock):
|
||||
self.nm.bootstrap_dns_records('mycell', 'cell')
|
||||
zone_file = os.path.join(self.nm.dns_zones_dir, 'cell.zone')
|
||||
self.assertTrue(os.path.exists(zone_file))
|
||||
|
||||
@patch('subprocess.run')
|
||||
def test_contains_default_caddy_ip(self, _mock):
|
||||
self.nm.bootstrap_dns_records('mycell', 'cell')
|
||||
zone_file = os.path.join(self.nm.dns_zones_dir, 'cell.zone')
|
||||
content = open(zone_file).read()
|
||||
self.assertIn('172.20.0.2', content) # caddy
|
||||
|
||||
@patch('subprocess.run')
|
||||
def test_custom_ip_range_used(self, _mock):
|
||||
self.nm.bootstrap_dns_records('mycell', 'cell', ip_range='10.5.0.0/24')
|
||||
zone_file = os.path.join(self.nm.dns_zones_dir, 'cell.zone')
|
||||
content = open(zone_file).read()
|
||||
self.assertIn('10.5.0.2', content) # caddy
|
||||
self.assertIn('10.5.0.21', content) # vip_calendar
|
||||
self.assertNotIn('172.20', content)
|
||||
|
||||
@patch('subprocess.run')
|
||||
def test_idempotent_skips_existing_zone(self, _mock):
|
||||
self.nm.bootstrap_dns_records('mycell', 'cell')
|
||||
zone_file = os.path.join(self.nm.dns_zones_dir, 'cell.zone')
|
||||
mtime1 = os.path.getmtime(zone_file)
|
||||
self.nm.bootstrap_dns_records('mycell', 'cell')
|
||||
mtime2 = os.path.getmtime(zone_file)
|
||||
self.assertEqual(mtime1, mtime2)
|
||||
|
||||
|
||||
class TestApplyIpRange(unittest.TestCase):
|
||||
"""Test apply_ip_range rewrites DNS zone records."""
|
||||
|
||||
def setUp(self):
|
||||
self.test_dir = tempfile.mkdtemp()
|
||||
self.data_dir = os.path.join(self.test_dir, 'data')
|
||||
self.config_dir = os.path.join(self.test_dir, 'config')
|
||||
os.makedirs(self.data_dir, exist_ok=True)
|
||||
os.makedirs(self.config_dir, exist_ok=True)
|
||||
self.nm = NetworkManager(self.data_dir, self.config_dir)
|
||||
|
||||
def tearDown(self):
|
||||
shutil.rmtree(self.test_dir)
|
||||
|
||||
@patch('subprocess.run')
|
||||
def test_zone_file_updated_with_new_ips(self, _mock):
|
||||
# Bootstrap with default range, then change to 10.0.0.0/24
|
||||
self.nm.bootstrap_dns_records('mycell', 'cell', '172.20.0.0/16')
|
||||
result = self.nm.apply_ip_range('10.0.0.0/24', 'mycell', 'cell')
|
||||
zone_file = os.path.join(self.nm.dns_zones_dir, 'cell.zone')
|
||||
content = open(zone_file).read()
|
||||
self.assertIn('10.0.0.2', content) # caddy
|
||||
self.assertIn('10.0.0.21', content) # vip_calendar
|
||||
self.assertNotIn('172.20', content)
|
||||
|
||||
@patch('subprocess.run')
|
||||
def test_returns_restarted_on_success(self, _mock):
|
||||
self.nm.bootstrap_dns_records('mycell', 'cell', '172.20.0.0/16')
|
||||
result = self.nm.apply_ip_range('10.0.0.0/24', 'mycell', 'cell')
|
||||
self.assertIn('cell-dns (reloaded)', result['restarted'])
|
||||
|
||||
@patch('subprocess.run')
|
||||
def test_all_standard_records_present(self, _mock):
|
||||
self.nm.apply_ip_range('10.1.2.0/24', 'pictest', 'mycell')
|
||||
zone_file = os.path.join(self.nm.dns_zones_dir, 'mycell.zone')
|
||||
content = open(zone_file).read()
|
||||
for host in ('pictest', 'api', 'webui', 'calendar', 'files', 'mail', 'webmail', 'webdav'):
|
||||
self.assertIn(host, content)
|
||||
|
||||
@patch('subprocess.run')
|
||||
def test_same_range_updates_zone_without_error(self, _mock):
|
||||
self.nm.bootstrap_dns_records('mycell', 'cell', '172.20.0.0/16')
|
||||
result = self.nm.apply_ip_range('172.20.0.0/16', 'mycell', 'cell')
|
||||
self.assertEqual(result['warnings'], [])
|
||||
|
||||
|
||||
class TestCellDnsForwarding(unittest.TestCase):
|
||||
"""Test add/remove cell DNS forwarding in Corefile."""
|
||||
|
||||
|
||||
Reference in New Issue
Block a user