Updated Branches:
  refs/heads/trunk 519716edc -> 15e103f9a

Add methods for exporting Libcloud Zone to BIND zone format.


Project: http://git-wip-us.apache.org/repos/asf/libcloud/repo
Commit: http://git-wip-us.apache.org/repos/asf/libcloud/commit/daddf453
Tree: http://git-wip-us.apache.org/repos/asf/libcloud/tree/daddf453
Diff: http://git-wip-us.apache.org/repos/asf/libcloud/diff/daddf453

Branch: refs/heads/trunk
Commit: daddf453869e83589b79202c5402a85aa1c6663e
Parents: 6791d03
Author: Tomaz Muraus <[email protected]>
Authored: Sat Sep 14 23:07:16 2013 +0200
Committer: Tomaz Muraus <[email protected]>
Committed: Sat Sep 14 23:07:16 2013 +0200

----------------------------------------------------------------------
 libcloud/dns/base.py           | 121 ++++++++++++++++++++++++++++++++++++
 libcloud/test/dns/test_base.py | 108 ++++++++++++++++++++++++++++++++
 2 files changed, 229 insertions(+)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/libcloud/blob/daddf453/libcloud/dns/base.py
----------------------------------------------------------------------
diff --git a/libcloud/dns/base.py b/libcloud/dns/base.py
index a80ea98..7bb680b 100644
--- a/libcloud/dns/base.py
+++ b/libcloud/dns/base.py
@@ -13,12 +13,17 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
+from __future__ import with_statement
+
 __all__ = [
     'Zone',
     'Record',
     'DNSDriver'
 ]
 
+import datetime
+
+from libcloud import __version__
 from libcloud.common.base import ConnectionUserAndKey, BaseDriver
 from libcloud.dns.types import RecordType
 
@@ -69,6 +74,13 @@ class Zone(object):
     def delete(self):
         return self.driver.delete_zone(zone=self)
 
+    def export_to_bind_format(self):
+        return self.driver.export_zone_to_bind_format(zone=self)
+
+    def export_zone_to_bind_format_file(self, file_path):
+        self.driver.export_zone_to_bind_format_file(zone=self,
+                                                    file_path=file_path)
+
     def __repr__(self):
         return ('<Zone: domain=%s, ttl=%s, provider=%s ...>' %
                 (self.domain, self.ttl, self.driver.name))
@@ -117,6 +129,14 @@ class Record(object):
     def delete(self):
         return self.driver.delete_record(record=self)
 
