diff --git a/SoftLayer/CLI/hardware/dns.py b/SoftLayer/CLI/hardware/dns.py new file mode 100644 index 000000000..19e611818 --- /dev/null +++ b/SoftLayer/CLI/hardware/dns.py @@ -0,0 +1,152 @@ +"""Sync DNS records.""" +# :license: MIT, see LICENSE for more details. + +import click + +import SoftLayer +from SoftLayer.CLI import environment +from SoftLayer.CLI import exceptions +from SoftLayer.CLI import formatting +from SoftLayer.CLI import helpers + + +@click.command(epilog="""If you don't specify any +arguments, it will attempt to update both the A and PTR records. If you don't +want to update both records, you may use the -a or --ptr arguments to limit +the records updated.""") +@click.argument('identifier') +@click.option('--a-record', '-a', + is_flag=True, + help="Sync the A record for the host") +@click.option('--aaaa-record', + is_flag=True, + help="Sync the AAAA record for the host") +@click.option('--ptr', is_flag=True, help="Sync the PTR record for the host") +@click.option('--ttl', + default=7200, + show_default=True, + type=click.INT, + help="Sets the TTL for the A and/or PTR records") +@environment.pass_env +def cli(env, identifier, a_record, aaaa_record, ptr, ttl): + """Sync DNS records.""" + + items = ['id', + 'globalIdentifier', + 'fullyQualifiedDomainName', + 'hostname', + 'domain', + 'primaryBackendIpAddress', + 'primaryIpAddress', + '''primaryNetworkComponent[ + id, primaryIpAddress, + primaryVersion6IpAddressRecord[ipAddress] + ]'''] + mask = "mask[%s]" % ','.join(items) + dns = SoftLayer.DNSManager(env.client) + server = SoftLayer.HardwareManager(env.client) + + hw_id = helpers.resolve_id(server.resolve_ids, identifier, 'VS') + instance = server.get_hardware(hw_id, mask=mask) + zone_id = helpers.resolve_id(dns.resolve_ids, + instance['domain'], + name='zone') + + def sync_a_record(): + """Sync A record.""" + records = dns.get_records(zone_id, + host=instance['hostname'], + record_type='a') + if not records: + # don't have a record, lets add one to the base zone + dns.create_record(zone['id'], + instance['hostname'], + 'a', + instance['primaryIpAddress'], + ttl=ttl) + else: + if len(records) != 1: + raise exceptions.CLIAbort("Aborting A record sync, found " + "%d A record exists!" % len(records)) + rec = records[0] + rec['data'] = instance['primaryIpAddress'] + rec['ttl'] = ttl + dns.edit_record(rec) + + def sync_aaaa_record(): + """Sync AAAA record.""" + records = dns.get_records(zone_id, + host=instance['hostname'], + record_type='aaaa') + try: + # done this way to stay within 80 character lines + component = instance['primaryNetworkComponent'] + record = component['primaryVersion6IpAddressRecord'] + ip_address = record['ipAddress'] + except KeyError: + raise exceptions.CLIAbort("%s does not have an ipv6 address" + % instance['fullyQualifiedDomainName']) + + if not records: + # don't have a record, lets add one to the base zone + dns.create_record(zone['id'], + instance['hostname'], + 'aaaa', + ip_address, + ttl=ttl) + else: + if len(records) != 1: + raise exceptions.CLIAbort("Aborting A record sync, found " + "%d A record exists!" % len(records)) + rec = records[0] + rec['data'] = ip_address + rec['ttl'] = ttl + dns.edit_record(rec) + + def sync_ptr_record(): + """Sync PTR record.""" + host_rec = instance['primaryIpAddress'].split('.')[-1] + ptr_domains = (env.client['Hardware_Server'] + .getReverseDomainRecords(id=instance['id'])[0]) + edit_ptr = None + for ptr in ptr_domains['resourceRecords']: + if ptr['host'] == host_rec: + ptr['ttl'] = ttl + edit_ptr = ptr + break + + if edit_ptr: + edit_ptr['data'] = instance['fullyQualifiedDomainName'] + dns.edit_record(edit_ptr) + else: + dns.create_record(ptr_domains['id'], + host_rec, + 'ptr', + instance['fullyQualifiedDomainName'], + ttl=ttl) + + if not instance['primaryIpAddress']: + raise exceptions.CLIAbort('No primary IP address associated with ' + 'this VS') + + zone = dns.get_zone(zone_id) + + go_for_it = env.skip_confirmations or formatting.confirm( + "Attempt to update DNS records for %s" + % instance['fullyQualifiedDomainName']) + + if not go_for_it: + raise exceptions.CLIAbort("Aborting DNS sync") + + both = False + if not ptr and not a_record and not aaaa_record: + both = True + + if both or a_record: + sync_a_record() + + if both or ptr: + sync_ptr_record() + + if aaaa_record: + sync_aaaa_record() diff --git a/SoftLayer/CLI/routes.py b/SoftLayer/CLI/routes.py index 5c37541df..97f004bcd 100644 --- a/SoftLayer/CLI/routes.py +++ b/SoftLayer/CLI/routes.py @@ -234,6 +234,7 @@ ('hardware:rescue', 'SoftLayer.CLI.hardware.power:rescue'), ('hardware:ready', 'SoftLayer.CLI.hardware.ready:cli'), ('hardware:toggle-ipmi', 'SoftLayer.CLI.hardware.toggle_ipmi:cli'), + ('hardware:dns-sync', 'SoftLayer.CLI.hardware.dns:cli'), ('securitygroup', 'SoftLayer.CLI.securitygroup'), ('securitygroup:list', 'SoftLayer.CLI.securitygroup.list:cli'), diff --git a/SoftLayer/managers/dns.py b/SoftLayer/managers/dns.py index a3fc322af..99545944f 100644 --- a/SoftLayer/managers/dns.py +++ b/SoftLayer/managers/dns.py @@ -226,6 +226,7 @@ def edit_record(self, record): :param dict record: the record to update """ + record.pop('isGatewayAddress', None) self.record.editObject(record, id=record['id']) def dump_zone(self, zone_id): diff --git a/tests/CLI/modules/server_tests.py b/tests/CLI/modules/server_tests.py index 29ec65d40..1d2995004 100644 --- a/tests/CLI/modules/server_tests.py +++ b/tests/CLI/modules/server_tests.py @@ -638,3 +638,190 @@ def test_bandwidth_hw_quite(self): self.assertEqual(output_summary[1]['Max Date'], date) self.assertEqual(output_summary[2]['Max GB'], 0.1172) self.assertEqual(output_summary[3]['Sum GB'], 0.0009) + + @mock.patch('SoftLayer.CLI.formatting.confirm') + def test_dns_sync_both(self, confirm_mock): + confirm_mock.return_value = True + getReverseDomainRecords = self.set_mock('SoftLayer_Hardware_Server', + 'getReverseDomainRecords') + getReverseDomainRecords.return_value = [{ + 'networkAddress': '172.16.1.100', + 'name': '2.240.16.172.in-addr.arpa', + 'resourceRecords': [{'data': 'test.softlayer.com.', + 'id': 100, + 'host': '12'}], + 'updateDate': '2013-09-11T14:36:57-07:00', + 'serial': 1234665663, + 'id': 123456, + }] + getResourceRecords = self.set_mock('SoftLayer_Dns_Domain', + 'getResourceRecords') + getResourceRecords.return_value = [] + createAargs = ({ + 'type': 'a', + 'host': 'hardware-test1', + 'domainId': 98765, + 'data': '172.16.1.100', + 'ttl': 7200 + },) + createPTRargs = ({ + 'type': 'ptr', + 'host': '100', + 'domainId': 123456, + 'data': 'hardware-test1.test.sftlyr.ws', + 'ttl': 7200 + },) + + result = self.run_command(['hw', 'dns-sync', '1000']) + + self.assert_no_fail(result) + self.assert_called_with('SoftLayer_Dns_Domain', 'getResourceRecords') + self.assert_called_with('SoftLayer_Hardware_Server', + 'getReverseDomainRecords') + self.assert_called_with('SoftLayer_Dns_Domain_ResourceRecord', + 'createObject', + args=createAargs) + self.assert_called_with('SoftLayer_Dns_Domain_ResourceRecord', + 'createObject', + args=createPTRargs) + + @mock.patch('SoftLayer.CLI.formatting.confirm') + def test_dns_sync_v6(self, confirm_mock): + confirm_mock.return_value = True + getResourceRecords = self.set_mock('SoftLayer_Dns_Domain', + 'getResourceRecords') + getResourceRecords.return_value = [] + server = self.set_mock('SoftLayer_Hardware_Server', 'getObject') + test_server = { + 'id': 1000, + 'hostname': 'hardware-test1', + 'domain': 'sftlyr.ws', + 'primaryIpAddress': '172.16.1.100', + 'fullyQualifiedDomainName': 'hw-test1.sftlyr.ws', + "primaryNetworkComponent": {} + } + server.return_value = test_server + + result = self.run_command(['hw', 'dns-sync', '--aaaa-record', '1000']) + + self.assertEqual(result.exit_code, 2) + self.assertIsInstance(result.exception, exceptions.CLIAbort) + + test_server['primaryNetworkComponent'] = { + 'primaryVersion6IpAddressRecord': { + 'ipAddress': '2607:f0d0:1b01:0023:0000:0000:0000:0004' + } + } + createV6args = ({ + 'type': 'aaaa', + 'host': 'hardware-test1', + 'domainId': 98765, + 'data': '2607:f0d0:1b01:0023:0000:0000:0000:0004', + 'ttl': 7200 + },) + server.return_value = test_server + result = self.run_command(['hw', 'dns-sync', '--aaaa-record', '1000']) + self.assert_no_fail(result) + self.assert_called_with('SoftLayer_Dns_Domain_ResourceRecord', + 'createObject', + args=createV6args) + + v6Record = { + 'id': 1, + 'ttl': 7200, + 'data': '2607:f0d0:1b01:0023:0000:0000:0000:0004', + 'host': 'hardware-test1', + 'type': 'aaaa' + } + + getResourceRecords = self.set_mock('SoftLayer_Dns_Domain', + 'getResourceRecords') + getResourceRecords.return_value = [v6Record] + editArgs = (v6Record,) + result = self.run_command(['hw', 'dns-sync', '--aaaa-record', '1000']) + self.assert_no_fail(result) + self.assert_called_with('SoftLayer_Dns_Domain_ResourceRecord', + 'editObject', + args=editArgs) + + getResourceRecords = self.set_mock('SoftLayer_Dns_Domain', + 'getResourceRecords') + getResourceRecords.return_value = [v6Record, v6Record] + result = self.run_command(['hw', 'dns-sync', '--aaaa-record', '1000']) + self.assertEqual(result.exit_code, 2) + self.assertIsInstance(result.exception, exceptions.CLIAbort) + + @mock.patch('SoftLayer.CLI.formatting.confirm') + def test_dns_sync_edit_a(self, confirm_mock): + confirm_mock.return_value = True + getResourceRecords = self.set_mock('SoftLayer_Dns_Domain', + 'getResourceRecords') + getResourceRecords.return_value = [ + {'id': 1, 'ttl': 7200, 'data': '1.1.1.1', + 'host': 'hardware-test1', 'type': 'a'} + ] + editArgs = ( + {'type': 'a', 'host': 'hardware-test1', 'data': '172.16.1.100', + 'id': 1, 'ttl': 7200}, + ) + result = self.run_command(['hw', 'dns-sync', '-a', '1000']) + self.assert_no_fail(result) + self.assert_called_with('SoftLayer_Dns_Domain_ResourceRecord', + 'editObject', + args=editArgs) + + getResourceRecords = self.set_mock('SoftLayer_Dns_Domain', + 'getResourceRecords') + getResourceRecords.return_value = [ + {'id': 1, 'ttl': 7200, 'data': '1.1.1.1', + 'host': 'hardware-test1', 'type': 'a'}, + {'id': 2, 'ttl': 7200, 'data': '1.1.1.1', + 'host': 'hardware-test1', 'type': 'a'} + ] + result = self.run_command(['hw', 'dns-sync', '-a', '1000']) + self.assertEqual(result.exit_code, 2) + self.assertIsInstance(result.exception, exceptions.CLIAbort) + + @mock.patch('SoftLayer.CLI.formatting.confirm') + def test_dns_sync_edit_ptr(self, confirm_mock): + confirm_mock.return_value = True + getReverseDomainRecords = self.set_mock('SoftLayer_Hardware_Server', + 'getReverseDomainRecords') + getReverseDomainRecords.return_value = [{ + 'networkAddress': '172.16.1.100', + 'name': '2.240.16.172.in-addr.arpa', + 'resourceRecords': [{'data': 'test.softlayer.com.', + 'id': 123, + 'host': '100'}], + 'updateDate': '2013-09-11T14:36:57-07:00', + 'serial': 1234665663, + 'id': 123456, + }] + editArgs = ({'host': '100', 'data': 'hardware-test1.test.sftlyr.ws', + 'id': 123, 'ttl': 7200},) + result = self.run_command(['hw', 'dns-sync', '--ptr', '1000']) + self.assert_no_fail(result) + self.assert_called_with('SoftLayer_Dns_Domain_ResourceRecord', + 'editObject', + args=editArgs) + + @mock.patch('SoftLayer.CLI.formatting.confirm') + def test_dns_sync_misc_exception(self, confirm_mock): + confirm_mock.return_value = False + result = self.run_command(['hw', 'dns-sync', '-a', '1000']) + self.assertEqual(result.exit_code, 2) + self.assertIsInstance(result.exception, exceptions.CLIAbort) + + guest = self.set_mock('SoftLayer_Hardware_Server', 'getObject') + test_guest = { + 'id': 1000, + 'primaryIpAddress': '', + 'hostname': 'hardware-test1', + 'domain': 'sftlyr.ws', + 'fullyQualifiedDomainName': 'hardware-test1.sftlyr.ws', + "primaryNetworkComponent": {} + } + guest.return_value = test_guest + result = self.run_command(['hw', 'dns-sync', '-a', '1000']) + self.assertEqual(result.exit_code, 2) + self.assertIsInstance(result.exception, exceptions.CLIAbort)