Repository: softlayer/softlayer-python
Pull request #1037 (2018-09-07 22:12:46)

New methods in DNS manager
The new methods allows to create MX, SRV and PTR records.
The CLI supports the creation of A, AAAA, MX, TXT, SPF, SRV, CNAME, and PTR records, it should fix the issue #584 


Patch:
diff --git a/SoftLayer/CLI/dns/record_add.py b/SoftLayer/CLI/dns/record_add.py
index fbf8213b..03caf8ff 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 c1b7b3b6..a3fc322a 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.


Test patch:
diff --git a/tests/CLI/modules/dns_tests.py b/tests/CLI/modules/dns_tests.py
index 836da74a..3c1329b3 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 8cd83a3a..070eed70 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)