+    def _get_numeric_id(self):
+        record_id = self.id
+
+        if record_id.isdigit():
+            record_id = int(record_id)
+
+        return record_id
+
     def __repr__(self):
         return ('<Record: zone=%s, name=%s, type=%s, data=%s, provider=%s '
                 '...>' %
@@ -358,10 +378,111 @@ class DNSDriver(BaseDriver):
         raise NotImplementedError(
             'delete_record not implemented for this driver')
 
+    def export_zone_to_bind_format(self, zone):
+        """
+        Export Zone object to the BIND compatible format.
+
+        :param zone: Zone to export.
+        :type  zone: :class:`Zone`
+
+        :return: Zone data in BIND compatible format.
+        :rtype: ``str``
+        """
+        if zone.type != 'master':
+            raise ValueError('You can only generate BIND out for master zones')
+
+        lines = []
+
+        # For consistent output, records are sorted based on the id
+        records = zone.list_records()
+        records = sorted(records, key=Record._get_numeric_id)
+
+        date = datetime.datetime.now().strftime('%Y-%m-%d %H:%m:%S')
+        values = {'version': __version__, 'date': date}
+
+        lines.append('; Generated by Libcloud v%(version)s on %(date)s' %
+                     values)
+        lines.append('$ORIGIN %(domain)s.' % {'domain': zone.domain})
+        lines.append('$TTL %(domain_ttl)s\n' % {'domain_ttl': zone.ttl})
+
+        for record in records:
+            line = self._get_bind_record_line(record=record)
+            lines.append(line)
+
+        output = '\n'.join(lines)
+        return output
+
+    def export_zone_to_bind_format_file(self, zone, file_path):
+        """
+        Export Zone object to the BIND compatible format and write result to a
+        file.
+
+        :param zone: Zone to export.
+        :type  zone: :class:`Zone`
+
+        :param file_path: File path where the output will be saved.
+        :type  file_path: ``str``
+
+        :return: Zone data in BIND compatible format.
+        :rtype: ``str``
+        """
+        result = self.export_zone_to_bind_format(zone=zone)
+
+        with open(file_path, 'w') as fp:
+            fp.write(result)
+
+    def _get_bind_record_line(self, record):
+        """
+        Generate BIND record line for the provided record.
+
+        :param record: Record to generate the line for.
+        :type  record: :class:`Record`
+
+        :return: Bind compatible record line.
+        :rtype: ``str``
+        """
+        parts = []
+
+        if record.name:
+            name = '%(name)s.%(domain)s' % {'name': record.name,
+                                            'domain': record.zone.domain}
+        else:
+            name = record.zone.domain
+
+        name += '.'
+
+        ttl = record.extra['ttl'] if 'ttl' in record.extra else record.zone.ttl
+        ttl = str(ttl)
+        data = record.data
+
+        if record.type in [RecordType.CNAME, RecordType.DNAME, RecordType.MX,
+                           RecordType.PTR, RecordType.SRV]:
+            # Make sure trailing dot is present
+            if data[len(data) - 1] != '.':
+                data += '.'
+
+        if record.type in [RecordType.TXT, RecordType.SPF] and ' ' in data:
+            # Escape the quotes
+            data = data.replace('"', '\\"')
+
+            # Quote the string
+            data = '"%s"' % (data)
+
+        if record.type in [RecordType.MX, RecordType.SRV]:
+            priority = str(record.extra['priority'])
+            parts = [name, ttl, 'IN', record.type, priority, data]
+        else:
+            parts = [name, ttl, 'IN', record.type, data]
+
+        line = '\t'.join(parts)
+        return line
+
     def _string_to_record_type(self, string):
         """
         Return a string representation of a DNS record type to a
         libcloud RecordType ENUM.
+
+        :rtype: ``str``
         """
         string = string.upper()
         record_type = getattr(RecordType, string)

http://git-wip-us.apache.org/repos/asf/libcloud/blob/daddf453/libcloud/test/dns/test_base.py
----------------------------------------------------------------------
diff --git a/libcloud/test/dns/test_base.py b/libcloud/test/dns/test_base.py
new file mode 100644
index 0000000..5b444c2
--- /dev/null
+++ b/libcloud/test/dns/test_base.py
@@ -0,0 +1,108 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+
+from __future__ import with_statement
+
+import sys
+import tempfile
+
+from mock import Mock
+
+from libcloud.test import unittest
+from libcloud.dns.base import DNSDriver, Zone, Record
+from libcloud.dns.types import RecordType
+
+
+MOCK_RECORDS_VALUES = [
+    {'id': 1, 'name': 'www', 'type': RecordType.A, 'data': '127.0.0.1'},
+    {'id': 2, 'name': 'www', 'type': RecordType.AAAA,
+     'data': '2a01:4f8:121:3121::2'},
+
+    # Custom TTL
+    {'id': 3, 'name': 'www', 'type': RecordType.A, 'data': '127.0.0.1',
+     'extra': {'ttl': 123}},
+
+    # Record without a name
+    {'id': 4, 'name': '', 'type': RecordType.A,
+     'data': '127.0.0.1'},
+
+    {'id': 5, 'name': 'test1', 'type': RecordType.TXT,
+     'data': 'test foo bar'},
+
+    # TXT record with quotes
+    {'id': 5, 'name': 'test2', 'type': RecordType.TXT,
+     'data': 'test "foo" "bar"'},
+
+    # Records with priority
+    {'id': 5, 'name': '', 'type': RecordType.MX,
+     'data': 'mx.example.com', 'extra': {'priority': 10}},
+    {'id': 5, 'name': '', 'type': RecordType.SRV,
+     'data': '10 3333 example.com', 'extra': {'priority': 20}},
+]
+
+
+class BaseTestCase(unittest.TestCase):
+    def setUp(self):
+        self.driver = DNSDriver('none', 'none')
+        self.tmp_file = tempfile.mkstemp()
+        self.tmp_path = self.tmp_file[1]
+
+    def test_export_zone_to_bind_format_slave_should_throw(self):
+        zone = Zone(id=1, domain='example.com', type='slave', ttl=900,
+                    driver=self.driver)
+        self.assertRaises(ValueError, zone.export_to_bind_format)
+
+    def test_export_zone_to_bind_format_success(self):
+        zone = Zone(id=1, domain='example.com', type='master', ttl=900,
+                    driver=self.driver)
+
+        mock_records = []
+
+        for values in MOCK_RECORDS_VALUES:
+            values = values.copy()
+            values['driver'] = self.driver
+            values['zone'] = zone
+            record = Record(**values)
+            mock_records.append(record)
+
+        self.driver.list_records = Mock()
+        self.driver.list_records.return_value = mock_records
+
+        result = self.driver.export_zone_to_bind_format(zone=zone)
+        self.driver.export_zone_to_bind_format_file(zone=zone,
+                                                    file_path=self.tmp_path)
+
+        with open(self.tmp_path, 'r') as fp:
+            content = fp.read()
+
+        lines1 = result.split('\n')
+        lines2 = content.split('\n')
+
+        for lines in [lines1, lines2]:
+            self.assertEqual(len(lines), 2 + 1 + 9)
+            self.assertRegexpMatches(lines[1], r'\$ORIGIN example\.com\.')
+            self.assertRegexpMatches(lines[2], r'\$TTL 900')
+
+            self.assertRegexpMatches(lines[4], 
r'www.example.com\.\s+900\s+IN\s+A\s+127\.0\.0\.1')
+            self.assertRegexpMatches(lines[5], 
r'www.example.com\.\s+900\s+IN\s+AAAA\s+2a01:4f8:121:3121::2')
+            self.assertRegexpMatches(lines[6], 
r'www.example.com\.\s+123\s+IN\s+A\s+127\.0\.0\.1')
+            self.assertRegexpMatches(lines[7], 
r'example.com\.\s+900\s+IN\s+A\s+127\.0\.0\.1')
+            self.assertRegexpMatches(lines[8], 
r'test1.example.com\.\s+900\s+IN\s+TXT\s+"test foo bar"')
+            self.assertRegexpMatches(lines[9], 
r'test2.example.com\.\s+900\s+IN\s+TXT\s+"test \\"foo\\" \\"bar\\""')
+            self.assertRegexpMatches(lines[10], 
r'example.com\.\s+900\s+IN\s+MX\s+10\s+mx.example.com')
+            self.assertRegexpMatches(lines[11], 
r'example.com\.\s+900\s+IN\s+SRV\s+20\s+10 3333 example.com')
+
+
+if __name__ == '__main__':
+    sys.exit(unittest.main())

Reply via email to