FloatingIP PTR record functionality

* Central methods update_floatingip, get_floatingip, list_floatingip
* API according to
* https://wiki.openstack.org/wiki/Designate/Blueprints/Reverse
* Put network functionanlity into a pluggable api class (In case we want
* more)

blueprint floating-ip-ptrs

Change-Id: Ic9194e5bcfc8d25126b9e84652144f58cddcccfe
This commit is contained in:
Endre Karlson 2014-01-14 12:38:20 +01:00
parent f78c67d07f
commit 6fd81240b7
25 changed files with 1662 additions and 2 deletions

View File

@ -0,0 +1,90 @@
# Copyright 2013 Hewlett-Packard Development Company, L.P.
#
# Author: Endre Karlson <endre.karlson@hp.com>
#
# 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 pecan
import re
from designate import exceptions
from designate import schema
from designate.api.v2.controllers import rest
from designate.api.v2.views import floatingips as floatingips_views
from designate.central import rpcapi as central_rpcapi
central_api = central_rpcapi.CentralAPI()
FIP_REGEX = '^(?P<region>[A-Za-z0-9\\.\\-_]{1,100}):' \
'(?P<id>[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-' \
'[0-9a-fA-F]{4}-[0-9a-fA-F]{12})$'
def fip_key_to_data(key):
m = re.match(FIP_REGEX, key)
# NOTE: Ensure that the fip matches region:floatingip_id or raise, if
# not this will cause a 500.
if m is None:
msg = 'Floating IP %s is not in the format of <region>:<uuid>'
raise exceptions.BadRequest(msg % key)
return m.groups()
class FloatingIPController(rest.RestController):
_view = floatingips_views.FloatingIPView()
_resource_schema = schema.Schema('v2', 'floatingip')
_collection_schema = schema.Schema('v2', 'floatingips')
@pecan.expose(template='json:', content_type='application/json')
def get_all(self, **params):
""" List Floating IP PTRs for a Tenant """
request = pecan.request
context = request.environ['context']
fips = central_api.list_floatingips(context)
return self._view.list(context, request, fips)
@pecan.expose(template='json:', content_type='application/json')
def patch_one(self, fip_key):
"""
Set or unset a PTR
"""
request = pecan.request
context = request.environ['context']
body = request.body_dict
region, id_ = fip_key_to_data(fip_key)
# Validate the request conforms to the schema
self._resource_schema.validate(body)
fip = central_api.update_floatingip(
context, region, id_, body['floatingip'])
if fip:
return self._view.basic(context, request, fip)
@pecan.expose(template='json:', content_type='application/json')
def get_one(self, fip_key):
"""
Get PTR
"""
request = pecan.request
context = request.environ['context']
region, id_ = fip_key_to_data(fip_key)
fip = central_api.get_floatingip(context, region, id_)
return self._view.basic(context, request, fip)

View File

@ -0,0 +1,21 @@
# Copyright 2013 Hewlett-Packard Development Company, L.P.
#
# Author: Endre Karlson <endre.karlson@hp.com>
#
# 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 designate.api.v2.controllers import rest
from designate.api.v2.controllers import floatingips
class ReverseController(rest.RestController):
floatingips = floatingips.FloatingIPController()

View File

@ -15,6 +15,7 @@
# under the License. # under the License.
from designate.openstack.common import log as logging from designate.openstack.common import log as logging
from designate.api.v2.controllers import limits from designate.api.v2.controllers import limits
from designate.api.v2.controllers import reverse
from designate.api.v2.controllers import schemas from designate.api.v2.controllers import schemas
from designate.api.v2.controllers import zones from designate.api.v2.controllers import zones
@ -28,4 +29,5 @@ class RootController(object):
""" """
limits = limits.LimitsController() limits = limits.LimitsController()
schemas = schemas.SchemasController() schemas = schemas.SchemasController()
reverse = reverse.ReverseController()
zones = zones.ZonesController() zones = zones.ZonesController()

View File

@ -0,0 +1,36 @@
# Copyright 2013 Hewlett-Packard Development Company, L.P.
#
# Author: Endre Karlson <endre.karlson@hp.com>
#
# 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 designate.api.v2.views import base as base_view
from designate.openstack.common import log as logging
LOG = logging.getLogger(__name__)
class FloatingIPView(base_view.BaseView):
""" Model a FloatingIP PTR record as a python dict """
_resource_name = 'floatingip'
_collection_name = 'floatingips'
def _get_base_href(self, parents=None):
return '%s/reverse/floatingips' % self.base_uri
def basic(self, context, request, data):
data['id'] = ":".join([data.pop('region'), data.pop('id')])
data['links'] = self._get_resource_links(
request, data, [data['id']])
return {
'floatingip': data}

View File

@ -41,4 +41,8 @@ cfg.CONF.register_opts([
cfg.IntOpt('max_recordset_name_len', default=255, cfg.IntOpt('max_recordset_name_len', default=255,
help="Maximum recordset name length", help="Maximum recordset name length",
deprecated_name='max_record_name_len'), deprecated_name='max_record_name_len'),
cfg.StrOpt('managed_resource_email', default='email@example.io',
help='E-Mail for Managed resources'),
cfg.StrOpt('managed_resource_tenant_id',
help="The Tenant ID that will own any managed resources.")
], group='service:central') ], group='service:central')

View File

@ -33,7 +33,7 @@ class CentralAPI(rpc_proxy.RpcProxy):
2.0 - Renamed most get_resources to find_resources 2.0 - Renamed most get_resources to find_resources
2.1 - Add quota methods 2.1 - Add quota methods
3.0 - RecordSet Changes 3.0 - RecordSet Changes
3.1 - Add floating ip ptr methods
""" """
def __init__(self, topic=None): def __init__(self, topic=None):
topic = topic if topic else cfg.CONF.central_topic topic = topic if topic else cfg.CONF.central_topic
@ -353,3 +353,17 @@ class CentralAPI(rpc_proxy.RpcProxy):
record_id=record_id) record_id=record_id)
return self.call(context, msg) return self.call(context, msg)
def list_floatingips(self, context):
msg = self.make_msg('list_floatingips')
return self.call(context, msg, version="3.1")
def get_floatingip(self, context, region, floatingip_id):
msg = self.make_msg('get_floatingip', region=region,
floatingip_id=floatingip_id)
return self.call(context, msg, version="3.1")
def update_floatingip(self, context, region, floatingip_id, values):
msg = self.make_msg('update_floatingip', region=region,
floatingip_id=floatingip_id, values=values)
return self.call(context, msg)

View File

