fix(dns): remove legacy zones after migration
Build and publish Docker image / build-and-publish (push) Successful in 7s
Build and publish Docker image / build-and-publish (push) Successful in 7s
This commit is contained in:
1 parent
079140ce49
commit
d8c1f0c604
1 file changed
+43
-8
@@ -2,8 +2,8 @@
|
|||||||
"""Move legacy per-user deSEC zones into the parent zone's CNAME wildcard model.
|
"""Move legacy per-user deSEC zones into the parent zone's CNAME wildcard model.
|
||||||
|
|
||||||
Run from the backend environment. The default is a read-only dry run; pass
|
Run from the backend environment. The default is a read-only dry run; pass
|
||||||
``--apply`` to copy records and cut over delegations. Child zones are retained
|
``--apply`` to copy records, cut over delegations, export the old zonefiles,
|
||||||
so their records remain available for rollback.
|
and remove the child zones that would otherwise override the parent records.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
@@ -117,7 +117,7 @@ def _validate_owner(owner: str) -> None:
|
|||||||
raise ValueError(f'Unsupported legacy namespace owner: {owner!r}')
|
raise ValueError(f'Unsupported legacy namespace owner: {owner!r}')
|
||||||
|
|
||||||
|
|
||||||
def build_plan(api: DeSec) -> Tuple[List[str], List[Dict], Dict[Tuple[str, str], Dict]]:
|
def build_plan(api: DeSec) -> Tuple[List[str], List[str], List[Dict], Dict[Tuple[str, str], Dict]]:
|
||||||
zones_response = api.request('GET', f'{api.base_url}/domains/')
|
zones_response = api.request('GET', f'{api.base_url}/domains/')
|
||||||
if zones_response.status_code != 200:
|
if zones_response.status_code != 200:
|
||||||
raise RuntimeError(f'Could not list deSEC zones ({zones_response.status_code})')
|
raise RuntimeError(f'Could not list deSEC zones ({zones_response.status_code})')
|
||||||
@@ -183,7 +183,7 @@ def build_plan(api: DeSec) -> Tuple[List[str], List[Dict], Dict[Tuple[str, str],
|
|||||||
continue
|
continue
|
||||||
raise RuntimeError(f'Conflicting parent RRset at {owner} {item["type"]}')
|
raise RuntimeError(f'Conflicting parent RRset at {owner} {item["type"]}')
|
||||||
|
|
||||||
return sorted(namespaces), copies, parent_map
|
return sorted(namespaces), legacy_zones, copies, parent_map
|
||||||
|
|
||||||
|
|
||||||
def _delete_rrset(api: DeSec, subname: str, record_type: str) -> None:
|
def _delete_rrset(api: DeSec, subname: str, record_type: str) -> None:
|
||||||
@@ -200,8 +200,34 @@ def _create_rrset(api: DeSec, payload: Dict) -> None:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _backup_and_remove_legacy_zones(api: DeSec, legacy_zones: Iterable[str], backup_dir: str) -> None:
|
||||||
|
"""Export old zones for rollback, then remove their competing authority."""
|
||||||
|
os.makedirs(backup_dir, mode=0o700, exist_ok=True)
|
||||||
|
zonefiles = {}
|
||||||
|
for zone in legacy_zones:
|
||||||
|
url = f'{api.base_url}/domains/{quote(zone, safe=".-")}/zonefile/'
|
||||||
|
response = api.request('GET', url)
|
||||||
|
if response.status_code != 200:
|
||||||
|
raise RuntimeError(f'Could not export legacy zone {zone} ({response.status_code})')
|
||||||
|
zonefiles[zone] = response.text
|
||||||
|
|
||||||
|
for zone, zonefile in zonefiles.items():
|
||||||
|
filename = os.path.join(backup_dir, f'{zone}.zone')
|
||||||
|
with open(filename, 'w', encoding='utf-8') as backup:
|
||||||
|
backup.write(zonefile)
|
||||||
|
print(f'Exported {len(zonefiles)} legacy zone backups to {backup_dir}')
|
||||||
|
|
||||||
|
for zone in legacy_zones:
|
||||||
|
url = f'{api.base_url}/domains/{quote(zone, safe=".-")}/'
|
||||||
|
response = api.request('DELETE', url)
|
||||||
|
if response.status_code not in {204, 404}:
|
||||||
|
raise RuntimeError(f'Could not remove legacy zone {zone} ({response.status_code})')
|
||||||
|
print(f'Removed legacy child zone {zone}')
|
||||||
|
|
||||||
|
|
||||||
def apply_plan(api: DeSec, namespaces: Iterable[str], copies: List[Dict],
|
def apply_plan(api: DeSec, namespaces: Iterable[str], copies: List[Dict],
|
||||||
parent_map: Dict[Tuple[str, str], Dict]) -> None:
|
legacy_zones: Iterable[str], parent_map: Dict[Tuple[str, str], Dict],
|
||||||
|
backup_dir: str) -> None:
|
||||||
# Copy child records and create wildcard records first; old delegations stay
|
# Copy child records and create wildcard records first; old delegations stay
|
||||||
# authoritative until all non-destructive preparation has completed.
|
# authoritative until all non-destructive preparation has completed.
|
||||||
for payload in copies:
|
for payload in copies:
|
||||||
@@ -246,6 +272,11 @@ def apply_plan(api: DeSec, namespaces: Iterable[str], copies: List[Dict],
|
|||||||
})
|
})
|
||||||
raise
|
raise
|
||||||
|
|
||||||
|
# Hosted child zones continue to answer authoritatively even after their
|
||||||
|
# parent NS/DS records are removed. Back them up and remove them so the
|
||||||
|
# new parent-zone CNAMEs and wildcards are actually served.
|
||||||
|
_backup_and_remove_legacy_zones(api, legacy_zones, backup_dir)
|
||||||
|
|
||||||
from database_service import database_connection
|
from database_service import database_connection
|
||||||
|
|
||||||
with database_connection() as connection:
|
with database_connection() as connection:
|
||||||
@@ -268,6 +299,10 @@ def apply_plan(api: DeSec, namespaces: Iterable[str], copies: List[Dict],
|
|||||||
def main() -> int:
|
def main() -> int:
|
||||||
parser = argparse.ArgumentParser(description=__doc__)
|
parser = argparse.ArgumentParser(description=__doc__)
|
||||||
parser.add_argument('--apply', action='store_true', help='Apply the reviewed DNS migration')
|
parser.add_argument('--apply', action='store_true', help='Apply the reviewed DNS migration')
|
||||||
|
parser.add_argument(
|
||||||
|
'--backup-dir', default='/tmp/anchorhost-delegated-dns-backup',
|
||||||
|
help='Directory for exported legacy zonefiles before their zones are deleted',
|
||||||
|
)
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
token = os.environ.get('DESEC_API_TOKEN')
|
token = os.environ.get('DESEC_API_TOKEN')
|
||||||
base_domain = os.environ.get('BASE_DOMAIN')
|
base_domain = os.environ.get('BASE_DOMAIN')
|
||||||
@@ -276,15 +311,15 @@ def main() -> int:
|
|||||||
|
|
||||||
api = DeSec(token, base_domain)
|
api = DeSec(token, base_domain)
|
||||||
try:
|
try:
|
||||||
namespaces, copies, parent_map = build_plan(api)
|
namespaces, legacy_zones, copies, parent_map = build_plan(api)
|
||||||
print(f'Namespaces to migrate: {len(namespaces)}')
|
print(f'Namespaces to migrate: {len(namespaces)}')
|
||||||
print(f'Legacy RRsets to copy: {len(copies)}')
|
print(f'Legacy RRsets to copy: {len(copies)}')
|
||||||
print('Namespaces:', ', '.join(namespaces))
|
print('Namespaces:', ', '.join(namespaces))
|
||||||
print('Record copies:', ', '.join(f'{item["type"]}:{item["subname"]}' for item in copies) or 'none')
|
print('Record copies:', ', '.join(f'{item["type"]}:{item["subname"]}' for item in copies) or 'none')
|
||||||
print('Mode:', 'APPLY' if args.apply else 'DRY RUN')
|
print('Mode:', 'APPLY' if args.apply else 'DRY RUN')
|
||||||
if args.apply:
|
if args.apply:
|
||||||
apply_plan(api, namespaces, copies, parent_map)
|
apply_plan(api, namespaces, copies, legacy_zones, parent_map, args.backup_dir)
|
||||||
print('DNS migration completed. Legacy child zones were retained for rollback.')
|
print('DNS migration completed. Legacy zonefiles are backed up; old child zones are removed.')
|
||||||
else:
|
else:
|
||||||
print('No changes made. Run with --apply to perform the migration.')
|
print('No changes made. Run with --apply to perform the migration.')
|
||||||
except Exception as error:
|
except Exception as error:
|
||||||
|
|||||||
Reference in new issue
Block a user