apply_cell_name() now skips multi-label zone files (split-horizon DDNS zones like pic2.pic.ngo.zone) and excludes '*' and '@' from hostname candidate detection, preventing the wildcard record from being renamed to the old cell name during a cell rename. update_split_horizon_zone() now deletes stale zone files from previous cell names sharing the same TLD (e.g. pic3.pic.ngo.zone when renaming to pic2.pic.ngo), eliminating orphaned DNS entries. _bootstrap_dns() now detects non-LAN domain modes and calls update_split_horizon_zone() instead of apply_ip_range(), preventing service records (api, calendar, files…) from being re-injected into the DDNS parent zone on every container restart. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -496,5 +496,131 @@ class TestUpdateSplitHorizonZone(unittest.TestCase):
|
||||
self.assertIn('calendar', content)
|
||||
|
||||
|
||||
class TestApplyCellName(unittest.TestCase):
|
||||
"""Tests for apply_cell_name — hostname rename in primary DNS zone."""
|
||||
|
||||
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(os.path.join(self.data_dir, 'dns'), exist_ok=True)
|
||||
os.makedirs(os.path.join(self.config_dir, 'dns'), exist_ok=True)
|
||||
self.nm = NetworkManager(self.data_dir, self.config_dir)
|
||||
|
||||
def tearDown(self):
|
||||
shutil.rmtree(self.test_dir)
|
||||
|
||||
def _write_zone(self, name: str, content: str):
|
||||
path = os.path.join(self.data_dir, 'dns', f'{name}.zone')
|
||||
with open(path, 'w') as f:
|
||||
f.write(content)
|
||||
return path
|
||||
|
||||
@patch('subprocess.run')
|
||||
def test_renames_hostname_in_primary_zone(self, _mock):
|
||||
"""Old cell name is replaced with new name in the primary zone."""
|
||||
self._write_zone('cell', (
|
||||
'$ORIGIN cell.\n'
|
||||
'@ 300 IN SOA ns1 admin 1 3600 900 86400 300\n'
|
||||
'@ 300 IN NS ns1\n'
|
||||
'oldname 300 IN A 172.20.0.2\n'
|
||||
'api 300 IN A 172.20.0.10\n'
|
||||
))
|
||||
self.nm.apply_cell_name('oldname', 'newname', reload=False)
|
||||
content = open(os.path.join(self.data_dir, 'dns', 'cell.zone')).read()
|
||||
self.assertIn('newname', content)
|
||||
self.assertNotIn('oldname', content)
|
||||
|
||||
@patch('subprocess.run')
|
||||
def test_does_not_corrupt_split_horizon_zone(self, _mock):
|
||||
"""A multi-label DDNS zone (e.g. pic2.pic.ngo.zone) must not be touched."""
|
||||
sh_path = self._write_zone('pic2.pic.ngo', (
|
||||
'$ORIGIN pic2.pic.ngo.\n'
|
||||
'@ 300 IN SOA ns1 admin 1 3600 900 86400 300\n'
|
||||
'@ 300 IN NS ns1\n'
|
||||
'@ 300 IN A 172.20.0.2\n'
|
||||
'* 300 IN A 172.20.0.2\n'
|
||||
))
|
||||
self._write_zone('cell', (
|
||||
'$ORIGIN cell.\n'
|
||||
'@ 300 IN SOA ns1 admin 1 3600 900 86400 300\n'
|
||||
'oldname 300 IN A 172.20.0.2\n'
|
||||
))
|
||||
self.nm.apply_cell_name('oldname', 'newname', reload=False)
|
||||
# Split-horizon zone must be unchanged (wildcard not renamed)
|
||||
sh_content = open(sh_path).read()
|
||||
self.assertNotIn('newname', sh_content)
|
||||
self.assertIn('* 300 IN A 172.20.0.2', sh_content)
|
||||
|
||||
@patch('subprocess.run')
|
||||
def test_wildcard_not_treated_as_hostname(self, _mock):
|
||||
"""Wildcard record in a zone must never be detected as the cell hostname."""
|
||||
zone_path = self._write_zone('cell', (
|
||||
'$ORIGIN cell.\n'
|
||||
'@ 300 IN SOA ns1 admin 1 3600 900 86400 300\n'
|
||||
'@ 300 IN A 172.20.0.2\n'
|
||||
'* 300 IN A 172.20.0.2\n'
|
||||
))
|
||||
self.nm.apply_cell_name('', 'newname', reload=False)
|
||||
content = open(zone_path).read()
|
||||
# Wildcard must remain; 'newname' must not appear
|
||||
self.assertIn('* 300 IN A', content)
|
||||
self.assertNotIn('newname', content)
|
||||
|
||||
@patch('subprocess.run')
|
||||
def test_skips_zone_with_local_in_name(self, _mock):
|
||||
"""Zones with 'local' in the filename are ignored."""
|
||||
local_path = self._write_zone('home.local', (
|
||||
'$ORIGIN home.local.\n'
|
||||
'oldname 300 IN A 172.20.0.2\n'
|
||||
))
|
||||
self.nm.apply_cell_name('oldname', 'newname', reload=False)
|
||||
content = open(local_path).read()
|
||||
self.assertIn('oldname', content)
|
||||
self.assertNotIn('newname', content)
|
||||
|
||||
|
||||
class TestUpdateSplitHorizonZoneStaleCleanup(unittest.TestCase):
|
||||
"""Tests for stale split-horizon zone deletion in update_split_horizon_zone."""
|
||||
|
||||
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(os.path.join(self.data_dir, 'dns'), exist_ok=True)
|
||||
os.makedirs(os.path.join(self.config_dir, 'dns'), 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_deletes_old_cell_zone_same_tld(self, _mock):
|
||||
"""When renaming pic3.pic.ngo → pic2.pic.ngo the old zone file is removed."""
|
||||
old_zone = os.path.join(self.data_dir, 'dns', 'pic3.pic.ngo.zone')
|
||||
with open(old_zone, 'w') as f:
|
||||
f.write('@ 300 IN A 172.20.0.2\n')
|
||||
self.nm.update_split_horizon_zone('pic2.pic.ngo', '172.20.0.2')
|
||||
self.assertFalse(os.path.exists(old_zone), 'stale pic3.pic.ngo.zone should be deleted')
|
||||
new_zone = os.path.join(self.data_dir, 'dns', 'pic2.pic.ngo.zone')
|
||||
self.assertTrue(os.path.exists(new_zone))
|
||||
|
||||
@patch('subprocess.run')
|
||||
def test_keeps_zone_for_different_tld(self, _mock):
|
||||
"""Zone files under a different TLD are not deleted."""
|
||||
other_zone = os.path.join(self.data_dir, 'dns', 'myhost.example.com.zone')
|
||||
with open(other_zone, 'w') as f:
|
||||
f.write('@ 300 IN A 1.2.3.4\n')
|
||||
self.nm.update_split_horizon_zone('pic2.pic.ngo', '172.20.0.2')
|
||||
self.assertTrue(os.path.exists(other_zone), 'unrelated zone must not be deleted')
|
||||
|
||||
@patch('subprocess.run')
|
||||
def test_keeps_current_effective_zone(self, _mock):
|
||||
"""The current effective_domain zone file is never deleted."""
|
||||
self.nm.update_split_horizon_zone('pic2.pic.ngo', '172.20.0.2')
|
||||
current_zone = os.path.join(self.data_dir, 'dns', 'pic2.pic.ngo.zone')
|
||||
self.assertTrue(os.path.exists(current_zone))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user