@ -27,6 +27,7 @@ from designate import policy
from designate import quota from designate import quota
from designate import utils from designate import utils
from designate.storage import api as storage_api from designate.storage import api as storage_api
from designate import network_api
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
@ -45,7 +46,7 @@ def wrap_backend_call():
class Service(rpc_service.Service): class Service(rpc_service.Service):
RPC_API_VERSION = '3.0' RPC_API_VERSION = '3.1'
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
backend_driver = cfg.CONF['service:central'].backend_driver backend_driver = cfg.CONF['service:central'].backend_driver
@ -69,6 +70,8 @@ class Service(rpc_service.Service):
self.quota = quota.get_quota() self.quota = quota.get_quota()
self.effective_tld = effectivetld.EffectiveTld() self.effective_tld = effectivetld.EffectiveTld()
self.network_api = network_api.get_api(cfg.CONF.network_api)
def start(self): def start(self):
self.backend.start() self.backend.start()
@ -964,3 +967,295 @@ class Service(rpc_service.Service):
'backend': backend_status, 'backend': backend_status,
'storage': storage_status 'storage': storage_status
} }
def _determine_floatingips(self, context, fips, records=None,
tenant_id=None):
"""
Given the context or tenant, records and fips it returns the valid
floatingips either with a associated record or not. Deletes invalid
records also.
Returns a list of tuples with FloatingIPs and it's Record.
"""
tenant_id = tenant_id or context.tenant_id
elevated_context = context.elevated()
elevated_context.all_tenants = True
criterion = {
'managed': True,
'managed_resource_type': 'ptr:floatingip',
}
records = self.find_records(elevated_context, criterion)
records = dict([(r['managed_extra'], r) for r in records])
invalid = []
data = {}
# First populate the list of FIPS
for fip_key, fip_values in fips.items():
# Check if the FIP has a record
record = records.get(fip_values['address'])
# NOTE: Now check if it's owned by the tenant that actually has the
# FIP in the external service and if not invalidate it (delete it)
# thus not returning it with in the tuple with the FIP, but None..
if record:
record_tenant = record['managed_tenant_id']
if record_tenant != tenant_id:
msg = "Invalid FloatingIP %s belongs to %s but record " \
"owner %s"
LOG.debug(msg, fip_key, tenant_id, record_tenant)
invalid.append(record)
record = None
data[fip_key] = (fip_values, record)
return data, invalid
def _invalidate_floatingips(self, context, records):
"""
Utility method to delete a list of records.
"""
elevated_context = context.elevated()
elevated_context.all_tenants = True
if records > 0:
for r in records:
msg = 'Deleting record %s for FIP %s'
LOG.debug(msg, r['id'], r['managed_resource_id'])
self.delete_record(elevated_context, r['domain_id'],
r['recordset_id'], r['id'])
def _format_floatingips(self, context, data, recordsets=None):
"""
Given a list of FloatingIP and Record tuples we look through creating
a new dict of FloatingIPs
"""
elevated_context = context.elevated()
elevated_context.all_tenants = True
fips = {}
for key, value in data.items():
fip_ptr = {
'address': value[0]['address'],
'id': value[0]['id'],
'region': value[0]['region'],
'ptrdname': None,
'ttl': None,
'description': None
}
# TTL population requires a present record in order to find the
# RS or Zone
if value[1]:
# We can have a recordset dict passed in
if (recordsets is not None and
value[1]['recordset_id'] in recordsets):
recordset = recordsets[value[1]['recordset_id']]
else:
recordset = self.storage_api.get_recordset(
elevated_context, value[1]['recordset_id'])
if recordset['ttl'] is not None:
fip_ptr['ttl'] = recordset['ttl']
else:
zone = self.get_domain(
elevated_context, value[1]['domain_id'])
fip_ptr['ttl'] = zone['ttl']
fip_ptr['ptrdname'] = value[1]['data']
else:
LOG.debug("No record information found for %s",
value[0]['id'])
# Store the "fip_record" with the region and it's id as key
fips[key] = fip_ptr
return fips
def _list_floatingips(self, context, region=None):
data = self.network_api.list_floatingips(context, region=region)
return self._list_to_dict(data, keys=['region', 'id'])
def _list_to_dict(self, data, keys=['id']):
new = {}
for i in data:
key = tuple([i[key] for key in keys])
new[key] = i
return new
def _get_floatingip(self, context, region, floatingip_id, fips):
if (region, floatingip_id) not in fips:
msg = 'FloatingIP %s in %s is not associated for tenant "%s"' % \
(floatingip_id, region, context.tenant_id)
raise exceptions.NotFound(msg)
return fips[region, floatingip_id]
# PTR ops
def list_floatingips(self, context):
"""
List Floating IPs PTR
A) We have service_catalog in the context and do a lookup using the
token pr Neutron in the SC
B) We lookup FIPs using the configured values for this deployment.
"""
elevated_context = context.elevated()
elevated_context.all_tenants = True
tenant_fips = self._list_floatingips(context)
valid, invalid = self._determine_floatingips(
elevated_context, tenant_fips)
self._invalidate_floatingips(context, invalid)
return self._format_floatingips(context, valid).values()
def get_floatingip(self, context, region, floatingip_id):
"""
Get Floating IP PTR
"""
elevated_context = context.elevated()
elevated_context.all_tenants = True
tenant_fips = self._list_floatingips(context, region=region)
self._get_floatingip(context, region, floatingip_id, tenant_fips)
valid, invalid = self._determine_floatingips(
elevated_context, tenant_fips)
self._invalidate_floatingips(context, invalid)
mangled = self._format_floatingips(context, valid)
return mangled[region, floatingip_id]
def _set_floatingip_reverse(self, context, region, floatingip_id, values):
"""
Set the FloatingIP's PTR record based on values.
"""
values.setdefault('description', None)
elevated_context = context.elevated()
elevated_context.all_tenants = True
tenant_fips = self._list_floatingips(context, region=region)
fip = self._get_floatingip(context, region, floatingip_id, tenant_fips)
zone_name = self.network_api.address_zone(fip['address'])
# NOTE: Find existing zone or create it..
try:
zone = self.storage_api.find_domain(
elevated_context, {'name': zone_name})
except exceptions.DomainNotFound:
msg = 'Creating zone for %s:%s - %s zone %s' % \
(floatingip_id, region, fip['address'], zone_name)
LOG.info(msg)
email = cfg.CONF['service:central'].managed_resource_email
tenant_id = cfg.CONF['service:central'].managed_resource_tenant_id
zone_values = {
'name': zone_name,
'email': email,
'tenant_id': tenant_id
}
zone = self.create_domain(elevated_context, zone_values)
record_name = self.network_api.address_name(fip['address'])
try:
# NOTE: Delete the current recormdset if any (also purges records)
LOG.debug("Removing old RRset / Record")
rset = self.find_recordset(
elevated_context, {'name': record_name, 'type': 'PTR'})
records = self.find_records(
elevated_context, {'recordset_id': rset['id']})
for record in records:
self.delete_record(
elevated_context,
rset['domain_id'],
rset['id'],
record['id'])
self.delete_recordset(elevated_context, zone['id'], rset['id'])
except exceptions.RecordSetNotFound:
pass
recordset_values = {
'name': record_name,
'type': 'PTR',
'ttl': values.get('ttl', None)
}
recordset = self.create_recordset(
elevated_context, zone['id'], recordset_values)
record_values = {
'data': values['ptrdname'],
'description': values['description'],
'type': 'PTR',
'managed': True,
'managed_extra': fip['address'],
'managed_resource_id': floatingip_id,
'managed_resource_region': region,
'managed_resource_type': 'ptr:floatingip',
'managed_tenant_id': context.tenant_id
}
record = self.create_record(
elevated_context, zone['id'], recordset['id'], record_values)
mangled = self._format_floatingips(
context, {(region, floatingip_id): (fip, record)},
{recordset['id']: recordset})
return mangled[region, floatingip_id]
def _unset_floatingip_reverse(self, context, region, floatingip_id):
"""
Unset the FloatingIP PTR record based on the
Service's FloatingIP ID > managed_resource_id
Tenant ID > managed_tenant_id
We find the record based on the criteria and delete it or raise.
"""
elevated_context = context.elevated()
elevated_context.all_tenants = True
criterion = {
'managed_resource_id': floatingip_id,
'managed_tenant_id': context.tenant_id
}
try:
record = self.storage_api.find_record(
elevated_context, criterion=criterion)
except exceptions.RecordNotFound:
msg = 'No such FloatingIP %s:%s' % (region, floatingip_id)
raise exceptions.NotFound(msg)
self.delete_record(
elevated_context,
record['domain_id'],
record['recordset_id'],
record['id'])
def update_floatingip(self, context, region, floatingip_id, values):
"""
We strictly see if values['ptrdname'] is str or None and set / unset
the requested FloatingIP's PTR record based on that.
"""
if values['ptrdname'] is None:
self._unset_floatingip_reverse(context, region, floatingip_id)
elif isinstance(values['ptrdname'], basestring):
return self._set_floatingip_reverse(
context, region, floatingip_id, values)

