Add support for floating IPs association
We should add floating ips realted methods, so users can associate or dissociate fips to their instances. Change-Id: I284caf6ab0b8297024983db79f5b6ecaea8f4f40 Implements: bp associate-dissociate-fip
This commit is contained in:
parent
aa57478af4
commit
6440dcc4d0
|
@ -30,3 +30,5 @@
|
|||
"mogan:instance:set_power_state": "rule:default"
|
||||
# Get Instance network information
|
||||
"mogan:instance:get_networks": "rule:default"
|
||||
# Associate floating IP to instance
|
||||
"mogan:instance:associate_floatingip": "rule:default"
|
||||
|
|
|
@ -17,6 +17,7 @@ import datetime
|
|||
|
||||
import jsonschema
|
||||
from oslo_log import log
|
||||
from oslo_utils import netutils
|
||||
import pecan
|
||||
from pecan import rest
|
||||
from six.moves import http_client
|
||||
|
@ -30,9 +31,11 @@ from mogan.api.controllers.v1 import utils as api_utils
|
|||
from mogan.api import expose
|
||||
from mogan.common import exception
|
||||
from mogan.common.i18n import _
|
||||
from mogan.common.i18n import _LW
|
||||
from mogan.common import policy
|
||||
from mogan.common import states
|
||||
from mogan.engine.baremetal import ironic_states as ir_states
|
||||
from mogan import network
|
||||
from mogan import objects
|
||||
|
||||
_DEFAULT_INSTANCE_RETURN_FIELDS = ('uuid', 'name', 'description',
|
||||
|
@ -78,6 +81,18 @@ _CREATE_INSTANCE_SCHEMA = {
|
|||
'additionalProperties': False,
|
||||
}
|
||||
|
||||
_ASSOCIATE_FIP_SCHEMA = {
|
||||
"$schema": "http://json-schema.org/schema#",
|
||||
"title": "Associate floating ip schema",
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'address': {'type': 'string', 'format': 'ip-address'},
|
||||
'fixed_address': {'type': 'string', 'format': 'ip-address'}
|
||||
},
|
||||
'required': ['address'],
|
||||
'additionalProperties': False
|
||||
}
|
||||
|
||||
|
||||
class InstanceStates(base.APIBase):
|
||||
"""API representation of the states of a instance."""
|
||||
|
@ -156,6 +171,114 @@ class InstanceStatesController(rest.RestController):
|
|||
pecan.response.location = link.build_url('instances', url_args)
|
||||
|
||||
|
||||
class FloatingIP(base.APIBase):
|
||||
"""API representation of the floatingip information for an instance."""
|
||||
|
||||
id = types.uuid
|
||||
"""The ID of the floating IP"""
|
||||
|
||||
port_id = types.uuid
|
||||
"""The ID of the port that associated to"""
|
||||
|
||||
|
||||
class FloatingIPController(rest.RestController):
|
||||
"""REST controller for Instance floatingips."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(FloatingIPController, self).__init__(*args, **kwargs)
|
||||
self.network_api = network.API()
|
||||
|
||||
_resource = None
|
||||
|
||||
# This _resource is used for authorization.
|
||||
def _get_resource(self, uuid, *args, **kwargs):
|
||||
self._resource = objects.Instance.get(pecan.request.context, uuid)
|
||||
return self._resource
|
||||
|
||||
@policy.authorize_wsgi("mogan:instance", "associate_floatingip", False)
|
||||
@expose.expose(FloatingIP, types.uuid, types.jsontype,
|
||||
status_code=http_client.CREATED)
|
||||
def post(self, instance_uuid, floatingip):
|
||||
"""Add(Associate) Floating Ip.
|
||||
|
||||
:param floatingip: The floating IP within the request body.
|
||||
"""
|
||||
# Add jsonschema validate
|
||||
_check_create_body(floatingip, _ASSOCIATE_FIP_SCHEMA)
|
||||
|
||||
instance = self._resource or self._get_resource(instance_uuid)
|
||||
address = floatingip['address']
|
||||
ports = instance.network_info
|
||||
|
||||
if not ports:
|
||||
msg = _('No ports associated to instance')
|
||||
raise wsme.exc.ClientSideError(
|
||||
msg, status_code=http_client.BAD_REQUEST)
|
||||
|
||||
fixed_address = None
|
||||
if 'fixed_address' in floatingip:
|
||||
fixed_address = floatingip['fixed_address']
|
||||
for port_id, port in ports.items():
|
||||
for port_address in port['fixed_ips']:
|
||||
if port_address['ip_address'] == fixed_address:
|
||||
break
|
||||
else:
|
||||
continue
|
||||
break
|
||||
else:
|
||||
msg = _('Specified fixed address not assigned to instance')
|
||||
raise wsme.exc.ClientSideError(
|
||||
msg, status_code=http_client.BAD_REQUEST)
|
||||
|
||||
if not fixed_address:
|
||||
for port_id, port in ports.items():
|
||||
for port_address in port['fixed_ips']:
|
||||
if netutils.is_valid_ipv4(port_address['ip_address']):
|
||||
fixed_address = port_address['ip_address']
|
||||
break
|
||||
else:
|
||||
continue
|
||||
break
|
||||
else:
|
||||
msg = _('Unable to associate floating IP %(address)s '
|
||||
'to any fixed IPs for instance %(id)s. '
|
||||
'Instance has no fixed IPv4 addresses to '
|
||||
'associate.') % ({'address': address,
|
||||
'id': instance.uuid})
|
||||
raise wsme.exc.ClientSideError(
|
||||
msg, status_code=http_client.BAD_REQUEST)
|
||||
if len(ports) > 1:
|
||||
LOG.warning(_LW('multiple ports exist, using the first '
|
||||
'IPv4 fixed_ip: %s'), fixed_address)
|
||||
|
||||
try:
|
||||
fip = self.network_api.associate_floating_ip(
|
||||
pecan.request.context, floating_address=address,
|
||||
port_id=port_id, fixed_address=fixed_address)
|
||||
except exception.FloatingIpNotFoundForAddress:
|
||||
msg = _('floating IP not found')
|
||||
raise wsme.exc.ClientSideError(
|
||||
msg, status_code=http_client.NOT_FOUND)
|
||||
except exception.Forbidden as e:
|
||||
raise wsme.exc.ClientSideError(
|
||||
msg, status_code=http_client.FORBIDDEN)
|
||||
except Exception as e:
|
||||
msg = _('Unable to associate floating IP %(address)s to '
|
||||
'fixed IP %(fixed_address)s for instance %(id)s. '
|
||||
'Error: %(error)s') % ({'address': address,
|
||||
'fixed_address': fixed_address,
|
||||
'id': instance.uuid, 'error': e})
|
||||
LOG.exception(msg)
|
||||
raise wsme.exc.ClientSideError(
|
||||
msg, status_code=http_client.BAD_REQUEST)
|
||||
|
||||
# Set the HTTP Location Header, user can get the floating ips
|
||||
# by locaton.
|
||||
url_args = '/'.join([instance_uuid, 'networks'])
|
||||
pecan.response.location = link.build_url('instances', url_args)
|
||||
return FloatingIP(id=fip['id'], port_id=fip['port_id'])
|
||||
|
||||
|
||||
class InstanceNetworks(base.APIBase):
|
||||
"""API representation of the networks of an instance."""
|
||||
|
||||
|
@ -166,6 +289,9 @@ class InstanceNetworks(base.APIBase):
|
|||
class InstanceNetworksController(rest.RestController):
|
||||
"""REST controller for Instance networks."""
|
||||
|
||||
floatingip = FloatingIPController()
|
||||
"""Expose floatingip as a sub-element of networks"""
|
||||
|
||||
_resource = None
|
||||
|
||||
# This _resource is used for authorization.
|
||||
|
@ -370,7 +496,7 @@ class InstanceController(rest.RestController):
|
|||
:param instance: a instance within the request body.
|
||||
"""
|
||||
# Add jsonschema validate
|
||||
_check_create_body(instance)
|
||||
_check_create_body(instance, _CREATE_INSTANCE_SCHEMA)
|
||||
|
||||
requested_networks = instance.pop('networks', None)
|
||||
instance_type_uuid = instance.get('instance_type_uuid')
|
||||
|
@ -455,17 +581,17 @@ class InstanceController(rest.RestController):
|
|||
pecan.request.engine_api.delete(pecan.request.context, rpc_instance)
|
||||
|
||||
|
||||
def _check_create_body(body):
|
||||
def _check_create_body(body, schema):
|
||||
"""Ensure all necessary keys are present and correct in create body.
|
||||
|
||||
Check that the user-specified create body is in the expected format and
|
||||
include the required information.
|
||||
|
||||
:param body: create instance body
|
||||
:param body: create body
|
||||
:raises: InvalidParameterValue if validation of create body fails.
|
||||
"""
|
||||
try:
|
||||
jsonschema.validate(body, _CREATE_INSTANCE_SCHEMA)
|
||||
jsonschema.validate(body, schema)
|
||||
except jsonschema.ValidationError as exc:
|
||||
raise exception.InvalidParameterValue(_('Invalid create body: %s') %
|
||||
exc)
|
||||
|
|
|
@ -97,6 +97,11 @@ class OperationNotPermitted(NotAuthorized):
|
|||
_msg_fmt = _("Operation not permitted.")
|
||||
|
||||
|
||||
class Forbidden(MoganException):
|
||||
msg_fmt = _("Forbidden")
|
||||
code = 403
|
||||
|
||||
|
||||
class HTTPForbidden(NotAuthorized):
|
||||
_msg_fmt = _("Access was denied to the following resource: %(resource)s")
|
||||
|
||||
|
@ -265,4 +270,12 @@ class DuplicateState(Conflict):
|
|||
_msg_fmt = _("Resource already exists.")
|
||||
|
||||
|
||||
class FloatingIpNotFoundForAddress(NotFound):
|
||||
msg_fmt = _("Floating IP not found for address %(address)s.")
|
||||
|
||||
|
||||
class FloatingIpMultipleFoundForAddress(MoganException):
|
||||
msg_fmt = _("Multiple floating IPs are found for address %(address)s.")
|
||||
|
||||
|
||||
ObjectActionError = obj_exc.ObjectActionError
|
||||
|
|
|
@ -97,6 +97,9 @@ instance_policies = [
|
|||
policy.RuleDefault('mogan:instance:get_networks',
|
||||
'rule:default',
|
||||
description='Get Instance network information'),
|
||||
policy.RuleDefault('mogan:instance:associate_floatingip',
|
||||
'rule:default',
|
||||
description='Associate a floating ip with an instance'),
|
||||
]
|
||||
|
||||
|
||||
|
|
|
@ -13,9 +13,10 @@
|
|||
from neutronclient.common import exceptions as neutron_exceptions
|
||||
from neutronclient.v2_0 import client as clientv20
|
||||
from oslo_log import log as logging
|
||||
from oslo_utils import excutils
|
||||
|
||||
from mogan.common import exception
|
||||
from mogan.common.i18n import _
|
||||
from mogan.common.i18n import _, _LE
|
||||
from mogan.common import keystone
|
||||
from mogan.conf import CONF
|
||||
|
||||
|
@ -94,3 +95,43 @@ class API(object):
|
|||
{'vif': port_id, 'instance': instance_uuid, 'exc': e})
|
||||
LOG.exception(msg)
|
||||
raise exception.NetworkError(msg)
|
||||
|
||||
def _safe_get_floating_ips(self, client, **kwargs):
|
||||
"""Get floating IP gracefully handling 404 from Neutron."""
|
||||
try:
|
||||
return client.list_floatingips(**kwargs)['floatingips']
|
||||
# If a neutron plugin does not implement the L3 API a 404 from
|
||||
# list_floatingips will be raised.
|
||||
except neutron_exceptions.NotFound:
|
||||
return []
|
||||
except neutron_exceptions.NeutronClientException as e:
|
||||
# bug/1513879 neutron client is currently using
|
||||
# NeutronClientException when there is no L3 API
|
||||
if e.status_code == 404:
|
||||
return []
|
||||
with excutils.save_and_reraise_exception():
|
||||
LOG.exception(_LE('Unable to access floating IP for %s'),
|
||||
', '.join(['%s %s' % (k, v)
|
||||
for k, v in kwargs.items()]))
|
||||
|
||||
def _get_floating_ip_by_address(self, client, address):
|
||||
"""Get floating IP from floating IP address."""
|
||||
if not address:
|
||||
raise exception.FloatingIpNotFoundForAddress(address=address)
|
||||
fips = self._safe_get_floating_ips(client, floating_ip_address=address)
|
||||
if len(fips) == 0:
|
||||
raise exception.FloatingIpNotFoundForAddress(address=address)
|
||||
elif len(fips) > 1:
|
||||
raise exception.FloatingIpMultipleFoundForAddress(address=address)
|
||||
return fips[0]
|
||||
|
||||
def associate_floating_ip(self, context, floating_address,
|
||||
port_id, fixed_address):
|
||||
"""Associate a floating IP with a fixed IP."""
|
||||
|
||||
client = get_client(context.auth_token)
|
||||
fip = self._get_floating_ip_by_address(client, floating_address)
|
||||
param = {'port_id': port_id,
|
||||
'fixed_ip_address': fixed_address}
|
||||
client.update_floatingip(fip['id'], {'floatingip': param})
|
||||
return fip
|
||||
|
|
Loading…
Reference in New Issue