diff --git a/SoftLayer/CLI/hardware/dns.py b/SoftLayer/CLI/hardware/dns.py new file mode 100644 index 000000000..3b7458003 --- /dev/null +++ b/SoftLayer/CLI/hardware/dns.py @@ -0,0 +1,64 @@ +"""Sync DNS records.""" +# :license: MIT, see LICENSE for more details. +# pylint: disable=duplicate-code + +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.""" + + mask = """mask[id, globalIdentifier, fullyQualifiedDomainName, hostname, domain, + primaryBackendIpAddress,primaryIpAddress, + primaryNetworkComponent[id,primaryIpAddress,primaryVersion6IpAddressRecord[ipAddress]]]""" + dns = SoftLayer.DNSManager(env.client) + server = SoftLayer.HardwareManager(env.client) + + server_id = helpers.resolve_id(server.resolve_ids, identifier, 'VS') + instance = server.get_hardware(server_id, mask=mask) + zone_id = helpers.resolve_id(dns.resolve_ids, instance['domain'], name='zone') + + if not instance['primaryIpAddress']: + raise exceptions.CLIAbort('No primary IP address associated with this hardware') + + 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 will be true only if no options are passed in, basically. + both = (not ptr) and (not a_record) and (not aaaa_record) + + if both or a_record: + dns.sync_host_record(zone_id, instance['hostname'], instance['primaryIpAddress'], 'a', ttl) + + if both or ptr: + # getReverseDomainRecords returns a list of 1 element, so just get the top. + ptr_domains = env.client['Hardware_Server'].getReverseDomainRecords(id=instance['id']).pop() + dns.sync_ptr_record(ptr_domains, instance['primaryIpAddress'], instance['fullyQualifiedDomainName'], ttl) + + if aaaa_record: + try: + # done this way to stay within 80 character lines + ipv6 = instance['primaryNetworkComponent']['primaryVersion6IpAddressRecord']['ipAddress'] + dns.sync_host_record(zone_id, instance['hostname'], ipv6, 'aaaa', ttl) + except KeyError: + raise exceptions.CLIAbort("%s does not have an ipv6 address" % instance['fullyQualifiedDomainName']) 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/CLI/virt/dns.py b/SoftLayer/CLI/virt/dns.py index ca600465d..26b4904cd 100644 --- a/SoftLayer/CLI/virt/dns.py +++ b/SoftLayer/CLI/virt/dns.py @@ -1,5 +1,6 @@ """Sync DNS records.""" # :license: MIT, see LICENSE for more details. +# pylint: disable=duplicate-code import click @@ -15,138 +16,49 @@ 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('--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, +@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) + mask = """mask[id, globalIdentifier, fullyQualifiedDomainName, hostname, domain, + primaryBackendIpAddress,primaryIpAddress, + primaryNetworkComponent[id,primaryIpAddress,primaryVersion6IpAddressRecord[ipAddress]]]""" dns = SoftLayer.DNSManager(env.client) - vsi = SoftLayer.VSManager(env.client) + server = SoftLayer.VSManager(env.client) - vs_id = helpers.resolve_id(vsi.resolve_ids, identifier, 'VS') - instance = vsi.get_instance(vs_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['Virtual_Guest'] - .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) + server_id = helpers.resolve_id(server.resolve_ids, identifier, 'VS') + instance = server.get_instance(server_id, mask=mask) + zone_id = helpers.resolve_id(dns.resolve_ids, instance['domain'], name='zone') if not instance['primaryIpAddress']: - raise exceptions.CLIAbort('No primary IP address associated with ' - 'this VS') - - zone = dns.get_zone(zone_id) + raise exceptions.CLIAbort('No primary IP address associated with this VS') go_for_it = env.skip_confirmations or formatting.confirm( - "Attempt to update DNS records for %s" - % instance['fullyQualifiedDomainName']) + "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 + # both will be true only if no options are passed in, basically. + both = (not ptr) and (not a_record) and (not aaaa_record) if both or a_record: - sync_a_record() + dns.sync_host_record(zone_id, instance['hostname'], instance['primaryIpAddress'], 'a', ttl) if both or ptr: - sync_ptr_record() + # getReverseDomainRecords returns a list of 1 element, so just get the top. + ptr_domains = env.client['Virtual_Guest'].getReverseDomainRecords(id=instance['id']).pop() + dns.sync_ptr_record(ptr_domains, instance['primaryIpAddress'], instance['fullyQualifiedDomainName'], ttl) if aaaa_record: - sync_aaaa_record() + try: + # done this way to stay within 80 character lines + ipv6 = instance['primaryNetworkComponent']['primaryVersion6IpAddressRecord']['ipAddress'] + dns.sync_host_record(zone_id, instance['hostname'], ipv6, 'aaaa', ttl) + except KeyError: + raise exceptions.CLIAbort("%s does not have an ipv6 address" % instance['fullyQualifiedDomainName']) diff --git a/SoftLayer/managers/dns.py b/SoftLayer/managers/dns.py index a3fc322af..3a2ea9147 100644 --- a/SoftLayer/managers/dns.py +++ b/SoftLayer/managers/dns.py @@ -7,6 +7,7 @@ """ import time +from SoftLayer import exceptions from SoftLayer import utils @@ -205,13 +206,11 @@ def get_records(self, zone_id, ttl=None, data=None, host=None, _filter['resourceRecords']['data'] = utils.query_filter(data) if record_type: - _filter['resourceRecords']['type'] = utils.query_filter( - record_type.lower()) + _filter['resourceRecords']['type'] = utils.query_filter(record_type.lower()) results = self.service.getResourceRecords( id=zone_id, - mask='id,expire,domainId,host,minimum,refresh,retry,' - 'mxPriority,ttl,type,data,responsiblePerson', + mask='id,expire,domainId,host,minimum,refresh,retry,mxPriority,ttl,type,data,responsiblePerson', filter=_filter.to_dict(), ) @@ -226,6 +225,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): @@ -235,3 +235,47 @@ def dump_zone(self, zone_id): """ return self.service.getZoneFileContents(id=zone_id) + + def sync_host_record(self, zone_id, hostname, ip_address, record_type='a', ttl=7200): + """For a given zone_id, will set hostname's A record to ip_address + + :param integer zone_id: The zone id for the domain + :param string hostname: host part of the record + :param string ip_address: data part of the record + :param integer ttl: TTL for the record + :param string record_type: 'a' or 'aaaa' + """ + records = self.get_records(zone_id, host=hostname, record_type=record_type) + if not records: + # don't have a record, lets add one to the base zone + self.create_record(zone_id, hostname, record_type, ip_address, ttl=ttl) + else: + if len(records) != 1: + raise exceptions.SoftLayerError("Aborting record sync, found %d records!" % len(records)) + rec = records[0] + rec['data'] = ip_address + rec['ttl'] = ttl + self.edit_record(rec) + + def sync_ptr_record(self, ptr_domains, ip_address, fqdn, ttl=7200): + """Sync PTR record. + + :param dict ptr_domains: result from SoftLayer_Virtual_Guest.getReverseDomainRecords or + SoftLayer_Hardware_Server.getReverseDomainRecords + :param string ip_address: ip address to sync with + :param string fqdn: Fully Qualified Domain Name + :param integer ttl: TTL for the record + """ + host_rec = ip_address.split('.')[-1] + edit_ptr = None + for ptr in ptr_domains['resourceRecords']: + if ptr.get('host', '') == host_rec: + ptr['ttl'] = ttl + edit_ptr = ptr + break + + if edit_ptr: + edit_ptr['data'] = fqdn + self.edit_record(edit_ptr) + else: + self.create_record(ptr_domains['id'], host_rec, 'ptr', fqdn, ttl=ttl) diff --git a/docs/cli/hardware.rst b/docs/cli/hardware.rst index 3e7eeaf4c..08fc273d6 100644 --- a/docs/cli/hardware.rst +++ b/docs/cli/hardware.rst @@ -94,3 +94,6 @@ This function updates the firmware of a server. If already at the latest version :prog: hw ready :show-nested: +.. click:: SoftLayer.CLI.hardware.dns-sync:cli + :prog: hw dns-sync + :show-nested: diff --git a/tests/CLI/modules/server_tests.py b/tests/CLI/modules/server_tests.py index 29ec65d40..14f8e9201 100644 --- a/tests/CLI/modules/server_tests.py +++ b/tests/CLI/modules/server_tests.py @@ -12,6 +12,7 @@ import sys from SoftLayer.CLI import exceptions +from SoftLayer import SoftLayerError from SoftLayer import testing import json @@ -638,3 +639,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': 12345, # from SoftLayer_Account::getDomains + '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': 12345, # from SoftLayer_Account::getDomains + '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, 1) + self.assertIsInstance(result.exception, SoftLayerError) + + @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, 1) + self.assertIsInstance(result.exception, SoftLayerError) + + @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': '100.1.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) diff --git a/tests/CLI/modules/vs/vs_tests.py b/tests/CLI/modules/vs/vs_tests.py index 203230913..3e57cb209 100644 --- a/tests/CLI/modules/vs/vs_tests.py +++ b/tests/CLI/modules/vs/vs_tests.py @@ -12,6 +12,7 @@ from SoftLayer.CLI import exceptions from SoftLayer.fixtures import SoftLayer_Virtual_Guest as SoftLayer_Virtual_Guest from SoftLayer import SoftLayerAPIError +from SoftLayer import SoftLayerError from SoftLayer import testing @@ -310,7 +311,7 @@ def test_dns_sync_both(self, confirm_mock): createAargs = ({ 'type': 'a', 'host': 'vs-test1', - 'domainId': 98765, + 'domainId': 12345, # from SoftLayer_Account::getDomains 'data': '172.16.240.2', 'ttl': 7200 },) @@ -365,7 +366,7 @@ def test_dns_sync_v6(self, confirm_mock): createV6args = ({ 'type': 'aaaa', 'host': 'vs-test1', - 'domainId': 98765, + 'domainId': 12345, 'data': '2607:f0d0:1b01:0023:0000:0000:0000:0004', 'ttl': 7200 },) @@ -398,8 +399,8 @@ def test_dns_sync_v6(self, confirm_mock): 'getResourceRecords') getResourceRecords.return_value = [v6Record, v6Record] result = self.run_command(['vs', 'dns-sync', '--aaaa-record', '100']) - self.assertEqual(result.exit_code, 2) - self.assertIsInstance(result.exception, exceptions.CLIAbort) + self.assertEqual(result.exit_code, 1) + self.assertIsInstance(result.exception, SoftLayerError) @mock.patch('SoftLayer.CLI.formatting.confirm') def test_dns_sync_edit_a(self, confirm_mock): @@ -429,8 +430,8 @@ def test_dns_sync_edit_a(self, confirm_mock): 'host': 'vs-test1', 'type': 'a'} ] result = self.run_command(['vs', 'dns-sync', '-a', '100']) - self.assertEqual(result.exit_code, 2) - self.assertIsInstance(result.exception, exceptions.CLIAbort) + self.assertEqual(result.exit_code, 1) + self.assertIsInstance(result.exception, SoftLayerError) @mock.patch('SoftLayer.CLI.formatting.confirm') def test_dns_sync_edit_ptr(self, confirm_mock):