View File

@ -46,6 +46,18 @@ class ConfigurationError(Base):
error_type = 'configuration_error' error_type = 'configuration_error'
class CommunicationFailure(Base):
error_code = 504
error_type = 'communication_failure'
class NeutronCommunicationFailure(CommunicationFailure):
"""
Raised in case one of the alledged Neutron endpoints fails.
"""
error_type = 'neutron_communication_failure'
class NoServersConfigured(ConfigurationError): class NoServersConfigured(ConfigurationError):
error_code = 500 error_code = 500
error_type = 'no_servers_configured' error_type = 'no_servers_configured'
@ -70,6 +82,11 @@ class BadRequest(Base):
error_type = 'bad_request' error_type = 'bad_request'
class NetworkEndpointNotFound(BadRequest):
error_type = 'no_endpoint'
error_code = 403
class InvalidOperation(BadRequest): class InvalidOperation(BadRequest):
error_code = 400 error_code = 400
error_type = 'invalid_operation' error_type = 'invalid_operation'

View File

@ -0,0 +1,115 @@
# Copyright 2013 Hewlett-Packard Development Company, L.P.
#
# Author: Endre Karlson <endre.karlson@hp.com>
#
# 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 dns import reversename
from oslo.config import cfg
from stevedore import driver
from designate import exceptions
from designate.openstack.common import log as logging
cfg.CONF.register_opts([
cfg.StrOpt('network_api', default='neutron', help='Which API to use.')
])
LOG = logging.getLogger()
def get_api(api):
mngr = driver.DriverManager('designate.network_api', api)
return mngr.driver()
class BaseAPI(object):
"""
Base API
"""
def _endpoints(self, service_catalog=None, service_type=None,
endpoint_type='publicURL', config_section=None,
region=None):
if service_catalog is not None:
endpoints = self._endpoints_from_catalog(
service_catalog, service_type, endpoint_type,
region=region)
elif config_section is not None:
endpoints = []
for u in cfg.CONF[config_section].endpoints:
e_region, e = u.split('|')
# Filter if region is given
if (e_region and region) and e_region != region:
continue
endpoints.append((e, e_region))
if not endpoints:
msg = 'Endpoints are not configured'
raise exceptions.ConfigurationError(msg)
else:
msg = 'No service_catalog and no configured endpoints'
raise exceptions.ConfigurationError(msg)
LOG.debug('Returning endpoints: %s' % endpoints)
return endpoints
def _endpoints_from_catalog(self, service_catalog, service_type,
endpoint_type, region=None):
"""
Return the endpoints for the given service from the context's sc
or lookup towards the configured keystone.
return [('http://endpoint', 'region')]
"""
urls = []
for svc in service_catalog:
if svc['type'] != service_type:
continue
for url in svc['endpoints']:
if endpoint_type in url:
if region is not None and url['region'] != region:
continue
urls.append((url[endpoint_type], url['region']))
if not urls:
raise exceptions.NetworkEndpointNotFound
return urls
def list_floatingips(self, context, region=None):
"""
List Floating IPs.
Should return something like:
[{
'address': '<ip address'>,
'region': '<region where this belongs>',
'id': '<id of the FIP>'
}]
"""
raise NotImplementedError
@staticmethod
def address_zone(address):
"""
Get the zone a address belongs to.
"""
parts = reversed(address.split('.')[:-1])
return '%s.in-addr.arpa.' % ".".join(parts)
@staticmethod
def address_name(address):
"""
Get the name for the address
"""
return reversename.from_address(address).to_text()

View File

@ -0,0 +1,80 @@
# Copyright 2013 Hewlett-Packard Development Company, L.P.
#
# Author: Endre Karlson <endre.karlson@hp.com>
#
# 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 uuid
from designate.openstack.common import log
from designate.network_api import BaseAPI
LOG = log.getLogger(__name__)
POOL = dict([(str(uuid.uuid4()), '192.168.2.%s' % i) for i in xrange(0, 254)])
ALLOCATIONS = {}
def _format_floatingip(id_, address):
return {
'region': 'RegionOne',
'address': address,
'id': id_
}
def allocate_floatingip(tenant_id, floatingip_id=None):
"""
Allocates a floating ip from the pool to the tenant.
"""
ALLOCATIONS.setdefault(tenant_id, {})
id_ = floatingip_id or POOL.keys()[0]
ALLOCATIONS[tenant_id][id_] = POOL.pop(id_)
values = _format_floatingip(id_, ALLOCATIONS[tenant_id][id_])
LOG.debug("Allocated to id_ %s to %s - %s", id_, tenant_id, values)
return values
def deallocate_floatingip(id_):
"""
Deallocate a floatingip
"""
LOG.debug('De-allocating %s' % id_)
for tenant_id, allocated in ALLOCATIONS.items():
if id_ in allocated:
POOL[id_] = allocated.pop(id_)
break
else:
raise KeyError('No such FloatingIP %s' % id_)
def reset_floatingips():
LOG.debug('Resetting any allocations.')
for tenant_id, allocated in ALLOCATIONS.items():
for key, value in allocated.items():
POOL[key] = allocated.pop(key)
class API(BaseAPI):
def list_floatingips(self, context, region=None):
if context.is_admin:
data = []
for tenant_id, allocated in ALLOCATIONS.items():
data.extend(allocated.items())
else:
data = ALLOCATIONS.get(context.tenant_id, {}).items()
formatted = [_format_floatingip(k, v) for k, v in data]
LOG.debug('Returning %i FloatingIPs: %s', len(formatted), formatted)
return formatted

View File

