summaryrefslogtreecommitdiffstats
path: root/nova
diff options
context:
space:
mode:
authorEd Leafe <ed@leafe.com>2011-02-17 22:09:26 +0000
committerEd Leafe <ed@leafe.com>2011-02-17 22:09:26 +0000
commitc0972233901774598fe6c836fcc3a0dd1f28f180 (patch)
treec3fe737f4e84031ffdd6316cdeb645d725ef6c0e /nova
parent396b02f876030f1f54b7af32cf94fccbbe1fe46b (diff)
parent52753aae3486f654b9fb19d6423fc26dc180644d (diff)
downloadnova-c0972233901774598fe6c836fcc3a0dd1f28f180.tar.gz
nova-c0972233901774598fe6c836fcc3a0dd1f28f180.tar.xz
nova-c0972233901774598fe6c836fcc3a0dd1f28f180.zip
Uncommitted changes using the wrong author, and re-committing under the correct author
Diffstat (limited to 'nova')
-rw-r--r--nova/adminclient.py20
-rw-r--r--nova/api/ec2/__init__.py22
-rw-r--r--nova/api/ec2/admin.py11
-rw-r--r--nova/api/ec2/cloud.py33
-rw-r--r--nova/api/openstack/__init__.py5
-rw-r--r--nova/api/openstack/common.py33
-rw-r--r--nova/api/openstack/servers.py33
-rw-r--r--nova/auth/ldapdriver.py37
-rw-r--r--nova/compute/api.py34
-rw-r--r--nova/compute/instance_types.py4
-rw-r--r--nova/compute/manager.py75
-rw-r--r--nova/compute/power_state.py4
-rw-r--r--nova/context.py5
-rw-r--r--nova/db/api.py20
-rw-r--r--nova/db/sqlalchemy/api.py68
-rw-r--r--nova/db/sqlalchemy/migrate_repo/versions/001_austin.py36
-rw-r--r--nova/db/sqlalchemy/migrate_repo/versions/002_bexar.py27
-rw-r--r--nova/db/sqlalchemy/migrate_repo/versions/003_add_label_to_networks.py51
-rw-r--r--nova/db/sqlalchemy/migration.py23
-rw-r--r--nova/db/sqlalchemy/models.py7
-rw-r--r--nova/db/sqlalchemy/session.py10
-rw-r--r--nova/flags.py8
-rw-r--r--nova/image/s3.py28
-rw-r--r--nova/log.py28
-rw-r--r--nova/network/linux_net.py88
-rw-r--r--nova/network/manager.py20
-rw-r--r--nova/rpc.py15
-rw-r--r--nova/tests/api/openstack/__init__.py28
-rw-r--r--nova/tests/api/openstack/test_common.py161
-rw-r--r--nova/tests/api/openstack/test_servers.py78
-rw-r--r--nova/tests/db/nova.austin.sqlitebin0 -> 44032 bytes
-rw-r--r--nova/tests/test_api.py35
-rw-r--r--nova/tests/test_compute.py63
-rw-r--r--nova/tests/test_log.py21
-rw-r--r--nova/tests/test_xenapi.py17
-rw-r--r--nova/twistd.py2
-rw-r--r--nova/utils.py61
-rw-r--r--nova/version.py2
-rw-r--r--nova/virt/fake.py15
-rw-r--r--nova/virt/xenapi/fake.py4
-rw-r--r--nova/virt/xenapi/vm_utils.py20
-rw-r--r--nova/virt/xenapi/vmops.py142
-rw-r--r--nova/virt/xenapi_conn.py10
-rw-r--r--nova/volume/api.py2
-rw-r--r--nova/volume/driver.py10
-rw-r--r--nova/volume/manager.py10
-rw-r--r--nova/volume/san.py335
47 files changed, 1525 insertions, 236 deletions
diff --git a/nova/adminclient.py b/nova/adminclient.py
index 3cdd8347f..c614b274c 100644
--- a/nova/adminclient.py
+++ b/nova/adminclient.py
@@ -21,6 +21,7 @@ Nova User API client library.
import base64
import boto
+import boto.exception
import httplib
from boto.ec2.regioninfo import RegionInfo
@@ -288,10 +289,14 @@ class NovaAdminClient(object):
def get_user(self, name):
"""Grab a single user by name."""
- user = self.apiconn.get_object('DescribeUser', {'Name': name},
- UserInfo)
- if user.username != None:
- return user
+ try:
+ return self.apiconn.get_object('DescribeUser',
+ {'Name': name},
+ UserInfo)
+ except boto.exception.BotoServerError, e:
+ if e.status == 400 and e.error_code == 'NotFound':
+ return None
+ raise
def has_user(self, username):
"""Determine if user exists."""
@@ -376,6 +381,13 @@ class NovaAdminClient(object):
'MemberUsers': member_users}
return self.apiconn.get_object('RegisterProject', params, ProjectInfo)
+ def modify_project(self, projectname, manager_user=None, description=None):
+ """Modifies an existing project."""
+ params = {'Name': projectname,
+ 'ManagerUser': manager_user,
+ 'Description': description}
+ return self.apiconn.get_status('ModifyProject', params)
+
def delete_project(self, projectname):
"""Permanently deletes the specified project."""
return self.apiconn.get_object('DeregisterProject',
diff --git a/nova/api/ec2/__init__.py b/nova/api/ec2/__init__.py
index e25943a13..1a06b3f01 100644
--- a/nova/api/ec2/__init__.py
+++ b/nova/api/ec2/__init__.py
@@ -21,7 +21,6 @@ Starting point for routing EC2 requests.
"""
import datetime
-import routes
import webob
import webob.dec
import webob.exc
@@ -171,7 +170,7 @@ class Authenticate(wsgi.Middleware):
req.path)
# Be explicit for what exceptions are 403, the rest bubble as 500
except (exception.NotFound, exception.NotAuthorized) as ex:
- LOG.audit(_("Authentication Failure: %s"), ex.args[0])
+ LOG.audit(_("Authentication Failure: %s"), unicode(ex))
raise webob.exc.HTTPForbidden()
# Authenticated!
@@ -233,7 +232,7 @@ class Authorizer(wsgi.Middleware):
super(Authorizer, self).__init__(application)
self.action_roles = {
'CloudController': {
- 'DescribeAvailabilityzones': ['all'],
+ 'DescribeAvailabilityZones': ['all'],
'DescribeRegions': ['all'],
'DescribeSnapshots': ['all'],
'DescribeKeyPairs': ['all'],
@@ -316,30 +315,31 @@ class Executor(wsgi.Application):
try:
result = api_request.invoke(context)
except exception.InstanceNotFound as ex:
- LOG.info(_('InstanceNotFound raised: %s'), ex.args[0],
+ LOG.info(_('InstanceNotFound raised: %s'), unicode(ex),
context=context)
ec2_id = cloud.id_to_ec2_id(ex.instance_id)
message = _('Instance %s not found') % ec2_id
return self._error(req, context, type(ex).__name__, message)
except exception.VolumeNotFound as ex:
- LOG.info(_('VolumeNotFound raised: %s'), ex.args[0],
+ LOG.info(_('VolumeNotFound raised: %s'), unicode(ex),
context=context)
ec2_id = cloud.id_to_ec2_id(ex.volume_id, 'vol-%08x')
message = _('Volume %s not found') % ec2_id
return self._error(req, context, type(ex).__name__, message)
except exception.NotFound as ex:
- LOG.info(_('NotFound raised: %s'), ex.args[0], context=context)
- return self._error(req, context, type(ex).__name__, ex.args[0])
+ LOG.info(_('NotFound raised: %s'), unicode(ex), context=context)
+ return self._error(req, context, type(ex).__name__, unicode(ex))
except exception.ApiError as ex:
- LOG.exception(_('ApiError raised: %s'), ex.args[0],
+ LOG.exception(_('ApiError raised: %s'), unicode(ex),
context=context)
if ex.code:
- return self._error(req, context, ex.code, ex.args[0])
+ return self._error(req, context, ex.code, unicode(ex))
else:
- return self._error(req, context, type(ex).__name__, ex.args[0])
+ return self._error(req, context, type(ex).__name__,
+ unicode(ex))
except Exception as ex:
extra = {'environment': req.environ}
- LOG.exception(_('Unexpected error raised: %s'), ex.args[0],
+ LOG.exception(_('Unexpected error raised: %s'), unicode(ex),
extra=extra, context=context)
return self._error(req,
context,
diff --git a/nova/api/ec2/admin.py b/nova/api/ec2/admin.py
index d7e899d12..735951082 100644
--- a/nova/api/ec2/admin.py
+++ b/nova/api/ec2/admin.py
@@ -184,6 +184,17 @@ class AdminController(object):
description=None,
member_users=None))
+ def modify_project(self, context, name, manager_user, description=None,
+ **kwargs):
+ """Modifies a project"""
+ msg = _("Modify project: %(name)s managed by"
+ " %(manager_user)s") % locals()
+ LOG.audit(msg, context=context)
+ manager.AuthManager().modify_project(name,
+ manager_user=manager_user,
+ description=description)
+ return True
+
def deregister_project(self, context, name):
"""Permanently deletes a project."""
LOG.audit(_("Delete project: %s"), name, context=context)
diff --git a/nova/api/ec2/cloud.py b/nova/api/ec2/cloud.py
index 00d044e95..6919cd8d2 100644
--- a/nova/api/ec2/cloud.py
+++ b/nova/api/ec2/cloud.py
@@ -327,7 +327,9 @@ class CloudController(object):
if not group_name is None:
groups = [g for g in groups if g.name in group_name]
- return {'securityGroupInfo': groups}
+ return {'securityGroupInfo':
+ list(sorted(groups,
+ key=lambda k: (k['ownerId'], k['groupName'])))}
def _format_security_group(self, context, group):
g = {}
@@ -512,8 +514,11 @@ class CloudController(object):
def get_console_output(self, context, instance_id, **kwargs):
LOG.audit(_("Get console output for instance %s"), instance_id,
context=context)
- # instance_id is passed in as a list of instances
- ec2_id = instance_id[0]
+ # instance_id may be passed in as a list of instances
+ if type(instance_id) == list:
+ ec2_id = instance_id[0]
+ else:
+ ec2_id = instance_id
instance_id = ec2_id_to_id(ec2_id)
output = self.compute_api.get_console_output(
context, instance_id=instance_id)
@@ -836,11 +841,26 @@ class CloudController(object):
self.compute_api.update(context, instance_id=instance_id, **kwargs)
return True
+ def _format_image(self, context, image):
+ """Convert from format defined by BaseImageService to S3 format."""
+ i = {}
+ i['imageId'] = image.get('id')
+ i['kernelId'] = image.get('kernel_id')
+ i['ramdiskId'] = image.get('ramdisk_id')
+ i['imageOwnerId'] = image.get('owner_id')
+ i['imageLocation'] = image.get('location')
+ i['imageState'] = image.get('status')
+ i['type'] = image.get('type')
+ i['isPublic'] = image.get('is_public')
+ i['architecture'] = image.get('architecture')
+ return i
+
def describe_images(self, context, image_id=None, **kwargs):
- # Note: image_id is a list!
+ # NOTE: image_id is a list!
images = self.image_service.index(context)
if image_id:
- images = filter(lambda x: x['imageId'] in image_id, images)
+ images = filter(lambda x: x['id'] in image_id, images)
+ images = [self._format_image(context, i) for i in images]
return {'imagesSet': images}
def deregister_image(self, context, image_id, **kwargs):
@@ -863,6 +883,9 @@ class CloudController(object):
% attribute)
try:
image = self.image_service.show(context, image_id)
+ image = self._format_image(context,
+ self.image_service.show(context,
+ image_id))
except IndexError:
raise exception.ApiError(_('invalid id: %s') % image_id)
result = {'image_id': image_id, 'launchPermission': []}
diff --git a/nova/api/openstack/__init__.py b/nova/api/openstack/__init__.py
index c70bb39ed..dc3738d4a 100644
--- a/nova/api/openstack/__init__.py
+++ b/nova/api/openstack/__init__.py
@@ -51,8 +51,8 @@ class FaultWrapper(wsgi.Middleware):
try:
return req.get_response(self.application)
except Exception as ex:
- LOG.exception(_("Caught error: %s"), str(ex))
- exc = webob.exc.HTTPInternalServerError(explanation=str(ex))
+ LOG.exception(_("Caught error: %s"), unicode(ex))
+ exc = webob.exc.HTTPInternalServerError(explanation=unicode(ex))
return faults.Fault(exc)
@@ -79,6 +79,7 @@ class APIRouter(wsgi.Router):
server_members["actions"] = "GET"
server_members['suspend'] = 'POST'
server_members['resume'] = 'POST'
+ server_members['reset_network'] = 'POST'
mapper.resource("server", "servers", controller=servers.Controller(),
collection={'detail': 'GET'},
diff --git a/nova/api/openstack/common.py b/nova/api/openstack/common.py
index 6d2fa16e8..1dc3767e2 100644
--- a/nova/api/openstack/common.py
+++ b/nova/api/openstack/common.py
@@ -18,22 +18,29 @@
from nova import exception
-def limited(items, req):
- """Return a slice of items according to requested offset and limit.
-
- items - a sliceable
- req - wobob.Request possibly containing offset and limit GET variables.
- offset is where to start in the list, and limit is the maximum number
- of items to return.
+def limited(items, request, max_limit=1000):
+ """
+ Return a slice of items according to requested offset and limit.
- If limit is not specified, 0, or > 1000, defaults to 1000.
+ @param items: A sliceable entity
+ @param request: `webob.Request` possibly containing 'offset' and 'limit'
+ GET variables. 'offset' is where to start in the list,
+ and 'limit' is the maximum number of items to return. If
+ 'limit' is not specified, 0, or > max_limit, we default
+ to max_limit.
+ @kwarg max_limit: The maximum number of items to return from 'items'
"""
+ try:
+ offset = int(request.GET.get('offset', 0))
+ except ValueError:
+ offset = 0
+
+ try:
+ limit = int(request.GET.get('limit', max_limit))
+ except ValueError:
+ limit = max_limit
- offset = int(req.GET.get('offset', 0))
- limit = int(req.GET.get('limit', 0))
- if not limit:
- limit = 1000
- limit = min(1000, limit)
+ limit = min(max_limit, limit or max_limit)
range_end = offset + limit
return items[offset:range_end]
diff --git a/nova/api/openstack/servers.py b/nova/api/openstack/servers.py
index 17c5519a1..bed633da8 100644
--- a/nova/api/openstack/servers.py
+++ b/nova/api/openstack/servers.py
@@ -64,6 +64,22 @@ def _translate_detail_keys(inst):
inst_dict['status'] = power_mapping[inst_dict['status']]
inst_dict['addresses'] = dict(public=[], private=[])
+
+ # grab single private fixed ip
+ try:
+ private_ip = inst['fixed_ip']['address']
+ if private_ip:
+ inst_dict['addresses']['private'].append(private_ip)
+ except KeyError:
+ LOG.debug(_("Failed to read private ip"))
+
+ # grab all public floating ips
+ try:
+ for floating in inst['fixed_ip']['floating_ips']:
+ inst_dict['addresses']['public'].append(floating['address'])
+ except KeyError:
+ LOG.debug(_("Failed to read public ip(s)"))
+
inst_dict['metadata'] = {}
inst_dict['hostId'] = ''
@@ -163,7 +179,8 @@ class Controller(wsgi.Controller):
display_name=env['server']['name'],
display_description=env['server']['name'],
key_name=key_pair['name'],
- key_data=key_pair['public_key'])
+ key_data=key_pair['public_key'],
+ onset_files=env.get('onset_files', []))
return _translate_keys(instances[0])
def update(self, req, id):
@@ -249,6 +266,20 @@ class Controller(wsgi.Controller):
return faults.Fault(exc.HTTPUnprocessableEntity())
return exc.HTTPAccepted()
+ def reset_network(self, req, id):
+ """
+ Reset networking on an instance (admin only).
+
+ """
+ context = req.environ['nova.context']
+ try:
+ self.compute_api.reset_network(context, id)
+ except:
+ readable = traceback.format_exc()
+ LOG.exception(_("Compute.api::reset_network %s"), readable)
+ return faults.Fault(exc.HTTPUnprocessableEntity())
+ return exc.HTTPAccepted()
+
def pause(self, req, id):
""" Permit Admins to Pause the server. """
ctxt = req.environ['nova.context']
diff --git a/nova/auth/ldapdriver.py b/nova/auth/ldapdriver.py
index e652f1caa..5da7751a0 100644
--- a/nova/auth/ldapdriver.py
+++ b/nova/auth/ldapdriver.py
@@ -74,6 +74,25 @@ LOG = logging.getLogger("nova.ldapdriver")
# in which we may want to change the interface a bit more.
+def _clean(attr):
+ """Clean attr for insertion into ldap"""
+ if attr is None:
+ return None
+ if type(attr) is unicode:
+ return str(attr)
+ return attr
+
+
+def sanitize(fn):
+ """Decorator to sanitize all args"""
+ def _wrapped(self, *args, **kwargs):
+ args = [_clean(x) for x in args]
+ kwargs = dict((k, _clean(v)) for (k, v) in kwargs)
+ return fn(self, *args, **kwargs)
+ _wrapped.func_name = fn.func_name
+ return _wrapped
+
+
class LdapDriver(object):
"""Ldap Auth driver
@@ -106,23 +125,27 @@ class LdapDriver(object):
self.conn.unbind_s()
return False
+ @sanitize
def get_user(self, uid):
"""Retrieve user by id"""
attr = self.__get_ldap_user(uid)
return self.__to_user(attr)
+ @sanitize
def get_user_from_access_key(self, access):
"""Retrieve user by access key"""
query = '(accessKey=%s)' % access
dn = FLAGS.ldap_user_subtree
return self.__to_user(self.__find_object(dn, query))
+ @sanitize
def get_project(self, pid):
"""Retrieve project by id"""
dn = self.__project_to_dn(pid)
attr = self.__find_object(dn, LdapDriver.project_pattern)
return self.__to_project(attr)
+ @sanitize
def get_users(self):
"""Retrieve list of users"""
attrs = self.__find_objects(FLAGS.ldap_user_subtree,
@@ -134,6 +157,7 @@ class LdapDriver(object):
users.append(user)
return users
+ @sanitize
def get_projects(self, uid=None):
"""Retrieve list of projects"""
pattern = LdapDriver.project_pattern
@@ -143,6 +167,7 @@ class LdapDriver(object):
pattern)
return [self.__to_project(attr) for attr in attrs]
+ @sanitize
def create_user(self, name, access_key, secret_key, is_admin):
"""Create a user"""
if self.__user_exists(name):
@@ -196,6 +221,7 @@ class LdapDriver(object):
self.conn.add_s(self.__uid_to_dn(name), attr)
return self.__to_user(dict(attr))
+ @sanitize
def create_project(self, name, manager_uid,
description=None, member_uids=None):
"""Create a project"""
@@ -231,6 +257,7 @@ class LdapDriver(object):
self.conn.add_s(dn, attr)
return self.__to_project(dict(attr))
+ @sanitize
def modify_project(self, project_id, manager_uid=None, description=None):
"""Modify an existing project"""
if not manager_uid and not description:
@@ -249,21 +276,25 @@ class LdapDriver(object):
dn = self.__project_to_dn(project_id)
self.conn.modify_s(dn, attr)
+ @sanitize
def add_to_project(self, uid, project_id):
"""Add user to project"""
dn = self.__project_to_dn(project_id)
return self.__add_to_group(uid, dn)
+ @sanitize
def remove_from_project(self, uid, project_id):
"""Remove user from project"""
dn = self.__project_to_dn(project_id)
return self.__remove_from_group(uid, dn)
+ @sanitize
def is_in_project(self, uid, project_id):
"""Check if user is in project"""
dn = self.__project_to_dn(project_id)
return self.__is_in_group(uid, dn)
+ @sanitize
def has_role(self, uid, role, project_id=None):
"""Check if user has role
@@ -273,6 +304,7 @@ class LdapDriver(object):
role_dn = self.__role_to_dn(role, project_id)
return self.__is_in_group(uid, role_dn)
+ @sanitize
def add_role(self, uid, role, project_id=None):
"""Add role for user (or user and project)"""
role_dn = self.__role_to_dn(role, project_id)
@@ -283,11 +315,13 @@ class LdapDriver(object):
else:
return self.__add_to_group(uid, role_dn)
+ @sanitize
def remove_role(self, uid, role, project_id=None):
"""Remove role for user (or user and project)"""
role_dn = self.__role_to_dn(role, project_id)
return self.__remove_from_group(uid, role_dn)
+ @sanitize
def get_user_roles(self, uid, project_id=None):
"""Retrieve list of roles for user (or user and project)"""
if project_id is None:
@@ -307,6 +341,7 @@ class LdapDriver(object):
roles = self.__find_objects(project_dn, query)
return [role['cn'][0] for role in roles]
+ @sanitize
def delete_user(self, uid):
"""Delete a user"""
if not self.__user_exists(uid):
@@ -332,12 +367,14 @@ class LdapDriver(object):
# Delete entry
self.conn.delete_s(self.__uid_to_dn(uid))
+ @sanitize
def delete_project(self, project_id):
"""Delete a project"""
project_dn = self.__project_to_dn(project_id)
self.__delete_roles(project_dn)
self.__delete_group(project_dn)
+ @sanitize
def modify_user(self, uid, access_key=None, secret_key=None, admin=None):
"""Modify an existing user"""
if not access_key and not secret_key and admin is None:
diff --git a/nova/compute/api.py b/nova/compute/api.py
index ac02dbcfa..0d2690c72 100644
--- a/nova/compute/api.py
+++ b/nova/compute/api.py
@@ -67,10 +67,10 @@ class API(base.Base):
"""Get the network topic for an instance."""
try:
instance = self.get(context, instance_id)
- except exception.NotFound as e:
+ except exception.NotFound:
LOG.warning(_("Instance %d was not found in get_network_topic"),
instance_id)
- raise e
+ raise
host = instance['host']
if not host:
@@ -85,10 +85,11 @@ class API(base.Base):
min_count=1, max_count=1,
display_name='', display_description='',
key_name=None, key_data=None, security_group='default',
- availability_zone=None, user_data=None):
+ availability_zone=None, user_data=None,
+ onset_files=None):
"""Create the number of instances requested if quota and
- other arguments check out ok."""
-
+ other arguments check out ok.
+ """
type_data = instance_types.INSTANCE_TYPES[instance_type]
num_instances = quota.allowed_instances(context, max_count, type_data)
if num_instances < min_count:
@@ -103,9 +104,9 @@ class API(base.Base):
if not is_vpn:
image = self.image_service.show(context, image_id)
if kernel_id is None:
- kernel_id = image.get('kernelId', None)
+ kernel_id = image.get('kernel_id', None)
if ramdisk_id is None:
- ramdisk_id = image.get('ramdiskId', None)
+ ramdisk_id = image.get('ramdisk_id', None)
# No kernel and ramdisk for raw images
if kernel_id == str(FLAGS.null_kernel):
kernel_id = None
@@ -156,7 +157,6 @@ class API(base.Base):
'key_data': key_data,
'locked': False,
'availability_zone': availability_zone}
-
elevated = context.elevated()
instances = []
LOG.debug(_("Going to run %s instances..."), num_instances)
@@ -193,7 +193,8 @@ class API(base.Base):
{"method": "run_instance",
"args": {"topic": FLAGS.compute_topic,
"instance_id": instance_id,
- "availability_zone": availability_zone}})
+ "availability_zone": availability_zone,
+ "onset_files": onset_files}})
for group_id in security_groups:
self.trigger_security_group_members_refresh(elevated, group_id)
@@ -293,10 +294,10 @@ class API(base.Base):
LOG.debug(_("Going to try to terminate %s"), instance_id)
try:
instance = self.get(context, instance_id)
- except exception.NotFound as e:
+ except exception.NotFound:
LOG.warning(_("Instance %d was not found during terminate"),
instance_id)
- raise e
+ raise
if (instance['state_description'] == 'terminating'):
LOG.warning(_("Instance %d is already being terminated"),
@@ -434,6 +435,10 @@ class API(base.Base):
"""Set the root/admin password for the given instance."""
self._cast_compute_message('set_admin_password', context, instance_id)
+ def inject_file(self, context, instance_id):
+ """Write a file to the given instance."""
+ self._cast_compute_message('inject_file', context, instance_id)
+
def get_ajax_console(self, context, instance_id):
"""Get a url to an AJAX Console"""
instance = self.get(context, instance_id)
@@ -466,6 +471,13 @@ class API(base.Base):
instance = self.get(context, instance_id)
return instance['locked']
+ def reset_network(self, context, instance_id):
+ """
+ Reset networking on the instance.
+
+ """
+ self._cast_compute_message('reset_network', context, instance_id)
+
def attach_volume(self, context, instance_id, volume_id, device):
if not re.match("^/dev/[a-z]d[a-z]+$", device):
raise exception.ApiError(_("Invalid device specified: %s. "
diff --git a/nova/compute/instance_types.py b/nova/compute/instance_types.py
index 196d6a8df..309313fd0 100644
--- a/nova/compute/instance_types.py
+++ b/nova/compute/instance_types.py
@@ -38,8 +38,8 @@ def get_by_type(instance_type):
if instance_type is None:
return FLAGS.default_instance_type
if instance_type not in INSTANCE_TYPES:
- raise exception.ApiError(_("Unknown instance type: %s"),
- instance_type)
+ raise exception.ApiError(_("Unknown instance type: %s") % \
+ instance_type, "Invalid")
return instance_type
diff --git a/nova/compute/manager.py b/nova/compute/manager.py
index f4418af26..b8d4b7ee9 100644
--- a/nova/compute/manager.py
+++ b/nova/compute/manager.py
@@ -34,6 +34,7 @@ terminating it.
:func:`nova.utils.import_object`
"""
+import base64
import datetime
import random
import string
@@ -127,10 +128,10 @@ class ComputeManager(manager.Manager):
info = self.driver.get_info(instance_ref['name'])
state = info['state']
except exception.NotFound:
- state = power_state.NOSTATE
+ state = power_state.FAILED
self.db.instance_set_state(context, instance_id, state)
- def get_console_topic(self, context, **_kwargs):
+ def get_console_topic(self, context, **kwargs):
"""Retrieves the console host for a project on this host
Currently this is just set in the flags for each compute
host."""
@@ -139,7 +140,7 @@ class ComputeManager(manager.Manager):
FLAGS.console_topic,
FLAGS.console_host)
- def get_network_topic(self, context, **_kwargs):
+ def get_network_topic(self, context, **kwargs):
"""Retrieves the network host for a project on this host"""
# TODO(vish): This method should be memoized. This will make
# the call to get_network_host cheaper, so that
@@ -158,21 +159,22 @@ class ComputeManager(manager.Manager):
@exception.wrap_exception
def refresh_security_group_rules(self, context,
- security_group_id, **_kwargs):
+ security_group_id, **kwargs):
"""This call passes straight through to the virtualization driver."""
return self.driver.refresh_security_group_rules(security_group_id)
@exception.wrap_exception
def refresh_security_group_members(self, context,
- security_group_id, **_kwargs):
+ security_group_id, **kwargs):
"""This call passes straight through to the virtualization driver."""
return self.driver.refresh_security_group_members(security_group_id)
@exception.wrap_exception
- def run_instance(self, context, instance_id, **_kwargs):
+ def run_instance(self, context, instance_id, **kwargs):
"""Launch a new instance with specified options."""
context = context.elevated()
instance_ref = self.db.instance_get(context, instance_id)
+ instance_ref.onset_files = kwargs.get('onset_files', [])
if instance_ref['name'] in self.driver.list_instances():
raise exception.Error(_("Instance has already been created"))
LOG.audit(_("instance %s: starting..."), instance_id,
@@ -323,28 +325,43 @@ class ComputeManager(manager.Manager):
"""Set the root/admin password for an instance on this server."""
context = context.elevated()
instance_ref = self.db.instance_get(context, instance_id)
- if instance_ref['state'] != power_state.RUNNING:
- logging.warn('trying to reset the password on a non-running '
- 'instance: %s (state: %s expected: %s)',
- instance_ref['id'],
- instance_ref['state'],
- power_state.RUNNING)
-
- logging.debug('instance %s: setting admin password',
+ instance_id = instance_ref['id']
+ instance_state = instance_ref['state']
+ expected_state = power_state.RUNNING
+ if instance_state != expected_state:
+ LOG.warn(_('trying to reset the password on a non-running '
+ 'instance: %(instance_id)s (state: %(instance_state)s '
+ 'expected: %(expected_state)s)') % locals())
+ LOG.audit(_('instance %s: setting admin password'),
instance_ref['name'])
if new_pass is None:
# Generate a random password
- new_pass = self._generate_password(FLAGS.password_length)
-
+ new_pass = utils.generate_password(FLAGS.password_length)
self.driver.set_admin_password(instance_ref, new_pass)
self._update_state(context, instance_id)
- def _generate_password(self, length=20):
- """Generate a random sequence of letters and digits
- to be used as a password.
- """
- chrs = string.letters + string.digits
- return "".join([random.choice(chrs) for i in xrange(length)])
+ @exception.wrap_exception
+ @checks_instance_lock
+ def inject_file(self, context, instance_id, path, file_contents):
+ """Write a file to the specified path on an instance on this server"""
+ context = context.elevated()
+ instance_ref = self.db.instance_get(context, instance_id)
+ instance_id = instance_ref['id']
+ instance_state = instance_ref['state']
+ expected_state = power_state.RUNNING
+ if instance_state != expected_state:
+ LOG.warn(_('trying to inject a file into a non-running '
+ 'instance: %(instance_id)s (state: %(instance_state)s '
+ 'expected: %(expected_state)s)') % locals())
+ # Files/paths *should* be base64-encoded at this point, but
+ # double-check to make sure.
+ b64_path = utils.ensure_b64_encoding(path)
+ b64_contents = utils.ensure_b64_encoding(file_contents)
+ plain_path = base64.b64decode(b64_path)
+ nm = instance_ref['name']
+ msg = _('instance %(nm)s: injecting file to %(plain_path)s') % locals()
+ LOG.audit(msg)
+ self.driver.inject_file(instance_ref, b64_path, b64_contents)
@exception.wrap_exception
@checks_instance_lock
@@ -498,6 +515,18 @@ class ComputeManager(manager.Manager):
instance_ref = self.db.instance_get(context, instance_id)
return instance_ref['locked']
+ @checks_instance_lock
+ def reset_network(self, context, instance_id):
+ """
+ Reset networking on the instance.
+
+ """
+ context = context.elevated()
+ instance_ref = self.db.instance_get(context, instance_id)
+ LOG.debug(_('instance %s: reset network'), instance_id,
+ context=context)
+ self.driver.reset_network(instance_ref)
+
@exception.wrap_exception
def get_console_output(self, context, instance_id):
"""Send the console output for an instance."""
@@ -511,7 +540,7 @@ class ComputeManager(manager.Manager):
def get_ajax_console(self, context, instance_id):
"""Return connection information for an ajax console"""
context = context.elevated()
- logging.debug(_("instance %s: getting ajax console"), instance_id)
+ LOG.debug(_("instance %s: getting ajax console"), instance_id)
instance_ref = self.db.instance_get(context, instance_id)
return self.driver.get_ajax_console(instance_ref)
diff --git a/nova/compute/power_state.py b/nova/compute/power_state.py
index 37039d2ec..adfc2dff0 100644
--- a/nova/compute/power_state.py
+++ b/nova/compute/power_state.py
@@ -27,6 +27,7 @@ SHUTDOWN = 0x04
SHUTOFF = 0x05
CRASHED = 0x06
SUSPENDED = 0x07
+FAILED = 0x08
def name(code):
@@ -38,5 +39,6 @@ def name(code):
SHUTDOWN: 'shutdown',
SHUTOFF: 'shutdown',
CRASHED: 'crashed',
- SUSPENDED: 'suspended'}
+ SUSPENDED: 'suspended',
+ FAILED: 'failed to spawn'}
return d[code]
diff --git a/nova/context.py b/nova/context.py
index f2669c9f1..0256bf448 100644
--- a/nova/context.py
+++ b/nova/context.py
@@ -28,7 +28,6 @@ from nova import utils
class RequestContext(object):
-
def __init__(self, user, project, is_admin=None, read_deleted=False,
remote_address=None, timestamp=None, request_id=None):
if hasattr(user, 'id'):
@@ -53,7 +52,7 @@ class RequestContext(object):
self.read_deleted = read_deleted
self.remote_address = remote_address
if not timestamp:
- timestamp = datetime.datetime.utcnow()
+ timestamp = utils.utcnow()
if isinstance(timestamp, str) or isinstance(timestamp, unicode):
timestamp = utils.parse_isotime(timestamp)
self.timestamp = timestamp
@@ -101,7 +100,7 @@ class RequestContext(object):
return cls(**values)
def elevated(self, read_deleted=False):
- """Return a version of this context with admin flag set"""
+ """Return a version of this context with admin flag set."""
return RequestContext(self.user_id,
self.project_id,
True,
diff --git a/nova/db/api.py b/nova/db/api.py
index 789cb8ebb..24245b843 100644
--- a/nova/db/api.py
+++ b/nova/db/api.py
@@ -288,11 +288,21 @@ def fixed_ip_disassociate_all_by_timeout(context, host, time):
return IMPL.fixed_ip_disassociate_all_by_timeout(context, host, time)
+def fixed_ip_get_all(context):
+ """Get all defined fixed ips."""
+ return IMPL.fixed_ip_get_all(context)
+
+
def fixed_ip_get_by_address(context, address):
"""Get a fixed ip by address or raise if it does not exist."""
return IMPL.fixed_ip_get_by_address(context, address)
+def fixed_ip_get_all_by_instance(context, instance_id):
+ """Get fixed ips by instance or raise if none exist."""
+ return IMPL.fixed_ip_get_all_by_instance(context, instance_id)
+
+
def fixed_ip_get_instance(context, address):
"""Get an instance for a fixed ip by address."""
return IMPL.fixed_ip_get_instance(context, address)
@@ -500,6 +510,11 @@ def network_get(context, network_id):
return IMPL.network_get(context, network_id)
+def network_get_all(context):
+ """Return all defined networks."""
+ return IMPL.network_get_all(context)
+
+
# pylint: disable-msg=C0103
def network_get_associated_fixed_ips(context, network_id):
"""Get all network's ips that have been associated."""
@@ -516,6 +531,11 @@ def network_get_by_instance(context, instance_id):
return IMPL.network_get_by_instance(context, instance_id)
+def network_get_all_by_instance(context, instance_id):
+ """Get all networks by instance id or raise if none exist."""
+ return IMPL.network_get_all_by_instance(context, instance_id)
+
+
def network_get_index(context, network_id):
"""Get non-conflicting index for network."""
return IMPL.network_get_index(context, network_id)
diff --git a/nova/db/sqlalchemy/api.py b/nova/db/sqlalchemy/api.py
index 895e7eabe..c950d68f6 100644
--- a/nova/db/sqlalchemy/api.py
+++ b/nova/db/sqlalchemy/api.py
@@ -19,6 +19,7 @@
Implementation of SQLAlchemy backend.
"""
+import datetime
import warnings
from nova import db
@@ -578,10 +579,21 @@ def fixed_ip_disassociate_all_by_timeout(_context, host, time):
'AND instance_id IS NOT NULL '
'AND allocated = 0',
{'host': host,
- 'time': time.isoformat()})
+ 'time': time})
return result.rowcount
+@require_admin_context
+def fixed_ip_get_all(context, session=None):
+ if not session:
+ session = get_session()
+ result = session.query(models.FixedIp).all()
+ if not result:
+ raise exception.NotFound(_('No fixed ips defined'))
+
+ return result
+
+
@require_context
def fixed_ip_get_by_address(context, address, session=None):
if not session:
@@ -608,6 +620,17 @@ def fixed_ip_get_instance(context, address):
@require_context
+def fixed_ip_get_all_by_instance(context, instance_id):
+ session = get_session()
+ rv = session.query(models.FixedIp).\
+ filter_by(instance_id=instance_id).\
+ filter_by(deleted=False)
+ if not rv:
+ raise exception.NotFound(_('No address for instance %s') % instance_id)
+ return rv
+
+
+@require_context
def fixed_ip_get_instance_v6(context, address):
session = get_session()
mac = utils.to_mac(address)
@@ -670,8 +693,14 @@ def instance_data_get_for_project(context, project_id):
def instance_destroy(context, instance_id):
session = get_session()
with session.begin():
- instance_ref = instance_get(context, instance_id, session=session)
- instance_ref.delete(session=session)
+ session.execute('update instances set deleted=1,'
+ 'deleted_at=:at where id=:id',
+ {'id': instance_id,
+ 'at': datetime.datetime.utcnow()})
+ session.execute('update security_group_instance_association '
+ 'set deleted=1,deleted_at=:at where instance_id=:id',
+ {'id': instance_id,
+ 'at': datetime.datetime.utcnow()})
@require_context
@@ -712,6 +741,7 @@ def instance_get_all(context):
return session.query(models.Instance).\
options(joinedload_all('fixed_ip.floating_ips')).\
options(joinedload('security_groups')).\
+ options(joinedload_all('fixed_ip.network')).\
filter_by(deleted=can_read_deleted(context)).\
all()
@@ -722,6 +752,7 @@ def instance_get_all_by_user(context, user_id):
return session.query(models.Instance).\
options(joinedload_all('fixed_ip.floating_ips')).\
options(joinedload('security_groups')).\
+ options(joinedload_all('fixed_ip.network')).\
filter_by(deleted=can_read_deleted(context)).\
filter_by(user_id=user_id).\
all()
@@ -733,6 +764,7 @@ def instance_get_all_by_host(context, host):
return session.query(models.Instance).\
options(joinedload_all('fixed_ip.floating_ips')).\
options(joinedload('security_groups')).\
+ options(joinedload_all('fixed_ip.network')).\
filter_by(host=host).\
filter_by(deleted=can_read_deleted(context)).\
all()
@@ -746,6 +778,7 @@ def instance_get_all_by_project(context, project_id):
return session.query(models.Instance).\
options(joinedload_all('fixed_ip.floating_ips')).\
options(joinedload('security_groups')).\
+ options(joinedload_all('fixed_ip.network')).\
filter_by(project_id=project_id).\
filter_by(deleted=can_read_deleted(context)).\
all()
@@ -759,6 +792,7 @@ def instance_get_all_by_reservation(context, reservation_id):
return session.query(models.Instance).\
options(joinedload_all('fixed_ip.floating_ips')).\
options(joinedload('security_groups')).\
+ options(joinedload_all('fixed_ip.network')).\
filter_by(reservation_id=reservation_id).\
filter_by(deleted=can_read_deleted(context)).\
all()
@@ -766,6 +800,7 @@ def instance_get_all_by_reservation(context, reservation_id):
return session.query(models.Instance).\
options(joinedload_all('fixed_ip.floating_ips')).\
options(joinedload('security_groups')).\
+ options(joinedload_all('fixed_ip.network')).\
filter_by(project_id=context.project_id).\
filter_by(reservation_id=reservation_id).\
filter_by(deleted=False).\
@@ -1043,6 +1078,15 @@ def network_get(context, network_id, session=None):
return result
+@require_admin_context
+def network_get_all(context):
+ session = get_session()
+ result = session.query(models.Network)
+ if not result:
+ raise exception.NotFound(_('No networks defined'))
+ return result
+
+
# NOTE(vish): pylint complains because of the long method name, but
# it fits with the names of the rest of the methods
# pylint: disable-msg=C0103
@@ -1087,6 +1131,19 @@ def network_get_by_instance(_context, instance_id):
@require_admin_context
+def network_get_all_by_instance(_context, instance_id):
+ session = get_session()
+ rv = session.query(models.Network).\
+ filter_by(deleted=False).\
+ join(models.Network.fixed_ips).\
+ filter_by(instance_id=instance_id).\
+ filter_by(deleted=False)
+ if not rv:
+ raise exception.NotFound(_('No network for instance %s') % instance_id)
+ return rv
+
+
+@require_admin_context
def network_set_host(context, network_id, host_id):
session = get_session()
with session.begin():
@@ -1583,6 +1640,11 @@ def security_group_destroy(context, security_group_id):
# TODO(vish): do we have to use sql here?
session.execute('update security_groups set deleted=1 where id=:id',
{'id': security_group_id})
+ session.execute('update security_group_instance_association '
+ 'set deleted=1,deleted_at=:at '
+ 'where security_group_id=:id',
+ {'id': security_group_id,
+ 'at': datetime.datetime.utcnow()})
session.execute('update security_group_rules set deleted=1 '
'where group_id=:id',
{'id': security_group_id})
diff --git a/nova/db/sqlalchemy/migrate_repo/versions/001_austin.py b/nova/db/sqlalchemy/migrate_repo/versions/001_austin.py
index a312a7190..9e7ab3554 100644
--- a/nova/db/sqlalchemy/migrate_repo/versions/001_austin.py
+++ b/nova/db/sqlalchemy/migrate_repo/versions/001_austin.py
@@ -134,6 +134,9 @@ instances = Table('instances', meta,
Column('ramdisk_id',
String(length=255, convert_unicode=False, assert_unicode=None,
unicode_error=None, _warn_on_bytestring=False)),
+ Column('server_name',
+ String(length=255, convert_unicode=False, assert_unicode=None,
+ unicode_error=None, _warn_on_bytestring=False)),
Column('launch_index', Integer()),
Column('key_name',
String(length=255, convert_unicode=False, assert_unicode=None,
@@ -178,23 +181,6 @@ instances = Table('instances', meta,
)
-iscsi_targets = Table('iscsi_targets', meta,
- Column('created_at', DateTime(timezone=False)),
- Column('updated_at', DateTime(timezone=False)),
- Column('deleted_at', DateTime(timezone=False)),
- Column('deleted', Boolean(create_constraint=True, name=None)),
- Column('id', Integer(), primary_key=True, nullable=False),
- Column('target_num', Integer()),
- Column('host',
- String(length=255, convert_unicode=False, assert_unicode=None,
- unicode_error=None, _warn_on_bytestring=False)),
- Column('volume_id',
- Integer(),
- ForeignKey('volumes.id'),
- nullable=True),
- )
-
-
key_pairs = Table('key_pairs', meta,
Column('created_at', DateTime(timezone=False)),
Column('updated_at', DateTime(timezone=False)),
@@ -522,24 +508,26 @@ def upgrade(migrate_engine):
# bind migrate_engine to your metadata
meta.bind = migrate_engine
- for table in (auth_tokens, export_devices, fixed_ips, floating_ips,
- instances, iscsi_targets, key_pairs, networks,
- projects, quotas, security_groups, security_group_inst_assoc,
- security_group_rules, services, users,
- user_project_association, user_project_role_association,
- user_role_association, volumes):
+ tables = [auth_tokens,
+ instances, key_pairs, networks, fixed_ips, floating_ips,
+ quotas, security_groups, security_group_inst_assoc,
+ security_group_rules, services, users, projects,
+ user_project_association, user_project_role_association,
+ user_role_association, volumes, export_devices]
+ for table in tables:
try:
table.create()
except Exception:
logging.info(repr(table))
logging.exception('Exception while creating table')
+ meta.drop_all(tables=tables)
raise
def downgrade(migrate_engine):
# Operations to reverse the above upgrade go here.
for table in (auth_tokens, export_devices, fixed_ips, floating_ips,
- instances, iscsi_targets, key_pairs, networks,
+ instances, key_pairs, networks,
projects, quotas, security_groups, security_group_inst_assoc,
security_group_rules, services, users,
user_project_association, user_project_role_association,
diff --git a/nova/db/sqlalchemy/migrate_repo/versions/002_bexar.py b/nova/db/sqlalchemy/migrate_repo/versions/002_bexar.py
index bd3a3e6f8..413536a59 100644
--- a/nova/db/sqlalchemy/migrate_repo/versions/002_bexar.py
+++ b/nova/db/sqlalchemy/migrate_repo/versions/002_bexar.py
@@ -41,6 +41,10 @@ networks = Table('networks', meta,
Column('id', Integer(), primary_key=True, nullable=False),
)
+volumes = Table('volumes', meta,
+ Column('id', Integer(), primary_key=True, nullable=False),
+ )
+
#
# New Tables
@@ -131,6 +135,23 @@ instance_actions = Table('instance_actions', meta,
)
+iscsi_targets = Table('iscsi_targets', meta,
+ Column('created_at', DateTime(timezone=False)),
+ Column('updated_at', DateTime(timezone=False)),
+ Column('deleted_at', DateTime(timezone=False)),
+ Column('deleted', Boolean(create_constraint=True, name=None)),
+ Column('id', Integer(), primary_key=True, nullable=False),
+ Column('target_num', Integer()),
+ Column('host',
+ String(length=255, convert_unicode=False, assert_unicode=None,
+ unicode_error=None, _warn_on_bytestring=False)),
+ Column('volume_id',
+ Integer(),
+ ForeignKey('volumes.id'),
+ nullable=True),
+ )
+
+
#
# Tables to alter
#
@@ -188,12 +209,16 @@ def upgrade(migrate_engine):
# Upgrade operations go here. Don't create your own engine;
# bind migrate_engine to your metadata
meta.bind = migrate_engine
- for table in (certificates, consoles, console_pools, instance_actions):
+
+ tables = [certificates, console_pools, consoles, instance_actions,
+ iscsi_targets]
+ for table in tables:
try:
table.create()
except Exception:
logging.info(repr(table))
logging.exception('Exception while creating table')
+ meta.drop_all(tables=tables)
raise
auth_tokens.c.user_id.alter(type=String(length=255,
diff --git a/nova/db/sqlalchemy/migrate_repo/versions/003_add_label_to_networks.py b/nova/db/sqlalchemy/migrate_repo/versions/003_add_label_to_networks.py
new file mode 100644
index 000000000..5ba7910f1
--- /dev/null
+++ b/nova/db/sqlalchemy/migrate_repo/versions/003_add_label_to_networks.py
@@ -0,0 +1,51 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2011 OpenStack LLC
+# 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.
+
+from sqlalchemy import *
+from migrate import *
+
+from nova import log as logging
+
+
+meta = MetaData()
+
+
+networks = Table('networks', meta,
+ Column('id', Integer(), primary_key=True, nullable=False),
+ )
+
+
+#
+# New Tables
+#
+
+
+#
+# Tables to alter
+#
+
+networks_label = Column(
+ 'label',
+ String(length=255, convert_unicode=False, assert_unicode=None,
+ unicode_error=None, _warn_on_bytestring=False))
+
+
+def upgrade(migrate_engine):
+ # Upgrade operations go here. Don't create your own engine;
+ # bind migrate_engine to your metadata
+ meta.bind = migrate_engine
+ networks.create_column(networks_label)
diff --git a/nova/db/sqlalchemy/migration.py b/nova/db/sqlalchemy/migration.py
index 33d14827b..d2671e1a3 100644
--- a/nova/db/sqlalchemy/migration.py
+++ b/nova/db/sqlalchemy/migration.py
@@ -17,12 +17,22 @@
# under the License.
import os
+import sys
from nova import flags
import sqlalchemy
from migrate.versioning import api as versioning_api
-from migrate.versioning import exceptions as versioning_exceptions
+
+try:
+ from migrate.versioning import exceptions as versioning_exceptions
+except ImportError:
+ try:
+ # python-migration changed location of exceptions after 1.6.3
+ # See LP Bug #717467
+ from migrate import exceptions as versioning_exceptions
+ except ImportError:
+ sys.exit(_("python-migrate is not installed. Exiting."))
FLAGS = flags.FLAGS
@@ -46,12 +56,15 @@ def db_version():
meta.reflect(bind=engine)
try:
for table in ('auth_tokens', 'export_devices', 'fixed_ips',
- 'floating_ips', 'instances', 'iscsi_targets',
+ 'floating_ips', 'instances',
'key_pairs', 'networks', 'projects', 'quotas',
- 'security_group_rules',
- 'security_group_instance_association', 'services',
+ 'security_group_instance_association',
+ 'security_group_rules', 'security_groups',
+ 'services',
'users', 'user_project_association',
- 'user_project_role_association', 'volumes'):
+ 'user_project_role_association',
+ 'user_role_association',
+ 'volumes'):
assert table in meta.tables
return db_version_control(1)
except AssertionError:
diff --git a/nova/db/sqlalchemy/models.py b/nova/db/sqlalchemy/models.py
index c54ebe3ba..e151bee18 100644
--- a/nova/db/sqlalchemy/models.py
+++ b/nova/db/sqlalchemy/models.py
@@ -311,10 +311,14 @@ class SecurityGroup(BASE, NovaBase):
secondary="security_group_instance_association",
primaryjoin='and_('
'SecurityGroup.id == '
- 'SecurityGroupInstanceAssociation.security_group_id,'
+ 'SecurityGroupInstanceAssociation.security_group_id,'
+ 'SecurityGroupInstanceAssociation.deleted == False,'
'SecurityGroup.deleted == False)',
secondaryjoin='and_('
'SecurityGroupInstanceAssociation.instance_id == Instance.id,'
+ # (anthony) the condition below shouldn't be necessary now that the
+ # association is being marked as deleted. However, removing this
+ # may cause existing deployments to choke, so I'm leaving it
'Instance.deleted == False)',
backref='security_groups')
@@ -369,6 +373,7 @@ class Network(BASE, NovaBase):
"vpn_public_port"),
{'mysql_engine': 'InnoDB'})
id = Column(Integer, primary_key=True)
+ label = Column(String(255))
injected = Column(Boolean, default=False)
cidr = Column(String(255), unique=True)
diff --git a/nova/db/sqlalchemy/session.py b/nova/db/sqlalchemy/session.py
index dc885f138..4a9a28f43 100644
--- a/nova/db/sqlalchemy/session.py
+++ b/nova/db/sqlalchemy/session.py
@@ -20,6 +20,7 @@ Session Handling for SQLAlchemy backend
"""
from sqlalchemy import create_engine
+from sqlalchemy import pool
from sqlalchemy.orm import sessionmaker
from nova import exception
@@ -37,9 +38,14 @@ def get_session(autocommit=True, expire_on_commit=False):
global _MAKER
if not _MAKER:
if not _ENGINE:
+ kwargs = {'pool_recycle': FLAGS.sql_idle_timeout,
+ 'echo': False}
+
+ if FLAGS.sql_connection.startswith('sqlite'):
+ kwargs['poolclass'] = pool.NullPool
+
_ENGINE = create_engine(FLAGS.sql_connection,
- pool_recycle=FLAGS.sql_idle_timeout,
- echo=False)
+ **kwargs)
_MAKER = (sessionmaker(bind=_ENGINE,
autocommit=autocommit,
expire_on_commit=expire_on_commit))
diff --git a/nova/flags.py b/nova/flags.py
index 43bc174d2..f64a62da9 100644
--- a/nova/flags.py
+++ b/nova/flags.py
@@ -208,7 +208,7 @@ def _get_my_ip():
(addr, port) = csock.getsockname()
csock.close()
return addr
- except socket.gaierror as ex:
+ except socket.error as ex:
return "127.0.0.1"
@@ -282,12 +282,14 @@ DEFINE_integer('auth_token_ttl', 3600, 'Seconds for auth tokens to linger')
DEFINE_string('state_path', os.path.join(os.path.dirname(__file__), '../'),
"Top-level directory for maintaining nova's state")
+DEFINE_string('logdir', None, 'output to a per-service log file in named '
+ 'directory')
DEFINE_string('sql_connection',
'sqlite:///$state_path/nova.sqlite',
'connection string for sql database')
-DEFINE_string('sql_idle_timeout',
- '3600',
+DEFINE_integer('sql_idle_timeout',
+ 3600,
'timeout for idle sql database connections')
DEFINE_integer('sql_max_retries', 12, 'sql connection attempts')
DEFINE_integer('sql_retry_interval', 10, 'sql connection retry interval')
diff --git a/nova/image/s3.py b/nova/image/s3.py
index 08a40f191..14135a1ee 100644
--- a/nova/image/s3.py
+++ b/nova/image/s3.py
@@ -36,6 +36,22 @@ from nova.image import service
FLAGS = flags.FLAGS
+def map_s3_to_base(image):
+ """Convert from S3 format to format defined by BaseImageService."""
+ i = {}
+ i['id'] = image.get('imageId')
+ i['name'] = image.get('imageId')
+ i['kernel_id'] = image.get('kernelId')
+ i['ramdisk_id'] = image.get('ramdiskId')
+ i['location'] = image.get('imageLocation')
+ i['owner_id'] = image.get('imageOwnerId')
+ i['status'] = image.get('imageState')
+ i['type'] = image.get('type')
+ i['is_public'] = image.get('isPublic')
+ i['architecture'] = image.get('architecture')
+ return i
+
+
class S3ImageService(service.BaseImageService):
def modify(self, context, image_id, operation):
@@ -65,26 +81,20 @@ class S3ImageService(service.BaseImageService):
'image_id': image_id}))
return image_id
- def _fix_image_id(self, images):
- """S3 has imageId but OpenStack wants id"""
- for image in images:
- if 'imageId' in image:
- image['id'] = image['imageId']
- return images
-
def index(self, context):
"""Return a list of all images that a user can see."""
response = self._conn(context).make_request(
method='GET',
bucket='_images')
- return self._fix_image_id(json.loads(response.read()))
+ images = json.loads(response.read())
+ return [map_s3_to_base(i) for i in images]
def show(self, context, image_id):
"""return a image object if the context has permissions"""
if FLAGS.connection_type == 'fake':
return {'imageId': 'bar'}
result = self.index(context)
- result = [i for i in result if i['imageId'] == image_id]
+ result = [i for i in result if i['id'] == image_id]
if not result:
raise exception.NotFound(_('Image %s could not be found')
% image_id)
diff --git a/nova/log.py b/nova/log.py
index e1c9f46f4..87a6dd51b 100644
--- a/nova/log.py
+++ b/nova/log.py
@@ -28,9 +28,12 @@ It also allows setting of formatting information through flags.
import cStringIO
+import inspect
import json
import logging
import logging.handlers
+import os
+import sys
import traceback
from nova import flags
@@ -91,7 +94,7 @@ critical = logging.critical
log = logging.log
# handlers
StreamHandler = logging.StreamHandler
-FileHandler = logging.FileHandler
+RotatingFileHandler = logging.handlers.RotatingFileHandler
# logging.SysLogHandler is nicer than logging.logging.handler.SysLogHandler.
SysLogHandler = logging.handlers.SysLogHandler
@@ -110,6 +113,18 @@ def _dictify_context(context):
return context
+def _get_binary_name():
+ return os.path.basename(inspect.stack()[-1][1])
+
+
+def get_log_file_path(binary=None):
+ if FLAGS.logfile:
+ return FLAGS.logfile
+ if FLAGS.logdir:
+ binary = binary or _get_binary_name()
+ return '%s.log' % (os.path.join(FLAGS.logdir, binary),)
+
+
def basicConfig():
logging.basicConfig()
for handler in logging.root.handlers:
@@ -122,8 +137,9 @@ def basicConfig():
syslog = SysLogHandler(address='/dev/log')
syslog.setFormatter(_formatter)
logging.root.addHandler(syslog)
- if FLAGS.logfile:
- logfile = FileHandler(FLAGS.logfile)
+ logpath = get_log_file_path()
+ if logpath:
+ logfile = RotatingFileHandler(logpath)
logfile.setFormatter(_formatter)
logging.root.addHandler(logfile)
@@ -191,6 +207,12 @@ class NovaLogger(logging.Logger):
kwargs.pop('exc_info')
self.error(message, **kwargs)
+
+def handle_exception(type, value, tb):
+ logging.root.critical(str(value), exc_info=(type, value, tb))
+
+
+sys.excepthook = handle_exception
logging.setLoggerClass(NovaLogger)
diff --git a/nova/network/linux_net.py b/nova/network/linux_net.py
index cdd1f666a..535ce87bc 100644
--- a/nova/network/linux_net.py
+++ b/nova/network/linux_net.py
@@ -20,6 +20,7 @@ Implements vlans, bridges, and iptables rules using linux utilities.
import os
from nova import db
+from nova import exception
from nova import flags
from nova import log as logging
from nova import utils
@@ -37,10 +38,13 @@ FLAGS = flags.FLAGS
flags.DEFINE_string('dhcpbridge_flagfile',
'/etc/nova/nova-dhcpbridge.conf',
'location of flagfile for dhcpbridge')
+flags.DEFINE_string('dhcp_domain',
+ 'novalocal',
+ 'domain to use for building the hostnames')
flags.DEFINE_string('networks_path', '$state_path/networks',
'Location to keep network config files')
-flags.DEFINE_string('public_interface', 'vlan1',
+flags.DEFINE_string('public_interface', 'eth0',
'Interface for public IP addresses')
flags.DEFINE_string('vlan_interface', 'eth0',
'network device for vlans')
@@ -50,6 +54,8 @@ flags.DEFINE_string('routing_source_ip', '$my_ip',
'Public IP of network host')
flags.DEFINE_bool('use_nova_chains', False,
'use the nova_ routing chains instead of default')
+flags.DEFINE_string('input_chain', 'INPUT',
+ 'chain to add nova_input to')
flags.DEFINE_string('dns_server', None,
'if set, uses specific dns server for dnsmasq')
@@ -152,6 +158,8 @@ def ensure_floating_forward(floating_ip, fixed_ip):
"""Ensure floating ip forwarding rule"""
_confirm_rule("PREROUTING", "-t nat -d %s -j DNAT --to %s"
% (floating_ip, fixed_ip))
+ _confirm_rule("OUTPUT", "-t nat -d %s -j DNAT --to %s"
+ % (floating_ip, fixed_ip))
_confirm_rule("SNATTING", "-t nat -s %s -j SNAT --to %s"
% (fixed_ip, floating_ip))
@@ -160,6 +168,8 @@ def remove_floating_forward(floating_ip, fixed_ip):
"""Remove forwarding for floating ip"""
_remove_rule("PREROUTING", "-t nat -d %s -j DNAT --to %s"
% (floating_ip, fixed_ip))
+ _remove_rule("OUTPUT", "-t nat -d %s -j DNAT --to %s"
+ % (floating_ip, fixed_ip))
_remove_rule("SNATTING", "-t nat -s %s -j SNAT --to %s"
% (fixed_ip, floating_ip))
@@ -177,32 +187,77 @@ def ensure_vlan(vlan_num):
LOG.debug(_("Starting VLAN inteface %s"), interface)
_execute("sudo vconfig set_name_type VLAN_PLUS_VID_NO_PAD")
_execute("sudo vconfig add %s %s" % (FLAGS.vlan_interface, vlan_num))
- _execute("sudo ifconfig %s up" % interface)
+ _execute("sudo ip link set %s up" % interface)
return interface
def ensure_bridge(bridge, interface, net_attrs=None):
- """Create a bridge unless it already exists"""
+ """Create a bridge unless it already exists.
+
+ :param interface: the interface to create the bridge on.
+ :param net_attrs: dictionary with attributes used to create the bridge.
+
+ If net_attrs is set, it will add the net_attrs['gateway'] to the bridge
+ using net_attrs['broadcast'] and net_attrs['cidr']. It will also add
+ the ip_v6 address specified in net_attrs['cidr_v6'] if use_ipv6 is set.
+
+ The code will attempt to move any ips that already exist on the interface
+ onto the bridge and reset the default gateway if necessary.
+ """
if not _device_exists(bridge):
LOG.debug(_("Starting Bridge interface for %s"), interface)
_execute("sudo brctl addbr %s" % bridge)
_execute("sudo brctl setfd %s 0" % bridge)
# _execute("sudo brctl setageing %s 10" % bridge)
_execute("sudo brctl stp %s off" % bridge)
- if interface:
- _execute("sudo brctl addif %s %s" % (bridge, interface))
+ _execute("sudo ip link set %s up" % bridge)
if net_attrs:
- _execute("sudo ifconfig %s %s broadcast %s netmask %s up" % \
- (bridge,
- net_attrs['gateway'],
- net_attrs['broadcast'],
- net_attrs['netmask']))
+ # NOTE(vish): The ip for dnsmasq has to be the first address on the
+ # bridge for it to respond to reqests properly
+ suffix = net_attrs['cidr'].rpartition('/')[2]
+ out, err = _execute("sudo ip addr add %s/%s brd %s dev %s" %
+ (net_attrs['gateway'],
+ suffix,
+ net_attrs['broadcast'],
+ bridge),
+ check_exit_code=False)
+ if err and err != "RTNETLINK answers: File exists\n":
+ raise exception.Error("Failed to add ip: %s" % err)
if(FLAGS.use_ipv6):
_execute("sudo ip -f inet6 addr change %s dev %s" %
(net_attrs['cidr_v6'], bridge))
- _execute("sudo ifconfig %s up" % bridge)
- else:
- _execute("sudo ifconfig %s up" % bridge)
+ # NOTE(vish): If the public interface is the same as the
+ # bridge, then the bridge has to be in promiscuous
+ # to forward packets properly.
+ if(FLAGS.public_interface == bridge):
+ _execute("sudo ip link set dev %s promisc on" % bridge)
+ if interface:
+ # NOTE(vish): This will break if there is already an ip on the
+ # interface, so we move any ips to the bridge
+ gateway = None
+ out, err = _execute("sudo route -n")
+ for line in out.split("\n"):
+ fields = line.split()
+ if fields and fields[0] == "0.0.0.0" and fields[-1] == interface:
+ gateway = fields[1]
+ out, err = _execute("sudo ip addr show dev %s scope global" %
+ interface)
+ for line in out.split("\n"):
+ fields = line.split()
+ if fields and fields[0] == "inet":
+ params = ' '.join(fields[1:-1])
+ _execute("sudo ip addr del %s dev %s" % (params, fields[-1]))
+ _execute("sudo ip addr add %s dev %s" % (params, bridge))
+ if gateway:
+ _execute("sudo route add 0.0.0.0 gw %s" % gateway)
+ out, err = _execute("sudo brctl addif %s %s" %
+ (bridge, interface),
+ check_exit_code=False)
+
+ if (err and err != "device %s is already a member of a bridge; can't "
+ "enslave it to bridge %s.\n" % (interface, bridge)):
+ raise exception.Error("Failed to add interface: %s" % err)
+
if FLAGS.use_nova_chains:
(out, err) = _execute("sudo iptables -N nova_forward",
check_exit_code=False)
@@ -313,8 +368,9 @@ interface %s
def _host_dhcp(fixed_ip_ref):
"""Return a host string for an address"""
instance_ref = fixed_ip_ref['instance']
- return "%s,%s.novalocal,%s" % (instance_ref['mac_address'],
+ return "%s,%s.%s,%s" % (instance_ref['mac_address'],
instance_ref['hostname'],
+ FLAGS.dhcp_domain,
fixed_ip_ref['address'])
@@ -329,7 +385,8 @@ def _execute(cmd, *args, **kwargs):
def _device_exists(device):
"""Check if ethernet device exists"""
- (_out, err) = _execute("ifconfig %s" % device, check_exit_code=False)
+ (_out, err) = _execute("ip link show dev %s" % device,
+ check_exit_code=False)
return not err
@@ -359,6 +416,7 @@ def _dnsmasq_cmd(net):
' --strict-order',
' --bind-interfaces',
' --conf-file=',
+ ' --domain=%s' % FLAGS.dhcp_domain,
' --pid-file=%s' % _dhcp_file(net['bridge'], 'pid'),
' --listen-address=%s' % net['gateway'],
' --except-interface=lo',
diff --git a/nova/network/manager.py b/nova/network/manager.py
index fbcbea131..b906a83ed 100644
--- a/nova/network/manager.py
+++ b/nova/network/manager.py
@@ -118,6 +118,10 @@ class NetworkManager(manager.Manager):
super(NetworkManager, self).__init__(*args, **kwargs)
def init_host(self):
+ """Do any initialization that needs to be run if this is a
+ standalone service.
+ """
+ self.driver.init_host()
# Set up networking for the projects for which we're already
# the designated network host.
ctxt = context.get_admin_context()
@@ -327,11 +331,12 @@ class FlatManager(NetworkManager):
pass
def create_networks(self, context, cidr, num_networks, network_size,
- cidr_v6, *args, **kwargs):
+ cidr_v6, label, *args, **kwargs):
"""Create networks based on parameters."""
fixed_net = IPy.IP(cidr)
fixed_net_v6 = IPy.IP(cidr_v6)
significant_bits_v6 = 64
+ count = 1
for index in range(num_networks):
start = index * network_size
significant_bits = 32 - int(math.log(network_size, 2))
@@ -344,6 +349,11 @@ class FlatManager(NetworkManager):
net['gateway'] = str(project_net[1])
net['broadcast'] = str(project_net.broadcast())
net['dhcp_start'] = str(project_net[2])
+ if num_networks > 1:
+ net['label'] = "%s_%d" % (label, count)
+ else:
+ net['label'] = label
+ count += 1
if(FLAGS.use_ipv6):
cidr_v6 = "%s/%s" % (fixed_net_v6[0], significant_bits_v6)
@@ -395,7 +405,6 @@ class FlatDHCPManager(FlatManager):
standalone service.
"""
super(FlatDHCPManager, self).init_host()
- self.driver.init_host()
self.driver.metadata_forward()
def setup_compute_network(self, context, instance_id):
@@ -465,7 +474,6 @@ class VlanManager(NetworkManager):
standalone service.
"""
super(VlanManager, self).init_host()
- self.driver.init_host()
self.driver.metadata_forward()
def allocate_fixed_ip(self, context, instance_id, *args, **kwargs):
@@ -503,6 +511,12 @@ class VlanManager(NetworkManager):
def create_networks(self, context, cidr, num_networks, network_size,
cidr_v6, vlan_start, vpn_start):
"""Create networks based on parameters."""
+ # Check that num_networks + vlan_start is not > 4094, fixes lp708025
+ if num_networks + vlan_start > 4094:
+ raise ValueError(_('The sum between the number of networks and'
+ ' the vlan start cannot be greater'
+ ' than 4094'))
+
fixed_net = IPy.IP(cidr)
fixed_net_v6 = IPy.IP(cidr_v6)
network_size_v6 = 1 << 64
diff --git a/nova/rpc.py b/nova/rpc.py
index 01fc6d44b..205bb524a 100644
--- a/nova/rpc.py
+++ b/nova/rpc.py
@@ -29,6 +29,7 @@ import uuid
from carrot import connection as carrot_connection
from carrot import messaging
+from eventlet import greenpool
from eventlet import greenthread
from nova import context
@@ -42,11 +43,13 @@ from nova import utils
FLAGS = flags.FLAGS
LOG = logging.getLogger('nova.rpc')
+flags.DEFINE_integer('rpc_thread_pool_size', 1024, 'Size of RPC thread pool')
+
class Connection(carrot_connection.BrokerConnection):
"""Connection instance object"""
@classmethod
- def instance(cls, new=False):
+ def instance(cls, new=True):
"""Returns the instance"""
if new or not hasattr(cls, '_instance'):
params = dict(hostname=FLAGS.rabbit_host,
@@ -155,11 +158,15 @@ class AdapterConsumer(TopicConsumer):
def __init__(self, connection=None, topic="broadcast", proxy=None):
LOG.debug(_('Initing the Adapter Consumer for %s') % topic)
self.proxy = proxy
+ self.pool = greenpool.GreenPool(FLAGS.rpc_thread_pool_size)
super(AdapterConsumer, self).__init__(connection=connection,
topic=topic)
+ def receive(self, *args, **kwargs):
+ self.pool.spawn_n(self._receive, *args, **kwargs)
+
@exception.wrap_exception
- def receive(self, message_data, message):
+ def _receive(self, message_data, message):
"""Magically looks for a method on the proxy object and calls it
Message data should be a dictionary with two keys:
@@ -246,7 +253,7 @@ def msg_reply(msg_id, reply=None, failure=None):
LOG.error(_("Returning exception %s to caller"), message)
LOG.error(tb)
failure = (failure[0].__name__, str(failure[1]), tb)
- conn = Connection.instance(True)
+ conn = Connection.instance()
publisher = DirectPublisher(connection=conn, msg_id=msg_id)
try:
publisher.send({'result': reply, 'failure': failure})
@@ -319,7 +326,7 @@ def call(context, topic, msg):
self.result = data['result']
wait_msg = WaitMessage()
- conn = Connection.instance(True)
+ conn = Connection.instance()
consumer = DirectConsumer(connection=conn, msg_id=msg_id)
consumer.register_callback(wait_msg)
diff --git a/nova/tests/api/openstack/__init__.py b/nova/tests/api/openstack/__init__.py
index 14eaaa62c..77b1dd37f 100644
--- a/nova/tests/api/openstack/__init__.py
+++ b/nova/tests/api/openstack/__init__.py
@@ -92,31 +92,3 @@ class RateLimitingMiddlewareTest(unittest.TestCase):
self.assertEqual(middleware.limiter.__class__.__name__, "Limiter")
middleware = RateLimitingMiddleware(simple_wsgi, service_host='foobar')
self.assertEqual(middleware.limiter.__class__.__name__, "WSGIAppProxy")
-
-
-class LimiterTest(unittest.TestCase):
-
- def test_limiter(self):
- items = range(2000)
- req = Request.blank('/')
- self.assertEqual(limited(items, req), items[:1000])
- req = Request.blank('/?offset=0')
- self.assertEqual(limited(items, req), items[:1000])
- req = Request.blank('/?offset=3')
- self.assertEqual(limited(items, req), items[3:1003])
- req = Request.blank('/?offset=2005')
- self.assertEqual(limited(items, req), [])
- req = Request.blank('/?limit=10')
- self.assertEqual(limited(items, req), items[:10])
- req = Request.blank('/?limit=0')
- self.assertEqual(limited(items, req), items[:1000])
- req = Request.blank('/?limit=3000')
- self.assertEqual(limited(items, req), items[:1000])
- req = Request.blank('/?offset=1&limit=3')
- self.assertEqual(limited(items, req), items[1:4])
- req = Request.blank('/?offset=3&limit=0')
- self.assertEqual(limited(items, req), items[3:1003])
- req = Request.blank('/?offset=3&limit=1500')
- self.assertEqual(limited(items, req), items[3:1003])
- req = Request.blank('/?offset=3000&limit=10')
- self.assertEqual(limited(items, req), [])
diff --git a/nova/tests/api/openstack/test_common.py b/nova/tests/api/openstack/test_common.py
new file mode 100644
index 000000000..9d9837cc9
--- /dev/null
+++ b/nova/tests/api/openstack/test_common.py
@@ -0,0 +1,161 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2010 OpenStack LLC.
+# 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.
+
+"""
+Test suites for 'common' code used throughout the OpenStack HTTP API.
+"""
+
+import unittest
+
+from webob import Request
+
+from nova.api.openstack.common import limited
+
+
+class LimiterTest(unittest.TestCase):
+ """
+ Unit tests for the `nova.api.openstack.common.limited` method which takes
+ in a list of items and, depending on the 'offset' and 'limit' GET params,
+ returns a subset or complete set of the given items.
+ """
+
+ def setUp(self):
+ """
+ Run before each test.
+ """
+ self.tiny = range(1)
+ self.small = range(10)
+ self.medium = range(1000)
+ self.large = range(10000)
+
+ def test_limiter_offset_zero(self):
+ """
+ Test offset key works with 0.
+ """
+ req = Request.blank('/?offset=0')
+ self.assertEqual(limited(self.tiny, req), self.tiny)
+ self.assertEqual(limited(self.small, req), self.small)
+ self.assertEqual(limited(self.medium, req), self.medium)
+ self.assertEqual(limited(self.large, req), self.large[:1000])
+
+ def test_limiter_offset_medium(self):
+ """
+ Test offset key works with a medium sized number.
+ """
+ req = Request.blank('/?offset=10')
+ self.assertEqual(limited(self.tiny, req), [])
+ self.assertEqual(limited(self.small, req), self.small[10:])
+ self.assertEqual(limited(self.medium, req), self.medium[10:])
+ self.assertEqual(limited(self.large, req), self.large[10:1010])
+
+ def test_limiter_offset_over_max(self):
+ """
+ Test offset key works with a number over 1000 (max_limit).
+ """
+ req = Request.blank('/?offset=1001')
+ self.assertEqual(limited(self.tiny, req), [])
+ self.assertEqual(limited(self.small, req), [])
+ self.assertEqual(limited(self.medium, req), [])
+ self.assertEqual(limited(self.large, req), self.large[1001:2001])
+
+ def test_limiter_offset_blank(self):
+ """
+ Test offset key works with a blank offset.
+ """
+ req = Request.blank('/?offset=')
+ self.assertEqual(limited(self.tiny, req), self.tiny)
+ self.assertEqual(limited(self.small, req), self.small)
+ self.assertEqual(limited(self.medium, req), self.medium)
+ self.assertEqual(limited(self.large, req), self.large[:1000])
+
+ def test_limiter_offset_bad(self):
+ """
+ Test offset key works with a BAD offset.
+ """
+ req = Request.blank(u'/?offset=\u0020aa')
+ self.assertEqual(limited(self.tiny, req), self.tiny)
+ self.assertEqual(limited(self.small, req), self.small)
+ self.assertEqual(limited(self.medium, req), self.medium)
+ self.assertEqual(limited(self.large, req), self.large[:1000])
+
+ def test_limiter_nothing(self):
+ """
+ Test request with no offset or limit
+ """
+ req = Request.blank('/')
+ self.assertEqual(limited(self.tiny, req), self.tiny)
+ self.assertEqual(limited(self.small, req), self.small)
+ self.assertEqual(limited(self.medium, req), self.medium)
+ self.assertEqual(limited(self.large, req), self.large[:1000])
+
+ def test_limiter_limit_zero(self):
+ """
+ Test limit of zero.
+ """
+ req = Request.blank('/?limit=0')
+ self.assertEqual(limited(self.tiny, req), self.tiny)
+ self.assertEqual(limited(self.small, req), self.small)
+ self.assertEqual(limited(self.medium, req), self.medium)
+ self.assertEqual(limited(self.large, req), self.large[:1000])
+
+ def test_limiter_limit_medium(self):
+ """
+ Test limit of 10.
+ """
+ req = Request.blank('/?limit=10')
+ self.assertEqual(limited(self.tiny, req), self.tiny)
+ self.assertEqual(limited(self.small, req), self.small)
+ self.assertEqual(limited(self.medium, req), self.medium[:10])
+ self.assertEqual(limited(self.large, req), self.large[:10])
+
+ def test_limiter_limit_over_max(self):
+ """
+ Test limit of 3000.
+ """
+ req = Request.blank('/?limit=3000')
+ self.assertEqual(limited(self.tiny, req), self.tiny)
+ self.assertEqual(limited(self.small, req), self.small)
+ self.assertEqual(limited(self.medium, req), self.medium)
+ self.assertEqual(limited(self.large, req), self.large[:1000])
+
+ def test_limiter_limit_and_offset(self):
+ """
+ Test request with both limit and offset.
+ """
+ items = range(2000)
+ req = Request.blank('/?offset=1&limit=3')
+ self.assertEqual(limited(items, req), items[1:4])
+ req = Request.blank('/?offset=3&limit=0')
+ self.assertEqual(limited(items, req), items[3:1003])
+ req = Request.blank('/?offset=3&limit=1500')
+ self.assertEqual(limited(items, req), items[3:1003])
+ req = Request.blank('/?offset=3000&limit=10')
+ self.assertEqual(limited(items, req), [])
+
+ def test_limiter_custom_max_limit(self):
+ """
+ Test a max_limit other than 1000.
+ """
+ items = range(2000)
+ req = Request.blank('/?offset=1&limit=3')
+ self.assertEqual(limited(items, req, max_limit=2000), items[1:4])
+ req = Request.blank('/?offset=3&limit=0')
+ self.assertEqual(limited(items, req, max_limit=2000), items[3:])
+ req = Request.blank('/?offset=3&limit=2500')
+ self.assertEqual(limited(items, req, max_limit=2000), items[3:])
+ req = Request.blank('/?offset=3000&limit=10')
+ self.assertEqual(limited(items, req, max_limit=2000), [])
diff --git a/nova/tests/api/openstack/test_servers.py b/nova/tests/api/openstack/test_servers.py
index 724f14f19..a7be0796e 100644
--- a/nova/tests/api/openstack/test_servers.py
+++ b/nova/tests/api/openstack/test_servers.py
@@ -15,6 +15,7 @@
# License for the specific language governing permissions and limitations
# under the License.
+import datetime
import json
import unittest
@@ -39,6 +40,13 @@ def return_server(context, id):
return stub_instance(id)
+def return_server_with_addresses(private, public):
+ def _return_server(context, id):
+ return stub_instance(id, private_address=private,
+ public_addresses=public)
+ return _return_server
+
+
def return_servers(context, user_id=1):
return [stub_instance(i, user_id) for i in xrange(5)]
@@ -55,9 +63,45 @@ def instance_address(context, instance_id):
return None
-def stub_instance(id, user_id=1):
- return Instance(id=id, state=0, image_id=10, user_id=user_id,
- display_name='server%s' % id)
+def stub_instance(id, user_id=1, private_address=None, public_addresses=None):
+ if public_addresses == None:
+ public_addresses = list()
+
+ instance = {
+ "id": id,
+ "admin_pass": "",
+ "user_id": user_id,
+ "project_id": "",
+ "image_id": 10,
+ "kernel_id": "",
+ "ramdisk_id": "",
+ "launch_index": 0,
+ "key_name": "",
+ "key_data": "",
+ "state": 0,
+ "state_description": "",
+ "memory_mb": 0,
+ "vcpus": 0,
+ "local_gb": 0,
+ "hostname": "",
+ "host": "",
+ "instance_type": "",
+ "user_data": "",
+ "reservation_id": "",
+ "mac_address": "",
+ "scheduled_at": datetime.datetime.now(),
+ "launched_at": datetime.datetime.now(),
+ "terminated_at": datetime.datetime.now(),
+ "availability_zone": "",
+ "display_name": "server%s" % id,
+ "display_description": "",
+ "locked": False}
+
+ instance["fixed_ip"] = {
+ "address": private_address,
+ "floating_ips": [{"address":ip} for ip in public_addresses]}
+
+ return instance
def fake_compute_api(cls, req, id):
@@ -105,6 +149,22 @@ class ServersTest(unittest.TestCase):
self.assertEqual(res_dict['server']['id'], '1')
self.assertEqual(res_dict['server']['name'], 'server1')
+ def test_get_server_by_id_with_addresses(self):
+ private = "192.168.0.3"
+ public = ["1.2.3.4"]
+ new_return_server = return_server_with_addresses(private, public)
+ self.stubs.Set(nova.db.api, 'instance_get', new_return_server)
+ req = webob.Request.blank('/v1.0/servers/1')
+ res = req.get_response(fakes.wsgi_app())
+ res_dict = json.loads(res.body)
+ self.assertEqual(res_dict['server']['id'], '1')
+ self.assertEqual(res_dict['server']['name'], 'server1')
+ addresses = res_dict['server']['addresses']
+ self.assertEqual(len(addresses["public"]), len(public))
+ self.assertEqual(addresses["public"][0], public[0])
+ self.assertEqual(len(addresses["private"]), 1)
+ self.assertEqual(addresses["private"][0], private)
+
def test_get_server_list(self):
req = webob.Request.blank('/v1.0/servers')
res = req.get_response(fakes.wsgi_app())
@@ -281,6 +341,18 @@ class ServersTest(unittest.TestCase):
res = req.get_response(fakes.wsgi_app())
self.assertEqual(res.status_int, 202)
+ def test_server_reset_network(self):
+ FLAGS.allow_admin_api = True
+ body = dict(server=dict(
+ name='server_test', imageId=2, flavorId=2, metadata={},
+ personality={}))
+ req = webob.Request.blank('/v1.0/servers/1/reset_network')
+ req.method = 'POST'
+ req.content_type = 'application/json'
+ req.body = json.dumps(body)
+ res = req.get_response(fakes.wsgi_app())
+ self.assertEqual(res.status_int, 202)
+
def test_server_diagnostics(self):
req = webob.Request.blank("/v1.0/servers/1/diagnostics")
req.method = "GET"
diff --git a/nova/tests/db/nova.austin.sqlite b/nova/tests/db/nova.austin.sqlite
new file mode 100644
index 000000000..ad1326bce
--- /dev/null
+++ b/nova/tests/db/nova.austin.sqlite
Binary files differ
diff --git a/nova/tests/test_api.py b/nova/tests/test_api.py
index 2569e262b..fa27825cd 100644
--- a/nova/tests/test_api.py
+++ b/nova/tests/test_api.py
@@ -248,16 +248,14 @@ class ApiEc2TestCase(test.TestCase):
self.mox.ReplayAll()
rv = self.ec2.get_all_security_groups()
- # I don't bother checkng that we actually find it here,
- # because the create/delete unit test further up should
- # be good enough for that.
- for group in rv:
- if group.name == security_group_name:
- self.assertEquals(len(group.rules), 1)
- self.assertEquals(int(group.rules[0].from_port), 80)
- self.assertEquals(int(group.rules[0].to_port), 81)
- self.assertEquals(len(group.rules[0].grants), 1)
- self.assertEquals(str(group.rules[0].grants[0]), '0.0.0.0/0')
+
+ group = [grp for grp in rv if grp.name == security_group_name][0]
+
+ self.assertEquals(len(group.rules), 1)
+ self.assertEquals(int(group.rules[0].from_port), 80)
+ self.assertEquals(int(group.rules[0].to_port), 81)
+ self.assertEquals(len(group.rules[0].grants), 1)
+ self.assertEquals(str(group.rules[0].grants[0]), '0.0.0.0/0')
self.expect_http()
self.mox.ReplayAll()
@@ -314,16 +312,13 @@ class ApiEc2TestCase(test.TestCase):
self.mox.ReplayAll()
rv = self.ec2.get_all_security_groups()
- # I don't bother checkng that we actually find it here,
- # because the create/delete unit test further up should
- # be good enough for that.
- for group in rv:
- if group.name == security_group_name:
- self.assertEquals(len(group.rules), 1)
- self.assertEquals(int(group.rules[0].from_port), 80)
- self.assertEquals(int(group.rules[0].to_port), 81)
- self.assertEquals(len(group.rules[0].grants), 1)
- self.assertEquals(str(group.rules[0].grants[0]), '::/0')
+
+ group = [grp for grp in rv if grp.name == security_group_name][0]
+ self.assertEquals(len(group.rules), 1)
+ self.assertEquals(int(group.rules[0].from_port), 80)
+ self.assertEquals(int(group.rules[0].to_port), 81)
+ self.assertEquals(len(group.rules[0].grants), 1)
+ self.assertEquals(str(group.rules[0].grants[0]), '::/0')
self.expect_http()
self.mox.ReplayAll()
diff --git a/nova/tests/test_compute.py b/nova/tests/test_compute.py
index 09f6ee94a..b049ac943 100644
--- a/nova/tests/test_compute.py
+++ b/nova/tests/test_compute.py
@@ -49,7 +49,7 @@ class ComputeTestCase(test.TestCase):
self.manager = manager.AuthManager()
self.user = self.manager.create_user('fake', 'fake', 'fake')
self.project = self.manager.create_project('fake', 'fake', 'fake')
- self.context = context.get_admin_context()
+ self.context = context.RequestContext('fake', 'fake', False)
def tearDown(self):
self.manager.delete_user(self.user)
@@ -69,6 +69,13 @@ class ComputeTestCase(test.TestCase):
inst['ami_launch_index'] = 0
return db.instance_create(self.context, inst)['id']
+ def _create_group(self):
+ values = {'name': 'testgroup',
+ 'description': 'testgroup',
+ 'user_id': self.user.id,
+ 'project_id': self.project.id}
+ return db.security_group_create(self.context, values)
+
def test_create_instance_defaults_display_name(self):
"""Verify that an instance cannot be created without a display_name."""
cases = [dict(), dict(display_name=None)]
@@ -82,21 +89,53 @@ class ComputeTestCase(test.TestCase):
def test_create_instance_associates_security_groups(self):
"""Make sure create associates security groups"""
- values = {'name': 'default',
- 'description': 'default',
- 'user_id': self.user.id,
- 'project_id': self.project.id}
- group = db.security_group_create(self.context, values)
+ group = self._create_group()
ref = self.compute_api.create(
self.context,
instance_type=FLAGS.default_instance_type,
image_id=None,
- security_group=['default'])
+ security_group=['testgroup'])
try:
self.assertEqual(len(db.security_group_get_by_instance(
- self.context, ref[0]['id'])), 1)
+ self.context, ref[0]['id'])), 1)
+ group = db.security_group_get(self.context, group['id'])
+ self.assert_(len(group.instances) == 1)
+ finally:
+ db.security_group_destroy(self.context, group['id'])
+ db.instance_destroy(self.context, ref[0]['id'])
+
+ def test_destroy_instance_disassociates_security_groups(self):
+ """Make sure destroying disassociates security groups"""
+ group = self._create_group()
+
+ ref = self.compute_api.create(
+ self.context,
+ instance_type=FLAGS.default_instance_type,
+ image_id=None,
+ security_group=['testgroup'])
+ try:
+ db.instance_destroy(self.context, ref[0]['id'])
+ group = db.security_group_get(self.context, group['id'])
+ self.assert_(len(group.instances) == 0)
finally:
db.security_group_destroy(self.context, group['id'])
+
+ def test_destroy_security_group_disassociates_instances(self):
+ """Make sure destroying security groups disassociates instances"""
+ group = self._create_group()
+
+ ref = self.compute_api.create(
+ self.context,
+ instance_type=FLAGS.default_instance_type,
+ image_id=None,
+ security_group=['testgroup'])
+
+ try:
+ db.security_group_destroy(self.context, group['id'])
+ group = db.security_group_get(context.get_admin_context(
+ read_deleted=True), group['id'])
+ self.assert_(len(group.instances) == 0)
+ finally:
db.instance_destroy(self.context, ref[0]['id'])
def test_run_terminate(self):
@@ -163,6 +202,14 @@ class ComputeTestCase(test.TestCase):
self.compute.set_admin_password(self.context, instance_id)
self.compute.terminate_instance(self.context, instance_id)
+ def test_inject_file(self):
+ """Ensure we can write a file to an instance"""
+ instance_id = self._create_instance()
+ self.compute.run_instance(self.context, instance_id)
+ self.compute.inject_file(self.context, instance_id, "/tmp/test",
+ "File Contents")
+ self.compute.terminate_instance(self.context, instance_id)
+
def test_snapshot(self):
"""Ensure instance can be snapshotted"""
instance_id = self._create_instance()
diff --git a/nova/tests/test_log.py b/nova/tests/test_log.py
index 868a5ead3..c2c9d7772 100644
--- a/nova/tests/test_log.py
+++ b/nova/tests/test_log.py
@@ -46,6 +46,27 @@ class RootLoggerTestCase(test.TestCase):
self.assert_(True) # didn't raise exception
+class LogHandlerTestCase(test.TestCase):
+ def test_log_path_logdir(self):
+ self.flags(logdir='/some/path')
+ self.assertEquals(log.get_log_file_path(binary='foo-bar'),
+ '/some/path/foo-bar.log')
+
+ def test_log_path_logfile(self):
+ self.flags(logfile='/some/path/foo-bar.log')
+ self.assertEquals(log.get_log_file_path(binary='foo-bar'),
+ '/some/path/foo-bar.log')
+
+ def test_log_path_none(self):
+ self.assertTrue(log.get_log_file_path(binary='foo-bar') is None)
+
+ def test_log_path_logfile_overrides_logdir(self):
+ self.flags(logdir='/some/other/path',
+ logfile='/some/path/foo-bar.log')
+ self.assertEquals(log.get_log_file_path(binary='foo-bar'),
+ '/some/path/foo-bar.log')
+
+
class NovaFormatterTestCase(test.TestCase):
def setUp(self):
super(NovaFormatterTestCase, self).setUp()
diff --git a/nova/tests/test_xenapi.py b/nova/tests/test_xenapi.py
index 9f5b266f3..6b8efc9d8 100644
--- a/nova/tests/test_xenapi.py
+++ b/nova/tests/test_xenapi.py
@@ -32,6 +32,7 @@ from nova.virt import xenapi_conn
from nova.virt.xenapi import fake as xenapi_fake
from nova.virt.xenapi import volume_utils
from nova.virt.xenapi.vmops import SimpleDH
+from nova.virt.xenapi.vmops import VMOps
from nova.tests.db import fakes as db_fakes
from nova.tests.xenapi import stubs
from nova.tests.glance import stubs as glance_stubs
@@ -141,6 +142,10 @@ class XenAPIVolumeTestCase(test.TestCase):
self.stubs.UnsetAll()
+def reset_network(*args):
+ pass
+
+
class XenAPIVMTestCase(test.TestCase):
"""
Unit tests for VM operations
@@ -162,6 +167,7 @@ class XenAPIVMTestCase(test.TestCase):
stubs.stubout_session(self.stubs, stubs.FakeSessionForVMTests)
stubs.stubout_get_this_vm_uuid(self.stubs)
stubs.stubout_stream_disk(self.stubs)
+ self.stubs.Set(VMOps, 'reset_network', reset_network)
glance_stubs.stubout_glance_client(self.stubs,
glance_stubs.FakeGlance)
self.conn = xenapi_conn.get_connection(False)
@@ -243,7 +249,8 @@ class XenAPIVMTestCase(test.TestCase):
# Check that the VM is running according to XenAPI.
self.assertEquals(vm['power_state'], 'Running')
- def _test_spawn(self, image_id, kernel_id, ramdisk_id):
+ def _test_spawn(self, image_id, kernel_id, ramdisk_id,
+ instance_type="m1.large"):
stubs.stubout_session(self.stubs, stubs.FakeSessionForVMTests)
values = {'name': 1,
'id': 1,
@@ -252,7 +259,7 @@ class XenAPIVMTestCase(test.TestCase):
'image_id': image_id,
'kernel_id': kernel_id,
'ramdisk_id': ramdisk_id,
- 'instance_type': 'm1.large',
+ 'instance_type': instance_type,
'mac_address': 'aa:bb:cc:dd:ee:ff',
}
conn = xenapi_conn.get_connection(False)
@@ -260,6 +267,12 @@ class XenAPIVMTestCase(test.TestCase):
conn.spawn(instance)
self.check_vm_record(conn)
+ def test_spawn_not_enough_memory(self):
+ FLAGS.xenapi_image_service = 'glance'
+ self.assertRaises(Exception,
+ self._test_spawn,
+ 1, 2, 3, "m1.xlarge")
+
def test_spawn_raw_objectstore(self):
FLAGS.xenapi_image_service = 'objectstore'
self._test_spawn(1, None, None)
diff --git a/nova/twistd.py b/nova/twistd.py
index 6390a8144..60ff7879a 100644
--- a/nova/twistd.py
+++ b/nova/twistd.py
@@ -43,8 +43,6 @@ else:
FLAGS = flags.FLAGS
-flags.DEFINE_string('logdir', None, 'directory to keep log files in '
- '(will be prepended to $logfile)')
class TwistdServerOptions(ServerOptions):
diff --git a/nova/utils.py b/nova/utils.py
index 5f5225289..42efa0008 100644
--- a/nova/utils.py
+++ b/nova/utils.py
@@ -20,13 +20,14 @@
System-level utilities and helper functions.
"""
+import base64
import datetime
import inspect
import json
import os
import random
-import subprocess
import socket
+import string
import struct
import sys
import time
@@ -36,6 +37,7 @@ import netaddr
from eventlet import event
from eventlet import greenthread
+from eventlet.green import subprocess
from nova import exception
from nova.exception import ProcessExecutionError
@@ -152,6 +154,42 @@ def execute(cmd, process_input=None, addl_env=None, check_exit_code=True):
return result
+def ssh_execute(ssh, cmd, process_input=None,
+ addl_env=None, check_exit_code=True):
+ LOG.debug(_("Running cmd (SSH): %s"), cmd)
+ if addl_env:
+ raise exception.Error("Environment not supported over SSH")
+
+ if process_input:
+ # This is (probably) fixable if we need it...
+ raise exception.Error("process_input not supported over SSH")
+
+ stdin_stream, stdout_stream, stderr_stream = ssh.exec_command(cmd)
+ channel = stdout_stream.channel
+
+ #stdin.write('process_input would go here')
+ #stdin.flush()
+
+ # NOTE(justinsb): This seems suspicious...
+ # ...other SSH clients have buffering issues with this approach
+ stdout = stdout_stream.read()
+ stderr = stderr_stream.read()
+ stdin_stream.close()
+
+ exit_status = channel.recv_exit_status()
+
+ # exit_status == -1 if no exit code was returned
+ if exit_status != -1:
+ LOG.debug(_("Result was %s") % exit_status)
+ if check_exit_code and exit_status != 0:
+ raise exception.ProcessExecutionError(exit_code=exit_status,
+ stdout=stdout,
+ stderr=stderr,
+ cmd=cmd)
+
+ return (stdout, stderr)
+
+
def abspath(s):
return os.path.join(os.path.dirname(__file__), s)
@@ -199,6 +237,15 @@ def generate_mac():
return ':'.join(map(lambda x: "%02x" % x, mac))
+def generate_password(length=20):
+ """Generate a random sequence of letters and digits
+ to be used as a password. Note that this is not intended
+ to represent the ultimate in security.
+ """
+ chrs = string.letters + string.digits
+ return "".join([random.choice(chrs) for i in xrange(length)])
+
+
def last_octet(address):
return int(address.split(".")[-1])
@@ -440,3 +487,15 @@ def dumps(value):
def loads(s):
return json.loads(s)
+
+
+def ensure_b64_encoding(val):
+ """Safety method to ensure that values expected to be base64-encoded
+ actually are. If they are, the value is returned unchanged. Otherwise,
+ the encoded value is returned.
+ """
+ try:
+ dummy = base64.decode(val)
+ return val
+ except TypeError:
+ return base64.b64encode(val)
diff --git a/nova/version.py b/nova/version.py
index 7b27acb6a..c3ecc2245 100644
--- a/nova/version.py
+++ b/nova/version.py
@@ -21,7 +21,7 @@ except ImportError:
'revision_id': 'LOCALREVISION',
'revno': 0}
-NOVA_VERSION = ['2011', '1']
+NOVA_VERSION = ['2011', '2']
YEAR, COUNT = NOVA_VERSION
FINAL = False # This becomes true at Release Candidate time
diff --git a/nova/virt/fake.py b/nova/virt/fake.py
index 161445b86..92749f38a 100644
--- a/nova/virt/fake.py
+++ b/nova/virt/fake.py
@@ -152,6 +152,21 @@ class FakeConnection(object):
"""
pass
+ def inject_file(self, instance, b64_path, b64_contents):
+ """
+ Writes a file on the specified instance.
+
+ The first parameter is an instance of nova.compute.service.Instance,
+ and so the instance is being specified as instance.name. The second
+ parameter is the base64-encoded path to which the file is to be
+ written on the instance; the third is the contents of the file, also
+ base64-encoded.
+
+ The work will be done asynchronously. This function returns a
+ task that allows the caller to detect when it is complete.
+ """
+ pass
+
def rescue(self, instance):
"""
Rescue the specified instance.
diff --git a/nova/virt/xenapi/fake.py b/nova/virt/xenapi/fake.py
index e8352771c..018d0dcd3 100644
--- a/nova/virt/xenapi/fake.py
+++ b/nova/virt/xenapi/fake.py
@@ -286,6 +286,10 @@ class SessionBase(object):
rec['currently_attached'] = False
rec['device'] = ''
+ def host_compute_free_memory(self, _1, ref):
+ #Always return 12GB available
+ return 12 * 1024 * 1024 * 1024
+
def xenapi_request(self, methodname, params):
if methodname.startswith('login'):
self._login(methodname, params)
diff --git a/nova/virt/xenapi/vm_utils.py b/nova/virt/xenapi/vm_utils.py
index 4bbd522c1..80cc3035d 100644
--- a/nova/virt/xenapi/vm_utils.py
+++ b/nova/virt/xenapi/vm_utils.py
@@ -139,6 +139,16 @@ class VMHelper(HelperBase):
return vm_ref
@classmethod
+ def ensure_free_mem(cls, session, instance):
+ instance_type = instance_types.INSTANCE_TYPES[instance.instance_type]
+ mem = long(instance_type['memory_mb']) * 1024 * 1024
+ #get free memory from host
+ host = session.get_xenapi_host()
+ host_free_mem = long(session.get_xenapi().host.
+ compute_free_memory(host))
+ return host_free_mem >= mem
+
+ @classmethod
def create_vbd(cls, session, vm_ref, vdi_ref, userdevice, bootable):
"""Create a VBD record. Returns a Deferred that gives the new
VBD reference."""
@@ -384,7 +394,7 @@ class VMHelper(HelperBase):
pv = True
elif pv_str.lower() == 'false':
pv = False
- LOG.debug(_("PV Kernel in VDI:%d"), pv)
+ LOG.debug(_("PV Kernel in VDI:%s"), pv)
return pv
@classmethod
@@ -440,6 +450,14 @@ class VMHelper(HelperBase):
return None
@classmethod
+ def lookup_kernel_ramdisk(cls, session, vm):
+ vm_rec = session.get_xenapi().VM.get_record(vm)
+ if 'PV_kernel' in vm_rec and 'PV_ramdisk' in vm_rec:
+ return (vm_rec['PV_kernel'], vm_rec['PV_ramdisk'])
+ else:
+ return (None, None)
+
+ @classmethod
def compile_info(cls, record):
"""Fill record with VM status information"""
LOG.info(_("(VM_UTILS) xenserver vm state -> |%s|"),
diff --git a/nova/virt/xenapi/vmops.py b/nova/virt/xenapi/vmops.py
index e84ce20c4..0168681f6 100644
--- a/nova/virt/xenapi/vmops.py
+++ b/nova/virt/xenapi/vmops.py
@@ -67,13 +67,19 @@ class VMOps(object):
raise exception.Duplicate(_('Attempted to create'
' non-unique name %s') % instance.name)
- bridge = db.network_get_by_instance(context.get_admin_context(),
- instance['id'])['bridge']
- network_ref = \
- NetworkHelper.find_network_with_bridge(self._session, bridge)
+ #ensure enough free memory is available
+ if not VMHelper.ensure_free_mem(self._session, instance):
+ name = instance['name']
+ LOG.exception(_('instance %(name)s: not enough free memory')
+ % locals())
+ db.instance_set_state(context.get_admin_context(),
+ instance['id'],
+ power_state.SHUTDOWN)
+ return
user = AuthManager().get_user(instance.user_id)
project = AuthManager().get_project(instance.project_id)
+
#if kernel is not present we must download a raw disk
if instance.kernel_id:
disk_image_type = ImageType.DISK
@@ -99,16 +105,70 @@ class VMOps(object):
instance, kernel, ramdisk, pv_kernel)
VMHelper.create_vbd(self._session, vm_ref, vdi_ref, 0, True)
- if network_ref:
- VMHelper.create_vif(self._session, vm_ref,
- network_ref, instance.mac_address)
+ # write network info
+ admin_context = context.get_admin_context()
+
+ # TODO(tr3buchet) - remove comment in multi-nic
+ # I've decided to go ahead and consider multiple IPs and networks
+ # at this stage even though they aren't implemented because these will
+ # be needed for multi-nic and there was no sense writing it for single
+ # network/single IP and then having to turn around and re-write it
+ IPs = db.fixed_ip_get_all_by_instance(admin_context, instance['id'])
+ for network in db.network_get_all_by_instance(admin_context,
+ instance['id']):
+ network_IPs = [ip for ip in IPs if ip.network_id == network.id]
+
+ def ip_dict(ip):
+ return {'netmask': network['netmask'],
+ 'enabled': '1',
+ 'ip': ip.address}
+
+ mac_id = instance.mac_address.replace(':', '')
+ location = 'vm-data/networking/%s' % mac_id
+ mapping = {'label': network['label'],
+ 'gateway': network['gateway'],
+ 'mac': instance.mac_address,
+ 'dns': [network['dns']],
+ 'ips': [ip_dict(ip) for ip in network_IPs]}
+ self.write_to_param_xenstore(vm_ref, {location: mapping})
+
+ # TODO(tr3buchet) - remove comment in multi-nic
+ # this bit here about creating the vifs will be updated
+ # in multi-nic to handle multiple IPs on the same network
+ # and multiple networks
+ # for now it works as there is only one of each
+ bridge = network['bridge']
+ network_ref = \
+ NetworkHelper.find_network_with_bridge(self._session, bridge)
+
+ if network_ref:
+ VMHelper.create_vif(self._session, vm_ref,
+ network_ref, instance.mac_address)
+
LOG.debug(_('Starting VM %s...'), vm_ref)
self._session.call_xenapi('VM.start', vm_ref, False, False)
instance_name = instance.name
LOG.info(_('Spawning VM %(instance_name)s created %(vm_ref)s.')
% locals())
+ def _inject_onset_files():
+ onset_files = instance.onset_files
+ if onset_files:
+ # Check if this is a JSON-encoded string and convert if needed.
+ if isinstance(onset_files, basestring):
+ try:
+ onset_files = json.loads(onset_files)
+ except ValueError:
+ LOG.exception(_("Invalid value for onset_files: '%s'")
+ % onset_files)
+ onset_files = []
+ # Inject any files, if specified
+ for path, contents in instance.onset_files:
+ LOG.debug(_("Injecting file path: '%s'") % path)
+ self.inject_file(instance, path, contents)
# NOTE(armando): Do we really need to do this in virt?
+ # NOTE(tr3buchet): not sure but wherever we do it, we need to call
+ # reset_network afterwards
timer = utils.LoopingCall(f=None)
def _wait_for_boot():
@@ -119,6 +179,8 @@ class VMOps(object):
if state == power_state.RUNNING:
LOG.debug(_('Instance %s: booted'), instance['name'])
timer.stop()
+ _inject_onset_files()
+ return True
except Exception, exc:
LOG.warn(exc)
LOG.exception(_('instance %s: failed to boot'),
@@ -127,8 +189,13 @@ class VMOps(object):
instance['id'],
power_state.SHUTDOWN)
timer.stop()
+ return False
timer.f = _wait_for_boot
+
+ # call reset networking
+ self.reset_network(instance)
+
return timer.start(interval=0.5, now=True)
def _get_vm_opaque_ref(self, instance_or_vm):
@@ -161,7 +228,8 @@ class VMOps(object):
instance_name = instance_or_vm.name
vm = VMHelper.lookup(self._session, instance_name)
if vm is None:
- raise Exception(_('Instance not present %s') % instance_name)
+ raise exception.NotFound(
+ _('Instance not present %s') % instance_name)
return vm
def snapshot(self, instance, image_id):
@@ -255,6 +323,32 @@ class VMOps(object):
raise RuntimeError(resp_dict['message'])
return resp_dict['message']
+ def inject_file(self, instance, b64_path, b64_contents):
+ """Write a file to the VM instance. The path to which it is to be
+ written and the contents of the file need to be supplied; both should
+ be base64-encoded to prevent errors with non-ASCII characters being
+ transmitted. If the agent does not support file injection, or the user
+ has disabled it, a NotImplementedError will be raised.
+ """
+ # Files/paths *should* be base64-encoded at this point, but
+ # double-check to make sure.
+ b64_path = utils.ensure_b64_encoding(b64_path)
+ b64_contents = utils.ensure_b64_encoding(b64_contents)
+
+ # Need to uniquely identify this request.
+ transaction_id = str(uuid.uuid4())
+ args = {'id': transaction_id, 'b64_path': b64_path,
+ 'b64_contents': b64_contents}
+ # If the agent doesn't support file injection, a NotImplementedError
+ # will be raised with the appropriate message.
+ resp = self._make_agent_call('inject_file', instance, '', args)
+ resp_dict = json.loads(resp)
+ if resp_dict['returncode'] != '0':
+ # There was some other sort of error; the message will contain
+ # a description of the error.
+ raise RuntimeError(resp_dict['message'])
+ return resp_dict['message']
+
def _shutdown(self, instance, vm):
"""Shutdown an instance """
state = self.get_info(instance['name'])['state']
@@ -286,8 +380,23 @@ class VMOps(object):
def _destroy_vm(self, instance, vm):
"""Destroys a VM record """
try:
- task = self._session.call_xenapi('Async.VM.destroy', vm)
- self._session.wait_for_task(instance.id, task)
+ kernel = None
+ ramdisk = None
+ if instance.kernel_id or instance.ramdisk_id:
+ (kernel, ramdisk) = VMHelper.lookup_kernel_ramdisk(
+ self._session, vm)
+ task1 = self._session.call_xenapi('Async.VM.destroy', vm)
+ LOG.debug(_("Removing kernel/ramdisk files"))
+ fn = "remove_kernel_ramdisk"
+ args = {}
+ if kernel:
+ args['kernel-file'] = kernel
+ if ramdisk:
+ args['ramdisk-file'] = ramdisk
+ task2 = self._session.async_call_plugin('glance', fn, args)
+ self._session.wait_for_task(instance.id, task1)
+ self._session.wait_for_task(instance.id, task2)
+ LOG.debug(_("kernel/ramdisk files removed"))
except self.XenAPI.Failure, exc:
LOG.exception(exc)
@@ -374,6 +483,14 @@ class VMOps(object):
# TODO: implement this!
return 'http://fakeajaxconsole/fake_url'
+ def reset_network(self, instance):
+ """
+ Creates uuid arg to pass to make_agent_call and calls it.
+
+ """
+ args = {'id': str(uuid.uuid4())}
+ resp = self._make_agent_call('resetnetwork', instance, '', args)
+
def list_from_xenstore(self, vm, path):
"""Runs the xenstore-ls command to get a listing of all records
from 'path' downward. Returns a dict with the sub-paths as keys,
@@ -443,6 +560,11 @@ class VMOps(object):
if 'TIMEOUT:' in err_msg:
LOG.error(_('TIMEOUT: The call to %(method)s timed out. '
'VM id=%(instance_id)s; args=%(strargs)s') % locals())
+ elif 'NOT IMPLEMENTED:' in err_msg:
+ LOG.error(_('NOT IMPLEMENTED: The call to %(method)s is not'
+ ' supported by the agent. VM id=%(instance_id)s;'
+ ' args=%(strargs)s') % locals())
+ raise NotImplementedError(err_msg)
else:
LOG.error(_('The call to %(method)s returned an error: %(e)s. '
'VM id=%(instance_id)s; args=%(strargs)s') % locals())
diff --git a/nova/virt/xenapi_conn.py b/nova/virt/xenapi_conn.py
index a0b0499b8..c2f65699f 100644
--- a/nova/virt/xenapi_conn.py
+++ b/nova/virt/xenapi_conn.py
@@ -168,6 +168,12 @@ class XenAPIConnection(object):
"""Set the root/admin password on the VM instance"""
self._vmops.set_admin_password(instance, new_pass)
+ def inject_file(self, instance, b64_path, b64_contents):
+ """Create a file on the VM instance. The file path and contents
+ should be base64-encoded.
+ """
+ self._vmops.inject_file(instance, b64_path, b64_contents)
+
def destroy(self, instance):
"""Destroy VM instance"""
self._vmops.destroy(instance)
@@ -188,6 +194,10 @@ class XenAPIConnection(object):
"""resume the specified instance"""
self._vmops.resume(instance, callback)
+ def reset_network(self, instance):
+ """reset networking for specified instance"""
+ self._vmops.reset_network(instance)
+
def get_info(self, instance_id):
"""Return data about VM instance"""
return self._vmops.get_info(instance_id)
diff --git a/nova/volume/api.py b/nova/volume/api.py
index 0bcd8a3b0..478c83486 100644
--- a/nova/volume/api.py
+++ b/nova/volume/api.py
@@ -45,7 +45,7 @@ class API(base.Base):
LOG.warn(_("Quota exceeeded for %(pid)s, tried to create"
" %(size)sG volume") % locals())
raise quota.QuotaError(_("Volume quota exceeded. You cannot "
- "create a volume of size %s") % size)
+ "create a volume of size %sG") % size)
options = {
'size': size,
diff --git a/nova/volume/driver.py b/nova/volume/driver.py
index da7307733..82f4c2f54 100644
--- a/nova/volume/driver.py
+++ b/nova/volume/driver.py
@@ -294,8 +294,10 @@ class ISCSIDriver(VolumeDriver):
self._execute("sudo ietadm --op delete --tid=%s" %
iscsi_target)
- def _get_name_and_portal(self, volume_name, host):
+ def _get_name_and_portal(self, volume):
"""Gets iscsi name and portal from volume name and host."""
+ volume_name = volume['name']
+ host = volume['host']
(out, _err) = self._execute("sudo iscsiadm -m discovery -t "
"sendtargets -p %s" % host)
for target in out.splitlines():
@@ -307,8 +309,7 @@ class ISCSIDriver(VolumeDriver):
def discover_volume(self, volume):
"""Discover volume on a remote host."""
- iscsi_name, iscsi_portal = self._get_name_and_portal(volume['name'],
- volume['host'])
+ iscsi_name, iscsi_portal = self._get_name_and_portal(volume)
self._execute("sudo iscsiadm -m node -T %s -p %s --login" %
(iscsi_name, iscsi_portal))
self._execute("sudo iscsiadm -m node -T %s -p %s --op update "
@@ -319,8 +320,7 @@ class ISCSIDriver(VolumeDriver):
def undiscover_volume(self, volume):
"""Undiscover volume on a remote host."""
- iscsi_name, iscsi_portal = self._get_name_and_portal(volume['name'],
- volume['host'])
+ iscsi_name, iscsi_portal = self._get_name_and_portal(volume)
self._execute("sudo iscsiadm -m node -T %s -p %s --op update "
"-n node.startup -v manual" %
(iscsi_name, iscsi_portal))
diff --git a/nova/volume/manager.py b/nova/volume/manager.py
index 6f8e25e19..d2f02e4e0 100644
--- a/nova/volume/manager.py
+++ b/nova/volume/manager.py
@@ -87,7 +87,7 @@ class VolumeManager(manager.Manager):
if volume['status'] in ['available', 'in-use']:
self.driver.ensure_export(ctxt, volume)
else:
- LOG.info(_("volume %s: skipping export"), volume_ref['name'])
+ LOG.info(_("volume %s: skipping export"), volume['name'])
def create_volume(self, context, volume_id):
"""Creates and exports the volume."""
@@ -111,10 +111,10 @@ class VolumeManager(manager.Manager):
LOG.debug(_("volume %s: creating export"), volume_ref['name'])
self.driver.create_export(context, volume_ref)
- except Exception as e:
+ except Exception:
self.db.volume_update(context,
volume_ref['id'], {'status': 'error'})
- raise e
+ raise
now = datetime.datetime.utcnow()
self.db.volume_update(context,
@@ -137,11 +137,11 @@ class VolumeManager(manager.Manager):
self.driver.remove_export(context, volume_ref)
LOG.debug(_("volume %s: deleting"), volume_ref['name'])
self.driver.delete_volume(volume_ref)
- except Exception as e:
+ except Exception:
self.db.volume_update(context,
volume_ref['id'],
{'status': 'error_deleting'})
- raise e
+ raise
self.db.volume_destroy(context, volume_id)
LOG.debug(_("volume %s: deleted successfully"), volume_ref['name'])
diff --git a/nova/volume/san.py b/nova/volume/san.py
new file mode 100644
index 000000000..26d6125e7
--- /dev/null
+++ b/nova/volume/san.py
@@ -0,0 +1,335 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2011 Justin Santa Barbara
+# 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.
+"""
+Drivers for san-stored volumes.
+The unique thing about a SAN is that we don't expect that we can run the volume
+ controller on the SAN hardware. We expect to access it over SSH or some API.
+"""
+
+import os
+import paramiko
+
+from nova import exception
+from nova import flags
+from nova import log as logging
+from nova.utils import ssh_execute
+from nova.volume.driver import ISCSIDriver
+
+LOG = logging.getLogger("nova.volume.driver")
+FLAGS = flags.FLAGS
+flags.DEFINE_boolean('san_thin_provision', 'true',
+ 'Use thin provisioning for SAN volumes?')
+flags.DEFINE_string('san_ip', '',
+ 'IP address of SAN controller')
+flags.DEFINE_string('san_login', 'admin',
+ 'Username for SAN controller')
+flags.DEFINE_string('san_password', '',
+ 'Password for SAN controller')
+flags.DEFINE_string('san_privatekey', '',
+ 'Filename of private key to use for SSH authentication')
+
+
+class SanISCSIDriver(ISCSIDriver):
+ """ Base class for SAN-style storage volumes
+ (storage providers we access over SSH)"""
+ #Override because SAN ip != host ip
+ def _get_name_and_portal(self, volume):
+ """Gets iscsi name and portal from volume name and host."""
+ volume_name = volume['name']
+
+ # TODO(justinsb): store in volume, remerge with generic iSCSI code
+ host = FLAGS.san_ip
+
+ (out, _err) = self._execute("sudo iscsiadm -m discovery -t "
+ "sendtargets -p %s" % host)
+
+ location = None
+ find_iscsi_name = self._build_iscsi_target_name(volume)
+ for target in out.splitlines():
+ if find_iscsi_name in target:
+ (location, _sep, iscsi_name) = target.partition(" ")
+ break
+ if not location:
+ raise exception.Error(_("Could not find iSCSI export "
+ " for volume %s") %
+ volume_name)
+
+ iscsi_portal = location.split(",")[0]
+ LOG.debug("iscsi_name=%s, iscsi_portal=%s" %
+ (iscsi_name, iscsi_portal))
+ return (iscsi_name, iscsi_portal)
+
+ def _build_iscsi_target_name(self, volume):
+ return "%s%s" % (FLAGS.iscsi_target_prefix, volume['name'])
+
+ # discover_volume is still OK
+ # undiscover_volume is still OK
+
+ def _connect_to_ssh(self):
+ ssh = paramiko.SSHClient()
+ #TODO(justinsb): We need a better SSH key policy
+ ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
+ if FLAGS.san_password:
+ ssh.connect(FLAGS.san_ip,
+ username=FLAGS.san_login,
+ password=FLAGS.san_password)
+ elif FLAGS.san_privatekey:
+ privatekeyfile = os.path.expanduser(FLAGS.san_privatekey)
+ # It sucks that paramiko doesn't support DSA keys
+ privatekey = paramiko.RSAKey.from_private_key_file(privatekeyfile)
+ ssh.connect(FLAGS.san_ip,
+ username=FLAGS.san_login,
+ pkey=privatekey)
+ else:
+ raise exception.Error("Specify san_password or san_privatekey")
+ return ssh
+
+ def _run_ssh(self, command, check_exit_code=True):
+ #TODO(justinsb): SSH connection caching (?)
+ ssh = self._connect_to_ssh()
+
+ #TODO(justinsb): Reintroduce the retry hack
+ ret = ssh_execute(ssh, command, check_exit_code=check_exit_code)
+
+ ssh.close()
+
+ return ret
+
+ def ensure_export(self, context, volume):
+ """Synchronously recreates an export for a logical volume."""
+ pass
+
+ def create_export(self, context, volume):
+ """Exports the volume."""
+ pass
+
+ def remove_export(self, context, volume):
+ """Removes an export for a logical volume."""
+ pass
+
+ def check_for_setup_error(self):
+ """Returns an error if prerequisites aren't met"""
+ if not (FLAGS.san_password or FLAGS.san_privatekey):
+ raise exception.Error("Specify san_password or san_privatekey")
+
+ if not (FLAGS.san_ip):
+ raise exception.Error("san_ip must be set")
+
+
+def _collect_lines(data):
+ """ Split lines from data into an array, trimming them """
+ matches = []
+ for line in data.splitlines():
+ match = line.strip()
+ matches.append(match)
+
+ return matches
+
+
+def _get_prefixed_values(data, prefix):
+ """Collect lines which start with prefix; with trimming"""
+ matches = []
+ for line in data.splitlines():
+ line = line.strip()
+ if line.startswith(prefix):
+ match = line[len(prefix):]
+ match = match.strip()
+ matches.append(match)
+
+ return matches
+
+
+class SolarisISCSIDriver(SanISCSIDriver):
+ """Executes commands relating to Solaris-hosted ISCSI volumes.
+ Basic setup for a Solaris iSCSI server:
+ pkg install storage-server SUNWiscsit
+ svcadm enable stmf
+ svcadm enable -r svc:/network/iscsi/target:default
+ pfexec itadm create-tpg e1000g0 ${MYIP}
+ pfexec itadm create-target -t e1000g0
+
+ Then grant the user that will be logging on lots of permissions.
+ I'm not sure exactly which though:
+ zfs allow justinsb create,mount,destroy rpool
+ usermod -P'File System Management' justinsb
+ usermod -P'Primary Administrator' justinsb
+
+ Also make sure you can login using san_login & san_password/san_privatekey
+ """
+
+ def _view_exists(self, luid):
+ cmd = "pfexec /usr/sbin/stmfadm list-view -l %s" % (luid)
+ (out, _err) = self._run_ssh(cmd,
+ check_exit_code=False)
+ if "no views found" in out:
+ return False
+
+ if "View Entry:" in out:
+ return True
+
+ raise exception.Error("Cannot parse list-view output: %s" % (out))
+
+ def _get_target_groups(self):
+ """Gets list of target groups from host."""
+ (out, _err) = self._run_ssh("pfexec /usr/sbin/stmfadm list-tg")
+ matches = _get_prefixed_values(out, 'Target group: ')
+ LOG.debug("target_groups=%s" % matches)
+ return matches
+
+ def _target_group_exists(self, target_group_name):
+ return target_group_name not in self._get_target_groups()
+
+ def _get_target_group_members(self, target_group_name):
+ (out, _err) = self._run_ssh("pfexec /usr/sbin/stmfadm list-tg -v %s" %
+ (target_group_name))
+ matches = _get_prefixed_values(out, 'Member: ')
+ LOG.debug("members of %s=%s" % (target_group_name, matches))
+ return matches
+
+ def _is_target_group_member(self, target_group_name, iscsi_target_name):
+ return iscsi_target_name in (
+ self._get_target_group_members(target_group_name))
+
+ def _get_iscsi_targets(self):
+ cmd = ("pfexec /usr/sbin/itadm list-target | "
+ "awk '{print $1}' | grep -v ^TARGET")
+ (out, _err) = self._run_ssh(cmd)
+ matches = _collect_lines(out)
+ LOG.debug("_get_iscsi_targets=%s" % (matches))
+ return matches
+
+ def _iscsi_target_exists(self, iscsi_target_name):
+ return iscsi_target_name in self._get_iscsi_targets()
+
+ def _build_zfs_poolname(self, volume):
+ #TODO(justinsb): rpool should be configurable
+ zfs_poolname = 'rpool/%s' % (volume['name'])
+ return zfs_poolname
+
+ def create_volume(self, volume):
+ """Creates a volume."""
+ if int(volume['size']) == 0:
+ sizestr = '100M'
+ else:
+ sizestr = '%sG' % volume['size']
+
+ zfs_poolname = self._build_zfs_poolname(volume)
+
+ thin_provision_arg = '-s' if FLAGS.san_thin_provision else ''
+ # Create a zfs volume
+ self._run_ssh("pfexec /usr/sbin/zfs create %s -V %s %s" %
+ (thin_provision_arg,
+ sizestr,
+ zfs_poolname))
+
+ def _get_luid(self, volume):
+ zfs_poolname = self._build_zfs_poolname(volume)
+
+ cmd = ("pfexec /usr/sbin/sbdadm list-lu | "
+ "grep -w %s | awk '{print $1}'" %
+ (zfs_poolname))
+
+ (stdout, _stderr) = self._run_ssh(cmd)
+
+ luid = stdout.strip()
+ return luid
+
+ def _is_lu_created(self, volume):
+ luid = self._get_luid(volume)
+ return luid
+
+ def delete_volume(self, volume):
+ """Deletes a volume."""
+ zfs_poolname = self._build_zfs_poolname(volume)
+ self._run_ssh("pfexec /usr/sbin/zfs destroy %s" %
+ (zfs_poolname))
+
+ def local_path(self, volume):
+ # TODO(justinsb): Is this needed here?
+ escaped_group = FLAGS.volume_group.replace('-', '--')
+ escaped_name = volume['name'].replace('-', '--')
+ return "/dev/mapper/%s-%s" % (escaped_group, escaped_name)
+
+ def ensure_export(self, context, volume):
+ """Synchronously recreates an export for a logical volume."""
+ #TODO(justinsb): On bootup, this is called for every volume.
+ # It then runs ~5 SSH commands for each volume,
+ # most of which fetch the same info each time
+ # This makes initial start stupid-slow
+ self._do_export(volume, force_create=False)
+
+ def create_export(self, context, volume):
+ self._do_export(volume, force_create=True)
+
+ def _do_export(self, volume, force_create):
+ # Create a Logical Unit (LU) backed by the zfs volume
+ zfs_poolname = self._build_zfs_poolname(volume)
+
+ if force_create or not self._is_lu_created(volume):
+ cmd = ("pfexec /usr/sbin/sbdadm create-lu /dev/zvol/rdsk/%s" %
+ (zfs_poolname))
+ self._run_ssh(cmd)
+
+ luid = self._get_luid(volume)
+ iscsi_name = self._build_iscsi_target_name(volume)
+ target_group_name = 'tg-%s' % volume['name']
+
+ # Create a iSCSI target, mapped to just this volume
+ if force_create or not self._target_group_exists(target_group_name):
+ self._run_ssh("pfexec /usr/sbin/stmfadm create-tg %s" %
+ (target_group_name))
+
+ # Yes, we add the initiatior before we create it!
+ # Otherwise, it complains that the target is already active
+ if force_create or not self._is_target_group_member(target_group_name,
+ iscsi_name):
+ self._run_ssh("pfexec /usr/sbin/stmfadm add-tg-member -g %s %s" %
+ (target_group_name, iscsi_name))
+ if force_create or not self._iscsi_target_exists(iscsi_name):
+ self._run_ssh("pfexec /usr/sbin/itadm create-target -n %s" %
+ (iscsi_name))
+ if force_create or not self._view_exists(luid):
+ self._run_ssh("pfexec /usr/sbin/stmfadm add-view -t %s %s" %
+ (target_group_name, luid))
+
+ def remove_export(self, context, volume):
+ """Removes an export for a logical volume."""
+
+ # This is the reverse of _do_export
+ luid = self._get_luid(volume)
+ iscsi_name = self._build_iscsi_target_name(volume)
+ target_group_name = 'tg-%s' % volume['name']
+
+ if self._view_exists(luid):
+ self._run_ssh("pfexec /usr/sbin/stmfadm remove-view -l %s -a" %
+ (luid))
+
+ if self._iscsi_target_exists(iscsi_name):
+ self._run_ssh("pfexec /usr/sbin/stmfadm offline-target %s" %
+ (iscsi_name))
+ self._run_ssh("pfexec /usr/sbin/itadm delete-target %s" %
+ (iscsi_name))
+
+ # We don't delete the tg-member; we delete the whole tg!
+
+ if self._target_group_exists(target_group_name):
+ self._run_ssh("pfexec /usr/sbin/stmfadm delete-tg %s" %
+ (target_group_name))
+
+ if self._is_lu_created(volume):
+ self._run_ssh("pfexec /usr/sbin/sbdadm delete-lu %s" %
+ (luid))