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())