@ -0,0 +1,148 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
#
# Copyright 2012 OpenStack 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.
#
# Copied partially from nova
from neutronclient.v2_0 import client as clientv20
from neutronclient.common import exceptions as neutron_exceptions
from oslo.config import cfg
from designate import exceptions
from designate.openstack.common import log as logging
from designate.openstack.common import threadgroup
from designate.network_api import BaseAPI
CONF = cfg.CONF
LOG = logging.getLogger(__name__)
neutron_opts = [
cfg.ListOpt('endpoints',
help='URL to use if None in the ServiceCatalog that is '
'passed by the requrest context. Format: <region>|<url>'),
cfg.StrOpt('endpoint_type', default='publicURL',
help="Endpoint type to use"),
cfg.IntOpt('timeout',
default=30,
help='timeout value for connecting to neutron in seconds'),
cfg.StrOpt('admin_username',
help='username for connecting to neutron in admin context'),
cfg.StrOpt('admin_password',
help='password for connecting to neutron in admin context',
secret=True),
cfg.StrOpt('admin_tenant_name',
help='tenant name for connecting to neutron in admin context'),
cfg.StrOpt('auth_url',
help='auth url for connecting to neutron in admin context'),
cfg.BoolOpt('insecure',
default=False,
help='if set, ignore any SSL validation issues'),
cfg.StrOpt('auth_strategy',
default='keystone',
help='auth strategy for connecting to '
'neutron in admin context'),
cfg.StrOpt('ca_certificates_file',
help='Location of ca certificates file to use for '
'neutron client requests.'),
]
cfg.CONF.register_opts(neutron_opts, group='network_api:neutron')
def get_client(context, endpoint):
params = {
'endpoint_url': endpoint,
'timeout': CONF['network_api:neutron'].timeout,
'insecure': CONF['network_api:neutron'].insecure,
'ca_cert': CONF['network_api:neutron'].ca_certificates_file,
}
if context.auth_token:
params['token'] = context.auth_token
params['auth_strategy'] = None
elif CONF['network_api:neutron'].admin_username is not None:
params['username'] = CONF['network_api:neutron'].admin_username
params['tenant_name'] = CONF['network_api:neutron'].admin_tenant_name
params['password'] = CONF['network_api:neutron'].admin_password
params['auth_url'] = CONF['network_api:neutron'].admin_auth_url
params['auth_strategy'] = CONF['network_api:neutron'].auth_strategy
return clientv20.Client(**params)
class API(BaseAPI):
"""
Interact with the Neutron API
"""
def list_floatingips(self, context, region=None):
"""
Get floating ips based on the current context from Neutron
"""
endpoints = self._endpoints(
service_catalog=context.service_catalog,
service_type='network',
endpoint_type=CONF['network_api:neutron'].endpoint_type,
config_section='network_api:neutron',
region=region)
tg = threadgroup.ThreadGroup()
failed = []
data = []
def _call(endpoint, region, *args, **kw):
client = get_client(context, endpoint=endpoint)
LOG.debug("Attempting to fetch FloatingIPs from %s @ %s",
endpoint, region)
try:
fips = client.list_floatingips(*args, **kw)
except neutron_exceptions.Unauthorized as e:
# NOTE: 401 might be that the user doesn't have neutron
# activated in a particular region, we'll just log the failure
# and go on with our lives.
msg = "Calling Neutron resulted in a 401, please investigate."
LOG.warning(msg)
LOG.exception(e)
return
except Exception as e:
LOG.error('Failed calling Neutron %s - %s', region, endpoint)
LOG.exception(e)
failed.append((e, endpoint, region))
return
for fip in fips['floatingips']:
data.append({
'id': fip['id'],
'address': fip['floating_ip_address'],
'region': region
})
LOG.debug("Added %i FloatingIPs from %s @ %s", len(data),
endpoint, region)
for endpoint, region in endpoints:
tg.add_thread(_call, endpoint, region,
tenant_id=context.tenant_id)
tg.wait()
# NOTE: Sadly tg code doesn't give us a good way to handle failures.
if failed:
msg = 'Failed retrieving FLoatingIPs from Neutron in %s' % \
", ".join(['%s - %s' % (i[1], i[2]) for i in failed])
raise exceptions.NeutronCommunicationFailure(msg)
return data

View File

@ -0,0 +1,54 @@
{
"$schema": "http://json-schema.org/draft-04/hyper-schema",
"id": "floatingip",
"title": "floatingip",
"description": "Floating IP PTR",
"additionalProperties": false,
"required": ["floatingip"],
"properties": {
"floatingip": {
"type": "object",
"additionalProperties": false,
"properties": {
"id": {
"type": "string",
"description": "Floating IP PTR identifier",
"pattern": "^[A-Za-z0-9\\.\\-_]{1,100}:([0-9a-fA-F]){8}-([0-9a-fA-F]){4}-([0-9a-fA-F]){4}-([0-9a-fA-F]){4}-([0-9a-fA-F]){12}$",
"readOnly": true
},
"ptrdname": {
"type": ["string", "null"],
"format": "hostname",
"required:": true
},
"description": {
"type": ["string", "null"],
"description": "Description for the PTR",
"maxLength": 160
},
"ttl": {
"type": "integer",
"description": "Default time to live",
"min": 0,
"max": 2147483647
},
"links": {
"type": "object",
"additionalProperties": false,
"properties": {
"self": {
"type": "string",
"format": "url"
}
}
}
}
}
}
}

View File

@ -0,0 +1,38 @@
{
"$schema": "http://json-schema.org/draft-04/hyper-schema",
"id": "floatingips",
"title": "floatingips",
"description": "Floating IP PTRs",
"additionalProperties": false,
"required": ["floatingips"],
"properties": {
"recordsets": {
"type": "array",
"description": "Floating IP",
"items": {"$ref": "floatingips#/properties/flaotingip"}
},
"links": {
"type": "object",
"additionalProperties": false,
"properties": {
"self": {
"type": "string",
"format": "url"
},
"next": {
"type": ["string", "null"],
"format": "url"
},
"previous": {
"type": ["string", "null"],
"format": "url"
}
}
}
}
}

View File

@ -0,0 +1,61 @@
# Copyright 2013 Hewlett-Packard Development Company, L.P.
#
# Author: Endre Karlson <endre.karlson@hp.com>
#
# 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.
#
# This is a placeholder for Havana backports.
# Do not use this number for new Icehouse work. New Icehouse work starts after
# all the placeholders.
#
# See https://blueprints.launchpad.net/nova/+spec/backportable-db-migrations
# http://lists.openstack.org/pipermail/openstack-dev/2013-March/006827.html
from designate.openstack.common import log as logging
from sqlalchemy import MetaData, Table, Column, Unicode
LOG = logging.getLogger(__name__)
meta = MetaData()
def upgrade(migrate_engine):
meta.bind = migrate_engine
records_table = Table('records', meta, autoload=True)
record_managed_tenant_id = Column(
'managed_tenant_id', Unicode(36), default=None, nullable=True)
record_managed_tenant_id.create(records_table, populate_default=True)
record_managed_resource_region = Column(
'managed_resource_region', Unicode(100), default=None, nullable=True)
record_managed_resource_region.create(records_table, populate_default=True)
record_managed_extra = Column(
'managed_extra', Unicode(100), default=None, nullable=True)
record_managed_extra.create(records_table, populate_default=True)
def downgrade(migrate_engine):
meta.bind = migrate_engine
records_table = Table('records', meta, autoload=True)
record_managed_tenant_id = Column(
'managed_tenant_id', Unicode(36), default=None, nullable=True)
record_managed_tenant_id.drop(records_table)
record_managed_resource_region = Column(
'managed_resource_region', Unicode(100), default=None, nullable=True)
record_managed_resource_region.drop(records_table)
record_extra = Column(
'managed_extra', Unicode(100), default=None, nullable=True)
record_extra.drop(records_table)

