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:
Andrei V. Ostapenko 2014-01-09 13:20:40 +02:00
parent fbde9ae88f
commit 9a7e113b3a
6 changed files with 511 additions and 0 deletions

View File

@ -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()

271
manila/compute/nova.py Normal file
View File

@ -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()

View File

@ -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.")

View File

View File

@ -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))

View File

@ -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