diff --git a/SoftLayer/CLI/dns/record_add.py b/SoftLayer/CLI/dns/record_add.py index fbf8213b2..03caf8ff2 100644 --- a/SoftLayer/CLI/dns/record_add.py +++ b/SoftLayer/CLI/dns/record_add.py @@ -5,24 +5,86 @@ import SoftLayer from SoftLayer.CLI import environment +from SoftLayer.CLI import exceptions from SoftLayer.CLI import helpers # pylint: disable=redefined-builtin @click.command() -@click.argument('zone') @click.argument('record') -@click.argument('type') +@click.argument('record_type') @click.argument('data') +@click.option('--zone', + help="Zone name or identifier that the resource record will be associated with.\n" + "Required for all record types except PTR") @click.option('--ttl', - type=click.INT, - default=7200, + default=900, show_default=True, help='TTL value in seconds, such as 86400') +@click.option('--priority', + default=10, + show_default=True, + help='The priority of the target host. (MX or SRV type only)') +@click.option('--protocol', + type=click.Choice(['tcp', 'udp', 'tls']), + default='tcp', + show_default=True, + help='The protocol of the service, usually either TCP or UDP. (SRV type only)') +@click.option('--port', + type=click.INT, + help='The TCP/UDP/TLS port on which the service is to be found. (SRV type only)') +@click.option('--service', + help='The symbolic name of the desired service. (SRV type only)') +@click.option('--weight', + default=5, + show_default=True, + help='Relative weight for records with same priority. (SRV type only)') @environment.pass_env -def cli(env, zone, record, type, data, ttl): - """Add resource record.""" +def cli(env, record, record_type, data, zone, ttl, priority, protocol, port, service, weight): + """Add resource record. + + Each resource record contains a RECORD and DATA property, defining a resource's name and it's target data. + Domains contain multiple types of resource records so it can take one of the following values: A, AAAA, CNAME, + MX, SPF, SRV, and PTR. + + About reverse records (PTR), the RECORD value must to be the public Ip Address of device you would like to manage + reverse DNS. + + slcli dns record-add 10.10.8.21 PTR myhost.com --ttl=900 + + Examples: + + slcli dns record-add myhost.com A 192.168.1.10 --zone=foobar.com --ttl=900 + + slcli dns record-add myhost.com AAAA 2001:DB8::1 --zone=foobar.com + + slcli dns record-add 192.168.1.2 MX 192.168.1.10 --zone=foobar.com --priority=11 --ttl=1800 + + slcli dns record-add myhost.com TXT "txt-verification=rXOxyZounZs87oacJSKvbUSIQ" --zone=2223334 + + slcli dns record-add myhost.com SPF "v=spf1 include:_spf.google.com ~all" --zone=2223334 + + slcli dns record-add myhost.com SRV 192.168.1.10 --zone=2223334 --service=foobar --port=80 --protocol=TCP + + """ manager = SoftLayer.DNSManager(env.client) - zone_id = helpers.resolve_id(manager.resolve_ids, zone, name='zone') - manager.create_record(zone_id, record, type, data, ttl=ttl) + record_type = record_type.upper() + + if zone and record_type != 'PTR': + zone_id = helpers.resolve_id(manager.resolve_ids, zone, name='zone') + + if record_type == 'MX': + manager.create_record_mx(zone_id, record, data, ttl=ttl, priority=priority) + elif record_type == 'SRV': + manager.create_record_srv(zone_id, record, data, protocol, port, service, + ttl=ttl, priority=priority, weight=weight) + else: + manager.create_record(zone_id, record, record_type, data, ttl=ttl) + + elif record_type == 'PTR': + manager.create_record_ptr(record, data, ttl=ttl) + else: + raise exceptions.CLIAbort("%s isn't a valid record type or zone is missing" % record_type) + + click.secho("%s record added successfully" % record_type, fg='green') diff --git a/SoftLayer/managers/dns.py b/SoftLayer/managers/dns.py index c1b7b3b60..a3fc322af 100644 --- a/SoftLayer/managers/dns.py +++ b/SoftLayer/managers/dns.py @@ -89,17 +89,81 @@ def create_record(self, zone_id, record, record_type, data, ttl=60): :param integer id: the zone's ID :param record: the name of the record to add - :param record_type: the type of record (A, AAAA, CNAME, MX, TXT, etc.) + :param record_type: the type of record (A, AAAA, CNAME, TXT, etc.) :param data: the record's value :param integer ttl: the TTL or time-to-live value (default: 60) """ - return self.record.createObject({ - 'domainId': zone_id, - 'ttl': ttl, + resource_record = self._generate_create_dict(record, record_type, data, + ttl, domainId=zone_id) + return self.record.createObject(resource_record) + + def create_record_mx(self, zone_id, record, data, ttl=60, priority=10): + """Create a mx resource record on a domain. + + :param integer id: the zone's ID + :param record: the name of the record to add + :param data: the record's value + :param integer ttl: the TTL or time-to-live value (default: 60) + :param integer priority: the priority of the target host + + """ + resource_record = self._generate_create_dict(record, 'MX', data, ttl, + domainId=zone_id, mxPriority=priority) + return self.record.createObject(resource_record) + + def create_record_srv(self, zone_id, record, data, protocol, port, service, + ttl=60, priority=20, weight=10): + """Create a resource record on a domain. + + :param integer id: the zone's ID + :param record: the name of the record to add + :param data: the record's value + :param string protocol: the protocol of the service, usually either TCP or UDP. + :param integer port: the TCP or UDP port on which the service is to be found. + :param string service: the symbolic name of the desired service. + :param integer ttl: the TTL or time-to-live value (default: 60) + :param integer priority: the priority of the target host (default: 20) + :param integer weight: relative weight for records with same priority (default: 10) + + """ + resource_record = self._generate_create_dict(record, 'SRV', data, ttl, domainId=zone_id, + priority=priority, protocol=protocol, port=port, + service=service, weight=weight) + + # The createObject won't creates SRV records unless we send the following complexType. + resource_record['complexType'] = 'SoftLayer_Dns_Domain_ResourceRecord_SrvType' + + return self.record.createObject(resource_record) + + def create_record_ptr(self, record, data, ttl=60): + """Create a reverse record. + + :param record: the public ip address of device for which you would like to manage reverse DNS. + :param data: the record's value + :param integer ttl: the TTL or time-to-live value (default: 60) + + """ + resource_record = self._generate_create_dict(record, 'PTR', data, ttl) + + return self.record.createObject(resource_record) + + @staticmethod + def _generate_create_dict(record, record_type, data, ttl, **kwargs): + """Returns a dict appropriate to pass into Dns_Domain_ResourceRecord::createObject""" + + # Basic dns record structure + resource_record = { 'host': record, - 'type': record_type, - 'data': data}) + 'data': data, + 'ttl': ttl, + 'type': record_type + } + + for (key, value) in kwargs.items(): + resource_record.setdefault(key, value) + + return resource_record def delete_record(self, record_id): """Delete a resource record by its ID. diff --git a/tests/CLI/modules/dns_tests.py b/tests/CLI/modules/dns_tests.py index 836da74a9..3c1329b39 100644 --- a/tests/CLI/modules/dns_tests.py +++ b/tests/CLI/modules/dns_tests.py @@ -72,11 +72,41 @@ def test_list_records(self): 'ttl': 7200}) def test_add_record(self): - result = self.run_command(['dns', 'record-add', '1234', 'hostname', - 'A', 'd', '--ttl=100']) + result = self.run_command(['dns', 'record-add', 'hostname', 'A', + 'data', '--zone=1234', '--ttl=100']) self.assert_no_fail(result) - self.assertEqual(result.output, "") + self.assertEqual(str(result.output), 'A record added successfully\n') + + def test_add_record_mx(self): + result = self.run_command(['dns', 'record-add', 'hostname', 'MX', + 'data', '--zone=1234', '--ttl=100', '--priority=25']) + + self.assert_no_fail(result) + self.assertEqual(str(result.output), 'MX record added successfully\n') + + def test_add_record_srv(self): + result = self.run_command(['dns', 'record-add', 'hostname', 'SRV', + 'data', '--zone=1234', '--protocol=udp', + '--port=88', '--ttl=100', '--weight=5']) + + self.assert_no_fail(result) + self.assertEqual(str(result.output), 'SRV record added successfully\n') + + def test_add_record_ptr(self): + result = self.run_command(['dns', 'record-add', '192.168.1.1', 'PTR', + 'hostname', '--ttl=100']) + + self.assert_no_fail(result) + self.assertEqual(str(result.output), 'PTR record added successfully\n') + + def test_add_record_abort(self): + result = self.run_command(['dns', 'record-add', 'hostname', 'A', + 'data', '--ttl=100']) + + self.assertEqual(result.exit_code, 2) + self.assertIsInstance(result.exception, exceptions.CLIAbort) + self.assertEqual(result.exception.message, "A isn't a valid record type or zone is missing") @mock.patch('SoftLayer.CLI.formatting.no_going_back') def test_delete_record(self, no_going_back_mock): diff --git a/tests/managers/dns_tests.py b/tests/managers/dns_tests.py index 8cd83a3a2..070eed707 100644 --- a/tests/managers/dns_tests.py +++ b/tests/managers/dns_tests.py @@ -91,6 +91,73 @@ def test_create_record(self): },)) self.assertEqual(res, {'name': 'example.com'}) + def test_create_record_mx(self): + res = self.dns_client.create_record_mx(1, 'test', 'testing', ttl=1200, priority=21) + + self.assert_called_with('SoftLayer_Dns_Domain_ResourceRecord', + 'createObject', + args=({ + 'domainId': 1, + 'ttl': 1200, + 'host': 'test', + 'type': 'MX', + 'data': 'testing', + 'mxPriority': 21 + },)) + self.assertEqual(res, {'name': 'example.com'}) + + def test_create_record_srv(self): + res = self.dns_client.create_record_srv(1, 'record', 'test_data', 'SLS', 8080, 'foobar', + ttl=1200, priority=21, weight=15) + + self.assert_called_with('SoftLayer_Dns_Domain_ResourceRecord', + 'createObject', + args=({ + 'complexType': 'SoftLayer_Dns_Domain_ResourceRecord_SrvType', + 'domainId': 1, + 'ttl': 1200, + 'host': 'record', + 'type': 'SRV', + 'data': 'test_data', + 'priority': 21, + 'weight': 15, + 'service': 'foobar', + 'port': 8080, + 'protocol': 'SLS' + },)) + self.assertEqual(res, {'name': 'example.com'}) + + def test_create_record_ptr(self): + res = self.dns_client.create_record_ptr('test', 'testing', ttl=1200) + + self.assert_called_with('SoftLayer_Dns_Domain_ResourceRecord', + 'createObject', + args=({ + 'ttl': 1200, + 'host': 'test', + 'type': 'PTR', + 'data': 'testing' + },)) + self.assertEqual(res, {'name': 'example.com'}) + + def test_generate_create_dict(self): + data = self.dns_client._generate_create_dict('foo', 'pmx', 'bar', 60, domainId=1234, + mxPriority=18, port=80, protocol='TCP', weight=25) + + assert_data = { + 'host': 'foo', + 'data': 'bar', + 'ttl': 60, + 'type': 'pmx', + 'domainId': 1234, + 'mxPriority': 18, + 'port': 80, + 'protocol': 'TCP', + 'weight': 25 + } + + self.assertEqual(data, assert_data) + def test_delete_record(self): self.dns_client.delete_record(1)