-
Notifications
You must be signed in to change notification settings - Fork 2
/
migrate_zone.py
480 lines (396 loc) · 14.2 KB
/
migrate_zone.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
# Copyright (c) 2021, Oracle and/or its affiliates. All rights reserved.
'''
Migrate zones from dynect to OCI
'''
import argparse
import getpass
import socket
import time
import traceback
import urllib.parse
import urllib.request
import requests
import oci
import dns.zone
import dns.query
import dns.xfr
from dyn.tm.session import DynectSession
from dyn.tm.zones import Zone
from dyn.tm.zones import SecondaryZone
from dyn.tm.zones import TSIG
from requests.adapters import HTTPAdapter
from oci.signer import Signer
MAX_POLL_ATTEMPTS = 100
parser = argparse.ArgumentParser(
description='Migrate zones from Dyn Managed DNS to OCI DNS'
)
zone_target_group = parser.add_mutually_exclusive_group(required=True)
zone_target_group.add_argument(
'--zone-name',
type=str,
default=None,
help='Name of the zone to migrate. Required if --zone-names-file is not used.'
)
zone_target_group.add_argument(
'--zone-names-file',
type=str,
default=None,
help='A file containing names of zones to migrate. Required if --zone-name is not used.'
)
parser.add_argument(
'dynect_customer',
type=str,
help='Name of the Dynect Customer which owns the zone to be transferred'
)
parser.add_argument(
'dynect_username',
type=str,
help='Username of a Dynect user that has permission to manage the zone in Dynect'
)
parser.add_argument(
'--dynect-password',
default='',
help='Password of the Dynect user'
)
parser.add_argument(
'--oci-compartment',
type=str,
help='OCI compartment to which to migrate the zone',
default=''
)
parser.add_argument(
'--oci-config-file',
type=str,
default='~/.oci/config',
help='The OCI config file to use for authentication'
)
parser.add_argument(
'--oci-config-profile',
type=str,
default='DEFAULT',
help='The OCI config profile to use for authentication'
)
parser.add_argument(
'--tsig-key-compartment',
type=str,
default='',
help='The OCI compartment containing any tsig keys that are used by zones to be migrated. ' \
'By default, the same as --oci-compartment'
)
parser.add_argument(
'--ignore-failures',
action='store_true',
help='If an error occurs while migrating a zone, skip that zone and continue trying to ' \
'migrate the rest.'
)
parser.add_argument(
'--no-ignore-failures',
action='store_false',
dest="ignore_failures",
help='If an error occurs while migrating a zone, exit the script without migrating any more ' \
'zones.'
)
parser.set_defaults(ignore_failures=True)
args = parser.parse_args()
dynect_password = args.dynect_password
if dynect_password == "":
dynect_password = getpass.getpass(prompt='Dynect password: ')
# determine if the dynect session should be configured to use a proxy
PROXY_HOST = None
PROXY_PORT = None
PROXY_USER = None
PROXY_PASS = None
proxies = urllib.request.getproxies()
https_proxy = proxies.get('https', proxies.get('all', None))
if https_proxy is not None:
parsed = urllib.parse.urlparse(https_proxy)
PROXY_HOST = parsed.hostname
PROXY_PORT = parsed.port
PROXY_USER = parsed.username
PROXY_PASS = parsed.password
dynect_session = DynectSession(
args.dynect_customer,
args.dynect_username,
dynect_password,
proxy_host=PROXY_HOST,
proxy_port=PROXY_PORT,
proxy_user=PROXY_USER,
proxy_pass=PROXY_PASS,
)
config = oci.config.from_file(args.oci_config_file, args.oci_config_profile)
OCI_DNS_BASE_URL = f'{oci.regions.endpoint_for("dns", config["region"])}/20180115'
CREATE_OCI_DNS_ZONE_FROM_ZONEFILE_URL = f'{OCI_DNS_BASE_URL}/actions/createZoneFromZoneFile'
OCI_DNS_ZONES_BASE_URL = f'{OCI_DNS_BASE_URL}/zones'
OCI_DNS_TSIG_KEYS_BASE_URL = f'{OCI_DNS_BASE_URL}/tsigKeys'
session = requests.session()
session.mount('http://', HTTPAdapter(max_retries=0))
auth = Signer(
tenancy=config['tenancy'],
user=config['user'],
fingerprint=config['fingerprint'],
private_key_file_location=config['key_file'],
)
opcprincipal = f'{{"tenantId": "{config["tenancy"]}", "subjectId": "{config["user"]}"}}'
headers = {
'opc-principal': opcprincipal,
'Accept': 'application/json',
}
compartment = args.oci_compartment
if compartment == '':
compartment = config['tenancy']
tsig_key_compartment = args.tsig_key_compartment
if tsig_key_compartment == '':
tsig_key_compartment = compartment
def poll_tsig_key_create(tsig_key_ocid):
poll_attempts = 0
while poll_attempts < MAX_POLL_ATTEMPTS:
poll_attempts += 1
response = session.get(
f'{OCI_DNS_TSIG_KEYS_BASE_URL}/{tsig_key_ocid}',
auth=auth,
headers=headers,
)
if response.status_code != requests.codes.ok:
raise Exception(
f'Failed to get lifecycle state for tsig key "{tsig_key_ocid}" (opc-request-id: '
f'"{response.headers.get("opc-request-id")}"): {response.json()}'
)
if response.json()['lifecycleState'] == 'CREATING':
time.sleep(5)
elif response.json()['lifecycleState'] == 'ACTIVE':
return
else:
raise Exception(
f'Unexpected status for tsig key "{tsig_key_ocid}" (opc-request-id: '
f'"{response.headers.get("opc-request-id")}"): {response.json()}'
)
raise Exception(f'Timed out waiting for tsig key "{tsig_key_ocid}" to finish being created')
def poll_zone_create(zone_ocid):
poll_attempts = 0
while poll_attempts < MAX_POLL_ATTEMPTS:
poll_attempts += 1
response = session.get(
f'{OCI_DNS_ZONES_BASE_URL}/{zone_ocid}',
auth=auth,
headers=headers,
)
if response.status_code != requests.codes.ok:
raise Exception(
f'Failed to get lifecycle state for zone "{zone_ocid}" (opc-request-id: ' \
f'"{response.headers.get("opc-request-id")}"): {response.json()}')
if response.json()['lifecycleState'] == 'CREATING':
time.sleep(5)
elif response.json()['lifecycleState'] == 'ACTIVE':
return
else:
raise Exception(
f'Unexpected status for zone "{zone_ocid}" (opc-request-id: '
f'"{response.headers.get("opc-request-id")}"): {response.json()}'
)
raise Exception(f'Timed out waiting for zone "{zone_ocid}" to finish being created')
def get_or_create_tsig_key(tsig_key_name):
# Fetch tsig keys in the tsig key compartment with a name matching the
# dynect secondary zone's tsig key name
response = session.get(
OCI_DNS_TSIG_KEYS_BASE_URL,
auth=auth,
headers=headers,
params={
'compartmentId': tsig_key_compartment,
'name': tsig_key_name,
}
)
if response.status_code == requests.codes.ok:
if len(response.json()) > 0:
tsig_key = response.json()[0]
# Verify the tsig key is active
if tsig_key['lifecycleState'] != 'ACTIVE':
raise Exception(
f'The OCI tsig key with the name "{tsig_key_name}" was in the '
f'"{tsig_key["lifecycleState"]}" state, but must be in the "ACTIVE" state'
)
return tsig_key['id']
else:
raise Exception(
f'Failed to look up tsig keys in OCI (opc-request-id: '
f'"{response.headers.get("opc-request-id")}")'
)
# Attempt to fetch the tsig key's details from dynect and create the tsig key in OCI.
try:
dynect_tsig_key = TSIG(tsig_key_name)
except Exception as ex:
raise Exception(
f'Failed to look up details for the tsig key "{tsig_key_name}" in Dynect'
) from ex
tsig_key_data = {
'name': tsig_key_name,
'algorithm': dynect_tsig_key.algorithm,
'secret': dynect_tsig_key.secret,
'compartmentId': tsig_key_compartment,
}
response = session.post(
OCI_DNS_TSIG_KEYS_BASE_URL,
auth=auth,
headers={
'Content-Type': 'application/json',
**headers
},
json=tsig_key_data,
)
if response.status_code != requests.codes.created:
raise Exception(
f'Failed to create tsig key with name "{tsig_key_name}" (opc-request-id: '
f'"{response.headers.get("opc-request-id")}"): {response.json()}'
)
tsig_key_ocid = response.json()['id']
print(
f'Creating tsig key "{tsig_key_name}" in OCI DNS. Tsig key OCID: "{tsig_key_ocid}". '
f'Waiting for tsig key creation to complete.'
)
try:
poll_tsig_key_create(tsig_key_ocid)
except Exception as ex:
raise Exception(
f'Encountered a problem while waiting for creation of tsig key "{tsig_key_name}" '
f'to complete'
) from ex
print(f'Creation of tsig key "{tsig_key_name}" in OCI DNS complete.')
return tsig_key_ocid
def create_zone(zone_name):
# Check if there is already an OCI zone in the compartment with the provided name
response = session.get(
OCI_DNS_ZONES_BASE_URL,
auth=auth,
headers=headers,
params={
'compartmentId': compartment,
'name': zone_name,
},
)
if response.status_code == requests.codes.ok:
if len(response.json()) > 0:
print(
f'Found existing OCI zone with name "{zone_name}". Zone OCID: '
f'"{response.json()[0]["id"]}. Skipping."'
)
return
try:
dynect_zone = Zone(zone_name)
except Exception as ex:
raise Exception(
f'Failed to look up the zone {zone_name} from Dynect. Verify the zone exists in '
f'Dynect and that the user has permission to look up the zone.'
) from ex
if dynect_zone._zone_type == 'Secondary':
# Create a secondary zone in OCI DNS
# Fetch the secondary zone
try:
dynect_secondary_zone = SecondaryZone(zone_name)
except Exception as ex:
raise Exception(
f'Failed to load the secondary zone {zone_name} from Dynect. The Dynect user may '
f'need the "SecondaryGet" permission in Dynect.'
) from ex
tsig_key_ocid = None
# Check if the secondary zone is configured to use a tsig key. If it is,
# check if a tsig key by that name has already been created in OCI. If it
# has not already been created, attempt to create it before creating the
# secondary zone.
if dynect_secondary_zone.tsig_key_name != '':
try:
tsig_key_ocid = get_or_create_tsig_key(dynect_secondary_zone.tsig_key_name)
except Exception as ex:
raise Exception(
f'Could not get or create tsig key for secondary zone {zone_name}'
) from ex
secondary_zone_data = {
'name': zone_name,
'compartmentId': compartment,
'zoneType': 'SECONDARY',
'externalMasters': [
{
'address': address,
'tsigKeyId': tsig_key_ocid,
} for address in dynect_secondary_zone._masters
]
}
response = session.post(
OCI_DNS_ZONES_BASE_URL,
auth=auth,
headers={
'Content-Type': 'application/json',
**headers
},
json=secondary_zone_data,
)
else:
# Acquire the zone's records by executing a zone transfer, and then create
# a zone in OCI DNS using the zone file created from the records obtained
# from the zone transfer
params = {
'compartmentId': compartment,
}
zone_name_with_dot = zone_name
if not zone_name_with_dot.endswith("."):
zone_name_with_dot = zone_name_with_dot + "."
try:
zonefile = dns.zone.from_xfr(
dns.query.xfr(
socket.gethostbyname('xfrout1.dynect.net'),
zone_name,
)
).to_text()
zonefile = f'$ORIGIN {zone_name_with_dot}\n{zonefile}'
except dns.xfr.TransferError:
raise Exception(
f'''
Failed to fetch the zone "{zone_name}" from Dyn Managed DNS.
This can happen if your public facing IP address has not been enabled as a
transfer server for the zone.
In order to use this migration script for PRIMARY zones, you must enable zone
transfers to your current public facing IP address for any PRIMARY zones you
wish to migrate.
To determine your current public facing IP address, visit:
"http://checkIP.dyn.com/".
To enable zone transfers to your IP address, follow the instructions here using
your IP address as the External Nameserver IP address:
"https://help.dyn.com/using-external-nameservers/".
Make sure that "Transfers" is selected for your IP address.
'''
)
response = session.post(
CREATE_OCI_DNS_ZONE_FROM_ZONEFILE_URL,
auth=auth,
headers={
'Content-Type': 'text/dns',
**headers
},
params=params,
data=zonefile,
)
if response.status_code != requests.codes.created:
raise Exception(
f'Failed to create zone with name "{zone_name}" (opc-request-id: '
f'"{response.headers.get("opc-request-id")}"): {response.json()}'
)
print(
f'Creating "{zone_name}" in OCI DNS. Zone OCID: {response.json()["id"]}. Waiting for '
f'zone creation to complete.'
)
poll_zone_create(response.json()["id"])
print(f'Creation of zone "{zone_name}" in OCI DNS complete.')
def migrate_zones():
if args.zone_name is not None:
zone_names = [args.zone_name]
if args.zone_names_file is not None:
with open(args.zone_names_file, 'r', encoding='UTF-8') as zone_names_file:
zone_names = zone_names_file.read().splitlines()
for zone_name in zone_names:
try:
create_zone(zone_name)
except Exception as ex:
if not args.ignore_failures:
raise ex
traceback.print_exc()
print(f'\nFailed to create the zone {zone_name}. Moving on to the next.\n')
migrate_zones()