View File

@ -143,10 +143,13 @@ class Record(Base):
hash = Column(String(32), nullable=False, unique=True) hash = Column(String(32), nullable=False, unique=True)
managed = Column(Boolean, default=False) managed = Column(Boolean, default=False)
managed_extra = Column(Unicode(100), default=None, nullable=True)
managed_plugin_type = Column(Unicode(50), default=None, nullable=True) managed_plugin_type = Column(Unicode(50), default=None, nullable=True)
managed_plugin_name = Column(Unicode(50), default=None, nullable=True) managed_plugin_name = Column(Unicode(50), default=None, nullable=True)
managed_resource_type = Column(Unicode(50), default=None, nullable=True) managed_resource_type = Column(Unicode(50), default=None, nullable=True)
managed_resource_region = Column(Unicode(100), default=None, nullable=True)
managed_resource_id = Column(UUID, default=None, nullable=True) managed_resource_id = Column(UUID, default=None, nullable=True)
managed_tenant_id = Column(Unicode(36), default=None, nullable=True)
status = Column(Enum(name='resource_statuses', *RESOURCE_STATUSES), status = Column(Enum(name='resource_statuses', *RESOURCE_STATUSES),
nullable=False, server_default='ACTIVE', nullable=False, server_default='ACTIVE',
default='ACTIVE') default='ACTIVE')

View File

@ -34,6 +34,8 @@ from designate.openstack.common import uuidutils
from designate.context import DesignateContext from designate.context import DesignateContext
from designate.tests import resources from designate.tests import resources
from designate import exceptions from designate import exceptions
from designate.network_api import fake as fake_network_api
from designate import network_api
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
@ -117,6 +119,14 @@ class DatabaseFixture(fixtures.Fixture):
shutil.copyfile(self.golden_db, self.working_copy) shutil.copyfile(self.golden_db, self.working_copy)
class NetworkAPIFixture(fixtures.Fixture):
def setUp(self):
super(NetworkAPIFixture, self).setUp()
self.api = network_api.get_api(cfg.CONF.network_api)
self.fake = fake_network_api
self.addCleanup(self.fake.reset_floatingips)
class TestCase(test.BaseTestCase): class TestCase(test.BaseTestCase):
quota_fixtures = [{ quota_fixtures = [{
'resource': 'domains', 'resource': 'domains',
@ -184,6 +194,11 @@ class TestCase(test.BaseTestCase):
] ]
} }
ptr_fixtures = [
{'ptrdname': 'srv1.example.com.'},
{'ptrdname': 'srv1.example.net.'}
]
def setUp(self): def setUp(self):
super(TestCase, self).setUp() super(TestCase, self).setUp()
@ -226,6 +241,11 @@ class TestCase(test.BaseTestCase):
group='storage:sqlalchemy' group='storage:sqlalchemy'
) )
self.config(network_api='fake')
self.config(
managed_resource_tenant_id='managing_tenant',
group='service:central')
self.CONF([], project='designate') self.CONF([], project='designate')
self.notifications = NotifierFixture() self.notifications = NotifierFixture()
@ -233,6 +253,9 @@ class TestCase(test.BaseTestCase):
self.useFixture(PolicyFixture()) self.useFixture(PolicyFixture())
self.network_api = NetworkAPIFixture()
self.useFixture(self.network_api)
self.admin_context = self.get_admin_context() self.admin_context = self.get_admin_context()
# Config Methods # Config Methods
@ -316,6 +339,11 @@ class TestCase(test.BaseTestCase):
_values.update(values) _values.update(values)
return _values return _values
def get_ptr_fixture(self, fixture=0, values={}):
_values = copy.copy(self.ptr_fixtures[fixture])
_values.update(values)
return _values
def get_zonefile_fixture(self, variant=None): def get_zonefile_fixture(self, variant=None):
if variant is None: if variant is None:
f = 'example.com.zone' f = 'example.com.zone'

View File

