Adds nova api needed for generic driver implementation
Generic driver will use nova instances to reach multitenancy, so this patch adds a module for interacting with nova. Partially implements: bp generic-driver Change-Id: Ic51971f0fe72bcbbe364e8f6aa4c59a8ec267c83
This commit is contained in:
parent
fbde9ae88f
commit
9a7e113b3a
|
@ -0,0 +1,34 @@
|
|||
# Copyright 2014 Mirantis Inc.
|
||||
# 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.
|
||||
|
||||
import oslo.config.cfg
|
||||
|
||||
import manila.openstack.common.importutils
|
||||
|
||||
_compute_opts = [
|
||||
oslo.config.cfg.StrOpt('compute_api_class',
|
||||
default='manila.compute.nova.API',
|
||||
help='The full class name of the '
|
||||
'compute API class to use'),
|
||||
]
|
||||
|
||||
oslo.config.cfg.CONF.register_opts(_compute_opts)
|
||||
|
||||
|
||||
def API():
|
||||
importutils = manila.openstack.common.importutils
|
||||
compute_api_class = oslo.config.cfg.CONF.compute_api_class
|
||||
cls = importutils.import_class(compute_api_class)
|
||||
return cls()
|
|
@ -0,0 +1,271 @@
|
|||
# Copyright 2014 Mirantis Inc.
|
||||
#
|
||||
# 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.
|
||||
|
||||
"""
|
||||
Handles all requests to Nova.
|
||||
"""
|
||||
|
||||
import sys
|
||||
|
||||
from novaclient import exceptions as nova_exception
|
||||
from novaclient import extension
|
||||
from novaclient import service_catalog
|
||||
from novaclient.v1_1 import client as nova_client
|
||||
from novaclient.v1_1.contrib import assisted_volume_snapshots
|
||||
from novaclient.v1_1 import servers as nova_servers
|
||||
from oslo.config import cfg
|
||||
|
||||
from manila.db import base
|
||||
from manila import exception
|
||||
from manila.openstack.common import log as logging
|
||||
|
||||
nova_opts = [
|
||||
cfg.StrOpt('nova_catalog_info',
|
||||
default='compute:nova:publicURL',
|
||||
help='Info to match when looking for nova in the service '
|
||||
'catalog. Format is : separated values of the form: '
|
||||
'<service_type>:<service_name>:<endpoint_type>'),
|
||||
cfg.StrOpt('nova_catalog_admin_info',
|
||||
default='compute:nova:adminURL',
|
||||
help='Same as nova_catalog_info, but for admin endpoint.'),
|
||||
cfg.StrOpt('os_region_name',
|
||||
default=None,
|
||||
help='region name of this node'),
|
||||
cfg.StrOpt('nova_ca_certificates_file',
|
||||
default=None,
|
||||
help='Location of ca certicates file to use for nova client '
|
||||
'requests.'),
|
||||
cfg.BoolOpt('nova_api_insecure',
|
||||
default=False,
|
||||
help='Allow to perform insecure SSL requests to nova'),
|
||||
cfg.StrOpt('nova_admin_username',
|
||||
default='nova',
|
||||
help='Nova admin username'),
|
||||
cfg.StrOpt('nova_admin_password',
|
||||
help='Nova admin password'),
|
||||
cfg.StrOpt('nova_admin_tenant_name',
|
||||
default='service',
|
||||
help='Nova admin tenant name'),
|
||||
cfg.StrOpt('nova_admin_auth_url',
|
||||
default='http://localhost:5000/v2.0',
|
||||
help='Identity service url'),
|
||||
]
|
||||
|
||||
CONF = cfg.CONF
|
||||
CONF.register_opts(nova_opts)
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def novaclient(context):
|
||||
if context.is_admin and context.project_id is None:
|
||||
c = nova_client.Client(CONF.nova_admin_username,
|
||||
CONF.nova_admin_password,
|
||||
CONF.nova_admin_tenant_name,
|
||||
CONF.nova_admin_auth_url)
|
||||
c.authenticate()
|
||||
return c
|
||||
compat_catalog = {
|
||||
'access': {'serviceCatalog': context.service_catalog or []}
|
||||
}
|
||||
sc = service_catalog.ServiceCatalog(compat_catalog)
|
||||
|
||||
nova_catalog_info = CONF.nova_catalog_info
|
||||
|
||||
info = nova_catalog_info
|
||||
service_type, service_name, endpoint_type = info.split(':')
|
||||
# extract the region if set in configuration
|
||||
if CONF.os_region_name:
|
||||
attr = 'region'
|
||||
filter_value = CONF.os_region_name
|
||||
else:
|
||||
attr = None
|
||||
filter_value = None
|
||||
url = sc.url_for(attr=attr,
|
||||
filter_value=filter_value,
|
||||
service_type=service_type,
|
||||
service_name=service_name,
|
||||
endpoint_type=endpoint_type)
|
||||
|
||||
LOG.debug(_('Novaclient connection created using URL: %s') % url)
|
||||
|
||||
extensions = [assisted_volume_snapshots]
|
||||
|
||||
c = nova_client.Client(context.user_id,
|
||||
context.auth_token,
|
||||
context.project_id,
|
||||
auth_url=url,
|
||||
insecure=CONF.nova_api_insecure,
|
||||
cacert=CONF.nova_ca_certificates_file,
|
||||
extensions=extensions)
|
||||
# noauth extracts user_id:project_id from auth_token
|
||||
c.client.auth_token = context.auth_token or '%s:%s' % (context.user_id,
|
||||
context.project_id)
|
||||
c.client.management_url = url
|
||||
return c
|
||||
|
||||
|
||||
def _untranslate_server_summary_view(server):
|
||||
"""Maps keys for servers summary view."""
|
||||
d = {}
|
||||
d['id'] = server.id
|
||||
d['status'] = server.status
|
||||
d['flavor'] = server.flavor['id']
|
||||
d['name'] = server.name
|
||||
d['image'] = server.image['id']
|
||||
d['created'] = server.created
|
||||
d['addresses'] = server.addresses
|
||||
d['networks'] = server.networks
|
||||
d['tenant_id'] = server.tenant_id
|
||||
d['user_id'] = server.user_id
|
||||
|
||||
return d
|
||||
|
||||
|
||||
def translate_server_exception(method):
|
||||
"""Transforms the exception for the instance but keeps its traceback
|
||||
intact.
|
||||
"""
|
||||
def wrapper(self, ctx, instance_id, *args, **kwargs):
|
||||
try:
|
||||
res = method(self, ctx, instance_id, *args, **kwargs)
|
||||
except nova_exception.ClientException:
|
||||
exc_type, exc_value, exc_trace = sys.exc_info()
|
||||
if isinstance(exc_value, nova_exception.NotFound):
|
||||
exc_value = exception.InstanceNotFound(instance_id=instance_id)
|
||||
elif isinstance(exc_value, nova_exception.BadRequest):
|
||||
exc_value = exception.InvalidInput(reason=exc_value.message)
|
||||
raise exc_value, None, exc_trace
|
||||
return res
|
||||
return wrapper
|
||||
|
||||
|
||||
class API(base.Base):
|
||||
"""API for interacting with novaclient."""
|
||||
|
||||
def server_create(self, context, name, image, flavor, key_name, user_data,
|
||||
security_groups, block_device_mapping=None,
|
||||
block_device_mapping_v2=None, nics=None,
|
||||
availability_zone=None, instance_count=1,
|
||||
admin_pass=None):
|
||||
return _untranslate_server_summary_view(
|
||||
novaclient(context).servers.create(
|
||||
name, image, flavor, userdata=user_data,
|
||||
security_groups=security_groups, key_name=key_name,
|
||||
block_device_mapping=block_device_mapping,
|
||||
block_device_mapping_v2=block_device_mapping_v2,
|
||||
nics=nics, availability_zone=availability_zone,
|
||||
min_count=instance_count, admin_pass=admin_pass)
|
||||
)
|
||||
|
||||
def server_delete(self, context, instance):
|
||||
novaclient(context).servers.delete(instance)
|
||||
|
||||
@translate_server_exception
|
||||
def server_get(self, context, instance_id):
|
||||
return _untranslate_server_summary_view(
|
||||
novaclient(context).servers.get(instance_id)
|
||||
)
|
||||
|
||||
def server_list(self, context, search_opts=None, all_tenants=False):
|
||||
if search_opts is None:
|
||||
search_opts = {}
|
||||
if all_tenants:
|
||||
search_opts['all_tenants'] = True
|
||||
else:
|
||||
search_opts['project_id'] = context.project_id
|
||||
servers = [_untranslate_server_summary_view(s)
|
||||
for s in novaclient(context).servers.list(True, search_opts)]
|
||||
|
||||
return servers
|
||||
|
||||
@translate_server_exception
|
||||
def server_pause(self, context, instance_id):
|
||||
novaclient(context).servers.pause(instance_id)
|
||||
|
||||
@translate_server_exception
|
||||
def server_unpause(self, context, instance_id):
|
||||
novaclient(context).servers.unpause(instance_id)
|
||||
|
||||
@translate_server_exception
|
||||
def server_suspend(self, context, instance_id):
|
||||
novaclient(context).servers.suspend(instance_id)
|
||||
|
||||
@translate_server_exception
|
||||
def server_resume(self, context, instance_id):
|
||||
novaclient(context).servers.resume(instance_id)
|
||||
|
||||
@translate_server_exception
|
||||
def server_reboot(self, context, instance_id, soft_reboot=False):
|
||||
hardness = nova_servers.REBOOT_HARD
|
||||
if soft_reboot:
|
||||
hardness = nova_servers.REBOOT_SOFT
|
||||
novaclient(context).servers.reboot(instance_id, hardness)
|
||||
|
||||
@translate_server_exception
|
||||
def server_rebuild(self, context, instance_id, image_id, password=None):
|
||||
return _untranslate_server_summary_view(
|
||||
novaclient(context).servers.rebuild(instance_id, image_id,
|
||||
password)
|
||||
)
|
||||
|
||||
@translate_server_exception
|
||||
def instance_volume_attach(self, context, instance_id, volume_id, device):
|
||||
return novaclient(context).volumes.create_server_volume(instance_id,
|
||||
volume_id,
|
||||
device)
|
||||
|
||||
@translate_server_exception
|
||||
def instance_volume_detach(self, context, instance_id, att_id):
|
||||
return novaclient(context).volumes.delete_server_volume(instance_id,
|
||||
att_id)
|
||||
|
||||
@translate_server_exception
|
||||
def instance_volumes_list(self, context, instance_id):
|
||||
from manila.volume.cinder import cinderclient
|
||||
|
||||
volumes = novaclient(context).volumes.get_server_volumes(instance_id)
|
||||
|
||||
for volume in volumes:
|
||||
volume_data = cinderclient(context).volumes.get(volume.id)
|
||||
volume.name = volume_data.display_name
|
||||
|
||||
return volumes
|
||||
|
||||
@translate_server_exception
|
||||
def server_update(self, context, instance_id, name):
|
||||
return _untranslate_server_summary_view(
|
||||
novaclient(context).servers.update(instance_id, name=name)
|
||||
)
|
||||
|
||||
def update_server_volume(self, context, instance_id, attachment_id,
|
||||
new_volume_id):
|
||||
novaclient(context).volumes.update_server_volume(instance_id,
|
||||
attachment_id,
|
||||
new_volume_id)
|
||||
|
||||
def keypair_create(self, context, name):
|
||||
return novaclient(context).keypairs.create(name)
|
||||
|
||||
def keypair_import(self, context, name, public_key):
|
||||
return novaclient(context).keypairs.create(name, public_key)
|
||||
|
||||
def keypair_delete(self, context, keypair_id):
|
||||
novaclient(context).keypairs.delete(keypair_id)
|
||||
|
||||
def keypair_list(self, context):
|
||||
return novaclient(context).keypairs.list()
|
||||
|
||||
def image_list(self, context):
|
||||
return novaclient(context).images.list()
|
|
@ -537,3 +537,7 @@ class VolumeNotFound(NotFound):
|
|||
|
||||
class VolumeSnapshotNotFound(NotFound):
|
||||
message = _("Snapshot %(snapshot_id)s could not be found.")
|
||||
|
||||
|
||||
class InstanceNotFound(NotFound):
|
||||
message = _("Instance %(instance_id)s could not be found.")
|
||||
|
|
|
@ -0,0 +1,201 @@
|
|||
# Copyright 2014 Mirantis Inc.
|
||||
#
|
||||
# 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 mock
|
||||
|
||||
from manila.compute import nova
|
||||
from manila import context
|
||||
from manila import exception
|
||||
from manila import test
|
||||
from manila.volume import cinder
|
||||
from novaclient import exceptions as nova_exception
|
||||
from novaclient.v1_1 import servers as nova_servers
|
||||
|
||||
|
||||
class Volume(object):
|
||||
def __init__(self, volume_id):
|
||||
self.id = volume_id
|
||||
self.display_name = volume_id
|
||||
|
||||
|
||||
class FakeNovaClient(object):
|
||||
class Servers(object):
|
||||
def get(self, instance_id):
|
||||
return {'id': instance_id}
|
||||
|
||||
def list(self, *args, **kwargs):
|
||||
return [{'id': 'id1'}, {'id': 'id2'}]
|
||||
|
||||
def create(self, *args, **kwargs):
|
||||
return {'id': 'created_id'}
|
||||
|
||||
def __getattr__(self, item):
|
||||
return None
|
||||
|
||||
class Volumes(object):
|
||||
def get(self, volume_id):
|
||||
return Volume(volume_id)
|
||||
|
||||
def list(self, detailed, *args, **kwargs):
|
||||
return [{'id': 'id1'}, {'id': 'id2'}]
|
||||
|
||||
def create(self, *args, **kwargs):
|
||||
return {'id': 'created_id'}
|
||||
|
||||
def __getattr__(self, item):
|
||||
return None
|
||||
|
||||
def __init__(self):
|
||||
self.servers = self.Servers()
|
||||
self.volumes = self.Volumes()
|
||||
self.keypairs = self.servers
|
||||
|
||||
|
||||
class NovaApiTestCase(test.TestCase):
|
||||
def setUp(self):
|
||||
super(NovaApiTestCase, self).setUp()
|
||||
|
||||
self.api = nova.API()
|
||||
self.novaclient = FakeNovaClient()
|
||||
self.ctx = context.get_admin_context()
|
||||
self.stubs.Set(nova, 'novaclient',
|
||||
mock.Mock(return_value=self.novaclient))
|
||||
self.stubs.Set(nova, '_untranslate_server_summary_view',
|
||||
lambda server: server)
|
||||
|
||||
def test_server_create(self):
|
||||
result = self.api.server_create(self.ctx, 'server_name', 'fake_image',
|
||||
'fake_flavor', None, None, None)
|
||||
self.assertEqual(result['id'], 'created_id')
|
||||
|
||||
def test_server_delete(self):
|
||||
self.stubs.Set(self.novaclient.servers, 'delete', mock.Mock())
|
||||
self.api.server_delete(self.ctx, 'id1')
|
||||
self.novaclient.servers.delete.assert_called_once_with('id1')
|
||||
|
||||
def test_server_get(self):
|
||||
instance_id = 'instance_id1'
|
||||
result = self.api.server_get(self.ctx, instance_id)
|
||||
self.assertEqual(result['id'], instance_id)
|
||||
|
||||
def test_server_get_failed(self):
|
||||
nova.novaclient.side_effect = nova_exception.NotFound(404)
|
||||
instance_id = 'instance_id'
|
||||
self.assertRaises(exception.InstanceNotFound,
|
||||
self.api.server_get, self.ctx, instance_id)
|
||||
|
||||
def test_server_list(self):
|
||||
self.assertEqual([{'id': 'id1'}, {'id': 'id2'}],
|
||||
self.api.server_list(self.ctx))
|
||||
|
||||
def test_server_pause(self):
|
||||
self.stubs.Set(self.novaclient.servers, 'pause', mock.Mock())
|
||||
self.api.server_pause(self.ctx, 'id1')
|
||||
self.novaclient.servers.pause.assert_called_once_with('id1')
|
||||
|
||||
def test_server_unpause(self):
|
||||
self.stubs.Set(self.novaclient.servers, 'unpause', mock.Mock())
|
||||
self.api.server_unpause(self.ctx, 'id1')
|
||||
self.novaclient.servers.unpause.assert_called_once_with('id1')
|
||||
|
||||
def test_server_suspend(self):
|
||||
self.stubs.Set(self.novaclient.servers, 'suspend', mock.Mock())
|
||||
self.api.server_suspend(self.ctx, 'id1')
|
||||
self.novaclient.servers.suspend.assert_called_once_with('id1')
|
||||
|
||||
def test_server_resume(self):
|
||||
self.stubs.Set(self.novaclient.servers, 'resume', mock.Mock())
|
||||
self.api.server_resume(self.ctx, 'id1')
|
||||
self.novaclient.servers.resume.assert_called_once_with('id1')
|
||||
|
||||
def test_server_reboot_hard(self):
|
||||
self.stubs.Set(self.novaclient.servers, 'reboot', mock.Mock())
|
||||
self.api.server_reboot(self.ctx, 'id1')
|
||||
self.novaclient.servers.reboot.assert_called_once_with('id1',
|
||||
nova_servers.REBOOT_HARD)
|
||||
|
||||
def test_server_reboot_soft(self):
|
||||
self.stubs.Set(self.novaclient.servers, 'reboot', mock.Mock())
|
||||
self.api.server_reboot(self.ctx, 'id1', True)
|
||||
self.novaclient.servers.reboot.assert_called_once_with('id1',
|
||||
nova_servers.REBOOT_SOFT)
|
||||
|
||||
def test_server_rebuild(self):
|
||||
self.stubs.Set(self.novaclient.servers, 'rebuild', mock.Mock())
|
||||
self.api.server_rebuild(self.ctx, 'id1', 'fake_image')
|
||||
self.novaclient.servers.rebuild.assert_called_once_with('id1',
|
||||
'fake_image',
|
||||
None)
|
||||
|
||||
def test_instance_volume_attach(self):
|
||||
self.stubs.Set(self.novaclient.volumes, 'create_server_volume',
|
||||
mock.Mock())
|
||||
self.api.instance_volume_attach(self.ctx, 'instance_id',
|
||||
'vol_id', 'device')
|
||||
self.novaclient.volumes.create_server_volume.\
|
||||
assert_called_once_with('instance_id', 'vol_id', 'device')
|
||||
|
||||
def test_instance_volume_detach(self):
|
||||
self.stubs.Set(self.novaclient.volumes, 'delete_server_volume',
|
||||
mock.Mock())
|
||||
self.api.instance_volume_detach(self.ctx, 'instance_id',
|
||||
'att_id')
|
||||
self.novaclient.volumes.delete_server_volume.\
|
||||
assert_called_once_with('instance_id', 'att_id')
|
||||
|
||||
def test_instance_volumes_list(self):
|
||||
self.stubs.Set(self.novaclient.volumes, 'get_server_volumes',
|
||||
mock.Mock(return_value=[Volume('id1'), Volume('id2')]))
|
||||
self.cinderclient = self.novaclient
|
||||
self.stubs.Set(cinder, 'cinderclient',
|
||||
mock.Mock(return_value=self.novaclient))
|
||||
result = self.api.instance_volumes_list(self.ctx, 'instance_id')
|
||||
self.assertEqual(len(result), 2)
|
||||
self.assertEqual(result[0].id, 'id1')
|
||||
self.assertEqual(result[1].id, 'id2')
|
||||
|
||||
def test_server_update(self):
|
||||
self.stubs.Set(self.novaclient.servers, 'update', mock.Mock())
|
||||
self.api.server_update(self.ctx, 'id1', 'new_name')
|
||||
self.novaclient.servers.update.assert_called_once_with('id1',
|
||||
name='new_name')
|
||||
|
||||
def test_update_server_volume(self):
|
||||
self.stubs.Set(self.novaclient.volumes, 'update_server_volume',
|
||||
mock.Mock())
|
||||
self.api.update_server_volume(self.ctx, 'instance_id', 'att_id',
|
||||
'new_vol_id')
|
||||
self.novaclient.volumes.update_server_volume.\
|
||||
assert_called_once_with('instance_id', 'att_id', 'new_vol_id')
|
||||
|
||||
def test_keypair_create(self):
|
||||
self.stubs.Set(self.novaclient.keypairs, 'create', mock.Mock())
|
||||
self.api.keypair_create(self.ctx, 'keypair_name')
|
||||
self.novaclient.keypairs.create.assert_called_once_with('keypair_name')
|
||||
|
||||
def test_keypair_import(self):
|
||||
self.stubs.Set(self.novaclient.keypairs, 'create', mock.Mock())
|
||||
self.api.keypair_import(self.ctx, 'keypair_name', 'fake_pub_key')
|
||||
self.novaclient.keypairs.create.\
|
||||
assert_called_once_with('keypair_name', 'fake_pub_key')
|
||||
|
||||
def test_keypair_delete(self):
|
||||
self.stubs.Set(self.novaclient.keypairs, 'delete', mock.Mock())
|
||||
self.api.keypair_delete(self.ctx, 'fake_keypair_id')
|
||||
self.novaclient.keypairs.delete.\
|
||||
assert_called_once_with('fake_keypair_id')
|
||||
|
||||
def test_keypair_list(self):
|
||||
self.assertEqual([{'id': 'id1'}, {'id': 'id2'}],
|
||||
self.api.keypair_list(self.ctx))
|
|
@ -22,6 +22,7 @@ SQLAlchemy>=0.7.8,<=0.7.99
|
|||
sqlalchemy-migrate>=0.7.2
|
||||
stevedore>=0.10
|
||||
python-cinderclient>=1.0.6
|
||||
python-novaclient>=2.15.0
|
||||
suds>=0.4
|
||||
WebOb>=1.2.3,<1.3
|
||||
wsgiref>=0.1.2
|
||||
|
|
Loading…
Reference in New Issue