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:
parent
f78c67d07f
commit
6fd81240b7
90
designate/api/v2/controllers/floatingips.py
Normal file
90
designate/api/v2/controllers/floatingips.py
Normal 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)
|
21
designate/api/v2/controllers/reverse.py
Normal file
21
designate/api/v2/controllers/reverse.py
Normal 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()
|
@ -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()
|
||||||
|
36
designate/api/v2/views/floatingips.py
Normal file
36
designate/api/v2/views/floatingips.py
Normal 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}
|
@ -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')
|
||||||
|
@ -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)
|
||||||
|
@ -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)
|
||||||
|
@ -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'
|
||||||
|
115
designate/network_api/__init__.py
Normal file
115
designate/network_api/__init__.py
Normal 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()
|
80
designate/network_api/fake.py
Normal file
80
designate/network_api/fake.py
Normal 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
|
148
designate/network_api/neutron.py
Normal file
148
designate/network_api/neutron.py
Normal 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
|
54
designate/resources/schemas/v2/floatingip.json
Normal file
54
designate/resources/schemas/v2/floatingip.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
38
designate/resources/schemas/v2/floatingips.json
Normal file
38
designate/resources/schemas/v2/floatingips.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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)
|
@ -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')
|
||||||
|
@ -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'
|
||||||
|
250
designate/tests/test_api/test_v2/test_floatingips.py
Normal file
250
designate/tests/test_api/test_v2/test_floatingips.py
Normal 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'])
|
@ -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'])
|
||||||
|
0
designate/tests/test_network_api/__init__.py
Normal file
0
designate/tests/test_network_api/__init__.py
Normal file
53
designate/tests/test_network_api/test_neutron.py
Normal file
53
designate/tests/test_network_api/test_neutron.py
Normal 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)
|
@ -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
|
||||||
|
53
doc/source/integrations.rst
Normal file
53
doc/source/integrations.rst
Normal 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.
|
@ -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
|
||||||
########################
|
########################
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
Loading…
Reference in New Issue
Block a user