@ -0,0 +1,250 @@
# Copyright 2013 Hewlett-Packard Development Company, L.P.
#
# Author: Endre Karlson <endre.karlson@managedit.ie>
#
# 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 designate.tests.test_api.test_v2 import ApiV2TestCase
"""
NOTE: Record invalidation is tested in Central tests
"""
class ApiV2ReverseFloatingIPTest(ApiV2TestCase):
def test_get_floatingip_no_record(self):
context = self.get_context(tenant='a')
fip = self.network_api.fake.allocate_floatingip(context.tenant)
response = self.client.get(
'/reverse/floatingips/%s' % ":".join([fip['region'], fip['id']]),
headers={'X-Test-Tenant-Id': context.tenant_id})
self.assertEqual(200, response.status_int)
self.assertEqual('application/json', response.content_type)
self.assertIn('floatingip', response.json)
#TODO(ekarlso): Remove the floatingip key - bug in v2 api
fip_record = response.json['floatingip']
self.assertEqual(":".join([fip['region'],
fip['id']]), fip_record['id'])
self.assertEqual(fip['address'], fip_record['address'])
self.assertEqual(None, fip_record['description'])
self.assertEqual(None, fip_record['ptrdname'])
def test_get_floatingip_with_record(self):
self.create_server()
fixture = self.get_ptr_fixture()
context = self.get_context(tenant='a')
fip = self.network_api.fake.allocate_floatingip(
context.tenant)
self.central_service.update_floatingip(
context, fip['region'], fip['id'], fixture)
response = self.client.get(
'/reverse/floatingips/%s' % ":".join([fip['region'], fip['id']]),
headers={'X-Test-Tenant-Id': context.tenant_id})
self.assertEqual(200, response.status_int)
self.assertEqual('application/json', response.content_type)
self.assertIn('floatingip', response.json)
# TODO(ekarlso): Remove the floatingip key - bug in v2 api
fip_record = response.json['floatingip']
self.assertEqual(":".join([fip['region'], fip['id']]),
fip_record['id'])
self.assertEqual(fip['address'], fip_record['address'])
self.assertEqual(None, fip_record['description'])
self.assertEqual(fixture['ptrdname'], fip_record['ptrdname'])
def test_get_floatingip_not_allocated(self):
url = '/reverse/floatingips/foo:04580c52-b253-4eb7-8791-fbb9de9f856f'
response = self.client.get(url, status=404)
self.assertIn('request_id', response.json)
self.assertEqual(404, response.json['code'])
self.assertEqual('not_found', response.json['type'])
def test_get_floatingip_invalid_key(self):
response = self.client.get('/reverse/floatingips/foo:bar', status=400)
self.assertIn('message', response.json)
self.assertIn('request_id', response.json)
self.assertEqual(400, response.json['code'])
self.assertEqual('bad_request', response.json['type'])
def test_list_floatingip_no_allocations(self):
response = self.client.get('/reverse/floatingips')
self.assertIn('floatingips', response.json)
self.assertIn('links', response.json)
self.assertEqual(0, len(response.json['floatingips']))
def test_list_floatingip_no_record(self):
context = self.get_context(tenant='a')
fip = self.network_api.fake.allocate_floatingip(context.tenant_id)
response = self.client.get(
'/reverse/floatingips',
headers={'X-Test-Tenant-Id': context.tenant_id})
self.assertIn('floatingips', response.json)
self.assertIn('links', response.json)
self.assertEqual(1, len(response.json['floatingips']))
#TODO(ekarlso): Remove the floatingip key - bug in v2 api
fip_record = response.json['floatingips'][0]['floatingip']
self.assertEqual(None, fip_record['ptrdname'])
self.assertEqual(":".join([fip['region'], fip['id']]),
fip_record['id'])
self.assertEqual(fip['address'], fip_record['address'])
self.assertEqual(None, fip_record['description'])
def test_list_floatingip_with_record(self):
self.create_server()
fixture = self.get_ptr_fixture()
context = self.get_context(tenant='a')
fip = self.network_api.fake.allocate_floatingip(context.tenant_id)
self.central_service.update_floatingip(
context, fip['region'], fip['id'], fixture)
response = self.client.get(
'/reverse/floatingips',
headers={'X-Test-Tenant-Id': context.tenant_id})
self.assertIn('floatingips', response.json)
self.assertIn('links', response.json)
self.assertEqual(1, len(response.json['floatingips']))
#TODO(ekarlso): Remove the floatingip key - bug in v2 api
fip_record = response.json['floatingips'][0]['floatingip']
self.assertEqual(fixture['ptrdname'], fip_record['ptrdname'])
self.assertEqual(":".join([fip['region'], fip['id']]),
fip_record['id'])
self.assertEqual(fip['address'], fip_record['address'])
self.assertEqual(None, fip_record['description'])
self.assertEqual(fixture['ptrdname'], fip_record['ptrdname'])
def test_set_floatingip(self):
self.create_server()
fixture = self.get_ptr_fixture()
fip = self.network_api.fake.allocate_floatingip('tenant')
response = self.client.patch_json(
'/reverse/floatingips/%s' % ":".join([fip['region'], fip['id']]),
{"floatingip": fixture},
headers={'X-Test-Tenant-Id': 'tenant'})
self.assertEqual(200, response.status_int)
self.assertEqual('application/json', response.content_type)
self.assertIn('floatingip', response.json)
fip_record = response.json['floatingip']
self.assertEqual(":".join([fip['region'], fip['id']]),
fip_record['id'])
self.assertEqual(fip['address'], fip_record['address'])
self.assertEqual(None, fip_record['description'])
self.assertEqual(fixture['ptrdname'], fip_record['ptrdname'])
def test_set_floatingip_not_allocated(self):
fixture = self.get_ptr_fixture()
fip = self.network_api.fake.allocate_floatingip('tenant')
self.network_api.fake.deallocate_floatingip(fip['id'])
response = self.client.patch_json(
'/reverse/floatingips/%s' % ":".join([fip['region'], fip['id']]),
{"floatingip": fixture}, status=404)
self.assertIn('message', response.json)
self.assertIn('request_id', response.json)
self.assertEqual(404, response.json['code'])
self.assertEqual('not_found', response.json['type'])
def test_set_floatingip_invalid_ptrdname(self):
fip = self.network_api.fake.allocate_floatingip('tenant')
response = self.client.patch_json(
'/reverse/floatingips/%s' % ":".join([fip['region'], fip['id']]),
{"floatingip": {'ptrxname': 'test'}}, status=400)
self.assertIn('message', response.json)
self.assertIn('request_id', response.json)
self.assertEqual(400, response.json['code'])
self.assertEqual('invalid_object', response.json['type'])
def test_set_floatingip_invalid_key(self):
response = self.client.patch_json(
'/reverse/floatingips/%s' % 'foo:random', {}, status=400)
self.assertIn('message', response.json)
self.assertIn('request_id', response.json)
self.assertEqual(400, response.json['code'])
self.assertEqual('bad_request', response.json['type'])
def test_unset_floatingip(self):
self.create_server()
fixture = self.get_ptr_fixture()
context = self.get_context(tenant='a')
fip = self.network_api.fake.allocate_floatingip(context.tenant_id)
# Unsetting via "None"
self.central_service.update_floatingip(
context, fip['region'], fip['id'], fixture)
# Unset PTR ('ptrdname' is None aka null in JSON)
response = self.client.patch_json(
'/reverse/floatingips/%s' % ":".join([fip['region'], fip['id']]),
{'floatingip': {'ptrdname': None}},
headers={'X-Test-Tenant-Id': context.tenant})
self.assertEqual(None, response.json)
self.assertEqual(200, response.status_int)
fip = self.central_service.get_floatingip(
context, fip['region'], fip['id'])
self.assertEqual(None, fip['ptrdname'])
def test_unset_floatingip_not_allocated(self):
self.create_server()
fixture = self.get_ptr_fixture()
context = self.get_context(tenant='a')
fip = self.network_api.fake.allocate_floatingip(context.tenant)
self.central_service.update_floatingip(
context, fip['region'], fip['id'], fixture)
self.network_api.fake.deallocate_floatingip(fip['id'])
response = self.client.patch_json(
'/reverse/floatingips/%s' % ":".join([fip['region'], fip['id']]),
{"floatingip": {'ptrdname': None}}, status=404)
self.assertIn('message', response.json)
self.assertIn('request_id', response.json)
self.assertEqual(404, response.json['code'])
self.assertEqual('not_found', response.json['type'])

View File

