Port cloudpipe extension to v2.1
This patch ports couldpipe extension from v2 to v2.1, and have v2 unit test cases shared between v2.1 and v2. Partially implements blueprint v2-on-v3-api Change-Id: Id40c637c0fc207e97b1d094805eb0cee6e9cd9c9
This commit is contained in:
parent
96b39341d5
commit
6007a019cb
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"cloudpipe": {
|
||||
"project_id": "cloudpipe-059f21e3-c20e-4efc-9e7a-eba2ab3c6f9a"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"instance_id": "1e9b8425-34af-488e-b969-4d46f4a6382e"
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"cloudpipes": [
|
||||
{
|
||||
"created_at": "2012-11-27T17:18:01Z",
|
||||
"instance_id": "27deecdb-baa3-4a26-9c82-32994b815b01",
|
||||
"internal_ip": "192.168.0.3",
|
||||
"project_id": "cloudpipe-fa1765bd-a352-49c7-a6b7-8ee108a3cb0c",
|
||||
"public_ip": "127.0.0.1",
|
||||
"public_port": 22,
|
||||
"state": "down"
|
||||
}
|
||||
]
|
||||
}
|
|
@ -82,6 +82,8 @@
|
|||
"compute_extension:v3:os-certificates:show": "",
|
||||
"compute_extension:v3:os-certificates:discoverable": "",
|
||||
"compute_extension:cloudpipe": "rule:admin_api",
|
||||
"compute_extension:v3:os-cloudpipe": "rule:admin_api",
|
||||
"compute_extension:v3:os-cloudpipe:discoverable": "",
|
||||
"compute_extension:cloudpipe_update": "rule:admin_api",
|
||||
"compute_extension:console_output": "",
|
||||
"compute_extension:v3:consoles:discoverable": "",
|
||||
|
|
|
@ -0,0 +1,171 @@
|
|||
# Copyright 2011 OpenStack Foundation
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
"""Connect your vlan to the world."""
|
||||
|
||||
from oslo.config import cfg
|
||||
from webob import exc
|
||||
|
||||
from nova.api.openstack import extensions
|
||||
from nova.api.openstack import wsgi
|
||||
from nova.cloudpipe import pipelib
|
||||
from nova import compute
|
||||
from nova.compute import utils as compute_utils
|
||||
from nova.compute import vm_states
|
||||
from nova import exception
|
||||
from nova.i18n import _
|
||||
from nova import network
|
||||
from nova.openstack.common import fileutils
|
||||
from nova.openstack.common import timeutils
|
||||
from nova import utils
|
||||
|
||||
CONF = cfg.CONF
|
||||
CONF.import_opt('keys_path', 'nova.crypto')
|
||||
|
||||
ALIAS = 'os-cloudpipe'
|
||||
authorize = extensions.extension_authorizer('compute', 'v3:' + ALIAS)
|
||||
|
||||
|
||||
class CloudpipeController(wsgi.Controller):
|
||||
"""Handle creating and listing cloudpipe instances."""
|
||||
|
||||
def __init__(self):
|
||||
self.compute_api = compute.API()
|
||||
self.network_api = network.API()
|
||||
self.cloudpipe = pipelib.CloudPipe()
|
||||
self.setup()
|
||||
|
||||
def setup(self):
|
||||
"""Ensure the keychains and folders exist."""
|
||||
# NOTE(vish): One of the drawbacks of doing this in the api is
|
||||
# the keys will only be on the api node that launched
|
||||
# the cloudpipe.
|
||||
fileutils.ensure_tree(CONF.keys_path)
|
||||
|
||||
def _get_all_cloudpipes(self, context):
|
||||
"""Get all cloudpipes."""
|
||||
instances = self.compute_api.get_all(context,
|
||||
search_opts={'deleted': False})
|
||||
return [instance for instance in instances
|
||||
if pipelib.is_vpn_image(instance['image_ref'])
|
||||
and instance['vm_state'] != vm_states.DELETED]
|
||||
|
||||
def _get_cloudpipe_for_project(self, context, project_id):
|
||||
"""Get the cloudpipe instance for a project ID."""
|
||||
cloudpipes = self._get_all_cloudpipes(context) or [None]
|
||||
return cloudpipes[0]
|
||||
|
||||
def _get_ip_and_port(self, instance):
|
||||
pass
|
||||
|
||||
def _vpn_dict(self, context, project_id, instance):
|
||||
elevated = context.elevated()
|
||||
rv = {'project_id': project_id}
|
||||
if not instance:
|
||||
rv['state'] = 'pending'
|
||||
return rv
|
||||
rv['instance_id'] = instance['uuid']
|
||||
rv['created_at'] = timeutils.isotime(instance['created_at'])
|
||||
nw_info = compute_utils.get_nw_info_for_instance(instance)
|
||||
if not nw_info:
|
||||
return rv
|
||||
vif = nw_info[0]
|
||||
ips = [ip for ip in vif.fixed_ips() if ip['version'] == 4]
|
||||
if ips:
|
||||
rv['internal_ip'] = ips[0]['address']
|
||||
# NOTE(vish): Currently network_api.get does an owner check on
|
||||
# project_id. This is probably no longer necessary
|
||||
# but rather than risk changes in the db layer,
|
||||
# we are working around it here by changing the
|
||||
# project_id in the context. This can be removed
|
||||
# if we remove the project_id check in the db.
|
||||
elevated.project_id = project_id
|
||||
network = self.network_api.get(elevated, vif['network']['id'])
|
||||
if network:
|
||||
vpn_ip = network['vpn_public_address']
|
||||
vpn_port = network['vpn_public_port']
|
||||
rv['public_ip'] = vpn_ip
|
||||
rv['public_port'] = vpn_port
|
||||
if vpn_ip and vpn_port:
|
||||
if utils.vpn_ping(vpn_ip, vpn_port):
|
||||
rv['state'] = 'running'
|
||||
else:
|
||||
rv['state'] = 'down'
|
||||
else:
|
||||
rv['state'] = 'invalid'
|
||||
return rv
|
||||
|
||||
@extensions.expected_errors((400, 403))
|
||||
def create(self, req, body):
|
||||
"""Create a new cloudpipe instance, if none exists.
|
||||
|
||||
Parameters: {cloudpipe: {'project_id': ''}}
|
||||
"""
|
||||
|
||||
context = req.environ['nova.context']
|
||||
authorize(context)
|
||||
params = body.get('cloudpipe', {})
|
||||
project_id = params.get('project_id', context.project_id)
|
||||
# NOTE(vish): downgrade to project context. Note that we keep
|
||||
# the same token so we can still talk to glance
|
||||
context.project_id = project_id
|
||||
context.user_id = 'project-vpn'
|
||||
context.is_admin = False
|
||||
context.roles = []
|
||||
instance = self._get_cloudpipe_for_project(context, project_id)
|
||||
if not instance:
|
||||
try:
|
||||
result = self.cloudpipe.launch_vpn_instance(context)
|
||||
instance = result[0][0]
|
||||
except exception.NoMoreNetworks:
|
||||
msg = _("Unable to claim IP for VPN instances, ensure it "
|
||||
"isn't running, and try again in a few minutes")
|
||||
raise exc.HTTPBadRequest(explanation=msg)
|
||||
return {'instance_id': instance['uuid']}
|
||||
|
||||
@extensions.expected_errors((400, 403, 404))
|
||||
def index(self, req):
|
||||
"""List running cloudpipe instances."""
|
||||
context = req.environ['nova.context']
|
||||
authorize(context)
|
||||
vpns = [self._vpn_dict(context, x['project_id'], x)
|
||||
for x in self._get_all_cloudpipes(context)]
|
||||
return {'cloudpipes': vpns}
|
||||
|
||||
|
||||
class Cloudpipe(extensions.V3APIExtensionBase):
|
||||
"""Adds actions to create cloudpipe instances.
|
||||
|
||||
When running with the Vlan network mode, you need a mechanism to route
|
||||
from the public Internet to your vlans. This mechanism is known as a
|
||||
cloudpipe.
|
||||
|
||||
At the time of creating this class, only OpenVPN is supported. Support for
|
||||
a SSH Bastion host is forthcoming.
|
||||
"""
|
||||
|
||||
name = "Cloudpipe"
|
||||
alias = ALIAS
|
||||
version = 1
|
||||
|
||||
def get_resources(self):
|
||||
resource = [extensions.ResourceExtension(ALIAS,
|
||||
CloudpipeController())]
|
||||
return resource
|
||||
|
||||
def get_controller_extensions(self):
|
||||
"""It's an abstract function V3APIExtensionBase and the extension
|
||||
will not be loaded without it.
|
||||
"""
|
||||
return []
|
|
@ -18,7 +18,8 @@ from oslo.config import cfg
|
|||
from oslo.utils import timeutils
|
||||
from webob import exc
|
||||
|
||||
from nova.api.openstack.compute.contrib import cloudpipe
|
||||
from nova.api.openstack.compute.contrib import cloudpipe as cloudpipe_v2
|
||||
from nova.api.openstack.compute.contrib import cloudpipe as cloudpipe_v21
|
||||
from nova.api.openstack import wsgi
|
||||
from nova.compute import utils as compute_utils
|
||||
from nova import exception
|
||||
|
@ -52,11 +53,13 @@ def utils_vpn_ping(addr, port, timoeout=0.05, session_id=None):
|
|||
return True
|
||||
|
||||
|
||||
class CloudpipeTest(test.NoDBTestCase):
|
||||
class CloudpipeTestV21(test.NoDBTestCase):
|
||||
cloudpipe = cloudpipe_v21
|
||||
url = '/v2/fake/os-cloudpipe'
|
||||
|
||||
def setUp(self):
|
||||
super(CloudpipeTest, self).setUp()
|
||||
self.controller = cloudpipe.CloudpipeController()
|
||||
super(CloudpipeTestV21, self).setUp()
|
||||
self.controller = self.cloudpipe.CloudpipeController()
|
||||
self.stubs.Set(self.controller.compute_api, "get_all",
|
||||
compute_api_get_all_empty)
|
||||
self.stubs.Set(utils, 'vpn_ping', utils_vpn_ping)
|
||||
|
@ -70,7 +73,7 @@ class CloudpipeTest(test.NoDBTestCase):
|
|||
fake_get_nw_info_for_instance)
|
||||
self.stubs.Set(self.controller.compute_api, "get_all",
|
||||
compute_api_get_all)
|
||||
req = fakes.HTTPRequest.blank('/v2/fake/os-cloudpipe')
|
||||
req = fakes.HTTPRequest.blank(self.url)
|
||||
res_dict = self.controller.index(req)
|
||||
response = {'cloudpipes': [{'project_id': 'other',
|
||||
'instance_id': 7777,
|
||||
|
@ -93,7 +96,7 @@ class CloudpipeTest(test.NoDBTestCase):
|
|||
network_api_get)
|
||||
self.stubs.Set(self.controller.compute_api, "get_all",
|
||||
compute_api_get_all)
|
||||
req = fakes.HTTPRequest.blank('/v2/fake/os-cloudpipe')
|
||||
req = fakes.HTTPRequest.blank(self.url)
|
||||
res_dict = self.controller.index(req)
|
||||
response = {'cloudpipes': [{'project_id': 'other',
|
||||
'internal_ip': '192.168.1.100',
|
||||
|
@ -111,7 +114,7 @@ class CloudpipeTest(test.NoDBTestCase):
|
|||
self.stubs.Set(self.controller.cloudpipe, 'launch_vpn_instance',
|
||||
launch_vpn_instance)
|
||||
body = {'cloudpipe': {'project_id': 1}}
|
||||
req = fakes.HTTPRequest.blank('/v2/fake/os-cloudpipe')
|
||||
req = fakes.HTTPRequest.blank(self.url)
|
||||
res_dict = self.controller.create(req, body)
|
||||
|
||||
response = {'instance_id': 7777}
|
||||
|
@ -124,7 +127,7 @@ class CloudpipeTest(test.NoDBTestCase):
|
|||
self.stubs.Set(self.controller.cloudpipe, 'launch_vpn_instance',
|
||||
launch_vpn_instance)
|
||||
body = {'cloudpipe': {'project_id': 1}}
|
||||
req = fakes.HTTPRequest.blank('/v2/fake/os-cloudpipe')
|
||||
req = fakes.HTTPRequest.blank(self.url)
|
||||
self.assertRaises(exc.HTTPBadRequest,
|
||||
self.controller.create, req, body)
|
||||
|
||||
|
@ -137,15 +140,19 @@ class CloudpipeTest(test.NoDBTestCase):
|
|||
self.stubs.Set(self.controller.compute_api, "get_all",
|
||||
compute_api_get_all)
|
||||
body = {'cloudpipe': {'project_id': 1}}
|
||||
req = fakes.HTTPRequest.blank('/v2/fake/os-cloudpipe')
|
||||
req = fakes.HTTPRequest.blank(self.url)
|
||||
res_dict = self.controller.create(req, body)
|
||||
response = {'instance_id': 7777}
|
||||
self.assertEqual(res_dict, response)
|
||||
|
||||
|
||||
class CloudpipesXMLSerializerTest(test.NoDBTestCase):
|
||||
class CloudpipeTestV2(CloudpipeTestV21):
|
||||
cloudpipe = cloudpipe_v2
|
||||
|
||||
|
||||
class CloudpipesXMLSerializerTestV2(test.NoDBTestCase):
|
||||
def test_default_serializer(self):
|
||||
serializer = cloudpipe.CloudpipeTemplate()
|
||||
serializer = cloudpipe_v2.CloudpipeTemplate()
|
||||
exemplar = dict(cloudpipe=dict(instance_id='1234-1234-1234-1234'))
|
||||
text = serializer.serialize(exemplar)
|
||||
tree = etree.fromstring(text)
|
||||
|
@ -155,7 +162,7 @@ class CloudpipesXMLSerializerTest(test.NoDBTestCase):
|
|||
self.assertEqual(child.text, exemplar['cloudpipe'][child.tag])
|
||||
|
||||
def test_index_serializer(self):
|
||||
serializer = cloudpipe.CloudpipesTemplate()
|
||||
serializer = cloudpipe_v2.CloudpipesTemplate()
|
||||
exemplar = dict(cloudpipes=[
|
||||
dict(
|
||||
project_id='1234',
|
||||
|
|
|
@ -152,6 +152,7 @@ policy_data = """
|
|||
"compute_extension:v3:os-certificates:create": "",
|
||||
"compute_extension:v3:os-certificates:show": "",
|
||||
"compute_extension:cloudpipe": "",
|
||||
"compute_extension:v3:os-cloudpipe": "",
|
||||
"compute_extension:cloudpipe_update": "",
|
||||
"compute_extension:config_drive": "",
|
||||
"compute_extension:v3:os-config-drive": "",
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"cloudpipe": {
|
||||
"project_id": "%(project_id)s"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"instance_id": "%(id)s"
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"cloudpipes": [
|
||||
{
|
||||
"created_at": "%(isotime)s",
|
||||
"instance_id": "%(uuid)s",
|
||||
"internal_ip": "%(ip)s",
|
||||
"project_id": "%(project_id)s",
|
||||
"public_ip": "%(ip)s",
|
||||
"public_port": 22,
|
||||
"state": "down"
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,71 @@
|
|||
# Copyright 2014 IBM Corp.
|
||||
#
|
||||
# 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 as uuid_lib
|
||||
|
||||
from oslo.config import cfg
|
||||
|
||||
from nova.cloudpipe import pipelib
|
||||
from nova.network import api as network_api
|
||||
from nova.tests.image import fake
|
||||
from nova.tests.integrated.v3 import api_sample_base
|
||||
|
||||
|
||||
CONF = cfg.CONF
|
||||
CONF.import_opt('vpn_image_id', 'nova.cloudpipe.pipelib')
|
||||
|
||||
|
||||
class CloudPipeSampleTest(api_sample_base.ApiSampleTestBaseV3):
|
||||
extension_name = "os-cloudpipe"
|
||||
|
||||
def setUp(self):
|
||||
super(CloudPipeSampleTest, self).setUp()
|
||||
|
||||
def get_user_data(self, project_id):
|
||||
"""Stub method to generate user data for cloudpipe tests."""
|
||||
return "VVNFUiBEQVRB\n"
|
||||
|
||||
def network_api_get(self, context, network_uuid):
|
||||
"""Stub to get a valid network and its information."""
|
||||
return {'vpn_public_address': '127.0.0.1',
|
||||
'vpn_public_port': 22}
|
||||
|
||||
self.stubs.Set(pipelib.CloudPipe, 'get_encoded_zip', get_user_data)
|
||||
self.stubs.Set(network_api.API, "get",
|
||||
network_api_get)
|
||||
|
||||
def generalize_subs(self, subs, vanilla_regexes):
|
||||
subs['project_id'] = 'cloudpipe-[0-9a-f-]+'
|
||||
return subs
|
||||
|
||||
def test_cloud_pipe_create(self):
|
||||
# Get api samples of cloud pipe extension creation.
|
||||
self.flags(vpn_image_id=fake.get_valid_image_id())
|
||||
project = {'project_id': 'cloudpipe-' + str(uuid_lib.uuid4())}
|
||||
response = self._do_post('os-cloudpipe', 'cloud-pipe-create-req',
|
||||
project)
|
||||
subs = self._get_regexes()
|
||||
subs.update(project)
|
||||
subs['image_id'] = CONF.vpn_image_id
|
||||
self._verify_response('cloud-pipe-create-resp', subs, response, 200)
|
||||
return project
|
||||
|
||||
def test_cloud_pipe_list(self):
|
||||
# Get api samples of cloud pipe extension get request.
|
||||
project = self.test_cloud_pipe_create()
|
||||
response = self._do_get('os-cloudpipe')
|
||||
subs = self._get_regexes()
|
||||
subs.update(project)
|
||||
subs['image_id'] = CONF.vpn_image_id
|
||||
self._verify_response('cloud-pipe-get-resp', subs, response, 200)
|
|
@ -66,6 +66,7 @@ nova.api.v3.extensions =
|
|||
block_device_mapping = nova.api.openstack.compute.plugins.v3.block_device_mapping:BlockDeviceMapping
|
||||
cells = nova.api.openstack.compute.plugins.v3.cells:Cells
|
||||
certificates = nova.api.openstack.compute.plugins.v3.certificates:Certificates
|
||||
cloudpipe = nova.api.openstack.compute.plugins.v3.cloudpipe:Cloudpipe
|
||||
config_drive = nova.api.openstack.compute.plugins.v3.config_drive:ConfigDrive
|
||||
console_auth_tokens = nova.api.openstack.compute.plugins.v3.console_auth_tokens:ConsoleAuthTokens
|
||||
console_output = nova.api.openstack.compute.plugins.v3.console_output:ConsoleOutput
|
||||
|
|
Loading…
Reference in New Issue