From 73c6d161855cf6e0b7f7cb6081891ca475efebb4 Mon Sep 17 00:00:00 2001 From: Andrew Bogott Date: Sun, 11 Dec 2011 04:15:26 -0600 Subject: Add an API for associating floating IPs with DNS entries. For blueprint public-and-private-dns Change-Id: Ia6c3f046db4dd4978aa5ef950fd472d3455fe301 --- doc/source/api_ext/ext_floating_ip_dns.rst | 160 ++++++++++++++ nova/api/openstack/v2/contrib/floating_ip_dns.py | 220 +++++++++++++++++++ nova/exception.py | 4 + nova/flags.py | 6 + nova/network/api.py | 39 ++++ nova/network/manager.py | 20 ++ nova/network/minidns.py | 12 +- .../openstack/v2/contrib/test_floating_ip_dns.py | 242 +++++++++++++++++++++ nova/tests/api/openstack/v2/test_extensions.py | 1 + nova/tests/test_network.py | 71 ++++++ 10 files changed, 774 insertions(+), 1 deletion(-) create mode 100644 doc/source/api_ext/ext_floating_ip_dns.rst create mode 100644 nova/api/openstack/v2/contrib/floating_ip_dns.py create mode 100644 nova/tests/api/openstack/v2/contrib/test_floating_ip_dns.py diff --git a/doc/source/api_ext/ext_floating_ip_dns.rst b/doc/source/api_ext/ext_floating_ip_dns.rst new file mode 100644 index 000000000..e2ee3b763 --- /dev/null +++ b/doc/source/api_ext/ext_floating_ip_dns.rst @@ -0,0 +1,160 @@ +About The Floating IP DNS Extension +================================ +The Floating IP DNS extension provides an interface for managing DNS records associated with IP addresses +allocated by the Floating Ips extension. Requests are dispatched to a DNS driver selected at startup. + +To obtain current information the extensions available to you, issue an EXTENSION query on the OpenStack system where it is installed, such as http://mycloud.com/v1.1/tenant/extension. + +Floating IPs Extension Overview +------------------------------- + +Name + Floating IP DNS + +Namespace + http://docs.openstack.org/ext/floating_ip_dns/api/v1.1 + +Alias + OPS-DNS + +Contact + Andrew Bogott + +Status + Alpha + +Extension Version + v1.0 (2011-12-22) + +Dependencies + Compute API v1.1 + Floating IPs Extension, v1.0 + +Doc Link (PDF) + http:// + +Doc Link (WADL) + http:// + +Short Description + This extension enables associated DNS entries with floating IPs. + +Sample Query Responses +---------------------- + +As shown below, responses to an EXTENSION query in XML or JSON provide basic information about the extension. + +Extension Query Response: XML:: + + None + +Extension Query Response: JSON:: + + {'extensions': + [{'updated': '2011-12-23T00:00:00+00:00', + 'name': 'Floating_ip_dns', + 'links': [], + 'namespace': 'http://docs.openstack.org/ext/floating_ip_dns/api/v1.1', + 'alias': 'os-floating-ip_dns', + 'description': 'Floating IP DNS support'}]} + +Document Change History +----------------------- + +============= ===================================== +Revision Date Summary of Changes +2011-12-23 Initial draft +============= ===================================== + + +Summary of Changes +================== +This extension to the Compute API enables management of DNS entries for floating IP addresses. + +New Action +---------- +None + +New Faults +---------- +None + +New Headers +----------- +None + +New Resources +------------- +Get a list of DNS Domains (aka 'zones') published by the DNS driver: + + GET /v1.1//os-floating-ip-dns/ + + # Sample Response: + { 'zones' : [ + {'zone' : 'example.org'} + {'zone' : 'example.net'}]} + + +Create a DNS entry: + + POST /v1.1//os-floating-ip-dns/ + + # Sample body: + { 'dns_entry' : + { 'name': 'instance1', + 'ip': '192.168.53.11', + 'dns_type': 'A', + 'zone': 'example.org'}} + + # Sample Response (success): + { 'dns_entry' : + { 'ip' : '192.168.53.11', + 'type' : 'A', + 'zone' : 'example.org', + 'name' : 'instance1' }} + + Failure Response Code: 409 (indicates an entry with name & zone already exists.) + +Find DNS entries for a given domain and name: + + GET /v1.1//os-floating-ip-dns/?name= + + # Sample Response: + { 'dns_entries' : [ + { 'ip' : '192.168.53.11', + 'type' : 'A', + 'zone' : , + 'name' : }]} + + +Find DNS entries for a given domain and ip: + + GET /v1.1//os-floating-ip-dns//?ip= + + # Sample Response: + { 'dns_entries' : [ + { 'ip' : , + 'type' : 'A', + 'zone' : , + 'name' : 'example1' } + { 'ip' : , + 'type' : 'A', + 'zone' : , + 'name' : 'example2' }]} + + +Delete a DNS entry: + +DELETE /v1.1//os-floating-ip-dns/?name= + + Normal Response Code: 200 + Failure Response Code: 404 (Entry to be deleted not found) + +New States +---------- +None + +Changes to the Cloud Servers Specification +------------------------------------------ +None + diff --git a/nova/api/openstack/v2/contrib/floating_ip_dns.py b/nova/api/openstack/v2/contrib/floating_ip_dns.py new file mode 100644 index 000000000..cc33241b3 --- /dev/null +++ b/nova/api/openstack/v2/contrib/floating_ip_dns.py @@ -0,0 +1,220 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 Andrew Bogott for the Wikimedia Foundation +# +# Licensed 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 limitations +# under the License + +import urllib + +import webob + +from nova.api.openstack import wsgi +from nova.api.openstack import xmlutil +from nova.api.openstack.v2 import extensions +from nova import exception +from nova import log as logging +from nova import network + + +LOG = logging.getLogger('nova.api.openstack.v2.contrib.floating_ip_dns') + + +def _translate_dns_entry_view(dns_entry): + result = {} + result['ip'] = dns_entry.get('ip') + result['id'] = dns_entry.get('id') + result['type'] = dns_entry.get('type') + result['zone'] = dns_entry.get('zone') + result['name'] = dns_entry.get('name') + return {'dns_entry': result} + + +def _translate_dns_entries_view(dns_entries): + return {'dns_entries': [_translate_dns_entry_view(entry)['dns_entry'] + for entry in dns_entries]} + + +def _translate_zone_entries_view(zonelist): + return {'zones': [{'zone': zone} for zone in zonelist]} + + +def _unquote_zone(zone): + """Unquoting function for receiving a zone name in a URL. + + Zone names tend to have .'s in them. Urllib doesn't quote dots, + but Routes tends to choke on them, so we need an extra level of + by-hand quoting here. + """ + return urllib.unquote(zone).replace('%2E', '.') + + +def _create_dns_entry(ip, name, zone): + return {'ip': ip, 'name': name, 'zone': zone} + + +class FloatingIPDNSController(object): + """DNS Entry controller for OpenStack API""" + + def __init__(self): + self.network_api = network.API() + super(FloatingIPDNSController, self).__init__() + + def show(self, req, id): + """Return a list of dns entries. If ip is specified, query for + names. if name is specified, query for ips. + Quoted domain (aka 'zone') specified as id.""" + context = req.environ['nova.context'] + params = req.str_GET + floating_ip = params['ip'] if 'ip' in params else "" + name = params['name'] if 'name' in params else "" + zone = _unquote_zone(id) + + if floating_ip: + entries = self.network_api.get_dns_entries_by_address(context, + floating_ip, + zone) + entrylist = [_create_dns_entry(floating_ip, entry, zone) + for entry in entries] + elif name: + entries = self.network_api.get_dns_entries_by_name(context, + name, zone) + entrylist = [_create_dns_entry(entry, name, zone) + for entry in entries] + else: + entrylist = [] + + return _translate_dns_entries_view(entrylist) + + def index(self, req): + """Return a list of available DNS zones.""" + + context = req.environ['nova.context'] + zones = self.network_api.get_dns_zones(context) + + return _translate_zone_entries_view(zones) + + def create(self, req, body): + """Add dns entry for name and address""" + context = req.environ['nova.context'] + + try: + entry = body['dns_entry'] + address = entry['ip'] + name = entry['name'] + dns_type = entry['dns_type'] + zone = entry['zone'] + except (TypeError, KeyError): + raise webob.exc.HTTPUnprocessableEntity() + + try: + self.network_api.add_dns_entry(context, address, name, + dns_type, zone) + except exception.FloatingIpDNSExists: + return webob.Response(status_int=409) + + return _translate_dns_entry_view({'ip': address, + 'name': name, + 'type': dns_type, + 'zone': zone}) + + def delete(self, req, id): + """Delete the entry identified by req and id. """ + context = req.environ['nova.context'] + params = req.str_GET + name = params['name'] if 'name' in params else "" + zone = _unquote_zone(id) + + try: + self.network_api.delete_dns_entry(context, name, zone) + except exception.NotFound: + return webob.Response(status_int=404) + + return webob.Response(status_int=200) + + +def make_dns_entry(elem): + elem.set('id') + elem.set('ip') + elem.set('type') + elem.set('zone') + elem.set('name') + + +def make_zone_entry(elem): + elem.set('zone') + + +class FloatingIPDNSTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('dns_entry', + selector='dns_entry') + make_dns_entry(root) + return xmlutil.MasterTemplate(root, 1) + + +class FloatingIPDNSsTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('dns_entries') + elem = xmlutil.SubTemplateElement(root, 'dns_entry', + selector='dns_entries') + make_dns_entry(elem) + return xmlutil.MasterTemplate(root, 1) + + +class ZonesTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('zones') + elem = xmlutil.SubTemplateElement(root, 'zone', + selector='zones') + make_zone_entry(elem) + return xmlutil.MasterTemplate(root, 1) + + +class FloatingIPDNSSerializer(xmlutil.XMLTemplateSerializer): + def index(self): + return ZonesTemplate() + + def show(self): + return FloatingIPDNSsTemplate() + + def default(self): + return FloatingIPDNSTemplate() + + +class Floating_ip_dns(extensions.ExtensionDescriptor): + """Floating IP DNS support""" + + name = "Floating_ip_dns" + alias = "os-floating-ip-dns" + namespace = "http://docs.openstack.org/ext/floating_ip_dns/api/v1.1" + updated = "2011-12-23:00:00+00:00" + + def __init__(self, ext_mgr): + self.network_api = network.API() + super(Floating_ip_dns, self).__init__(ext_mgr) + + def get_resources(self): + resources = [] + + body_serializers = { + 'application/xml': FloatingIPDNSSerializer(), + } + + serializer = wsgi.ResponseSerializer(body_serializers) + + res = extensions.ResourceExtension('os-floating-ip-dns', + FloatingIPDNSController(), + serializer=serializer) + resources.append(res) + + return resources diff --git a/nova/exception.py b/nova/exception.py index 043322607..95e34afe2 100644 --- a/nova/exception.py +++ b/nova/exception.py @@ -555,6 +555,10 @@ class FloatingIpNotFound(NotFound): message = _("Floating ip not found for id %(id)s.") +class FloatingIpDNSExists(Invalid): + message = _("The DNS entry %(name)s already exists in zone %(zone)s.") + + class FloatingIpNotFoundForAddress(FloatingIpNotFound): message = _("Floating ip not found for address %(address)s.") diff --git a/nova/flags.py b/nova/flags.py index 599dcb0f7..9c4aeffeb 100644 --- a/nova/flags.py +++ b/nova/flags.py @@ -370,6 +370,12 @@ DEFINE_string('instance_dns_manager', 'DNS Manager for instance IPs') DEFINE_string('instance_dns_zone', '', 'DNS Zone for instance IPs') +DEFINE_string('floating_ip_dns_manager', + 'nova.network.dns_driver.DNSDriver', + 'DNS Manager for floating IPs') +DEFINE_multistring('floating_ip_dns_zones', '', + 'DNS zones for floating IPs.' + 'e.g. "example.org"') DEFINE_string('network_manager', 'nova.network.manager.VlanManager', 'Manager for network') DEFINE_string('volume_manager', 'nova.volume.manager.VolumeManager', diff --git a/nova/network/api.py b/nova/network/api.py index 89a746359..e0a5afdd9 100644 --- a/nova/network/api.py +++ b/nova/network/api.py @@ -192,3 +192,42 @@ class API(base.Base): return rpc.call(context, FLAGS.network_topic, {'method': 'get_instance_uuids_by_ip_filter', 'args': args}) + + def get_dns_zones(self, context): + """Returns a list of available dns zones. + These can be used to create DNS entries for floating ips. + """ + return rpc.call(context, + FLAGS.network_topic, + {'method': 'get_dns_zones'}) + + def add_dns_entry(self, context, address, name, dns_type, zone): + """Create specified DNS entry for address""" + args = {'address': address, + 'dns_name': name, + 'dns_type': dns_type, + 'dns_zone': zone} + return rpc.call(context, FLAGS.network_topic, + {'method': 'add_dns_entry', + 'args': args}) + + def delete_dns_entry(self, context, name, zone): + """Delete the specified dns entry.""" + args = {'dns_name': name, 'dns_zone': zone} + return rpc.call(context, FLAGS.network_topic, + {'method': 'delete_dns_entry', + 'args': args}) + + def get_dns_entries_by_address(self, context, address, zone): + """Get entries for address and zone""" + args = {'address': address, 'dns_zone': zone} + return rpc.call(context, FLAGS.network_topic, + {'method': 'get_dns_entries_by_address', + 'args': args}) + + def get_dns_entries_by_name(self, context, name, zone): + """Get entries for name and zone""" + args = {'name': name, 'dns_zone': zone} + return rpc.call(context, FLAGS.network_topic, + {'method': 'get_dns_entries_by_name', + 'args': args}) diff --git a/nova/network/manager.py b/nova/network/manager.py index 2d62581e0..a13505009 100644 --- a/nova/network/manager.py +++ b/nova/network/manager.py @@ -440,6 +440,24 @@ class FloatingIP(object): fixed_address) return [floating_ip['address'] for floating_ip in floating_ips] + def get_dns_zones(self, context): + return self.floating_dns_manager.get_zones() + + def add_dns_entry(self, context, address, dns_name, dns_type, dns_zone): + self.floating_dns_manager.create_entry(dns_name, address, + dns_type, dns_zone) + + def delete_dns_entry(self, context, dns_name, dns_zone): + self.floating_dns_manager.delete_entry(dns_name, dns_zone) + + def get_dns_entries_by_address(self, context, address, dns_zone): + return self.floating_dns_manager.get_entries_by_address(address, + dns_zone) + + def get_dns_entries_by_name(self, context, name, dns_zone): + return self.floating_dns_manager.get_entries_by_name(name, + dns_zone) + class NetworkManager(manager.SchedulerDependentManager): """Implements common network manager functionality. @@ -468,6 +486,8 @@ class NetworkManager(manager.SchedulerDependentManager): self.driver = utils.import_object(network_driver) temp = utils.import_object(FLAGS.instance_dns_manager) self.instance_dns_manager = temp + temp = utils.import_object(FLAGS.floating_ip_dns_manager) + self.floating_dns_manager = temp self.network_api = network_api.API() self.compute_api = compute_api.API() super(NetworkManager, self).__init__(service_name='network', diff --git a/nova/network/minidns.py b/nova/network/minidns.py index e955ae7bd..b26ea9aeb 100644 --- a/nova/network/minidns.py +++ b/nova/network/minidns.py @@ -16,6 +16,7 @@ import os import shutil import tempfile +from nova import exception from nova import flags @@ -41,7 +42,7 @@ class MiniDNS(object): f.close() def get_zones(self): - return ["example.org", "example.com", "example.gov"] + return flags.FLAGS.floating_ip_dns_zones def qualify(self, name, zone): if zone: @@ -52,6 +53,10 @@ class MiniDNS(object): return qualified def create_entry(self, name, address, type, dnszone): + + if self.get_entries_by_name(name, dnszone): + raise exception.FloatingIpDNSExists(name=name, zone=dnszone) + outfile = open(self.filename, 'a+') outfile.write("%s %s %s\n" % (address, self.qualify(name, dnszone), type)) @@ -69,6 +74,7 @@ class MiniDNS(object): return entry def delete_entry(self, name, dnszone=""): + deleted = False infile = open(self.filename, 'r') outfile = tempfile.NamedTemporaryFile('w', delete=False) for line in infile: @@ -76,9 +82,13 @@ class MiniDNS(object): if ((not entry) or entry['name'] != self.qualify(name, dnszone).lower()): outfile.write(line) + else: + deleted = True infile.close() outfile.close() shutil.move(outfile.name, self.filename) + if not deleted: + raise exception.NotFound def rename_entry(self, address, name, dnszone): infile = open(self.filename, 'r') diff --git a/nova/tests/api/openstack/v2/contrib/test_floating_ip_dns.py b/nova/tests/api/openstack/v2/contrib/test_floating_ip_dns.py new file mode 100644 index 000000000..d658fa542 --- /dev/null +++ b/nova/tests/api/openstack/v2/contrib/test_floating_ip_dns.py @@ -0,0 +1,242 @@ +# Copyright 2011 Andrew Bogott for the Wikimedia Foundation +# All Rights Reserved. +# +# Licensed 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 limitations +# under the License. + +import sys + +from lxml import etree +import webob +import urllib + +from nova.api.openstack.v2.contrib import floating_ips +from nova.api.openstack.v2.contrib import floating_ip_dns +from nova import context +from nova import db +from nova import network +from nova import rpc +from nova import test +from nova.tests.api.openstack import fakes +from nova import utils + + +name = "arbitraryname" +name2 = "anotherarbitraryname" + +testaddress = '10.0.0.66' +testaddress2 = '10.0.0.67' + +zone = "example.org" +zone2 = "example.net" +floating_ip_id = '1' + + +def _quote_zone(zone): + """ + Zone names tend to have .'s in them. Urllib doesn't quote dots, + but Routes tends to choke on them, so we need an extra level of + by-hand quoting here. This function needs to duplicate the one in + python-novaclient/novaclient/v1_1/floating_ip_dns.py + """ + return urllib.quote(zone.replace('.', '%2E')) + + +def network_api_get_floating_ip(self, context, id): + return {'id': floating_ip_id, 'address': testaddress, + 'fixed_ip': None} + + +def network_get_dns_zones(self, context): + return ['foo', 'bar', 'baz', 'quux'] + + +def network_get_dns_entries_by_address(self, context, address, zone): + return [name, name2] + + +def network_get_dns_entries_by_name(self, context, address, zone): + return [testaddress, testaddress2] + + +def network_add_dns_entry(self, context, address, name, dns_type, zone): + return {'dns_entry': {'ip': testaddress, + 'name': name, + 'type': dns_type, + 'zone': zone}} + + +class FloatingIpDNSTest(test.TestCase): + def _create_floating_ip(self): + """Create a floating ip object.""" + host = "fake_host" + return db.floating_ip_create(self.context, + {'address': testaddress, + 'host': host}) + + def _delete_floating_ip(self): + db.floating_ip_destroy(self.context, testaddress) + + def setUp(self): + super(FloatingIpDNSTest, self).setUp() + self.stubs.Set(network.api.API, "get_dns_zones", + network_get_dns_zones) + self.stubs.Set(network.api.API, "get_dns_entries_by_address", + network_get_dns_entries_by_address) + self.stubs.Set(network.api.API, "get_dns_entries_by_name", + network_get_dns_entries_by_name) + self.stubs.Set(network.api.API, "get_floating_ip", + network_api_get_floating_ip) + self.stubs.Set(network.api.API, "add_dns_entry", + network_add_dns_entry) + + self.context = context.get_admin_context() + + self._create_floating_ip() + self.dns_controller = floating_ip_dns.FloatingIPDNSController() + + def tearDown(self): + self._delete_floating_ip() + super(FloatingIpDNSTest, self).tearDown() + + def test_dns_zones_list(self): + req = fakes.HTTPRequest.blank('/v2/123/os-floating-ip-dns') + res_dict = self.dns_controller.index(req) + entries = res_dict['zones'] + self.assertTrue(entries) + self.assertEqual(entries[0]['zone'], "foo") + self.assertEqual(entries[1]['zone'], "bar") + self.assertEqual(entries[2]['zone'], "baz") + self.assertEqual(entries[3]['zone'], "quux") + + def test_get_dns_entries_by_address(self): + qparams = {'ip': testaddress} + params = "?%s" % urllib.urlencode(qparams) if qparams else "" + + req = fakes.HTTPRequest.blank('/v2/123/os-floating-ip-dns/%s%s' % + (_quote_zone(zone), params)) + entries = self.dns_controller.show(req, _quote_zone(zone)) + + self.assertEqual(len(entries['dns_entries']), 2) + self.assertEqual(entries['dns_entries'][0]['name'], + name) + self.assertEqual(entries['dns_entries'][1]['name'], + name2) + self.assertEqual(entries['dns_entries'][0]['zone'], + zone) + + def test_get_dns_entries_by_name(self): + qparams = {'name': name} + params = "?%s" % urllib.urlencode(qparams) if qparams else "" + + req = fakes.HTTPRequest.blank('/v2/123/os-floating-ip-dns/%s%s' % + (_quote_zone(zone), params)) + entries = self.dns_controller.show(req, _quote_zone(zone)) + + self.assertEqual(len(entries['dns_entries']), 2) + self.assertEqual(entries['dns_entries'][0]['ip'], + testaddress) + self.assertEqual(entries['dns_entries'][1]['ip'], + testaddress2) + self.assertEqual(entries['dns_entries'][0]['zone'], + zone) + + def test_create(self): + body = {'dns_entry': + {'name': name, + 'ip': testaddress, + 'dns_type': 'A', + 'zone': zone}} + req = fakes.HTTPRequest.blank('/v2/123/os-floating-ip-dns') + entry = self.dns_controller.create(req, body) + + self.assertEqual(entry['dns_entry']['ip'], testaddress) + + def test_delete(self): + self.called = False + self.deleted_zone = "" + self.deleted_name = "" + + def network_delete_dns_entry(fakeself, context, req, id): + self.called = True + self.deleted_zone = id + + self.stubs.Set(network.api.API, "delete_dns_entry", + network_delete_dns_entry) + + qparams = {'name': name} + params = "?%s" % urllib.urlencode(qparams) if qparams else "" + + req = fakes.HTTPRequest.blank('/v2/123/os-floating-ip-dns/%s%s' % + (_quote_zone(zone), params)) + entries = self.dns_controller.delete(req, _quote_zone(zone)) + + self.assertTrue(self.called) + self.assertEquals(self.deleted_zone, zone) + + +class FloatingIpDNSSerializerTest(test.TestCase): + def test_default_serializer(self): + serializer = floating_ip_dns.FloatingIPDNSSerializer() + text = serializer.serialize(dict( + dns_entry=dict( + ip=testaddress, + type='A', + zone=zone, + name=name))) + + tree = etree.fromstring(text) + + self.assertEqual('dns_entry', tree.tag) + self.assertEqual(testaddress, tree.get('ip')) + self.assertEqual(zone, tree.get('zone')) + self.assertEqual(name, tree.get('name')) + + def test_index_serializer(self): + serializer = floating_ip_dns.FloatingIPDNSSerializer() + text = serializer.serialize(dict( + zones=[ + dict(zone=zone), + dict(zone=zone2)]), 'index') + + tree = etree.fromstring(text) + self.assertEqual('zones', tree.tag) + self.assertEqual(2, len(tree)) + self.assertEqual(zone, tree[0].get('zone')) + self.assertEqual(zone2, tree[1].get('zone')) + + def test_show_serializer(self): + serializer = floating_ip_dns.FloatingIPDNSSerializer() + text = serializer.serialize(dict( + dns_entries=[ + dict(ip=testaddress, + type='A', + zone=zone, + name=name), + dict(ip=testaddress2, + type='C', + zone=zone, + name=name2)]), 'show') + + tree = etree.fromstring(text) + self.assertEqual('dns_entries', tree.tag) + self.assertEqual(2, len(tree)) + self.assertEqual('dns_entry', tree[0].tag) + self.assertEqual('dns_entry', tree[1].tag) + self.assertEqual(testaddress, tree[0].get('ip')) + self.assertEqual('A', tree[0].get('type')) + self.assertEqual(zone, tree[0].get('zone')) + self.assertEqual(name, tree[0].get('name')) + self.assertEqual(testaddress2, tree[1].get('ip')) + self.assertEqual('C', tree[1].get('type')) + self.assertEqual(zone, tree[1].get('zone')) + self.assertEqual(name2, tree[1].get('name')) diff --git a/nova/tests/api/openstack/v2/test_extensions.py b/nova/tests/api/openstack/v2/test_extensions.py index ad017e665..302095fe8 100644 --- a/nova/tests/api/openstack/v2/test_extensions.py +++ b/nova/tests/api/openstack/v2/test_extensions.py @@ -108,6 +108,7 @@ class ExtensionControllerTest(ExtensionTestCase): "FlavorExtraSpecs", "FlavorExtraData", "Floating_ips", + "Floating_ip_dns", "Fox In Socks", "Hosts", "Keypairs", diff --git a/nova/tests/test_network.py b/nova/tests/test_network.py index dc26b0298..9ed779f4e 100644 --- a/nova/tests/test_network.py +++ b/nova/tests/test_network.py @@ -19,11 +19,13 @@ import mox from nova import context from nova import db from nova import exception +from nova import flags from nova import log as logging from nova import rpc from nova import test from nova import utils from nova.network import manager as network_manager +from nova.network import api as network_api from nova.tests import fake_network @@ -1123,11 +1125,17 @@ class FloatingIPTestCase(test.TestCase): def setUp(self): super(FloatingIPTestCase, self).setUp() self.network = TestFloatingIPManager() + temp = utils.import_object('nova.network.minidns.MiniDNS') + self.network.floating_dns_manager = temp self.network.db = db self.project_id = 'testproject' self.context = context.RequestContext('testuser', self.project_id, is_admin=False) + def tearDown(self): + super(FloatingIPTestCase, self).tearDown() + self.network.floating_dns_manager.delete_dns_file() + def test_double_deallocation(self): instance_ref = db.api.instance_create(self.context, {"project_id": self.project_id}) @@ -1138,3 +1146,66 @@ class FloatingIPTestCase(test.TestCase): instance_id=instance_ref['id']) self.network.deallocate_for_instance(self.context, instance_id=instance_ref['id']) + + def test_floating_dns_zones(self): + zone1 = "example.org" + zone2 = "example.com" + flags.FLAGS.floating_ip_dns_zones = [zone1, zone2] + + zones = self.network.get_dns_zones(self.context) + self.assertEqual(len(zones), 2) + self.assertEqual(zones[0], zone1) + self.assertEqual(zones[1], zone2) + + def test_floating_dns_create_conflict(self): + zone = "example.org" + address1 = "10.10.10.11" + name1 = "foo" + name2 = "bar" + + self.network.add_dns_entry(self.context, address1, name1, "A", zone) + + self.assertRaises(exception.FloatingIpDNSExists, + self.network.add_dns_entry, self.context, + address1, name1, "A", zone) + + def test_floating_create_and_get(self): + zone = "example.org" + address1 = "10.10.10.11" + name1 = "foo" + name2 = "bar" + entries = self.network.get_dns_entries_by_address(self.context, + address1, zone) + self.assertFalse(entries) + + self.network.add_dns_entry(self.context, address1, name1, "A", zone) + self.network.add_dns_entry(self.context, address1, name2, "A", zone) + entries = self.network.get_dns_entries_by_address(self.context, + address1, zone) + self.assertEquals(len(entries), 2) + self.assertEquals(entries[0], name1) + self.assertEquals(entries[1], name2) + + entries = self.network.get_dns_entries_by_name(self.context, + name1, zone) + self.assertEquals(len(entries), 1) + self.assertEquals(entries[0], address1) + + def test_floating_dns_delete(self): + zone = "example.org" + address1 = "10.10.10.11" + name1 = "foo" + name2 = "bar" + + self.network.add_dns_entry(self.context, address1, name1, "A", zone) + self.network.add_dns_entry(self.context, address1, name2, "A", zone) + self.network.delete_dns_entry(self.context, name1, zone) + + entries = self.network.get_dns_entries_by_address(self.context, + address1, zone) + self.assertEquals(len(entries), 1) + self.assertEquals(entries[0], name2) + + self.assertRaises(exception.NotFound, + self.network.delete_dns_entry, self.context, + name1, zone) -- cgit