diff options
| author | Ryan Lane <rlane@wikimedia.org> | 2011-02-07 22:59:55 +0000 |
|---|---|---|
| committer | Ryan Lane <rlane@wikimedia.org> | 2011-02-07 22:59:55 +0000 |
| commit | 4c50ddee48971c76f0f6252295747b89de5d3697 (patch) | |
| tree | 070c64df2390a9b0952afe46ba98729fe33c6867 /nova | |
| parent | c02a587ea03fecde26f49bec52f8d96aa551979a (diff) | |
| parent | 5a2e8b2b6571636b2cf9ddc36a4c509fedde9b93 (diff) | |
| download | nova-4c50ddee48971c76f0f6252295747b89de5d3697.tar.gz nova-4c50ddee48971c76f0f6252295747b89de5d3697.tar.xz nova-4c50ddee48971c76f0f6252295747b89de5d3697.zip | |
Merge from trunk
Diffstat (limited to 'nova')
| -rw-r--r-- | nova/adminclient.py | 20 | ||||
| -rw-r--r-- | nova/api/ec2/__init__.py | 19 | ||||
| -rw-r--r-- | nova/api/ec2/admin.py | 11 | ||||
| -rw-r--r-- | nova/api/ec2/cloud.py | 7 | ||||
| -rw-r--r-- | nova/api/openstack/__init__.py | 4 | ||||
| -rw-r--r-- | nova/compute/manager.py | 22 | ||||
| -rw-r--r-- | nova/db/sqlalchemy/api.py | 16 | ||||
| -rw-r--r-- | nova/db/sqlalchemy/migrate_repo/versions/001_austin.py | 24 | ||||
| -rw-r--r-- | nova/db/sqlalchemy/migrate_repo/versions/002_bexar.py | 24 | ||||
| -rw-r--r-- | nova/db/sqlalchemy/migration.py | 11 | ||||
| -rw-r--r-- | nova/db/sqlalchemy/models.py | 6 | ||||
| -rw-r--r-- | nova/log.py | 7 | ||||
| -rw-r--r-- | nova/network/linux_net.py | 7 | ||||
| -rw-r--r-- | nova/scheduler/simple.py | 8 | ||||
| -rw-r--r-- | nova/tests/db/nova.austin.sqlite | bin | 0 -> 44032 bytes | |||
| -rw-r--r-- | nova/tests/test_compute.py | 55 | ||||
| -rw-r--r-- | nova/version.py | 2 | ||||
| -rw-r--r-- | nova/volume/api.py | 2 |
18 files changed, 179 insertions, 66 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..ddcdc673c 100644 --- a/nova/api/ec2/__init__.py +++ b/nova/api/ec2/__init__.py @@ -171,7 +171,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! @@ -316,30 +316,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 9683e2ebd..00d044e95 100644 --- a/nova/api/ec2/cloud.py +++ b/nova/api/ec2/cloud.py @@ -714,7 +714,12 @@ class CloudController(object): r = {} r['reservationId'] = instance['reservation_id'] r['ownerId'] = instance['project_id'] - r['groupSet'] = self._convert_to_set([], 'groups') + security_group_names = [] + if instance.get('security_groups'): + for security_group in instance['security_groups']: + security_group_names.append(security_group['name']) + r['groupSet'] = self._convert_to_set(security_group_names, + 'groupId') r['instancesSet'] = [] reservations[instance['reservation_id']] = r reservations[instance['reservation_id']]['instancesSet'].append(i) diff --git a/nova/api/openstack/__init__.py b/nova/api/openstack/__init__.py index c70bb39ed..056c7dd27 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) diff --git a/nova/compute/manager.py b/nova/compute/manager.py index 0f9bf301f..f4418af26 100644 --- a/nova/compute/manager.py +++ b/nova/compute/manager.py @@ -37,7 +37,6 @@ terminating it. import datetime import random import string -import logging import socket import functools @@ -231,22 +230,25 @@ class ComputeManager(manager.Manager): instance_ref = self.db.instance_get(context, instance_id) LOG.audit(_("Terminating instance %s"), instance_id, context=context) - if not FLAGS.stub_network: - address = self.db.instance_get_floating_address(context, - instance_ref['id']) - if address: - LOG.debug(_("Disassociating address %s"), address, + fixed_ip = instance_ref.get('fixed_ip') + if not FLAGS.stub_network and fixed_ip: + floating_ips = fixed_ip.get('floating_ips') or [] + for floating_ip in floating_ips: + address = floating_ip['address'] + LOG.debug("Disassociating address %s", address, context=context) # NOTE(vish): Right now we don't really care if the ip is # disassociated. We may need to worry about # checking this later. + network_topic = self.db.queue_get_for(context, + FLAGS.network_topic, + floating_ip['host']) rpc.cast(context, - self.get_network_topic(context), + network_topic, {"method": "disassociate_floating_ip", "args": {"floating_address": address}}) - address = self.db.instance_get_fixed_address(context, - instance_ref['id']) + address = fixed_ip['address'] if address: LOG.debug(_("Deallocating address %s"), address, context=context) @@ -256,7 +258,7 @@ class ComputeManager(manager.Manager): self.network_manager.deallocate_fixed_ip(context.elevated(), address) - volumes = instance_ref.get('volumes', []) or [] + volumes = instance_ref.get('volumes') or [] for volume in volumes: self.detach_volume(context, instance_id, volume['id']) if instance_ref['state'] == power_state.SHUTOFF: diff --git a/nova/db/sqlalchemy/api.py b/nova/db/sqlalchemy/api.py index 895e7eabe..85250d56e 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 @@ -670,8 +671,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 @@ -1583,6 +1590,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..366944591 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)), @@ -523,7 +509,7 @@ def upgrade(migrate_engine): meta.bind = migrate_engine 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, @@ -539,7 +525,7 @@ def upgrade(migrate_engine): 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..699b837f8 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,7 +209,8 @@ 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): + for table in (certificates, consoles, console_pools, instance_actions, + iscsi_targets): try: table.create() except Exception: diff --git a/nova/db/sqlalchemy/migration.py b/nova/db/sqlalchemy/migration.py index 33d14827b..2a13c5466 100644 --- a/nova/db/sqlalchemy/migration.py +++ b/nova/db/sqlalchemy/migration.py @@ -46,12 +46,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..7efb36c0e 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') diff --git a/nova/log.py b/nova/log.py index e1c9f46f4..b541488bd 100644 --- a/nova/log.py +++ b/nova/log.py @@ -31,6 +31,7 @@ import cStringIO import json import logging import logging.handlers +import sys import traceback from nova import flags @@ -191,6 +192,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..de0e488ae 100644 --- a/nova/network/linux_net.py +++ b/nova/network/linux_net.py @@ -37,6 +37,9 @@ 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') @@ -313,8 +316,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']) @@ -359,6 +363,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/scheduler/simple.py b/nova/scheduler/simple.py index baf4966d4..0191ceb3d 100644 --- a/nova/scheduler/simple.py +++ b/nova/scheduler/simple.py @@ -43,7 +43,9 @@ class SimpleScheduler(chance.ChanceScheduler): def schedule_run_instance(self, context, instance_id, *_args, **_kwargs): """Picks a host that is up and has the fewest running instances.""" instance_ref = db.instance_get(context, instance_id) - if instance_ref['availability_zone'] and context.is_admin: + if (instance_ref['availability_zone'] + and ':' in instance_ref['availability_zone'] + and context.is_admin): zone, _x, host = instance_ref['availability_zone'].partition(':') service = db.service_get_by_args(context.elevated(), host, 'nova-compute') @@ -75,7 +77,9 @@ class SimpleScheduler(chance.ChanceScheduler): def schedule_create_volume(self, context, volume_id, *_args, **_kwargs): """Picks a host that is up and has the fewest volumes.""" volume_ref = db.volume_get(context, volume_id) - if (':' in volume_ref['availability_zone']) and context.is_admin: + if (volume_ref['availability_zone'] + and ':' in volume_ref['availability_zone'] + and context.is_admin): zone, _x, host = volume_ref['availability_zone'].partition(':') service = db.service_get_by_args(context.elevated(), host, 'nova-volume') diff --git a/nova/tests/db/nova.austin.sqlite b/nova/tests/db/nova.austin.sqlite Binary files differnew file mode 100644 index 000000000..ad1326bce --- /dev/null +++ b/nova/tests/db/nova.austin.sqlite diff --git a/nova/tests/test_compute.py b/nova/tests/test_compute.py index 09f6ee94a..2aa0690e7 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): 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/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, |