@ -1398,3 +1398,268 @@ class CentralServiceTest(CentralTestCase):
with testtools.ExpectedException(exceptions.Forbidden): with testtools.ExpectedException(exceptions.Forbidden):
self.central_service.count_records(self.get_context()) self.central_service.count_records(self.get_context())
def test_get_floatingip_no_record(self):
self.create_server()
context = self.get_context(tenant='a')
fip = self.network_api.fake.allocate_floatingip(context.tenant_id)
fip_ptr = self.central_service.get_floatingip(
context, fip['region'], fip['id'])
self.assertEqual(fip['region'], fip_ptr['region'])
self.assertEqual(fip['id'], fip_ptr['id'])
self.assertEqual(fip['address'], fip_ptr['address'])
self.assertEqual(None, fip_ptr['ptrdname'])
def test_get_floatingip_with_record(self):
self.create_server()
context = self.get_context(tenant='a')
fixture = self.get_ptr_fixture()
fip = self.network_api.fake.allocate_floatingip(context.tenant_id)
expected = self.central_service.update_floatingip(
context, fip['region'], fip['id'], fixture)
actual = self.central_service.get_floatingip(
context, fip['region'], fip['id'])
self.assertEqual(expected, actual)
self.assertEqual(expected, actual)
def test_get_floatingip_not_allocated(self):
context = self.get_context(tenant='a')
fip = self.network_api.fake.allocate_floatingip(context.tenant_id)
self.network_api.fake.deallocate_floatingip(fip['id'])
with testtools.ExpectedException(exceptions.NotFound):
self.central_service.get_floatingip(
context, fip['region'], fip['id'])
def test_get_floatingip_deallocated_and_invalidate(self):
self.create_server()
context_a = self.get_context(tenant='a')
elevated_a = context_a.elevated()
elevated_a.all_tenants = True
context_b = self.get_context(tenant='b')
fixture = self.get_ptr_fixture()
# First allocate and create a FIP as tenant a
fip = self.network_api.fake.allocate_floatingip(context_a.tenant_id)
self.central_service.update_floatingip(
context_a, fip['region'], fip['id'], fixture)
self.network_api.fake.deallocate_floatingip(fip['id'])
with testtools.ExpectedException(exceptions.NotFound):
self.central_service.get_floatingip(
context_a, fip['region'], fip['id'])
# Ensure that the record is still in DB (No invalidation)
criterion = {
'managed_resource_id': fip['id'],
'managed_tenant_id': context_a.tenant_id}
self.central_service.find_record(elevated_a, criterion)
# Now give the fip id to tenant 'b' and see that it get's deleted
self.network_api.fake.allocate_floatingip(
context_b.tenant_id, fip['id'])
# There should be a fip returned with ptrdname of None
fip_ptr = self.central_service.get_floatingip(
context_b, fip['region'], fip['id'])
self.assertEqual(None, fip_ptr['ptrdname'])
# Ensure that the old record for tenant a for the fip now owned by
# tenant b is gone
with testtools.ExpectedException(exceptions.RecordNotFound):
self.central_service.find_record(elevated_a, criterion)
def test_list_floatingips_no_allocations(self):
context = self.get_context(tenant='a')
fips = self.central_service.list_floatingips(context)
self.assertEqual(0, len(fips))
def test_list_floatingips_no_record(self):
context = self.get_context(tenant='a')
fip = self.network_api.fake.allocate_floatingip(context.tenant_id)
fips = self.central_service.list_floatingips(context)
self.assertEqual(1, len(fips))
self.assertEqual(None, fips[0]['ptrdname'])
self.assertEqual(fip['id'], fips[0]['id'])
self.assertEqual(fip['region'], fips[0]['region'])
self.assertEqual(fip['address'], fips[0]['address'])
self.assertEqual(None, fips[0]['description'])
def test_list_floatingips_with_record(self):
self.create_server()
context = self.get_context(tenant='a')
fixture = self.get_ptr_fixture()
fip = self.network_api.fake.allocate_floatingip(context.tenant_id)
fip_ptr = self.central_service.update_floatingip(
context, fip['region'], fip['id'], fixture)
fips = self.central_service.list_floatingips(context)
self.assertEqual(1, len(fips))
self.assertEqual(fip_ptr['ptrdname'], fips[0]['ptrdname'])
self.assertEqual(fip_ptr['id'], fips[0]['id'])
self.assertEqual(fip_ptr['region'], fips[0]['region'])
self.assertEqual(fip_ptr['address'], fips[0]['address'])
self.assertEqual(fip_ptr['description'], fips[0]['description'])
def test_list_floatingips_deallocated_and_invalidate(self):
self.create_server()
context_a = self.get_context(tenant='a')
elevated_a = context_a.elevated()
elevated_a.all_tenants = True
context_b = self.get_context(tenant='b')
fixture = self.get_ptr_fixture()
# First allocate and create a FIP as tenant a
fip = self.network_api.fake.allocate_floatingip(context_a.tenant_id)
self.central_service.update_floatingip(
context_a, fip['region'], fip['id'], fixture)
self.network_api.fake.deallocate_floatingip(fip['id'])
fips = self.central_service.list_floatingips(context_a)
self.assertEqual([], fips)
# Ensure that the record is still in DB (No invalidation)
criterion = {
'managed_resource_id': fip['id'],
'managed_tenant_id': context_a.tenant_id}
self.central_service.find_record(elevated_a, criterion)
# Now give the fip id to tenant 'b' and see that it get's deleted
self.network_api.fake.allocate_floatingip(
context_b.tenant_id, fip['id'])
# There should be a fip returned with ptrdname of None
fips = self.central_service.list_floatingips(context_b)
self.assertEqual(1, len(fips))
self.assertEqual(None, fips[0]['ptrdname'])
# Ensure that the old record for tenant a for the fip now owned by
# tenant b is gone
with testtools.ExpectedException(exceptions.RecordNotFound):
self.central_service.find_record(elevated_a, criterion)
def test_set_floatingip(self):
self.create_server()
context = self.get_context(tenant='a')
fixture = self.get_ptr_fixture()
fip = self.network_api.fake.allocate_floatingip(context.tenant_id)
fip_ptr = self.central_service.update_floatingip(
context, fip['region'], fip['id'], fixture)
self.assertEqual(fixture['ptrdname'], fip_ptr['ptrdname'])
self.assertEqual(fip['address'], fip_ptr['address'])
self.assertEqual(None, fip_ptr['description'])
self.assertIsNotNone(fip_ptr['ttl'])
def test_set_floatingip_removes_old_rrset_and_record(self):
self.create_server()
context_a = self.get_context(tenant='a')
elevated_a = context_a.elevated()
elevated_a.all_tenants = True
context_b = self.get_context(tenant='b')
fixture = self.get_ptr_fixture()
# Test that re-setting as tenant a an already set floatingip leaves
# only 1 record
fip = self.network_api.fake.allocate_floatingip(context_a.tenant_id)
self.central_service.update_floatingip(
context_a, fip['region'], fip['id'], fixture)
fixture2 = self.get_ptr_fixture(fixture=1)
self.central_service.update_floatingip(
context_a, fip['region'], fip['id'], fixture2)
count = self.central_service.count_records(
elevated_a, {'managed_resource_id': fip['id']})
self.assertEqual(1, count)
self.network_api.fake.deallocate_floatingip(fip['id'])
# Now test that tenant b allocating the same fip and setting a ptr
# deletes any records
fip = self.network_api.fake.allocate_floatingip(
context_b.tenant_id, fip['id'])
self.central_service.update_floatingip(
context_b, fip['region'], fip['id'], fixture)
count = self.central_service.count_records(
elevated_a, {'managed_resource_id': fip['id']})
self.assertEqual(1, count)
def test_set_floatingip_not_allocated(self):
context = self.get_context(tenant='a')
fixture = self.get_ptr_fixture()
fip = self.network_api.fake.allocate_floatingip(context.tenant_id)
self.network_api.fake.deallocate_floatingip(fip['id'])
# If one attempts to assign a de-allocated FIP or not-owned it should
# fail with BadRequest
with testtools.ExpectedException(exceptions.NotFound):
fixture = self.central_service.update_floatingip(
context, fip['region'], fip['id'], fixture)
def test_unset_floatingip(self):
self.create_server()
context = self.get_context(tenant='a')
fixture = self.get_ptr_fixture()
fip = self.network_api.fake.allocate_floatingip(context.tenant_id)
fip_ptr = self.central_service.update_floatingip(
context, fip['region'], fip['id'], fixture)
self.assertEqual(fixture['ptrdname'], fip_ptr['ptrdname'])
self.assertEqual(fip['address'], fip_ptr['address'])
self.assertEqual(None, fip_ptr['description'])
self.assertIsNotNone(fip_ptr['ttl'])
self.central_service.update_floatingip(
context, fip['region'], fip['id'], {'ptrdname': None})
self.central_service.get_floatingip(
context, fip['region'], fip['id'])

