diff --git a/doc/source/api_ext/ext_floating_ip_dns.rst b/doc/source/api_ext/ext_floating_ip_dns.rst index 632949ca2d4a..ef7229b6e283 100644 --- a/doc/source/api_ext/ext_floating_ip_dns.rst +++ b/doc/source/api_ext/ext_floating_ip_dns.rst @@ -85,70 +85,90 @@ None New Resources ------------- -Get a list of DNS Domains (aka 'zones') published by the DNS driver: +Get a list of registered DNS Domains published by the DNS drivers: GET /v1.1//os-floating-ip-dns/ # Sample Response: - { 'zones' : [ - {'zone' : 'example.org'} - {'zone' : 'example.net'}]} + {'zone_entries' : [ + {'domain': 'domain1.example.org', 'scope': 'public', 'project': 'proj1'} + {'domain': 'domain2.example.net', 'scope': 'public', 'project': 'proj2'} + {'domain': 'example.net', 'scope': 'public', 'project': ''} + {'domain': 'example.internal', 'scope': 'private', 'zone': 'zone1'}]} -Create a DNS entry: +Create or modify a DNS domain: - POST /v1.1//os-floating-ip-dns/ + PUT /v1.1//os-floating-ip-dns/ + + # Sample body, public domain: + {'zone_entry' : + {'scope': 'public', + 'project' : 'project1'}} + + # Sample body, public (projectless) domain: + {'zone_entry' : + {'scope': 'public'}} + + # Sample Response, public domain (success): + {'zone_entry' : + {'zone': 'domain1.example.org', + 'scope': 'public', + 'project': 'project1'}} + + # Sample body, private domain: + {'zone_entry' : + {'scope': 'private', + 'availability_zone': 'zone1'}} + + # Sample Response, private domain (success): + {'zone_entry' : + {'zone': 'domain1.private', + 'scope': 'private', + 'availability_zone': 'zone1'}} + + Failure Response Code: 403 (Insufficient permissions.) + + +Delete a DNS domain and all associated host entries: + +DELETE /v1.1//os-floating-ip-dns/ + + Normal Response Code: 200 + Failure Response Code: 404 (Domain to be deleted not found.) + Failure Response Code: 403 (Insufficient permissions to delete.) + + +Create or modify a DNS entry: + + PUT /v1.1//os-floating-ip-dns//entries/ # Sample body: { 'dns_entry' : - { 'name': 'instance1', - 'ip': '192.168.53.11', - 'dns_type': 'A', - 'zone': 'example.org'}} + { 'ip': '192.168.53.11', + 'dns_type': 'A' }} # Sample Response (success): { 'dns_entry' : - { 'ip' : '192.168.53.11', - 'type' : 'A', - 'zone' : 'example.org', + { 'type' : 'A', 'name' : 'instance1' }} - Failure Response Code: 409 (indicates an entry with name & zone already exists.) +Find unique DNS entry for a given domain and name: -Change the ip address of an existing DNS entry: - - PUT /v1.1//os-floating-ip-dns/ - - # Sample body: - { 'dns_entry' : - { 'name': 'instance1', - 'ip': '192.168.53.99'}} - - # Sample Response (success): - { 'dns_entry' : - { 'ip' : '192.168.53.99', - 'name' : 'instance1', - 'zone' : 'example.org'}} - - Failure Response Code: 404 (Entry to be modified not found) - - -Find DNS entries for a given domain and name: - - GET /v1.1//os-floating-ip-dns/?name= + GET /v1.1//os-floating-ip-dns//entries/ # Sample Response: - { 'dns_entries' : [ + { 'dns_entry' : { 'ip' : '192.168.53.11', 'type' : 'A', 'zone' : , - 'name' : }]} + 'name' : }} Find DNS entries for a given domain and ip: - GET /v1.1//os-floating-ip-dns//?ip= + GET /v1.1//os-floating-ip-dns//entries?ip= # Sample Response: { 'dns_entries' : [ @@ -164,11 +184,12 @@ Find DNS entries for a given domain and ip: Delete a DNS entry: -DELETE /v1.1//os-floating-ip-dns/?name= +DELETE /v1.1//os-floating-ip-dns//entries/ Normal Response Code: 200 Failure Response Code: 404 (Entry to be deleted not found) + New States ---------- None @@ -176,4 +197,3 @@ None Changes to the Cloud Servers Specification ------------------------------------------ None - diff --git a/etc/nova/policy.json b/etc/nova/policy.json index 44bbee65d4d8..6e56f7ad2669 100644 --- a/etc/nova/policy.json +++ b/etc/nova/policy.json @@ -6,7 +6,7 @@ "compute:create": [], "compute:create:attach_network": [], "compute:create:attach_volume": [], - "compute:get_all" :[], + "compute:get_all": [], "volume:create": [], @@ -47,6 +47,8 @@ "network:modify_dns_entry": [], "network:delete_dns_entry": [], "network:get_dns_entries_by_address": [], - "network:get_dns_entries_by_name": [] + "network:get_dns_entries_by_name": [], + "network:create_private_dns_domain": [], + "network:create_public_dns_domain": [], + "network:delete_dns_domain": [] } - diff --git a/nova/api/openstack/compute/contrib/floating_ip_dns.py b/nova/api/openstack/compute/contrib/floating_ip_dns.py index 032d5bd7e979..99e4e4e47aab 100644 --- a/nova/api/openstack/compute/contrib/floating_ip_dns.py +++ b/nova/api/openstack/compute/contrib/floating_ip_dns.py @@ -39,6 +39,9 @@ def make_dns_entry(elem): def make_zone_entry(elem): elem.set('zone') + elem.set('scope') + elem.set('project') + elem.set('availability_zone') class FloatingIPDNSTemplate(xmlutil.TemplateBuilder): @@ -58,11 +61,19 @@ class FloatingIPDNSsTemplate(xmlutil.TemplateBuilder): return xmlutil.MasterTemplate(root, 1) +class ZoneTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('zone_entry', + selector='zone_entry') + make_zone_entry(root) + return xmlutil.MasterTemplate(root, 1) + + class ZonesTemplate(xmlutil.TemplateBuilder): def construct(self): - root = xmlutil.TemplateElement('zones') - elem = xmlutil.SubTemplateElement(root, 'zone', - selector='zones') + root = xmlutil.TemplateElement('zone_entries') + elem = xmlutil.SubTemplateElement(root, 'zone_entry', + selector='zone_entries') make_zone_entry(elem) return xmlutil.MasterTemplate(root, 1) @@ -82,8 +93,18 @@ def _translate_dns_entries_view(dns_entries): for entry in dns_entries]} -def _translate_zone_entries_view(zonelist): - return {'zones': [{'zone': zone} for zone in zonelist]} +def _translate_zone_entry_view(zone_entry): + result = {} + result['domain'] = zone_entry.get('domain') + result['scope'] = zone_entry.get('scope') + result['project'] = zone_entry.get('project') + result['availability_zone'] = zone_entry.get('availability_zone') + return {'zone_entry': result} + + +def _translate_zone_entries_view(zone_entries): + return {'zone_entries': [_translate_zone_entry_view(entry)['zone_entry'] + for entry in zone_entries]} def _unquote_zone(zone): @@ -100,102 +121,153 @@ def _create_dns_entry(ip, name, zone): return {'ip': ip, 'name': name, 'zone': zone} -class FloatingIPDNSController(object): - """DNS Entry controller for OpenStack API""" +def _create_domain_entry(domain, scope=None, project=None, av_zone=None): + return {'domain': domain, 'scope': scope, 'project': project, + 'availability_zone': av_zone} + + +class FloatingIPDNSDomainController(object): + """DNS domain controller for OpenStack API""" def __init__(self): self.network_api = network.API() - super(FloatingIPDNSController, self).__init__() - - @wsgi.serializers(xml=FloatingIPDNSsTemplate) - 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.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) + super(FloatingIPDNSDomainController, self).__init__() @wsgi.serializers(xml=ZonesTemplate) def index(self, req): """Return a list of available DNS zones.""" - context = req.environ['nova.context'] zones = self.network_api.get_dns_zones(context) + zonelist = [_create_domain_entry(zone['domain'], + zone.get('scope'), + zone.get('project'), + zone.get('availability_zone')) + for zone in zones] - return _translate_zone_entries_view(zones) + return _translate_zone_entries_view(zonelist) + + @wsgi.serializers(xml=ZoneTemplate) + def update(self, req, id, body): + """Add or modify domain entry""" + context = req.environ['nova.context'] + fqdomain = _unquote_zone(id) + try: + entry = body['zone_entry'] + scope = entry['scope'] + except (TypeError, KeyError): + raise webob.exc.HTTPUnprocessableEntity() + project = entry.get('project', None) + av_zone = entry.get('availability_zone', None) + if (not scope or + project and av_zone or + scope == 'private' and project or + scope == 'public' and av_zone): + raise webob.exc.HTTPUnprocessableEntity() + try: + if scope == 'private': + self.network_api.create_private_dns_domain(context, + fqdomain, + av_zone) + return _translate_zone_entry_view({'domain': fqdomain, + 'scope': scope, + 'availability_zone': av_zone}) + else: + self.network_api.create_public_dns_domain(context, + fqdomain, + project) + return _translate_zone_entry_view({'domain': fqdomain, + 'scope': 'public', + 'project': project}) + except exception.NotAuthorized or exception.AdminRequired: + return webob.Response(status_int=403) + + def delete(self, req, id): + """Delete the domain identified by id. """ + context = req.environ['nova.context'] + params = req.str_GET + zone = _unquote_zone(id) + + # Delete the whole domain + try: + self.network_api.delete_dns_domain(context, zone) + except exception.NotAuthorized or exception.AdminRequired: + return webob.Response(status_int=403) + except exception.NotFound: + return webob.Response(status_int=404) + + return webob.Response(status_int=200) + + +class FloatingIPDNSEntryController(object): + """DNS Entry controller for OpenStack API""" + + def __init__(self): + self.network_api = network.API() + super(FloatingIPDNSEntryController, self).__init__() @wsgi.serializers(xml=FloatingIPDNSTemplate) - def create(self, req, body): - """Add dns entry for name and address""" + def show(self, req, zone_id, id): + """Return the DNS entry that corresponds to zone_id and id.""" context = req.environ['nova.context'] + zone = _unquote_zone(zone_id) + name = id + entries = self.network_api.get_dns_entries_by_name(context, + name, zone) + entry = _create_dns_entry(entries[0], name, zone) + return _translate_dns_entry_view(entry) + + @wsgi.serializers(xml=FloatingIPDNSsTemplate) + def index(self, req, zone_id): + """Return a list of dns entries for the specified zone and ip.""" + context = req.environ['nova.context'] + params = req.GET + floating_ip = params.get('ip') + zone = _unquote_zone(zone_id) + + if not floating_ip: + raise webob.exc.HTTPUnprocessableEntity() + + 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] + + return _translate_dns_entries_view(entrylist) + + @wsgi.serializers(xml=FloatingIPDNSTemplate) + def update(self, req, zone_id, id, body): + """Add or modify dns entry""" + context = req.environ['nova.context'] + zone = _unquote_zone(zone_id) + name = id 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: + entries = self.network_api.get_dns_entries_by_name(context, name, zone) + if not entries: + # create! self.network_api.add_dns_entry(context, address, name, dns_type, zone) - except exception.FloatingIpDNSExists: - return webob.Response(status_int=409) + else: + # modify! + self.network_api.modify_dns_entry(context, name, address, zone) return _translate_dns_entry_view({'ip': address, 'name': name, 'type': dns_type, 'zone': zone}) - def update(self, req, id, body): - """Modify a dns entry.""" - context = req.environ['nova.context'] - zone = _unquote_zone(id) - - try: - entry = body['dns_entry'] - name = entry['name'] - new_ip = entry['ip'] - except (TypeError, KeyError): - raise webob.exc.HTTPUnprocessableEntity() - - try: - self.network_api.modify_dns_entry(context, name, - new_ip, zone) - except exception.NotFound: - return webob.Response(status_int=404) - - return _translate_dns_entry_view({'ip': new_ip, - 'name': name, - 'zone': zone}) - - def delete(self, req, id): + def delete(self, req, zone_id, id): """Delete the entry identified by req and id. """ context = req.environ['nova.context'] - params = req.GET - name = params['name'] if 'name' in params else "" - zone = _unquote_zone(id) + zone = _unquote_zone(zone_id) + name = id try: self.network_api.delete_dns_entry(context, name, zone) @@ -221,7 +293,13 @@ class Floating_ip_dns(extensions.ExtensionDescriptor): resources = [] res = extensions.ResourceExtension('os-floating-ip-dns', - FloatingIPDNSController()) + FloatingIPDNSDomainController()) + resources.append(res) + + res = extensions.ResourceExtension('entries', + FloatingIPDNSEntryController(), + parent={'member_name': 'zone', + 'collection_name': 'os-floating-ip-dns'}) resources.append(res) return resources diff --git a/nova/db/api.py b/nova/db/api.py index 5ec8bfadee34..9d29a0218848 100644 --- a/nova/db/api.py +++ b/nova/db/api.py @@ -324,6 +324,32 @@ def floating_ip_set_auto_assigned(context, address): """Set auto_assigned flag to floating ip""" return IMPL.floating_ip_set_auto_assigned(context, address) + +def dnsdomain_list(context): + """Get a list of all zones in our database, public and private.""" + return IMPL.dnsdomain_list(context) + + +def dnsdomain_register_for_zone(context, fqdomain, zone): + """Associated a DNS domain with an availability zone""" + return IMPL.dnsdomain_register_for_zone(context, fqdomain, zone) + + +def dnsdomain_register_for_project(context, fqdomain, project): + """Associated a DNS domain with a project id""" + return IMPL.dnsdomain_register_for_project(context, fqdomain, project) + + +def dnsdomain_unregister(context, fqdomain): + """Purge associations for the specified DNS zone""" + return IMPL.dnsdomain_unregister(context, fqdomain) + + +def dnsdomain_get(context, fqdomain): + """Get the db record for the specified domain.""" + return IMPL.dnsdomain_get(context, fqdomain) + + #################### diff --git a/nova/db/sqlalchemy/api.py b/nova/db/sqlalchemy/api.py index 714927de95da..b81de6d61163 100644 --- a/nova/db/sqlalchemy/api.py +++ b/nova/db/sqlalchemy/api.py @@ -695,6 +695,77 @@ def floating_ip_update(context, address, values): floating_ip_ref.save(session=session) +@require_context +def _dnsdomain_get(context, session, fqdomain): + return model_query(context, models.DNSDomain, + session=session, read_deleted="no").\ + filter_by(domain=fqdomain).\ + with_lockmode('update').\ + first() + + +@require_context +def dnsdomain_get(context, fqdomain): + session = get_session() + with session.begin(): + return _dnsdomain_get(context, session, fqdomain) + + +@require_admin_context +def _dnsdomain_get_or_create(context, session, fqdomain): + domain_ref = _dnsdomain_get(context, session, fqdomain) + if not domain_ref: + dns_ref = models.DNSDomain() + dns_ref.update({'domain': fqdomain, + 'availability_zone': None, + 'project_id': None}) + return dns_ref + + return domain_ref + + +@require_admin_context +def dnsdomain_register_for_zone(context, fqdomain, zone): + session = get_session() + with session.begin(): + domain_ref = _dnsdomain_get_or_create(context, session, fqdomain) + domain_ref.scope = 'private' + domain_ref.availability_zone = zone + domain_ref.save(session=session) + + +@require_admin_context +def dnsdomain_register_for_project(context, fqdomain, project): + session = get_session() + with session.begin(): + domain_ref = _dnsdomain_get_or_create(context, session, fqdomain) + domain_ref.scope = 'public' + domain_ref.project_id = project + domain_ref.save(session=session) + + +@require_admin_context +def dnsdomain_unregister(context, fqdomain): + session = get_session() + with session.begin(): + domain_ref = _dnsdomain_get(context, session, fqdomain) + if domain_ref: + domain_ref.delete(session=session) + + +@require_context +def dnsdomain_list(context): + session = get_session() + records = model_query(context, models.DNSDomain, + session=session, read_deleted="no").\ + with_lockmode('update').all() + domains = [] + for record in records: + domains.append(record.domain) + + return domains + + ################### diff --git a/nova/db/sqlalchemy/migrate_repo/versions/072_add_dns_table.py b/nova/db/sqlalchemy/migrate_repo/versions/072_add_dns_table.py new file mode 100644 index 000000000000..7a78413d3349 --- /dev/null +++ b/nova/db/sqlalchemy/migrate_repo/versions/072_add_dns_table.py @@ -0,0 +1,66 @@ +# Copyright 2012 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. + +from sqlalchemy import Boolean, Column, DateTime, ForeignKey +from sqlalchemy import MetaData, String, Table +from nova import log as logging + +meta = MetaData() + +# +# New Tables +# +dns_domains = Table('dns_domains', meta, + Column('created_at', DateTime(timezone=False)), + Column('updated_at', DateTime(timezone=False)), + Column('deleted_at', DateTime(timezone=False)), + Column('deleted', Boolean(create_constraint=True, name=None)), + Column('domain', + String(length=512, convert_unicode=False, assert_unicode=None, + unicode_error=None, _warn_on_bytestring=False), + primary_key=True, nullable=False), + Column('scope', + String(length=255, convert_unicode=False, assert_unicode=None, + unicode_error=None, _warn_on_bytestring=False)), + Column('availability_zone', + String(length=255, convert_unicode=False, assert_unicode=None, + unicode_error=None, _warn_on_bytestring=False)), + Column('project_id', + String(length=255, convert_unicode=False, assert_unicode=None, + unicode_error=None, _warn_on_bytestring=False), + ForeignKey('projects.id')) + ) + + +def upgrade(migrate_engine): + meta.bind = migrate_engine + + # load instances for fk + instances = Table('projects', meta, autoload=True) + + # create dns_domains table + try: + dns_domains.create() + except Exception: + logging.error(_("Table |%s| not created!"), repr(dns_domains)) + raise + + +def downgrade(migrate_engine): + try: + dns_domains.drop() + except Exception: + logging.error(_("dns_domains table not dropped")) + raise diff --git a/nova/db/sqlalchemy/models.py b/nova/db/sqlalchemy/models.py index e35d84c6be5b..c5d9f0812bb9 100644 --- a/nova/db/sqlalchemy/models.py +++ b/nova/db/sqlalchemy/models.py @@ -749,6 +749,19 @@ class Project(BASE, NovaBase): backref='projects') +class DNSDomain(BASE, NovaBase): + """Represents a DNS domain with availability zone or project info.""" + __tablename__ = 'dns_domains' + domain = Column(String(512), primary_key=True) + scope = Column(String(255)) + availability_zone = Column(String(255)) + project_id = Column(String(255)) + project = relationship(Project, + primaryjoin=project_id == Project.id, + foreign_keys=[Project.id], + uselist=False) + + class UserProjectRoleAssociation(BASE, NovaBase): __tablename__ = 'user_project_role_association' user_id = Column(String(255), primary_key=True) diff --git a/nova/network/api.py b/nova/network/api.py index b9adba98df62..a608a908a83e 100644 --- a/nova/network/api.py +++ b/nova/network/api.py @@ -264,6 +264,13 @@ class API(base.Base): {'method': 'delete_dns_entry', 'args': args}) + def delete_dns_domain(self, context, fqdomain): + """Delete the specified dns domain.""" + args = {'fqdomain': fqdomain} + return rpc.call(context, FLAGS.network_topic, + {'method': 'delete_dns_domain', + 'args': args}) + def get_dns_entries_by_address(self, context, address, zone): """Get entries for address and zone""" args = {'address': address, 'dns_zone': zone} @@ -277,3 +284,17 @@ class API(base.Base): return rpc.call(context, FLAGS.network_topic, {'method': 'get_dns_entries_by_name', 'args': args}) + + def create_private_dns_domain(self, context, fqdomain, availability_zone): + """Create a private DNS domain with nova availability zone.""" + args = {'fqdomain': fqdomain, 'zone': availability_zone} + return rpc.call(context, FLAGS.network_topic, + {'method': 'create_private_dns_domain', + 'args': args}) + + def create_public_dns_domain(self, context, fqdomain, project=None): + """Create a private DNS domain with optional nova project.""" + args = {'fqdomain': fqdomain, 'project': project} + return rpc.call(context, FLAGS.network_topic, + {'method': 'create_public_dns_domain', + 'args': args}) diff --git a/nova/network/dns_driver.py b/nova/network/dns_driver.py index 495628e121e1..7ec6302c9be7 100644 --- a/nova/network/dns_driver.py +++ b/nova/network/dns_driver.py @@ -36,3 +36,9 @@ class DNSDriver(object): def get_entries_by_name(self, _name, _dnszone=""): return [] + + def create_domain(self, _fqdomain): + pass + + def delete_domain(self, _fqdomain): + pass diff --git a/nova/network/manager.py b/nova/network/manager.py index 4117903ef10a..68440be2d851 100644 --- a/nova/network/manager.py +++ b/nova/network/manager.py @@ -497,9 +497,42 @@ class FloatingIP(object): fixed_address) return [floating_ip['address'] for floating_ip in floating_ips] + def _prepare_domain_entry(self, context, domain): + domainref = self.db.dnsdomain_get(context, domain) + scope = domainref.scope + if scope == 'private': + av_zone = domainref.availability_zone + this_zone = {'domain': domain, + 'scope': scope, + 'availability_zone': av_zone} + else: + project = domainref.project_id + this_zone = {'domain': domain, + 'scope': scope, + 'project': project} + return this_zone + @wrap_check_policy def get_dns_zones(self, context): - return self.floating_dns_manager.get_zones() + zones = [] + + db_zone_list = self.db.dnsdomain_list(context) + floating_driver_zone_list = self.floating_dns_manager.get_zones() + instance_driver_zone_list = self.instance_dns_manager.get_zones() + + for db_zone in db_zone_list: + if (db_zone in floating_driver_zone_list or + db_zone in instance_driver_zone_list): + zone_entry = self._prepare_domain_entry(context, db_zone) + if zone_entry: + zones.append(zone_entry) + else: + LOG.warn(_('Database inconsistency: DNS domain |%s| is ' + 'registered in the Nova db but not visible to ' + 'either the floating or instance DNS driver. It ' + 'will be ignored.'), db_zone) + + return zones @wrap_check_policy def add_dns_entry(self, context, address, dns_name, dns_type, dns_zone): @@ -525,6 +558,34 @@ class FloatingIP(object): return self.floating_dns_manager.get_entries_by_name(name, dns_zone) + @wrap_check_policy + def create_private_dns_domain(self, context, fqdomain, zone): + self.db.dnsdomain_register_for_zone(context, fqdomain, zone) + try: + self.instance_dns_manager.create_domain(fqdomain) + except exception.FloatingIpDNSExists: + LOG.warn(_('Domain |%(domain)s| already exists, ' + 'changing zone to |%(zone)s|.'), + {'domain': fqdomain, 'zone': zone}) + + @wrap_check_policy + def create_public_dns_domain(self, context, fqdomain, project): + self.db.dnsdomain_register_for_project(context, fqdomain, project) + try: + self.floating_dns_manager.create_domain(fqdomain) + except exception.FloatingIpDNSExists: + LOG.warn(_('Domain |%(domain)s| already exists, ' + 'changing project to |%(project)s|.'), + {'domain': fqdomain, 'project': project}) + + @wrap_check_policy + def delete_dns_domain(self, context, fqdomain): + self.db.dnsdomain_unregister(context, fqdomain) + self.floating_dns_manager.delete_domain(fqdomain) + + def _get_project_for_domain(self, context, fqdomain): + return self.db.dnsdomain_project(context, fqdomain) + class NetworkManager(manager.SchedulerDependentManager): """Implements common network manager functionality. @@ -553,6 +614,7 @@ class NetworkManager(manager.SchedulerDependentManager): self.driver = utils.import_object(network_driver) temp = utils.import_object(FLAGS.instance_dns_manager) self.instance_dns_manager = temp + self.instance_dns_domain = FLAGS.instance_dns_zone temp = utils.import_object(FLAGS.floating_ip_dns_manager) self.floating_dns_manager = temp self.network_api = network_api.API() @@ -1038,6 +1100,25 @@ class NetworkManager(manager.SchedulerDependentManager): raise exception.FixedIpNotFoundForSpecificInstance( instance_id=instance_id, ip=address) + def _validate_instance_zone_for_dns_domain(self, context, instance_id): + instance = self.db.instance_get(context, instance_id) + instance_zone = instance.get('availability_zone') + if not self.instance_dns_domain: + return True + instance_domain = self.instance_dns_domain + domainref = self.db.dnsdomain_get(context, instance_zone) + dns_zone = domainref.availability_zone + if dns_zone and (dns_zone != instance_zone): + LOG.warn(_('instance-dns-zone is |%(domain)s|, ' + 'which is in availability zone |%(zone)s|. ' + 'Instance |%(instance)s| is in zone |%(zone2)s|. ' + 'No DNS record will be created.'), + {'domain': instance_domain, 'zone': dns_zone, + 'instance': instance_id, 'zone2': instance_zone}) + return False + else: + return True + def allocate_fixed_ip(self, context, instance_id, network, **kwargs): """Gets a fixed ip from the pool.""" # TODO(vish): when this is called by compute, we can associate compute @@ -1065,12 +1146,15 @@ class NetworkManager(manager.SchedulerDependentManager): instance_ref = self.db.instance_get(context, instance_id) name = instance_ref['display_name'] - uuid = instance_ref['uuid'] - self.instance_dns_manager.create_entry(name, address, - "A", FLAGS.instance_dns_zone) - self.instance_dns_manager.create_entry(uuid, address, - "A", FLAGS.instance_dns_zone) + if self._validate_instance_zone_for_dns_domain(context, instance_id): + uuid = instance_ref['uuid'] + self.instance_dns_manager.create_entry(name, address, + "A", + self.instance_dns_domain) + self.instance_dns_manager.create_entry(uuid, address, + "A", + self.instance_dns_domain) self._setup_network(context, network) return address @@ -1084,8 +1168,9 @@ class NetworkManager(manager.SchedulerDependentManager): self._do_trigger_security_group_members_refresh_for_instance( instance_id) - for name in self.instance_dns_manager.get_entries_by_address(address): - self.instance_dns_manager.delete_entry(name) + if self._validate_instance_zone_for_dns_domain(context, instance_id): + for n in self.instance_dns_manager.get_entries_by_address(address): + self.instance_dns_manager.delete_entry(n) if FLAGS.force_dhcp_release: network = self._get_network_by_id(context, diff --git a/nova/network/minidns.py b/nova/network/minidns.py index 81305c8c470a..7d1a186a98a0 100644 --- a/nova/network/minidns.py +++ b/nova/network/minidns.py @@ -42,7 +42,14 @@ class MiniDNS(object): f.close() def get_zones(self): - return flags.FLAGS.floating_ip_dns_zones + entries = [] + infile = open(self.filename, 'r') + for line in infile: + entry = self.parse_line(line) + if entry and entry['address'].lower() == 'domain'.lower(): + entries.append(entry['name']) + infile.close() + return entries def qualify(self, name, zone): if zone: @@ -75,6 +82,10 @@ class MiniDNS(object): entry['address'] = vals[0] entry['name'] = vals[1] entry['type'] = vals[2] + if entry['address'] == 'domain': + entry['domain'] = entry['name'] + else: + entry['domain'] = entry['name'].partition('.')[2] return entry def delete_entry(self, name, dnszone=""): @@ -138,3 +149,30 @@ class MiniDNS(object): def delete_dns_file(self): os.remove(self.filename) + + def create_domain(self, fqdomain): + if self.get_entries_by_name(fqdomain, ''): + raise exception.FloatingIpDNSExists(name=fqdomain, zone='') + + outfile = open(self.filename, 'a+') + outfile.write("%s %s %s\n" % + ('domain', fqdomain, 'domain')) + outfile.close() + + def delete_domain(self, fqdomain): + deleted = False + infile = open(self.filename, 'r') + outfile = tempfile.NamedTemporaryFile('w', delete=False) + for line in infile: + entry = self.parse_line(line) + if ((not entry) or + entry['domain'] != fqdomain): + outfile.write(line) + else: + print "deleted %s" % entry + deleted = True + infile.close() + outfile.close() + shutil.move(outfile.name, self.filename) + if not deleted: + raise exception.NotFound diff --git a/nova/tests/api/openstack/compute/contrib/test_floating_ip_dns.py b/nova/tests/api/openstack/compute/contrib/test_floating_ip_dns.py index 9bc6aa6c585a..9e15f0106ddf 100644 --- a/nova/tests/api/openstack/compute/contrib/test_floating_ip_dns.py +++ b/nova/tests/api/openstack/compute/contrib/test_floating_ip_dns.py @@ -15,6 +15,7 @@ from lxml import etree import urllib +import webob from nova.api.openstack.compute.contrib import floating_ip_dns from nova import context @@ -51,7 +52,11 @@ def network_api_get_floating_ip(self, context, id): def network_get_dns_zones(self, context): - return ['foo', 'bar', 'baz', 'quux'] + return [{'domain': 'example.org', 'scope': 'public'}, + {'domain': 'example.com', 'scope': 'public', + 'project': 'project1'}, + {'domain': 'private.example.com', 'scope': 'private', + 'availability_zone': 'avzone'}] def network_get_dns_entries_by_address(self, context, address, zone): @@ -59,7 +64,7 @@ def network_get_dns_entries_by_address(self, context, address, zone): def network_get_dns_entries_by_name(self, context, address, zone): - return [testaddress, testaddress2] + return [testaddress] def network_add_dns_entry(self, context, address, name, dns_type, zone): @@ -104,7 +109,9 @@ class FloatingIpDNSTest(test.TestCase): self.context = context.get_admin_context() self._create_floating_ip() - self.dns_controller = floating_ip_dns.FloatingIPDNSController() + temp = floating_ip_dns.FloatingIPDNSDomainController() + self.domain_controller = temp + self.entry_controller = floating_ip_dns.FloatingIPDNSEntryController() def tearDown(self): self._delete_floating_ip() @@ -112,21 +119,26 @@ class FloatingIpDNSTest(test.TestCase): 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'] + res_dict = self.domain_controller.index(req) + entries = res_dict['zone_entries'] 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") + self.assertEqual(entries[0]['domain'], "example.org") + self.assertFalse(entries[0]['project']) + self.assertFalse(entries[0]['availability_zone']) + self.assertEqual(entries[1]['domain'], "example.com") + self.assertEqual(entries[1]['project'], "project1") + self.assertFalse(entries[1]['availability_zone']) + self.assertEqual(entries[2]['domain'], "private.example.com") + self.assertFalse(entries[2]['project']) + self.assertEqual(entries[2]['availability_zone'], "avzone") 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)) + req = fakes.HTTPRequest.blank('/v2/123/os-floating-ip-dns/%s/entries%s' + % (_quote_zone(zone), params)) + entries = self.entry_controller.index(req, _quote_zone(zone)) self.assertEqual(len(entries['dns_entries']), 2) self.assertEqual(entries['dns_entries'][0]['name'], @@ -137,96 +149,141 @@ class FloatingIpDNSTest(test.TestCase): 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/entries/%s' % + (_quote_zone(zone), name)) + entry = self.entry_controller.show(req, _quote_zone(zone), name) - 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'], + self.assertEqual(entry['dns_entry']['ip'], testaddress) - self.assertEqual(entries['dns_entries'][1]['ip'], - testaddress2) - self.assertEqual(entries['dns_entries'][0]['zone'], + self.assertEqual(entry['dns_entry']['zone'], zone) - def test_create(self): + def test_create_entry(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) - + {'ip': testaddress, + 'dns_type': 'A'}} + req = fakes.HTTPRequest.blank( + '/v2/123/os-floating-ip-dns/%s/entries/%s' % + (_quote_zone(zone), name)) + entry = self.entry_controller.update(req, _quote_zone(zone), + name, body) self.assertEqual(entry['dns_entry']['ip'], testaddress) - def test_delete(self): + def test_create_domain(self): + req = fakes.HTTPRequest.blank('/v2/123/os-floating-ip-dns/%s' % + _quote_zone(zone)) + body = {'zone_entry': + {'scope': 'private', + 'project': 'testproject'}} + self.assertRaises(webob.exc.HTTPUnprocessableEntity, + self.domain_controller.update, + req, _quote_zone(zone), body) + + body = {'zone_entry': + {'scope': 'public', + 'availability_zone': 'zone1'}} + self.assertRaises(webob.exc.HTTPUnprocessableEntity, + self.domain_controller.update, + req, _quote_zone(zone), body) + + body = {'zone_entry': + {'scope': 'public', + 'project': 'testproject'}} + entry = self.domain_controller.update(req, _quote_zone(zone), body) + self.assertEqual(entry['zone_entry']['domain'], zone) + self.assertEqual(entry['zone_entry']['scope'], 'public') + self.assertEqual(entry['zone_entry']['project'], 'testproject') + + body = {'zone_entry': + {'scope': 'private', + 'availability_zone': 'zone1'}} + entry = self.domain_controller.update(req, _quote_zone(zone), body) + self.assertEqual(entry['zone_entry']['domain'], zone) + self.assertEqual(entry['zone_entry']['scope'], 'private') + self.assertEqual(entry['zone_entry']['availability_zone'], 'zone1') + + def test_delete_entry(self): self.called = False - self.deleted_zone = "" + self.deleted_domain = "" self.deleted_name = "" - def network_delete_dns_entry(fakeself, context, req, id): + def network_delete_dns_entry(fakeself, context, name, domain): self.called = True - self.deleted_zone = id + self.deleted_domain = domain + self.deleted_name = name 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)) + req = fakes.HTTPRequest.blank( + '/v2/123/os-floating-ip-dns/%s/entries/%s' % + (_quote_zone(zone), name)) + entries = self.entry_controller.delete(req, _quote_zone(zone), name) self.assertTrue(self.called) - self.assertEquals(self.deleted_zone, zone) + self.assertEquals(self.deleted_domain, zone) + self.assertEquals(self.deleted_name, name) + + def test_delete_domain(self): + self.called = False + self.deleted_domain = "" + self.deleted_name = "" + + def network_delete_dns_domain(fakeself, context, fqdomain): + self.called = True + self.deleted_domain = fqdomain + + self.stubs.Set(network.api.API, "delete_dns_domain", + network_delete_dns_domain) + + req = fakes.HTTPRequest.blank('/v2/123/os-floating-ip-dns/%s' % + _quote_zone(zone)) + entries = self.domain_controller.delete(req, _quote_zone(zone)) + + self.assertTrue(self.called) + self.assertEquals(self.deleted_domain, zone) def test_modify(self): body = {'dns_entry': - {'name': name, - 'ip': testaddress2}} - req = fakes.HTTPRequest.blank('/v2/123/os-floating-ip-dns/%s' % - zone) - entry = self.dns_controller.update(req, zone, body) + {'ip': testaddress2, + 'dns_type': 'A'}} + req = fakes.HTTPRequest.blank( + '/v2/123/os-floating-ip-dns/%s/entries/%s' % (zone, name)) + entry = self.entry_controller.update(req, zone, name, body) self.assertEqual(entry['dns_entry']['ip'], testaddress2) class FloatingIpDNSSerializerTest(test.TestCase): - def test_default_serializer(self): - serializer = floating_ip_dns.FloatingIPDNSTemplate() - 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): + def test_zones_serializer(self): serializer = floating_ip_dns.ZonesTemplate() text = serializer.serialize(dict( - zones=[ - dict(zone=zone), - dict(zone=zone2)])) + zone_entries=[ + dict(zone=zone, scope='public', project='testproject'), + dict(zone=zone2, scope='private', + availability_zone='avzone')])) tree = etree.fromstring(text) - self.assertEqual('zones', tree.tag) + self.assertEqual('zone_entries', tree.tag) self.assertEqual(2, len(tree)) self.assertEqual(zone, tree[0].get('zone')) self.assertEqual(zone2, tree[1].get('zone')) + self.assertEqual('avzone', tree[1].get('availability_zone')) - def test_show_serializer(self): + def test_zone_serializer(self): + serializer = floating_ip_dns.ZoneTemplate() + text = serializer.serialize(dict( + zone_entry=dict(zone=zone, + scope='public', + project='testproject'))) + + tree = etree.fromstring(text) + self.assertEqual('zone_entry', tree.tag) + self.assertEqual(zone, tree.get('zone')) + self.assertEqual('testproject', tree.get('project')) + + def test_entries_serializer(self): serializer = floating_ip_dns.FloatingIPDNSsTemplate() text = serializer.serialize(dict( dns_entries=[ @@ -252,3 +309,19 @@ class FloatingIpDNSSerializerTest(test.TestCase): self.assertEqual('C', tree[1].get('type')) self.assertEqual(zone, tree[1].get('zone')) self.assertEqual(name2, tree[1].get('name')) + + def test_entry_serializer(self): + serializer = floating_ip_dns.FloatingIPDNSTemplate() + 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')) diff --git a/nova/tests/policy.json b/nova/tests/policy.json index a12a84ab1c51..6048425d8533 100644 --- a/nova/tests/policy.json +++ b/nova/tests/policy.json @@ -4,7 +4,7 @@ "compute:create:attach_volume": [], "compute:get": [], - "compute:get_all" :[], + "compute:get_all": [], "compute:update": [], @@ -122,5 +122,8 @@ "network:modify_dns_entry": [], "network:delete_dns_entry": [], "network:get_dns_entries_by_address": [], - "network:get_dns_entries_by_name": [] + "network:get_dns_entries_by_name": [], + "network:create_private_dns_domain": [], + "network:create_public_dns_domain": [], + "network:delete_dns_domain": [] } diff --git a/nova/tests/test_db_api.py b/nova/tests/test_db_api.py index 29e1a0520bcc..1afb5cd3c19f 100644 --- a/nova/tests/test_db_api.py +++ b/nova/tests/test_db_api.py @@ -521,3 +521,27 @@ class AggregateDBApiTestCase(test.TestCase): self.assertRaises(exception.AggregateHostNotFound, db.aggregate_host_delete, ctxt, result.id, _get_fake_aggr_hosts()[0]) + + def test_dns_registration(self): + domain1 = 'test.domain.one' + domain2 = 'test.domain.two' + testzone = 'testzone' + ctxt = context.get_admin_context() + + db.dnsdomain_register_for_zone(ctxt, domain1, testzone) + domain_ref = db.dnsdomain_get(ctxt, domain1) + zone = domain_ref.availability_zone + scope = domain_ref.scope + self.assertEqual(scope, 'private') + self.assertEqual(zone, testzone) + + db.dnsdomain_register_for_project(ctxt, domain2, + self.project_id) + domain_ref = db.dnsdomain_get(ctxt, domain2) + project = domain_ref.project_id + scope = domain_ref.scope + self.assertEqual(project, self.project_id) + self.assertEqual(scope, 'public') + + db.dnsdomain_unregister(ctxt, domain1) + db.dnsdomain_unregister(ctxt, domain2) diff --git a/nova/tests/test_network.py b/nova/tests/test_network.py index 8b230376bf9c..7b8bd3aa3f01 100644 --- a/nova/tests/test_network.py +++ b/nova/tests/test_network.py @@ -277,6 +277,8 @@ class FlatNetworkTestCase(test.TestCase): db.instance_get(self.context, 1).AndReturn({'display_name': HOST, 'uuid': 'test-00001'}) + db.instance_get(mox.IgnoreArg(), + mox.IgnoreArg()).AndReturn({'availability_zone': ''}) db.fixed_ip_associate_pool(mox.IgnoreArg(), mox.IgnoreArg(), mox.IgnoreArg()).AndReturn('192.168.0.101') @@ -343,6 +345,8 @@ class FlatNetworkTestCase(test.TestCase): db.instance_get(self.context, 1).AndReturn({'display_name': HOST, 'uuid': 'test-00001'}) + db.instance_get(mox.IgnoreArg(), + mox.IgnoreArg()).AndReturn({'availability_zone': ''}) db.fixed_ip_associate_pool(mox.IgnoreArg(), mox.IgnoreArg(), mox.IgnoreArg()).AndReturn(fixedip) @@ -750,7 +754,8 @@ class VlanNetworkTestCase(test.TestCase): db.instance_get(mox.IgnoreArg(), mox.IgnoreArg()).AndReturn({'security_groups': - [{'id': 0}]}) + [{'id': 0}], + 'availability_zone': ''}) db.fixed_ip_associate_pool(mox.IgnoreArg(), mox.IgnoreArg(), mox.IgnoreArg()).AndReturn('192.168.0.101') @@ -1264,16 +1269,6 @@ class FloatingIPTestCase(test.TestCase): 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" @@ -1327,6 +1322,49 @@ class FloatingIPTestCase(test.TestCase): self.network.delete_dns_entry, self.context, name1, zone) + def test_floating_dns_domains_public(self): + zone1 = "testzone" + domain1 = "example.org" + domain2 = "example.com" + address1 = '10.10.10.10' + entryname = 'testentry' + + context_admin = context.RequestContext('testuser', 'testproject', + is_admin=True) + + self.assertRaises(exception.AdminRequired, + self.network.create_public_dns_domain, self.context, + domain1, zone1) + self.network.create_public_dns_domain(context_admin, domain1, + 'testproject') + self.network.create_public_dns_domain(context_admin, domain2, + 'fakeproject') + + domains = self.network.get_dns_zones(self.context) + self.assertEquals(len(domains), 2) + self.assertEquals(domains[0]['domain'], domain1) + self.assertEquals(domains[1]['domain'], domain2) + self.assertEquals(domains[0]['project'], 'testproject') + self.assertEquals(domains[1]['project'], 'fakeproject') + + self.network.add_dns_entry(self.context, address1, entryname, + 'A', domain1) + entries = self.network.get_dns_entries_by_name(self.context, + entryname, domain1) + self.assertEquals(len(entries), 1) + self.assertEquals(entries[0], address1) + + self.assertRaises(exception.AdminRequired, + self.network.delete_dns_domain, self.context, + domain1) + self.network.delete_dns_domain(context_admin, domain1) + self.network.delete_dns_domain(context_admin, domain2) + + # Verify that deleting the domain deleted the associated entry + entries = self.network.get_dns_entries_by_name(self.context, + entryname, domain1) + self.assertFalse(entries) + class NetworkPolicyTestCase(test.TestCase): def setUp(self): @@ -1355,3 +1393,44 @@ class NetworkPolicyTestCase(test.TestCase): network_manager.check_policy(self.context, 'get_all') self.mox.UnsetStubs() self.mox.VerifyAll() + + +class InstanceDNSTestCase(test.TestCase): + """Tests nova.network.manager instance DNS""" + def setUp(self): + super(InstanceDNSTestCase, self).setUp() + self.network = TestFloatingIPManager() + temp = utils.import_object('nova.network.minidns.MiniDNS') + self.network.instance_dns_manager = temp + temp = utils.import_object('nova.network.dns_driver.DNSDriver') + 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(InstanceDNSTestCase, self).tearDown() + self.network.instance_dns_manager.delete_dns_file() + + def test_dns_domains_private(self): + zone1 = 'testzone' + domain1 = 'example.org' + + context_admin = context.RequestContext('testuser', 'testproject', + is_admin=True) + + self.assertRaises(exception.AdminRequired, + self.network.create_private_dns_domain, self.context, + domain1, zone1) + + self.network.create_private_dns_domain(context_admin, domain1, zone1) + domains = self.network.get_dns_zones(self.context) + self.assertEquals(len(domains), 1) + self.assertEquals(domains[0]['domain'], domain1) + self.assertEquals(domains[0]['availability_zone'], zone1) + + self.assertRaises(exception.AdminRequired, + self.network.delete_dns_domain, self.context, + domain1) + self.network.delete_dns_domain(context_admin, domain1)