View File

@ -0,0 +1,53 @@
# Copyright 2013 Hewlett-Packard Development Company, L.P.
#
# Author: Endre Karlson <endre.karlson@hp.com>
#
# 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 designate import exceptions
from designate.network_api import get_api
from designate.tests import TestCase
from neutronclient.v2_0 import client as clientv20
from neutronclient.common import exceptions as neutron_exceptions
from oslo.config import cfg
from mock import patch
import testtools
cfg.CONF.import_group('network_api:neutron', 'designate.network_api.neutron')
class NeutronAPITest(TestCase):
def setUp(self):
super(NeutronAPITest, self).setUp()
self.config(endpoints=['RegionOne|http://localhost:9696'],
group='network_api:neutron')
self.api = get_api('neutron')
@patch.object(clientv20.Client, 'list_floatingips',
side_effect=neutron_exceptions.Unauthorized)
def test_unauthorized_returns_empty(self, _):
context = self.get_context(tenant='a', auth_token='test')
fips = self.api.list_floatingips(context)
self.assertEqual(0, len(fips))
@patch.object(clientv20.Client, 'list_floatingips',
side_effect=neutron_exceptions.NeutronException)
def test_communication_failure(self, _):
context = self.get_context(tenant='a', auth_token='test')
with testtools.ExpectedException(
exceptions.NeutronCommunicationFailure):
self.api.list_floatingips(context)

View File

@ -32,6 +32,7 @@ also be found on the `OpenStack wiki`_.
production-architecture production-architecture
glossary glossary
backends backends
integrations
Indices and tables Indices and tables

View File

@ -0,0 +1,53 @@
============
Integrations
============
This page overviews integrations with other services like Neutron and others to
make use of Designate more convenient.
Reverse - FloatingIP
====================
The FloatingIP PTR feature of Designate relies on information of the FloatingIP
which is in a different service then Designate itself. It can be in any service
as long as their is a "plugin" for it that can be loaded via the configuration
setting called "network_api".
* Controller, views and schemas in the V2 API
* RPC Client towards Central used by the API and Sink
* Logic in Central to make it convenient for setting, unsetting, listing and
getting FloatingIP PTR records compared to the Records themselves which would
be more work. (This is outlined in code docstrings for the specific methods.)
* Sink handlers for the varios backend to help us be more concistent.
Record invalidation
^^^^^^^^^^^^^^^^^^^
Happens mainly happens via comparing a Tenant's FloatngIPs
towards the list we have of Records which are of a certain plugin type and
with the use of a Sink handler that listens for incoming events from the
various services.
Configuring Neutron
-------------------
Configuring the FloatingIP feature is really simple:
[network_api:neutron]
# endpoints = RegionOne|http://localhost:9696
# endpoint_type = publicURL
# timeout = 30
# admin_username = designate
# admin_password = designate
# admin_tenant_name = designate
# auth_url = http://localhost:35357/v2.0
# insecure = False
# auth_strategy = keystone
# ca_certificates_file = /etc/path/to/ca.pem
Note that using admin_user, admin_password and admin_tenant_name is optional,
if not present we'll piggyback on the context.auth_token passed in by the API.
.. note..
If "endpoints" is not configured and there's no service catalog is present
in the context passed by the API to Central the request will fail in
a NoEndpoint exception.

View File

@ -22,6 +22,9 @@ debug = False
# Change to "sudo" to skip the filtering and just run the comand directly # Change to "sudo" to skip the filtering and just run the comand directly
root_helper = sudo root_helper = sudo
# Which networking API to use, Defaults to neutron
# network_api = neutron
######################## ########################
## Service Configuration ## Service Configuration
######################## ########################
@ -51,6 +54,15 @@ root_helper = sudo
# Maximum record name length # Maximum record name length
#max_record_name_len = 255 #max_record_name_len = 255
## Managed resources settings
# Email to use for managed resources like domains created by the FloatingIP API
# managed_resource_email = root@example.io.
# Tenant ID to own all managed resources - like auto-created records etc.
# managed_resource_tenant_id = 123456
#----------------------- #-----------------------
# API Service # API Service
#----------------------- #-----------------------
@ -99,6 +111,21 @@ root_helper = sudo
# correspond to a [handler:my_driver] section below or else in the config # correspond to a [handler:my_driver] section below or else in the config
#enabled_notification_handlers = nova_fixed #enabled_notification_handlers = nova_fixed
##############
## Network API
##############
[network_api:neutron]
# endpoints = RegionOne|http://localhost:9696
# endpoint_type = publicURL
# timeout = 30
# admin_username = designate
# admin_password = designate
# admin_tenant_name = designate
# auth_url = http://localhost:35357/v2.0
# insecure = False
# auth_strategy = keystone
# ca_certificates_file = /etc/path/to/ca.pem
######################## ########################
## Storage Configuration ## Storage Configuration
######################## ########################

View File

@ -12,6 +12,7 @@ PasteDeploy>=1.5.0
pbr>=0.5.21,<1.0 pbr>=0.5.21,<1.0
pecan>=0.2.0 pecan>=0.2.0
python-keystoneclient>=0.3.2 python-keystoneclient>=0.3.2
python-neutronclient>=2.3.0,<3
Routes>=1.12.3 Routes>=1.12.3
SQLAlchemy>=0.7.8,<=0.7.99 SQLAlchemy>=0.7.8,<=0.7.99
sqlalchemy-migrate>=0.7.2 sqlalchemy-migrate>=0.7.2

View File

@ -69,6 +69,10 @@ designate.backend =
nsd4slave = designate.backend.impl_nsd4slave:NSD4SlaveBackend nsd4slave = designate.backend.impl_nsd4slave:NSD4SlaveBackend
multi = designate.backend.impl_multi:MultiBackend multi = designate.backend.impl_multi:MultiBackend
designate.network_api =
fake = designate.network_api.fake:API
neutron = designate.network_api.neutron:API
designate.quota = designate.quota =
noop = designate.quota.impl_noop:NoopQuota noop = designate.quota.impl_noop:NoopQuota
storage = designate.quota.impl_storage:StorageQuota storage = designate.quota.impl_storage:StorageQuota