From 1aadd5e961a6f1983454f5f49f077f0be0d6cba8 Mon Sep 17 00:00:00 2001 From: Lvov Maxim Date: Fri, 20 May 2011 12:53:49 +0400 Subject: added support for reserving certain network for certain project --- bin/nova-manage | 7 ++++--- nova/network/manager.py | 2 ++ 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/bin/nova-manage b/bin/nova-manage index db964064d..c64f25c23 100755 --- a/bin/nova-manage +++ b/bin/nova-manage @@ -515,11 +515,11 @@ class NetworkCommands(object): """Class for managing networks.""" def create(self, fixed_range=None, num_networks=None, - network_size=None, vlan_start=None, + network_size=None, vlan_start=None, project_id=None, vpn_start=None, fixed_range_v6=None, label='public'): """Creates fixed ips for host by range arguments: fixed_range=FLAG, [num_networks=FLAG], - [network_size=FLAG], [vlan_start=FLAG], + [network_size=FLAG], [vlan_start=FLAG], [project_id=None], [vpn_start=FLAG], [fixed_range_v6=FLAG]""" if not fixed_range: msg = _('Fixed range in the form of 10.0.0.0/8 is ' @@ -545,7 +545,8 @@ class NetworkCommands(object): vlan_start=int(vlan_start), vpn_start=int(vpn_start), cidr_v6=fixed_range_v6, - label=label) + label=label, + project_id=project_id) except ValueError, e: print e raise e diff --git a/nova/network/manager.py b/nova/network/manager.py index 5a6fdde5a..3422b72c1 100644 --- a/nova/network/manager.py +++ b/nova/network/manager.py @@ -564,6 +564,8 @@ class VlanManager(NetworkManager): net['dhcp_start'] = str(project_net[3]) net['vlan'] = vlan net['bridge'] = 'br%s' % vlan + if kwargs.has_key('project_id'): + net['project_id'] = kwargs['project_id'] if(FLAGS.use_ipv6): cidr_v6 = '%s/%s' % (fixed_net_v6[start_v6], significant_bits_v6) -- cgit From 4c1fd8a303a8eb1287fd83b04bd767171221d117 Mon Sep 17 00:00:00 2001 From: Lvov Maxim Date: Mon, 30 May 2011 11:25:21 +0400 Subject: add support for keyword arguments --- bin/nova-manage | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/bin/nova-manage b/bin/nova-manage index c64f25c23..3d09f9064 100755 --- a/bin/nova-manage +++ b/bin/nova-manage @@ -1132,9 +1132,18 @@ def main(): action = argv.pop(0) matches = lazy_match(action, actions) action, fn = matches[0] + + fn_args, fn_kwargs = [], {} + for arg in argv: + if '=' in arg: + key, value = arg.split('=') + fn_kwargs[key] = value + else: + fn_args.append(arg) + # call the action with the remaining arguments try: - fn(*argv) + fn(*fn_args, **fn_kwargs) sys.exit(0) except TypeError: print _("Possible wrong number of arguments supplied") -- cgit From b85e089f807249eacc66172e56a1a69d450fafba Mon Sep 17 00:00:00 2001 From: Lvov Maxim Date: Mon, 30 May 2011 11:30:22 +0400 Subject: project_id moved to be last --- bin/nova-manage | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/bin/nova-manage b/bin/nova-manage index 3d09f9064..8faf072bc 100755 --- a/bin/nova-manage +++ b/bin/nova-manage @@ -515,12 +515,12 @@ class NetworkCommands(object): """Class for managing networks.""" def create(self, fixed_range=None, num_networks=None, - network_size=None, vlan_start=None, project_id=None, - vpn_start=None, fixed_range_v6=None, label='public'): + network_size=None, vlan_start=None, vpn_start=None, + fixed_range_v6=None, label='public', project_id=None): """Creates fixed ips for host by range arguments: fixed_range=FLAG, [num_networks=FLAG], - [network_size=FLAG], [vlan_start=FLAG], [project_id=None], - [vpn_start=FLAG], [fixed_range_v6=FLAG]""" + [network_size=FLAG], [vlan_start=FLAG], [vpn_start=FLAG], + [fixed_range_v6=FLAG], [project_id=None]""" if not fixed_range: msg = _('Fixed range in the form of 10.0.0.0/8 is ' 'required to create networks.') -- cgit From f53e7d16181e0a7141956d835f71c09d76508c2d Mon Sep 17 00:00:00 2001 From: Lvov Maxim Date: Thu, 9 Jun 2011 22:02:01 +0400 Subject: parse options with optparse, options prepended '--' --- bin/nova-manage | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/bin/nova-manage b/bin/nova-manage index 8faf072bc..76bed16f7 100755 --- a/bin/nova-manage +++ b/bin/nova-manage @@ -62,6 +62,8 @@ import sys import time import IPy +from inspect import getargspec +from optparse import OptionParser # If ../nova/__init__.py exists, add ../ to Python search path, so that # it will override what happens to be installed in /usr/(local/)lib/python... @@ -1133,14 +1135,18 @@ def main(): matches = lazy_match(action, actions) action, fn = matches[0] - fn_args, fn_kwargs = [], {} - for arg in argv: - if '=' in arg: - key, value = arg.split('=') - fn_kwargs[key] = value - else: - fn_args.append(arg) - + func_args = getargspec(fn).args + parser = OptionParser() + for arg in func_args: + dasharg = "--%s" % arg + parser.add_option(dasharg) + + (opts, fn_args) = parser.parse_args(argv) + fn_kwargs = vars(opts) + for k, v in fn_kwargs.items(): + if v is None: + del fn_kwargs[k] + # call the action with the remaining arguments try: fn(*fn_args, **fn_kwargs) -- cgit From 3f69522284833a52373ccf1e1006d9b2f20ccb06 Mon Sep 17 00:00:00 2001 From: Lvov Maxim Date: Fri, 10 Jun 2011 17:33:54 +0400 Subject: decorators for action methods added --- .bzrignore | 3 +++ bin/nova-manage | 37 ++++++++++++++++++++++++++++--------- 2 files changed, 31 insertions(+), 9 deletions(-) diff --git a/.bzrignore b/.bzrignore index 14d8028f7..351b2bef7 100644 --- a/.bzrignore +++ b/.bzrignore @@ -13,3 +13,6 @@ nova/vcsversion.py clean.sqlite run_tests.log tests.sqlite +nova/tests/instance-* +tags + diff --git a/bin/nova-manage b/bin/nova-manage index 76bed16f7..2903e411c 100755 --- a/bin/nova-manage +++ b/bin/nova-manage @@ -105,6 +105,21 @@ flags.DEFINE_flag(flags.HelpshortFlag()) flags.DEFINE_flag(flags.HelpXMLFlag()) +# Decorators for actions +def positionargs(*args, **kwargs): + def _decorator(func): + req = args[-1] + func.__dict__['arguments'] = ' '.join("%s" % i for i in args[:req]) + func.__dict__['optargs'] = ' '.join("%s" % i for i in args[req:-1]) + return func + return _decorator + +def optionargs(*args, **kwargs): + def _decorator(func): + func.__dict__.setdefault('options', []).insert(0, (args, kwargs)) + return func + return _decorator + def param2id(object_id): """Helper function to convert various id types to internal id. args: [object_id], e.g. 'vol-0000000a' or 'volume-0000000a' or '10' @@ -515,7 +530,11 @@ class FloatingIpCommands(object): class NetworkCommands(object): """Class for managing networks.""" - + @optionargs('--project', dest="project_id", help='Project for network') + @optionargs('--vlan', dest="vlan_start", help='VLAN ID') + @positionargs('fixed_range', 'num_networks', + 'network_size', 'vlan_start', 'vpn_start', + 'fixed_range_v6', 'label', 'project_id', 1) def create(self, fixed_range=None, num_networks=None, network_size=None, vlan_start=None, vpn_start=None, fixed_range_v6=None, label='public', project_id=None): @@ -1135,25 +1154,25 @@ def main(): matches = lazy_match(action, actions) action, fn = matches[0] - func_args = getargspec(fn).args - parser = OptionParser() - for arg in func_args: - dasharg = "--%s" % arg - parser.add_option(dasharg) - + usage = "%prog " + "%s %s %s [%s] [options]" % (sys.argv[2], + sys.argv[3], fn.arguments, fn.optargs) + parser = OptionParser(usage=usage) + for ar, kw in fn.options: + parser.add_option(*ar, **kw) (opts, fn_args) = parser.parse_args(argv) fn_kwargs = vars(opts) + for k, v in fn_kwargs.items(): if v is None: del fn_kwargs[k] - + # call the action with the remaining arguments try: fn(*fn_args, **fn_kwargs) sys.exit(0) except TypeError: print _("Possible wrong number of arguments supplied") - print "%s %s: %s" % (category, action, fn.__doc__) + print "" raise except Exception: print _("Command failed, please check log for more info") -- cgit From 8de8ed78c2d3d6ca8342134234e0cc361611de54 Mon Sep 17 00:00:00 2001 From: Lvov Maxim Date: Sat, 11 Jun 2011 00:30:30 +0400 Subject: removed posargs decorator, all methods decorated --- bin/nova-manage | 146 ++++++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 126 insertions(+), 20 deletions(-) diff --git a/bin/nova-manage b/bin/nova-manage index ce9f07f56..0b92c349a 100755 --- a/bin/nova-manage +++ b/bin/nova-manage @@ -61,7 +61,6 @@ import sys import time import IPy -from inspect import getargspec from optparse import OptionParser # If ../nova/__init__.py exists, add ../ to Python search path, so that @@ -107,15 +106,7 @@ flags.DEFINE_flag(flags.HelpXMLFlag()) # Decorators for actions -def positionargs(*args, **kwargs): - def _decorator(func): - req = args[-1] - func.__dict__['arguments'] = ' '.join("%s" % i for i in args[:req]) - func.__dict__['optargs'] = ' '.join("%s" % i for i in args[req:-1]) - return func - return _decorator - -def optionargs(*args, **kwargs): +def args(*args, **kwargs): def _decorator(func): func.__dict__.setdefault('options', []).insert(0, (args, kwargs)) return func @@ -138,6 +129,7 @@ class VpnCommands(object): self.manager = manager.AuthManager() self.pipe = pipelib.CloudPipe() + @args('--project', dest="project", metavar='', help='Project name') def list(self, project=None): """Print a listing of the VPN data for one or all projects. @@ -183,10 +175,12 @@ class VpnCommands(object): self.pipe.launch_vpn_instance(p.id) time.sleep(10) + @args('--project', dest="project_id", metavar='', help='Project name') def run(self, project_id): """Start the VPN for a given project.""" self.pipe.launch_vpn_instance(project_id) + @args('--project', dest="project_id", metavar='', help='Project name') def change(self, project_id, ip, port): """Change the ip and port for a vpn. @@ -222,6 +216,7 @@ class ShellCommands(object): Falls back to Python shell if unavailable""" self.run('python') + @args('--shell', dest="shell", metavar='', help='Python shell') def run(self, shell=None): """Runs a Python interactive interpreter. @@ -259,6 +254,7 @@ class ShellCommands(object): readline.parse_and_bind("tab:complete") code.interact() + @args('--path', dest='path', metavar='', help='Script path') def script(self, path): """Runs the script from the specifed path with flags set properly. arguments: path""" @@ -271,12 +267,18 @@ class RoleCommands(object): def __init__(self): self.manager = manager.AuthManager() + @args('--user', dest="user", metavar='', help='User name') + @args('--role', dest="role", metavar='', help='User role') + @args('--project', dest="project", metavar='', help='Project name') def add(self, user, role, project=None): """adds role to user if project is specified, adds project specific role arguments: user, role [project]""" self.manager.add_role(user, role, project) + @args('--user', dest="user", metavar='', help='User name') + @args('--role', dest="role", metavar='', help='User role') + @args('--project', dest="project", metavar='', help='Project name') def has(self, user, role, project=None): """checks to see if user has role if project is specified, returns True if user has @@ -284,6 +286,9 @@ class RoleCommands(object): arguments: user, role [project]""" print self.manager.has_role(user, role, project) + @args('--user', dest="user", metavar='', help='User name') + @args('--role', dest="role", metavar='', help='User role') + @args('--project', dest="project", metavar='', help='Project name') def remove(self, user, role, project=None): """removes role from user if project is specified, removes project specific role @@ -311,6 +316,9 @@ class UserCommands(object): def __init__(self): self.manager = manager.AuthManager() + @args('--name', dest="name", metavar='', help='Admin name') + @args('--access', dest="access", metavar='', help='Access') + @args('--secret', dest="secret", metavar='', help='Secret') def admin(self, name, access=None, secret=None): """creates a new admin and prints exports arguments: name [access] [secret]""" @@ -320,6 +328,9 @@ class UserCommands(object): _db_error(e) self._print_export(user) + @args('--name', dest="name", metavar='', help='User name') + @args('--access', dest="access", metavar='', help='Access') + @args('--secret', dest="secret", metavar='', help='Secret') def create(self, name, access=None, secret=None): """creates a new user and prints exports arguments: name [access] [secret]""" @@ -329,11 +340,13 @@ class UserCommands(object): _db_error(e) self._print_export(user) + @args('--name', dest="name", metavar='', help='User name') def delete(self, name): """deletes an existing user arguments: name""" self.manager.delete_user(name) + @args('--name', dest="name", metavar='', help='User name') def exports(self, name): """prints access and secrets for user in export format arguments: name""" @@ -349,6 +362,10 @@ class UserCommands(object): for user in self.manager.get_users(): print user.name + @args('--name', dest="name", metavar='', help='User name') + @args('--access', dest="access_key", metavar='', help='Access key') + @args('--secret', dest="secret_key", metavar='', help='Secret key') + @args('--is_admin', dest='is_admin', metavar="<'T'|'F'>", help='Is admin?') def modify(self, name, access_key, secret_key, is_admin): """update a users keys & admin flag arguments: accesskey secretkey admin @@ -362,6 +379,8 @@ class UserCommands(object): is_admin = False self.manager.modify_user(name, access_key, secret_key, is_admin) + @args('--name', dest="user_id", metavar='', help='User name') + @args('--project', dest="project_id", metavar='', help='Project name') def revoke(self, user_id, project_id=None): """revoke certs for a user arguments: user_id [project_id]""" @@ -377,6 +396,8 @@ class ProjectCommands(object): def __init__(self): self.manager = manager.AuthManager() + @args('--project', dest="project_id", metavar='', help='Project name') + @args('--user', dest="user_id", metavar='', help='User name') def add(self, project_id, user_id): """Adds user to project arguments: project_id user_id""" @@ -386,6 +407,9 @@ class ProjectCommands(object): print ex raise + @args('--project', dest="name", metavar='', help='Project name') + @args('--user', dest="project_manager", metavar='', help='Project manager') + @args('--desc', dest="description", metavar='', help='Description') def create(self, name, project_manager, description=None): """Creates a new project arguments: name project_manager [description]""" @@ -395,6 +419,9 @@ class ProjectCommands(object): print ex raise + @args('--project', dest="name", metavar='', help='Project name') + @args('--user', dest="project_manager", metavar='', help='Project manager') + @args('--desc', dest="description", metavar='', help='Description') def modify(self, name, project_manager, description=None): """Modifies a project arguments: name project_manager [description]""" @@ -404,6 +431,7 @@ class ProjectCommands(object): print ex raise + @args('--project', dest="name", metavar='', help='Project name') def delete(self, name): """Deletes an existing project arguments: name""" @@ -413,6 +441,9 @@ class ProjectCommands(object): print ex raise + @args('--project', dest="project_id", metavar='', help='Project name') + @args('--user', dest="user_id", metavar='', help='User name') + @args('--file', dest="filename", metavar='', help='File name(Default: novarc)') def environment(self, project_id, user_id, filename='novarc'): """Exports environment variables to an sourcable file arguments: project_id user_id [filename='novarc]""" @@ -424,12 +455,16 @@ class ProjectCommands(object): with open(filename, 'w') as f: f.write(rc) + @args('--user', dest="username", metavar='', help='User name') def list(self, username=None): """Lists all projects arguments: [username]""" for project in self.manager.get_projects(username): print project.name + @args('--project', dest="project_id", metavar='', help='Project name') + @args('--key', dest="key", metavar='', help='Key') + @args('--value', dest="value", metavar='', help='Value') def quota(self, project_id, key=None, value=None): """Set or display quotas for project arguments: project_id [key] [value]""" @@ -447,6 +482,8 @@ class ProjectCommands(object): value = 'unlimited' print '%s: %s' % (key, value) + @args('--project', dest="project_id", metavar='', help='Project name') + @args('--user', dest="user_id", metavar='', help='User name') def remove(self, project_id, user_id): """Removes user from project arguments: project_id user_id""" @@ -456,6 +493,7 @@ class ProjectCommands(object): print ex raise + @args('--project', dest="project_id", metavar='', help='Project name') def scrub(self, project_id): """Deletes data associated with project arguments: project_id""" @@ -466,6 +504,9 @@ class ProjectCommands(object): for group in groups: db.security_group_destroy(ctxt, group['id']) + @args('--project', dest="project_id", metavar='', help='Project name') + @args('--user', dest="user_id", metavar='', help='User name') + @args('--file', dest="filename", metavar='', help='File name(Default: nova.zip)') def zipfile(self, project_id, user_id, filename='nova.zip'): """Exports credentials for project to a zip file arguments: project_id user_id [filename='nova.zip]""" @@ -492,6 +533,7 @@ AccountCommands = ProjectCommands class FixedIpCommands(object): """Class for managing fixed ip.""" + @args('--host', dest="host", metavar='', help='Host') def list(self, host=None): """Lists all fixed ips (optionally by host) arguments: [host]""" ctxt = context.get_admin_context() @@ -528,6 +570,8 @@ class FixedIpCommands(object): class FloatingIpCommands(object): """Class for managing floating ip.""" + @args('--host', dest="host", metavar='', help='Host') + @args('--ip_range', dest="range", metavar='', help='IP range') def create(self, host, range): """Creates floating ips for host by range arguments: host ip_range""" @@ -536,6 +580,7 @@ class FloatingIpCommands(object): {'address': str(address), 'host': host}) + @args('--ip_range', dest="ip_range", metavar='', help='IP range') def delete(self, ip_range): """Deletes floating ips by range arguments: range""" @@ -543,6 +588,7 @@ class FloatingIpCommands(object): db.floating_ip_destroy(context.get_admin_context(), str(address)) + @args('--host', dest="host", metavar='', help='Host') def list(self, host=None): """Lists all floating ips (optionally by host) arguments: [host]""" @@ -563,11 +609,14 @@ class FloatingIpCommands(object): class NetworkCommands(object): """Class for managing networks.""" - @optionargs('--project', dest="project_id", help='Project for network') - @optionargs('--vlan', dest="vlan_start", help='VLAN ID') - @positionargs('fixed_range', 'num_networks', - 'network_size', 'vlan_start', 'vpn_start', - 'fixed_range_v6', 'gateway_v6', 'label', 'project_id', 1) + @args('--network', dest="fixed_range", metavar='', help='Network') + @args('--num_networks', dest="num_networks", metavar='', help='How many networks create') + @args('--network_size', dest="network_size", metavar='', help='How many hosts in network') + @args('--vlan', dest="vlan_start", metavar='', help='vlan id') + @args('--vpn', dest="vpn_start", help='vpn start') + @args('--fixed_range_v6', dest="fixed_range_v6", help='fixed ipv6 range') + @args('--gateway_v6', dest="gateway_v6", help='ipv6 gateway') + @args('--project', dest="project_id", metavar='', help='Project for network') def create(self, fixed_range=None, num_networks=None, network_size=None, vlan_start=None, vpn_start=None, fixed_range_v6=None, gateway_v6=None, label='public', project_id=None): @@ -618,6 +667,7 @@ class NetworkCommands(object): network.dhcp_start, network.dns) + @args('--network', dest="fixed_range", metavar='', help='Network to delete') def delete(self, fixed_range): """Deletes a network""" network = db.network_get_by_cidr(context.get_admin_context(), \ @@ -631,6 +681,7 @@ class NetworkCommands(object): class VmCommands(object): """Class for mangaging VM instances.""" + @args('--host', dest="host", metavar='', help='Host') def list(self, host=None): """Show a list of all instances @@ -674,6 +725,8 @@ class VmCommands(object): instance['availability_zone'], instance['launch_index']) + @args('--ec2_id', dest='ec2_id', metavar='', help='EC2 ID') + @args('--dest', dest='dest', metavar='', help='destanation node') def live_migration(self, ec2_id, dest): """Migrates a running instance to a new machine. @@ -710,6 +763,8 @@ class VmCommands(object): class ServiceCommands(object): """Enable and disable running services""" + @args('--host', dest='host', metavar='', help='Host') + @args('--service', dest='service', metavar='', help='Nova service') def list(self, host=None, service=None): """Show a list of all running services. Filter by host & service name. args: [host] [service]""" @@ -731,6 +786,8 @@ class ServiceCommands(object): active, art, svc['updated_at']) + @args('--host', dest='host', metavar='', help='Host') + @args('--service', dest='service', metavar='', help='Nova service') def enable(self, host, service): """Enable scheduling for a service args: host service""" @@ -741,6 +798,8 @@ class ServiceCommands(object): return db.service_update(ctxt, svc['id'], {'disabled': False}) + @args('--host', dest='host', metavar='', help='Host') + @args('--service', dest='service', metavar='', help='Nova service') def disable(self, host, service): """Disable scheduling for a service args: host service""" @@ -751,6 +810,7 @@ class ServiceCommands(object): return db.service_update(ctxt, svc['id'], {'disabled': True}) + @args('--host', dest='host', metavar='', help='Host') def describe_resource(self, host): """Describes cpu/memory/hdd info for host. @@ -784,6 +844,7 @@ class ServiceCommands(object): val['memory_mb'], val['local_gb']) + @args('--host', dest='host', metavar='', help='Host') def update_resource(self, host): """Updates available vcpu/memory/disk info for host. @@ -811,6 +872,7 @@ class DbCommands(object): def __init__(self): pass + @args('--version', dest='version', metavar='', help='Database version') def sync(self, version=None): """Sync the database up to the most recent version.""" return migration.db_sync(version) @@ -834,10 +896,11 @@ class VersionCommands(object): class VolumeCommands(object): """Methods for dealing with a cloud in an odd state""" + @args('--volume', dest='volume_id', metavar='', help='Volume ID') def delete(self, volume_id): """Delete a volume, bypassing the check that it must be available. - args: volume_id_id""" + args: volume_id""" ctxt = context.get_admin_context() volume = db.volume_get(ctxt, param2id(volume_id)) host = volume['host'] @@ -858,6 +921,7 @@ class VolumeCommands(object): {"method": "delete_volume", "args": {"volume_id": volume['id']}}) + @args('--volume', dest='volume_id', metavar='', help='Volume ID') def reattach(self, volume_id): """Re-attach a volume that has previously been attached to an instance. Typically called after a compute host @@ -889,6 +953,14 @@ class InstanceTypeCommands(object): val["flavorid"], val["swap"], val["rxtx_quota"], val["rxtx_cap"], deleted) + @args('--name', dest='name', metavar='', help='Name of instance type/flavor') + @args('--memory', dest='memory', metavar='', help='Memory size') + @args('--cpu', dest='vcpus', metavar='', help='Number cpus') + @args('--local_gb', dest='local_gb', metavar='', help='local_gb') + @args('--flavor', dest='flavorid', metavar='', help='Flavor ID') + @args('--swap', dest='swap', metavar='', help='Swap') + @args('--rxtx_quota', dest='rxtx_quota', metavar='', help='rxtx_quota') + @args('--rxtx_cap', dest='rxtx_cap', metavar='', help='rxtx_cap') def create(self, name, memory, vcpus, local_gb, flavorid, swap=0, rxtx_quota=0, rxtx_cap=0): """Creates instance types / flavors @@ -917,6 +989,7 @@ class InstanceTypeCommands(object): else: print "%s created" % name + @args('--name', dest='name', metavar='', help='Name of instance type/flavor') def delete(self, name, purge=None): """Marks instance types / flavors as deleted arguments: name""" @@ -938,6 +1011,7 @@ class InstanceTypeCommands(object): else: print "%s %s" % (name, verb) + @args('--name', dest='name', metavar='', help='Name of instance type/flavor') def list(self, name=None): """Lists all active or specific instance types / flavors arguments: [name]""" @@ -988,6 +1062,13 @@ class ImageCommands(object): except Exception as exc: print _("Failed to register %(path)s: %(exc)s") % locals() + @args('--image', dest='image', metavar='', help='Image') + @args('--kernel', dest='kernel', metavar='', help='Kernel') + @args('--ram', dest='ramdisk', metavar='', help='RAM disk') + @args('--owner', dest='owner', metavar='', help='Image owner') + @args('--name', dest='name', metavar='', help='Image name') + @args('--public', dest='is_public', metavar="<'T'|'F'>", help='Image public or not') + @args('--arch', dest='architecture', metavar='', help='Architecture') def all_register(self, image, kernel, ramdisk, owner, name=None, is_public='T', architecture='x86_64'): """Uploads an image, kernel, and ramdisk into the image_service @@ -1001,6 +1082,15 @@ class ImageCommands(object): architecture, 'ami', 'ami', kernel_id, ramdisk_id) + @args('--path', dest='path', metavar='', help='Image path') + @args('--owner', dest='owner', metavar='', help='Image owner') + @args('--name', dest='name', metavar='', help='Image name') + @args('--public', dest='is_public', metavar="<'T'|'F'>", help='Image public or not') + @args('--arch', dest='architecture', metavar='', help='Architecture') + @args('--cont_format', dest='container_format', metavar='', help='Container format(default: bare)') + @args('--disk_format', dest='disk_format', metavar='', help='Disk format(default: raw)') + @args('--kernel', dest='kernel_id', metavar='', help='Kernel') + @args('--ram', dest='ramdisk_id', metavar='', help='RAM disk') def image_register(self, path, owner, name=None, is_public='T', architecture='x86_64', container_format='bare', disk_format='raw', kernel_id=None, ramdisk_id=None): @@ -1013,6 +1103,11 @@ class ImageCommands(object): owner, name, is_public, architecture, kernel_id, ramdisk_id) + @args('--path', dest='path', metavar='', help='Image path') + @args('--owner', dest='owner', metavar='', help='Image owner') + @args('--name', dest='name', metavar='', help='Image name') + @args('--public', dest='is_public', metavar="<'T'|'F'>", help='Image public or not') + @args('--arch', dest='architecture', metavar='', help='Architecture') def kernel_register(self, path, owner, name=None, is_public='T', architecture='x86_64'): """Uploads a kernel into the image_service @@ -1021,6 +1116,11 @@ class ImageCommands(object): return self._register('aki', 'aki', path, owner, name, is_public, architecture) + @args('--path', dest='path', metavar='', help='Image path') + @args('--owner', dest='owner', metavar='', help='Image owner') + @args('--name', dest='name', metavar='', help='Image name') + @args('--public', dest='is_public', metavar="<'T'|'F'>", help='Image public or not') + @args('--arch', dest='architecture', metavar='', help='Architecture') def ramdisk_register(self, path, owner, name=None, is_public='T', architecture='x86_64'): """Uploads a ramdisk into the image_service @@ -1074,6 +1174,7 @@ class ImageCommands(object): except Exception as exc: print _("Failed to convert %(old)s: %(exc)s") % locals() + @args('--dir', dest='directory', metavar='', help='Images directory') def convert(self, directory): """Uploads old objectstore images in directory to new service arguments: directory""" @@ -1199,10 +1300,14 @@ def main(): matches = lazy_match(action, actions) action, fn = matches[0] - usage = "%prog " + "%s %s %s [%s] [options]" % (sys.argv[2], - sys.argv[3], fn.arguments, fn.optargs) + # For not decorated methods + arguments = getattr(fn ,'arguments', '') + optargs = getattr(fn ,'optargs', '') + options = getattr(fn ,'options', '') + + usage = "%prog " + "%s %s [options]" % (sys.argv[2], sys.argv[3]) parser = OptionParser(usage=usage) - for ar, kw in fn.options: + for ar, kw in options: parser.add_option(*ar, **kw) (opts, fn_args) = parser.parse_args(argv) fn_kwargs = vars(opts) @@ -1217,7 +1322,8 @@ def main(): sys.exit(0) except TypeError: print _("Possible wrong number of arguments supplied") - print "" + print fn.__doc__ + parser.print_help() raise except Exception: print _("Command failed, please check log for more info") -- cgit From 8ded8e4bde3825efc8a886aad40d54d0c9d126dc Mon Sep 17 00:00:00 2001 From: Lvov Maxim Date: Sat, 11 Jun 2011 00:42:44 +0400 Subject: small pep8 fixes --- bin/nova-manage | 6 +++--- nova/network/manager.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/bin/nova-manage b/bin/nova-manage index 0b92c349a..23ebe3c93 100755 --- a/bin/nova-manage +++ b/bin/nova-manage @@ -1301,9 +1301,9 @@ def main(): action, fn = matches[0] # For not decorated methods - arguments = getattr(fn ,'arguments', '') - optargs = getattr(fn ,'optargs', '') - options = getattr(fn ,'options', '') + arguments = getattr(fn, 'arguments', '') + optargs = getattr(fn, 'optargs', '') + options = getattr(fn, 'options', '') usage = "%prog " + "%s %s [options]" % (sys.argv[2], sys.argv[3]) parser = OptionParser(usage=usage) diff --git a/nova/network/manager.py b/nova/network/manager.py index ad93114fe..a7f5aac60 100644 --- a/nova/network/manager.py +++ b/nova/network/manager.py @@ -571,7 +571,7 @@ class VlanManager(NetworkManager): net['dhcp_start'] = str(project_net[3]) net['vlan'] = vlan net['bridge'] = 'br%s' % vlan - if kwargs.has_key('project_id'): + if 'project_id' in kwargs: net['project_id'] = kwargs['project_id'] if(FLAGS.use_ipv6): cidr_v6 = '%s/%s' % (fixed_net_v6[start_v6], -- cgit From 3a83aaf4f14ac25dee8b5a84779a61ca4633012a Mon Sep 17 00:00:00 2001 From: Lvov Maxim Date: Sat, 11 Jun 2011 00:49:03 +0400 Subject: 'network list' prints project id --- bin/nova-manage | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/bin/nova-manage b/bin/nova-manage index 23ebe3c93..67cf0bd51 100755 --- a/bin/nova-manage +++ b/bin/nova-manage @@ -657,15 +657,17 @@ class NetworkCommands(object): def list(self): """List all created networks""" - print "%-18s\t%-15s\t%-15s\t%-15s" % (_('network'), + print "%-18s\t%-15s\t%-15s\t%-15s\t%-15s" % (_('network'), _('netmask'), _('start address'), - 'DNS') + 'DNS', + 'project') for network in db.network_get_all(context.get_admin_context()): - print "%-18s\t%-15s\t%-15s\t%-15s" % (network.cidr, + print "%-18s\t%-15s\t%-15s\t%-15s\t%-15s" % (network.cidr, network.netmask, network.dhcp_start, - network.dns) + network.dns, + network.project_id) @args('--network', dest="fixed_range", metavar='', help='Network to delete') def delete(self, fixed_range): -- cgit From 87d90616df651787f52a59be37457335eb881ed7 Mon Sep 17 00:00:00 2001 From: Johannes Erdfelt Date: Wed, 15 Jun 2011 22:57:05 +0000 Subject: Workaround windows agent bugs where some responses have trailing \\r\\n --- nova/virt/xenapi/vmops.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/nova/virt/xenapi/vmops.py b/nova/virt/xenapi/vmops.py index 190bf7c20..5ca041059 100644 --- a/nova/virt/xenapi/vmops.py +++ b/nova/virt/xenapi/vmops.py @@ -526,7 +526,9 @@ class VMOps(object): # No response from the agent return resp_dict = json.loads(resp) - return resp_dict['message'] + # Some old versions of the windows agent have a trailing \\r\\n + # (ie CRLF escaped) for some reason. Strip that off + return resp_dict['message'].replace('\\r\\n', '') if timeout: vm_ref = self._get_vm_opaque_ref(instance) @@ -592,7 +594,9 @@ class VMOps(object): # There was some sort of error; the message will contain # a description of the error. raise RuntimeError(resp_dict['message']) - agent_pub = int(resp_dict['message']) + # Some old versions of the windows agent have a trailing \\r\\n + # (ie CRLF escaped) for some reason. Strip that off + agent_pub = int(resp_dict['message'].replace('\\r\\n', '')) dh.compute_shared(agent_pub) enc_pass = dh.encrypt(new_pass) # Send the encrypted password -- cgit From 46e016348ff4303310af328fa1af8fab513632c9 Mon Sep 17 00:00:00 2001 From: Johannes Erdfelt Date: Wed, 15 Jun 2011 23:11:00 +0000 Subject: Add trailing LF (\n) to password for compatibility with old agents --- nova/virt/xenapi/vmops.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/nova/virt/xenapi/vmops.py b/nova/virt/xenapi/vmops.py index 5ca041059..24e293a7b 100644 --- a/nova/virt/xenapi/vmops.py +++ b/nova/virt/xenapi/vmops.py @@ -526,8 +526,8 @@ class VMOps(object): # No response from the agent return resp_dict = json.loads(resp) - # Some old versions of the windows agent have a trailing \\r\\n - # (ie CRLF escaped) for some reason. Strip that off + # Some old versions of the Windows agent have a trailing \\r\\n + # (ie CRLF escaped) for some reason. Strip that off. return resp_dict['message'].replace('\\r\\n', '') if timeout: @@ -594,11 +594,13 @@ class VMOps(object): # There was some sort of error; the message will contain # a description of the error. raise RuntimeError(resp_dict['message']) - # Some old versions of the windows agent have a trailing \\r\\n - # (ie CRLF escaped) for some reason. Strip that off + # Some old versions of the Windows agent have a trailing \\r\\n + # (ie CRLF escaped) for some reason. Strip that off. agent_pub = int(resp_dict['message'].replace('\\r\\n', '')) dh.compute_shared(agent_pub) - enc_pass = dh.encrypt(new_pass) + # Some old versions of Linux and Windows agent expect trailing \n + # on password to work correctly. + enc_pass = dh.encrypt(new_pass + '\n') # Send the encrypted password password_transaction_id = str(uuid.uuid4()) password_args = {'id': password_transaction_id, 'enc_pass': enc_pass} -- cgit From 220ddb2bd5413ea5fa2bff450f4fb3aba136e909 Mon Sep 17 00:00:00 2001 From: Isaku Yamahata Date: Thu, 16 Jun 2011 20:29:10 +0900 Subject: api/ec2: check user permission for start/stop instances This patch adds precise permission check for start/stop instances. --- nova/api/ec2/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/nova/api/ec2/__init__.py b/nova/api/ec2/__init__.py index 890d57fe7..857fa96c3 100644 --- a/nova/api/ec2/__init__.py +++ b/nova/api/ec2/__init__.py @@ -262,6 +262,8 @@ class Authorizer(wsgi.Middleware): 'TerminateInstances': ['projectmanager', 'sysadmin'], 'RebootInstances': ['projectmanager', 'sysadmin'], 'UpdateInstance': ['projectmanager', 'sysadmin'], + 'StartInstances': ['projectmanager', 'sysadmin'], + 'StopInstances': ['projectmanager', 'sysadmin'], 'DeleteVolume': ['projectmanager', 'sysadmin'], 'DescribeImages': ['all'], 'DeregisterImage': ['projectmanager', 'sysadmin'], -- cgit From 009874210e818752d5f206df01313242c728e5f8 Mon Sep 17 00:00:00 2001 From: Isaku Yamahata Date: Thu, 16 Jun 2011 20:29:10 +0900 Subject: api/ec2: check user permission for start/stop instances This patch adds precise permission check for start/stop instances. --- nova/api/ec2/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/nova/api/ec2/__init__.py b/nova/api/ec2/__init__.py index 1915d007d..b8f0dd576 100644 --- a/nova/api/ec2/__init__.py +++ b/nova/api/ec2/__init__.py @@ -261,6 +261,8 @@ class Authorizer(wsgi.Middleware): 'TerminateInstances': ['projectmanager', 'sysadmin'], 'RebootInstances': ['projectmanager', 'sysadmin'], 'UpdateInstance': ['projectmanager', 'sysadmin'], + 'StartInstances': ['projectmanager', 'sysadmin'], + 'StopInstances': ['projectmanager', 'sysadmin'], 'DeleteVolume': ['projectmanager', 'sysadmin'], 'DescribeImages': ['all'], 'DeregisterImage': ['projectmanager', 'sysadmin'], -- cgit From df63c8e14da8c93453d4d7485829e38e0db30711 Mon Sep 17 00:00:00 2001 From: Isaku Yamahata Date: Thu, 16 Jun 2011 20:35:49 +0900 Subject: ec2utils: consolidate 'vol-%08x' and 'snap-%08x' By introducing helper functions, consolidate scattered '{vol, snap}-%08x' --- nova/api/ec2/__init__.py | 4 ++-- nova/api/ec2/cloud.py | 14 ++++++-------- nova/api/ec2/ec2utils.py | 11 +++++++++++ nova/tests/test_cloud.py | 16 ++++++++-------- 4 files changed, 27 insertions(+), 18 deletions(-) diff --git a/nova/api/ec2/__init__.py b/nova/api/ec2/__init__.py index 857fa96c3..35f67d39b 100644 --- a/nova/api/ec2/__init__.py +++ b/nova/api/ec2/__init__.py @@ -327,13 +327,13 @@ class Executor(wsgi.Application): except exception.VolumeNotFound as ex: LOG.info(_('VolumeNotFound raised: %s'), unicode(ex), context=context) - ec2_id = ec2utils.id_to_ec2_id(ex.volume_id, 'vol-%08x') + ec2_id = ec2utils.id_to_ec2_vol_id(ex.volume_id) message = _('Volume %s not found') % ec2_id return self._error(req, context, type(ex).__name__, message) except exception.SnapshotNotFound as ex: LOG.info(_('SnapshotNotFound raised: %s'), unicode(ex), context=context) - ec2_id = ec2utils.id_to_ec2_id(ex.snapshot_id, 'snap-%08x') + ec2_id = ec2utils.id_to_ec2_snap_id(ex.snapshot_id) message = _('Snapshot %s not found') % ec2_id return self._error(req, context, type(ex).__name__, message) except exception.NotFound as ex: diff --git a/nova/api/ec2/cloud.py b/nova/api/ec2/cloud.py index a59173103..d36eb7a04 100644 --- a/nova/api/ec2/cloud.py +++ b/nova/api/ec2/cloud.py @@ -305,9 +305,8 @@ class CloudController(object): def _format_snapshot(self, context, snapshot): s = {} - s['snapshotId'] = ec2utils.id_to_ec2_id(snapshot['id'], 'snap-%08x') - s['volumeId'] = ec2utils.id_to_ec2_id(snapshot['volume_id'], - 'vol-%08x') + s['snapshotId'] = ec2utils.id_to_ec2_snap_id(snapshot['id']) + s['volumeId'] = ec2utils.id_to_ec2_vol_id(snapshot['volume_id']) s['status'] = snapshot['status'] s['startTime'] = snapshot['created_at'] s['progress'] = snapshot['progress'] @@ -641,7 +640,7 @@ class CloudController(object): instance_data = '%s[%s]' % (instance_ec2_id, volume['instance']['host']) v = {} - v['volumeId'] = ec2utils.id_to_ec2_id(volume['id'], 'vol-%08x') + v['volumeId'] = ec2utils.id_to_ec2_vol_id(volume['id']) v['status'] = volume['status'] v['size'] = volume['size'] v['availabilityZone'] = volume['availability_zone'] @@ -663,8 +662,7 @@ class CloudController(object): else: v['attachmentSet'] = [{}] if volume.get('snapshot_id') != None: - v['snapshotId'] = ec2utils.id_to_ec2_id(volume['snapshot_id'], - 'snap-%08x') + v['snapshotId'] = ec2utils.id_to_ec2_snap_id(volume['snapshot_id']) else: v['snapshotId'] = None @@ -727,7 +725,7 @@ class CloudController(object): 'instanceId': ec2utils.id_to_ec2_id(instance_id), 'requestId': context.request_id, 'status': volume['attach_status'], - 'volumeId': ec2utils.id_to_ec2_id(volume_id, 'vol-%08x')} + 'volumeId': ec2utils.id_to_ec2_vol_id(volume_id)} def detach_volume(self, context, volume_id, **kwargs): volume_id = ec2utils.ec2_id_to_id(volume_id) @@ -739,7 +737,7 @@ class CloudController(object): 'instanceId': ec2utils.id_to_ec2_id(instance['id']), 'requestId': context.request_id, 'status': volume['attach_status'], - 'volumeId': ec2utils.id_to_ec2_id(volume_id, 'vol-%08x')} + 'volumeId': ec2utils.id_to_ec2_vol_id(volume_id)} def _convert_to_set(self, lst, label): if lst is None or lst == []: diff --git a/nova/api/ec2/ec2utils.py b/nova/api/ec2/ec2utils.py index 222e1de1e..bcdf2ba78 100644 --- a/nova/api/ec2/ec2utils.py +++ b/nova/api/ec2/ec2utils.py @@ -34,6 +34,17 @@ def id_to_ec2_id(instance_id, template='i-%08x'): return template % instance_id +def id_to_ec2_snap_id(instance_id): + """Convert an snapshot ID (int) to an ec2 snapshot ID + (snap-[base 16 number])""" + return id_to_ec2_id(instance_id, 'snap-%08x') + + +def id_to_ec2_vol_id(instance_id): + """Convert an volume ID (int) to an ec2 volume ID (vol-[base 16 number])""" + return id_to_ec2_id(instance_id, 'vol-%08x') + + _c2u = re.compile('(((?<=[a-z])[A-Z])|([A-Z](?![A-Z]|$)))') diff --git a/nova/tests/test_cloud.py b/nova/tests/test_cloud.py index 6a6256c20..2efcd304d 100644 --- a/nova/tests/test_cloud.py +++ b/nova/tests/test_cloud.py @@ -171,7 +171,7 @@ class CloudTestCase(test.TestCase): vol2 = db.volume_create(self.context, {}) result = self.cloud.describe_volumes(self.context) self.assertEqual(len(result['volumeSet']), 2) - volume_id = ec2utils.id_to_ec2_id(vol2['id'], 'vol-%08x') + volume_id = ec2utils.id_to_ec2_vol_id(vol2['id']) result = self.cloud.describe_volumes(self.context, volume_id=[volume_id]) self.assertEqual(len(result['volumeSet']), 1) @@ -187,7 +187,7 @@ class CloudTestCase(test.TestCase): snap = db.snapshot_create(self.context, {'volume_id': vol['id'], 'volume_size': vol['size'], 'status': "available"}) - snapshot_id = ec2utils.id_to_ec2_id(snap['id'], 'snap-%08x') + snapshot_id = ec2utils.id_to_ec2_snap_id(snap['id']) result = self.cloud.create_volume(self.context, snapshot_id=snapshot_id) @@ -224,7 +224,7 @@ class CloudTestCase(test.TestCase): snap2 = db.snapshot_create(self.context, {'volume_id': vol['id']}) result = self.cloud.describe_snapshots(self.context) self.assertEqual(len(result['snapshotSet']), 2) - snapshot_id = ec2utils.id_to_ec2_id(snap2['id'], 'snap-%08x') + snapshot_id = ec2utils.id_to_ec2_snap_id(snap2['id']) result = self.cloud.describe_snapshots(self.context, snapshot_id=[snapshot_id]) self.assertEqual(len(result['snapshotSet']), 1) @@ -238,7 +238,7 @@ class CloudTestCase(test.TestCase): def test_create_snapshot(self): """Makes sure create_snapshot works.""" vol = db.volume_create(self.context, {'status': "available"}) - volume_id = ec2utils.id_to_ec2_id(vol['id'], 'vol-%08x') + volume_id = ec2utils.id_to_ec2_vol_id(vol['id']) result = self.cloud.create_snapshot(self.context, volume_id=volume_id) @@ -255,7 +255,7 @@ class CloudTestCase(test.TestCase): vol = db.volume_create(self.context, {'status': "available"}) snap = db.snapshot_create(self.context, {'volume_id': vol['id'], 'status': "available"}) - snapshot_id = ec2utils.id_to_ec2_id(snap['id'], 'snap-%08x') + snapshot_id = ec2utils.id_to_ec2_snap_id(snap['id']) result = self.cloud.delete_snapshot(self.context, snapshot_id=snapshot_id) @@ -553,7 +553,7 @@ class CloudTestCase(test.TestCase): def test_update_of_volume_display_fields(self): vol = db.volume_create(self.context, {}) self.cloud.update_volume(self.context, - ec2utils.id_to_ec2_id(vol['id'], 'vol-%08x'), + ec2utils.id_to_ec2_vol_id(vol['id']), display_name='c00l v0lum3') vol = db.volume_get(self.context, vol['id']) self.assertEqual('c00l v0lum3', vol['display_name']) @@ -562,7 +562,7 @@ class CloudTestCase(test.TestCase): def test_update_of_volume_wont_update_private_fields(self): vol = db.volume_create(self.context, {}) self.cloud.update_volume(self.context, - ec2utils.id_to_ec2_id(vol['id'], 'vol-%08x'), + ec2utils.id_to_ec2_vol_id(vol['id']), mountpoint='/not/here') vol = db.volume_get(self.context, vol['id']) self.assertEqual(None, vol['mountpoint']) @@ -804,7 +804,7 @@ class CloudTestCase(test.TestCase): def test_run_with_snapshot(self): """Makes sure run/stop/start instance with snapshot works.""" vol = self._volume_create() - ec2_volume_id = ec2utils.id_to_ec2_id(vol['id'], 'vol-%08x') + ec2_volume_id = ec2utils.id_to_ec2_vol_id(vol['id']) ec2_snapshot1_id = self._create_snapshot(ec2_volume_id) snapshot1_id = ec2utils.ec2_id_to_id(ec2_snapshot1_id) -- cgit From 72730a3ba122a86f736513b9dab886bc4087bdc5 Mon Sep 17 00:00:00 2001 From: Isaku Yamahata Date: Thu, 16 Jun 2011 21:25:02 +0900 Subject: db/model: add root_device_name column to instances table The root_device_name column is necessary to support ec2 RootDeviceName. --- .../versions/022_add_root_device_name.py | 47 ++++++++++++++++++++++ nova/db/sqlalchemy/models.py | 2 + 2 files changed, 49 insertions(+) create mode 100644 nova/db/sqlalchemy/migrate_repo/versions/022_add_root_device_name.py diff --git a/nova/db/sqlalchemy/migrate_repo/versions/022_add_root_device_name.py b/nova/db/sqlalchemy/migrate_repo/versions/022_add_root_device_name.py new file mode 100644 index 000000000..6b98b9890 --- /dev/null +++ b/nova/db/sqlalchemy/migrate_repo/versions/022_add_root_device_name.py @@ -0,0 +1,47 @@ +# Copyright 2011 OpenStack LLC. +# Copyright 2011 Isaku Yamahata +# +# 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 Column, Integer, MetaData, Table, String + +meta = MetaData() + + +# Just for the ForeignKey and column creation to succeed, these are not the +# actual definitions of instances or services. +instances = Table('instances', meta, + Column('id', Integer(), primary_key=True, nullable=False), + ) + +# +# New Column +# +root_device_name = Column( + 'root_device_name', + String(length=255, convert_unicode=False, assert_unicode=None, + unicode_error=None, _warn_on_bytestring=False), + nullable=True) + + +def upgrade(migrate_engine): + # Upgrade operations go here. Don't create your own engine; + # bind migrate_engine to your metadata + meta.bind = migrate_engine + instances.create_column(root_device_name) + + +def downgrade(migrate_engine): + # Operations to reverse the above upgrade go here. + meta.bind = migrate_engine + instances.drop_column('root_device_name') diff --git a/nova/db/sqlalchemy/models.py b/nova/db/sqlalchemy/models.py index 23e1bd112..62293daba 100644 --- a/nova/db/sqlalchemy/models.py +++ b/nova/db/sqlalchemy/models.py @@ -234,6 +234,8 @@ class Instance(BASE, NovaBase): os_type = Column(String(255)) vm_mode = Column(String(255)) + root_device_name = Column(String(255)) + # TODO(vish): see Ewan's email about state improvements, probably # should be in a driver base class or some such # vmstate_state = running, halted, suspended, paused -- cgit From 1a54498af5bc2b81bf8bf6e3b9a4ad4cc2db79e2 Mon Sep 17 00:00:00 2001 From: Isaku Yamahata Date: Thu, 16 Jun 2011 21:25:35 +0900 Subject: api/ec2/image: support block device mapping This patch adds --block-device-mapping support for register image and instance creation. --- nova/api/ec2/cloud.py | 189 ++++++++++++++++++++++++++++++++++++++++------- nova/api/ec2/ec2utils.py | 19 +++++ nova/compute/api.py | 82 +++++++++++++++----- nova/compute/manager.py | 20 ++--- nova/image/s3.py | 25 +++++++ 5 files changed, 282 insertions(+), 53 deletions(-) diff --git a/nova/api/ec2/cloud.py b/nova/api/ec2/cloud.py index d36eb7a04..1e594cd73 100644 --- a/nova/api/ec2/cloud.py +++ b/nova/api/ec2/cloud.py @@ -27,6 +27,7 @@ import IPy import os import urllib import tempfile +import time import shutil from nova import compute @@ -75,6 +76,73 @@ def _gen_key(context, user_id, key_name): return {'private_key': private_key, 'fingerprint': fingerprint} +# TODO(yamahata): hypervisor dependent default device name +_DEFAULT_ROOT_DEVICE_NAME = '/dev/sda1' + + +def _parse_block_device_mapping(bdm): + """Parse BlockDeviceMappingItemType into flat hash + BlockDevicedMapping..DeviceName + BlockDevicedMapping..Ebs.SnapshotId + BlockDevicedMapping..Ebs.VolumeSize + BlockDevicedMapping..Ebs.DeleteOnTermination + BlockDevicedMapping..Ebs.NoDevice + BlockDevicedMapping..VirtualName + => remove .Ebs and allow volume id in SnapshotId + """ + ebs = bdm.pop('ebs', None) + if ebs: + ec2_id = ebs.pop('snapshot_id') + id = ec2utils.ec2_id_to_id(ec2_id) + if ec2_id.startswith('snap-'): + bdm['snapshot_id'] = id + elif ec2_id.startswith('vol-'): + bdm['volume_id'] = id + ebs.setdefault('delete_on_termination', True) + bdm.update(ebs) + return bdm + + +def _format_block_device_mapping(bdm): + """Contruct BlockDeviceMappingItemType + {'device_name': '...', 'Snapshot_Id': , ...} + => BlockDeviceMappingItemType + """ + keys = (('deviceName', 'device_name'), + ('virtualName', 'virtual_name')) + item = {} + for name, k in keys: + if k in bdm: + item[name] = bdm[k] + if bdm.get('no_device'): + item['noDevice'] = True + if ('snapshot_id' in bdm) or ('volume_id' in bdm): + ebs_keys = (('snapshotId', 'snapshot_id'), + ('snapshotId', 'volume_id'), # snapshotId is abused + ('volumeSize', 'volume_size'), + ('deleteOnTermination', 'delete_on_termination')) + ebs = {} + for name, k in ebs_keys: + if k in bdm: + if k == 'snapshot_id': + ebs[name] = ec2utils.id_to_ec2_snap_id(bdm[k]) + elif k == 'volume_id': + ebs[name] = ec2utils.id_to_ec2_vol_id(bdm[k]) + else: + ebs[name] = bdm[k] + assert 'snapshotId' in ebs + item['ebs'] = ebs + return item + + +def _format_mappings(mappings, result): + """Format multiple BlockDeviceMappingItemType""" + block_device_mapping = [_format_block_device_mapping(bdm) for bdm in + mappings] + if block_device_mapping: + result['blockDeviceMapping'] = block_device_mapping + + class CloudController(object): """ CloudController provides the critical dispatch between inbound API calls through the endpoint and messages @@ -177,7 +245,7 @@ class CloudController(object): # TODO(vish): replace with real data 'ami': 'sda1', 'ephemeral0': 'sda2', - 'root': '/dev/sda1', + 'root': _DEFAULT_ROOT_DEVICE_NAME, 'swap': 'sda3'}, 'hostname': hostname, 'instance-action': 'none', @@ -761,6 +829,37 @@ class CloudController(object): assert len(i) == 1 return i[0] + def _format_instance_bdm(self, context, instance_id, root_device_name, + result): + """Format InstanceBlockDeviceMappingResponseItemType""" + root_device_type = 'instance-store' + mapping = [] + for bdm in db.block_device_mapping_get_all_by_instance(context, + instance_id): + volume_id = bdm['volume_id'] + if (volume_id is None or bdm['no_device']): + continue + + if (bdm['device_name'] == root_device_name and + (bdm['snapshot_id'] or bdm['volume_id'])): + assert not bdm['virtual_name'] + root_device_type = 'ebs' + + vol = self.volume_api.get(context, volume_id=volume_id) + LOG.debug(_("vol = %s\n"), vol) + # TODO(yamahata): volume attach time + ebs = {'volumeId': volume_id, + 'deleteOnTermination': bdm['delete_on_termination'], + 'attachTime': vol['attach_time'] or '-', + 'status': vol['status'], } + res = {'deviceName': bdm['device_name'], + 'ebs': ebs, } + mapping.append(res) + + if mapping: + result['blockDeviceMapping'] = mapping + result['rootDeviceType'] = root_device_type + def _format_instances(self, context, instance_id=None, **kwargs): # TODO(termie): this method is poorly named as its name does not imply # that it will be making a variety of database calls @@ -822,6 +921,10 @@ class CloudController(object): i['amiLaunchIndex'] = instance['launch_index'] i['displayName'] = instance['display_name'] i['displayDescription'] = instance['display_description'] + i['rootDeviceName'] = (instance['root_device_name'] or + _DEFAULT_ROOT_DEVICE_NAME) + self._format_instance_bdm(context, instance_id, + i['rootDeviceName'], i) host = instance['host'] zone = self._get_availability_zone_by_host(context, host) i['placement'] = {'availabilityZone': zone} @@ -907,24 +1010,7 @@ class CloudController(object): if kwargs.get('ramdisk_id'): ramdisk = self._get_image(context, kwargs['ramdisk_id']) kwargs['ramdisk_id'] = ramdisk['id'] - for bdm in kwargs.get('block_device_mapping', []): - # NOTE(yamahata) - # BlockDevicedMapping..DeviceName - # BlockDevicedMapping..Ebs.SnapshotId - # BlockDevicedMapping..Ebs.VolumeSize - # BlockDevicedMapping..Ebs.DeleteOnTermination - # BlockDevicedMapping..VirtualName - # => remove .Ebs and allow volume id in SnapshotId - ebs = bdm.pop('ebs', None) - if ebs: - ec2_id = ebs.pop('snapshot_id') - id = ec2utils.ec2_id_to_id(ec2_id) - if ec2_id.startswith('snap-'): - bdm['snapshot_id'] = id - elif ec2_id.startswith('vol-'): - bdm['volume_id'] = id - ebs.setdefault('delete_on_termination', True) - bdm.update(ebs) + _parse_block_device_mapping(bdm) image = self._get_image(context, kwargs['image_id']) @@ -1078,6 +1164,20 @@ class CloudController(object): i['imageType'] = display_mapping.get(image_type) i['isPublic'] = image.get('is_public') == True i['architecture'] = image['properties'].get('architecture') + + properties = image['properties'] + root_device_name = ec2utils.properties_root_device_name(properties) + root_device_type = 'instance-store' + for bdm in properties.get('block_device_mapping', []): + if (bdm.get('device_name') == root_device_name and + ('snapshot_id' in bdm or 'volume_id' in bdm) and + not bdm.get('no_device')): + root_device_type = 'ebs' + i['rootDeviceName'] = (root_device_name or _DEFAULT_ROOT_DEVICE_NAME) + i['rootDeviceType'] = root_device_type + + _format_mappings(properties.get('block_device_mapping', []), i) + return i def describe_images(self, context, image_id=None, **kwargs): @@ -1102,30 +1202,65 @@ class CloudController(object): self.image_service.delete(context, internal_id) return {'imageId': image_id} + def _register_image(self, context, metadata): + image = self.image_service.create(context, metadata) + image_type = self._image_type(image.get('container_format')) + image_id = self.image_ec2_id(image['id'], image_type) + return image_id + def register_image(self, context, image_location=None, **kwargs): if image_location is None and 'name' in kwargs: image_location = kwargs['name'] metadata = {'properties': {'image_location': image_location}} - image = self.image_service.create(context, metadata) - image_type = self._image_type(image.get('container_format')) - image_id = self.image_ec2_id(image['id'], - image_type) + + if 'root_device_name' in kwargs: + metadata['properties']['root_device_name'] = \ + kwargs.get('root_device_name') + + mappings = [_parse_block_device_mapping(bdm) for bdm in + kwargs.get('block_device_mapping', [])] + if mappings: + metadata['properties']['block_device_mapping'] = mappings + + image_id = self._register_image(context, metadata) msg = _("Registered image %(image_location)s with" " id %(image_id)s") % locals() LOG.audit(msg, context=context) return {'imageId': image_id} def describe_image_attribute(self, context, image_id, attribute, **kwargs): - if attribute != 'launchPermission': + def _block_device_mapping_attribute(image, result): + _format_mappings( + image['properties'].get('block_device_mapping', []), result) + + def _launch_permission_attribute(image, result): + result['launchPermission'] = [] + if image['is_public']: + result['launchPermission'].append({'group': 'all'}) + + def _root_device_name_attribute(image, result): + result['rootDeviceName'] = \ + ec2utils.properties_root_device_name(image['properties']) + if result['rootDeviceName'] is None: + result['rootDeviceName'] = _DEFAULT_ROOT_DEVICE_NAME + + supported_attributes = { + 'blockDeviceMapping': _block_device_mapping_attribute, + 'launchPermission': _launch_permission_attribute, + 'rootDeviceName': _root_device_name_attribute, + } + + fn = supported_attributes.get(attribute) + if fn is None: raise exception.ApiError(_('attribute not supported: %s') % attribute) try: image = self._get_image(context, image_id) except exception.NotFound: raise exception.ImageNotFound(image_id=image_id) - result = {'imageId': image_id, 'launchPermission': []} - if image['is_public']: - result['launchPermission'].append({'group': 'all'}) + + result = {'imageId': image_id} + fn(image, result) return result def modify_image_attribute(self, context, image_id, attribute, diff --git a/nova/api/ec2/ec2utils.py b/nova/api/ec2/ec2utils.py index bcdf2ba78..9839f8604 100644 --- a/nova/api/ec2/ec2utils.py +++ b/nova/api/ec2/ec2utils.py @@ -135,3 +135,22 @@ def dict_from_dotted_str(items): args[key] = value return args + + +def properties_root_device_name(properties): + """get root device name from image meta data. + If it isn't specified, return None. + """ + root_device_name = None + + # NOTE(yamahata): see image_service.s3.s3create() + for bdm in properties.get('mappings', []): + if bdm['virtual'] == 'root': + root_device_name = bdm['device'] + + # NOTE(yamahata): register_image's command line can override + # .manifest.xml + if 'root_device_name' in properties: + root_device_name = properties['root_device_name'] + + return root_device_name diff --git a/nova/compute/api.py b/nova/compute/api.py index 18363ace0..d89dfb746 100644 --- a/nova/compute/api.py +++ b/nova/compute/api.py @@ -32,6 +32,7 @@ from nova import quota from nova import rpc from nova import utils from nova import volume +from nova.api.ec2 import ec2utils from nova.compute import instance_types from nova.compute import power_state from nova.compute.utils import terminate_volumes @@ -220,6 +221,9 @@ class API(base.Base): if reservation_id is None: reservation_id = utils.generate_uid('r') + root_device_name = ec2utils.properties_root_device_name( + image['properties']) + base_options = { 'reservation_id': reservation_id, 'image_ref': image_href, @@ -243,10 +247,62 @@ class API(base.Base): 'metadata': metadata, 'availability_zone': availability_zone, 'os_type': os_type, - 'vm_mode': vm_mode} + 'vm_mode': vm_mode, + 'root_device_name': root_device_name} return (num_instances, base_options, security_groups) + def _update_image_block_device_mapping(self, elevated_context, instance_id, + mappings): + """tell vm driver to create ephemeral/swap device at boot time by + updating BlockDeviceMapping + """ + for bdm in mappings: + LOG.debug(_("bdm %s"), bdm) + + virtual_name = bdm['virtualName'] + if virtual_name == 'ami' or virtual_name == 'root': + continue + + assert (virtual_name == 'swap' or + virtual_name.startswith('ephemeral')) + values = { + 'instance_id': instance_id, + 'device_name': bdm['deviceName'], + 'virtual_name': virtual_name, } + self.db.block_device_mapping_create(elevated_context, values) + + def _update_block_device_mapping(self, elevated_context, instance_id, + block_device_mapping): + """tell vm driver to attach volume at boot time by updating + BlockDeviceMapping + """ + for bdm in block_device_mapping: + LOG.debug(_('bdm %s'), bdm) + assert 'device_name' in bdm + + values = { + 'instance_id': instance_id, + 'device_name': bdm['device_name'], + 'delete_on_termination': bdm.get('delete_on_termination'), + 'virtual_name': bdm.get('virtual_name'), + 'snapshot_id': bdm.get('snapshot_id'), + 'volume_id': bdm.get('volume_id'), + 'volume_size': bdm.get('volume_size'), + 'no_device': bdm.get('no_device')} + + # NOTE(yamahata): NoDevice eliminates devices defined in image + # files by command line option. + # (--block-device-mapping) + if bdm.get('virtual_name') == 'NoDevice': + values['no_device'] = True + for k in ('delete_on_termination', 'volume_id', + 'snapshot_id', 'volume_id', 'volume_size', + 'virtual_name'): + values[k] = None + + self.db.block_device_mapping_create(elevated_context, values) + def create_db_entry_for_new_instance(self, context, base_options, security_groups, block_device_mapping, num=1): """Create an entry in the DB for this new instance, @@ -268,22 +324,14 @@ class API(base.Base): instance_id, security_group_id) - # NOTE(yamahata) - # tell vm driver to attach volume at boot time by updating - # BlockDeviceMapping - for bdm in block_device_mapping: - LOG.debug(_('bdm %s'), bdm) - assert 'device_name' in bdm - values = { - 'instance_id': instance_id, - 'device_name': bdm['device_name'], - 'delete_on_termination': bdm.get('delete_on_termination'), - 'virtual_name': bdm.get('virtual_name'), - 'snapshot_id': bdm.get('snapshot_id'), - 'volume_id': bdm.get('volume_id'), - 'volume_size': bdm.get('volume_size'), - 'no_device': bdm.get('no_device')} - self.db.block_device_mapping_create(elevated, values) + # BlockDeviceMapping table + self._update_image_block_device_mapping(elevated, instance_id, + image['properties'].get('mappings', [])) + self._update_block_device_mapping(elevated, instance_id, + image['properties'].get('block_device_mapping', [])) + # override via command line option + self._update_block_device_mapping(elevated, instance_id, + block_device_mapping) # Set sane defaults if not specified updates = dict(hostname=self.hostname_factory(instance_id)) diff --git a/nova/compute/manager.py b/nova/compute/manager.py index 8ab744855..146cdbb9f 100644 --- a/nova/compute/manager.py +++ b/nova/compute/manager.py @@ -227,6 +227,17 @@ class ComputeManager(manager.SchedulerDependentManager): for bdm in self.db.block_device_mapping_get_all_by_instance( context, instance_id): LOG.debug(_("setting up bdm %s"), bdm) + + if bdm['no_device']: + continue + if bdm['virtual_name']: + # TODO(yamahata): + # block devices for swap and ephemeralN will be + # created by virt driver locally in compute node. + assert (bdm['virtual_name'] == 'swap' or + bdm['virtual_name'].startswith('ephemeral')) + continue + if ((bdm['snapshot_id'] is not None) and (bdm['volume_id'] is None)): # TODO(yamahata): default name and description @@ -259,15 +270,6 @@ class ComputeManager(manager.SchedulerDependentManager): block_device_mapping.append({'device_path': dev_path, 'mount_device': bdm['device_name']}) - elif bdm['virtual_name'] is not None: - # TODO(yamahata): ephemeral/swap device support - LOG.debug(_('block_device_mapping: ' - 'ephemeral device is not supported yet')) - else: - # TODO(yamahata): NoDevice support - assert bdm['no_device'] - LOG.debug(_('block_device_mapping: ' - 'no device is not supported yet')) return block_device_mapping diff --git a/nova/image/s3.py b/nova/image/s3.py index 9e95bd698..b1afc6a81 100644 --- a/nova/image/s3.py +++ b/nova/image/s3.py @@ -141,6 +141,28 @@ class S3ImageService(service.BaseImageService): except Exception: arch = 'x86_64' + # NOTE(yamahata): + # EC2 ec2-budlne-image --block-device-mapping accepts + # = where + # virtual name = {ami, root, swap, ephemeral} + # where N is no negative integer + # device name = the device name seen by guest kernel. + # They are converted into + # block_device_mapping/mapping/{virtual, device} + # + # Do NOT confuse this with ec2-register's block device mapping + # argument. + mappings = [] + try: + block_device_mapping = manifest.findall('machine_configuration/' + 'block_device_mapping/' + 'mapping') + for bdm in block_device_mapping: + mappings.append({'virtual': bdm.find('virtual').text, + 'device': bdm.find('device').text}) + except Exception: + mappings = [] + properties = metadata['properties'] properties['project_id'] = context.project_id properties['architecture'] = arch @@ -151,6 +173,9 @@ class S3ImageService(service.BaseImageService): if ramdisk_id: properties['ramdisk_id'] = ec2utils.ec2_id_to_id(ramdisk_id) + if mappings: + properties['mappings'] = mappings + metadata.update({'disk_format': image_format, 'container_format': image_format, 'status': 'queued', -- cgit From 46c7e018ee3aed0f0e27ae3544193adb5fa62958 Mon Sep 17 00:00:00 2001 From: Isaku Yamahata Date: Thu, 16 Jun 2011 21:25:57 +0900 Subject: api/ec2: support CreateImage --- nova/api/ec2/__init__.py | 1 + nova/api/ec2/cloud.py | 101 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 102 insertions(+) diff --git a/nova/api/ec2/__init__.py b/nova/api/ec2/__init__.py index 35f67d39b..cf1734281 100644 --- a/nova/api/ec2/__init__.py +++ b/nova/api/ec2/__init__.py @@ -271,6 +271,7 @@ class Authorizer(wsgi.Middleware): 'DescribeImageAttribute': ['all'], 'ModifyImageAttribute': ['projectmanager', 'sysadmin'], 'UpdateImage': ['projectmanager', 'sysadmin'], + 'CreateImage': ['projectmanager', 'sysadmin'], }, 'AdminController': { # All actions have the same permission: ['none'] (the default) diff --git a/nova/api/ec2/cloud.py b/nova/api/ec2/cloud.py index 1e594cd73..b38061eaf 100644 --- a/nova/api/ec2/cloud.py +++ b/nova/api/ec2/cloud.py @@ -1291,3 +1291,104 @@ class CloudController(object): internal_id = ec2utils.ec2_id_to_id(image_id) result = self.image_service.update(context, internal_id, dict(kwargs)) return result + + # TODO(yamahata): race condition + # At the moment there is no way to prevent others from + # manipulating instances/volumes/snapshots. + # As other code doesn't take it into consideration, here we don't + # care of it for now. Ostrich algorithm + def create_image(self, context, instance_id, **kwargs): + # NOTE(yamahata): name/description are ignored by register_image(), + # do so here + #description = kwargs.get('name') + #description = kwargs.get('description') + no_reboot = kwargs.get('no_reboot', False) + + ec2_instance_id = instance_id + instance_id = ec2utils.ec2_id_to_id(ec2_instance_id) + instance = self.compute_api.get(context, instance_id) + + # stop the instance if necessary + restart_instance = False + if not no_reboot: + state_description = instance['state_description'] + + # if the instance is in subtle state, refuse to proceed. + if state_description not in ('running', 'stopping', 'stopped'): + raise exception.InstanceNotRunning(instance_id=ec2_instance_id) + + if state_description == 'running': + restart_instance = True + self.compute_api.stop(context, instance_id=instance_id) + + # wait instance for really stopped + while state_description != 'stopped': + time.sleep(1) + instance = self.compute_api.get(context, instance_id) + state_description = instance['state_description'] + # NOTE(yamahata): timeout and error? + + src_image = self.image_service.show(context, instance['image_id']) + properties = src_image['properties'] + if instance['root_device_name']: + properties['root_device_name'] = instance['root_device_name'] + + mapping = [] + bdms = db.block_device_mapping_get_all_by_instance(context, + instance_id) + for bdm in bdms: + if bdm.no_device: + continue + m = {} + for attr in ('device_name', 'snapshot_id', 'volume_id', + 'volume_size', 'delete_on_termination', 'no_device', + 'virtual_name'): + val = getattr(bdm, attr) + if val is not None: + m[attr] = val + + volume_id = m.get('volume_id') + if m.get('snapshot_id') and volume_id: + # create snapshot based on volume_id + vol = self.volume_api.get(context, volume_id=volume_id) + # NOTE(yamahata): Should we wait for snapshot creation? + # Linux LVM snapshot creation completes in + # short time, it doesn't matter for now. + snapshot = self.volume_api.create_snapshot( + context, volume_id=volume_id, name=vol['display_name'], + description=vol['display_description']) + m['snapshot_id'] = snapshot['id'] + del m['volume_id'] + + if m: + mapping.append(m) + + for m in properties.get('mappings', []): + virtual_name = m['virtual'] + if virtual_name in ('ami', 'root'): + continue + + assert (virtual_name == 'swap' or + virtual_name.startswith('ephemeral')) + device_name = m['device_name'] + if device_name in [b.device_name for b in mapping + if not b.no_device]: + continue + + # NOTE(yamahata): swap and ephemeral devices are specified in + # AMI, but disabled for this instance by user. + # So disable those device by no_device. + mapping.append({'device_name': device_name, 'no_device': True}) + + if mapping: + properties['block_device_mapping'] = mapping + + for attr in ('status', 'location', 'id'): + del src_image[attr] + + image_id = self._register_image(context, src_image) + + if restart_instance: + self.compute_api.start(context, instance_id=instance_id) + + return {'imageId': image_id} -- cgit From 8da93c0352996ec733f3678b9ca02b72a96498c2 Mon Sep 17 00:00:00 2001 From: Isaku Yamahata Date: Thu, 16 Jun 2011 21:44:50 +0900 Subject: fix conflict with rebasing. --- .../versions/022_add_root_device_name.py | 47 ---------------------- .../versions/025_add_root_device_name.py | 47 ++++++++++++++++++++++ 2 files changed, 47 insertions(+), 47 deletions(-) delete mode 100644 nova/db/sqlalchemy/migrate_repo/versions/022_add_root_device_name.py create mode 100644 nova/db/sqlalchemy/migrate_repo/versions/025_add_root_device_name.py diff --git a/nova/db/sqlalchemy/migrate_repo/versions/022_add_root_device_name.py b/nova/db/sqlalchemy/migrate_repo/versions/022_add_root_device_name.py deleted file mode 100644 index 6b98b9890..000000000 --- a/nova/db/sqlalchemy/migrate_repo/versions/022_add_root_device_name.py +++ /dev/null @@ -1,47 +0,0 @@ -# Copyright 2011 OpenStack LLC. -# Copyright 2011 Isaku Yamahata -# -# 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 Column, Integer, MetaData, Table, String - -meta = MetaData() - - -# Just for the ForeignKey and column creation to succeed, these are not the -# actual definitions of instances or services. -instances = Table('instances', meta, - Column('id', Integer(), primary_key=True, nullable=False), - ) - -# -# New Column -# -root_device_name = Column( - 'root_device_name', - String(length=255, convert_unicode=False, assert_unicode=None, - unicode_error=None, _warn_on_bytestring=False), - nullable=True) - - -def upgrade(migrate_engine): - # Upgrade operations go here. Don't create your own engine; - # bind migrate_engine to your metadata - meta.bind = migrate_engine - instances.create_column(root_device_name) - - -def downgrade(migrate_engine): - # Operations to reverse the above upgrade go here. - meta.bind = migrate_engine - instances.drop_column('root_device_name') diff --git a/nova/db/sqlalchemy/migrate_repo/versions/025_add_root_device_name.py b/nova/db/sqlalchemy/migrate_repo/versions/025_add_root_device_name.py new file mode 100644 index 000000000..6b98b9890 --- /dev/null +++ b/nova/db/sqlalchemy/migrate_repo/versions/025_add_root_device_name.py @@ -0,0 +1,47 @@ +# Copyright 2011 OpenStack LLC. +# Copyright 2011 Isaku Yamahata +# +# 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 Column, Integer, MetaData, Table, String + +meta = MetaData() + + +# Just for the ForeignKey and column creation to succeed, these are not the +# actual definitions of instances or services. +instances = Table('instances', meta, + Column('id', Integer(), primary_key=True, nullable=False), + ) + +# +# New Column +# +root_device_name = Column( + 'root_device_name', + String(length=255, convert_unicode=False, assert_unicode=None, + unicode_error=None, _warn_on_bytestring=False), + nullable=True) + + +def upgrade(migrate_engine): + # Upgrade operations go here. Don't create your own engine; + # bind migrate_engine to your metadata + meta.bind = migrate_engine + instances.create_column(root_device_name) + + +def downgrade(migrate_engine): + # Operations to reverse the above upgrade go here. + meta.bind = migrate_engine + instances.drop_column('root_device_name') -- cgit From 7bc76ef403507f6762f782ff4d305cf2718346d5 Mon Sep 17 00:00:00 2001 From: Isaku Yamahata Date: Thu, 16 Jun 2011 22:17:54 +0900 Subject: ec2/cloud.py: fix mismerge --- nova/api/ec2/cloud.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/nova/api/ec2/cloud.py b/nova/api/ec2/cloud.py index b38061eaf..1bf8bbd35 100644 --- a/nova/api/ec2/cloud.py +++ b/nova/api/ec2/cloud.py @@ -1010,7 +1010,8 @@ class CloudController(object): if kwargs.get('ramdisk_id'): ramdisk = self._get_image(context, kwargs['ramdisk_id']) kwargs['ramdisk_id'] = ramdisk['id'] - _parse_block_device_mapping(bdm) + for bdm in kwargs.get('block_device_mapping', []): + _parse_block_device_mapping(bdm) image = self._get_image(context, kwargs['image_id']) -- cgit From 774e201a04addf95fab2253998967b212588cb0a Mon Sep 17 00:00:00 2001 From: Isaku Yamahata Date: Thu, 16 Jun 2011 22:18:06 +0900 Subject: compute/api: fix mismerge due to instance creation change --- nova/compute/api.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/nova/compute/api.py b/nova/compute/api.py index d89dfb746..5b56d7865 100644 --- a/nova/compute/api.py +++ b/nova/compute/api.py @@ -250,7 +250,7 @@ class API(base.Base): 'vm_mode': vm_mode, 'root_device_name': root_device_name} - return (num_instances, base_options, security_groups) + return (num_instances, base_options, security_groups, image) def _update_image_block_device_mapping(self, elevated_context, instance_id, mappings): @@ -303,7 +303,7 @@ class API(base.Base): self.db.block_device_mapping_create(elevated_context, values) - def create_db_entry_for_new_instance(self, context, base_options, + def create_db_entry_for_new_instance(self, context, image, base_options, security_groups, block_device_mapping, num=1): """Create an entry in the DB for this new instance, including any related table updates (such as security @@ -393,7 +393,7 @@ class API(base.Base): """Provision the instances by passing the whole request to the Scheduler for execution. Returns a Reservation ID related to the creation of all of these instances.""" - num_instances, base_options, security_groups = \ + num_instances, base_options, security_groups, image = \ self._check_create_parameters( context, instance_type, image_href, kernel_id, ramdisk_id, @@ -429,7 +429,7 @@ class API(base.Base): Returns a list of instance dicts. """ - num_instances, base_options, security_groups = \ + num_instances, base_options, security_groups, image = \ self._check_create_parameters( context, instance_type, image_href, kernel_id, ramdisk_id, @@ -444,7 +444,7 @@ class API(base.Base): instances = [] LOG.debug(_("Going to run %s instances..."), num_instances) for num in range(num_instances): - instance = self.create_db_entry_for_new_instance(context, + instance = self.create_db_entry_for_new_instance(context, image, base_options, security_groups, block_device_mapping, num=num) instances.append(instance) -- cgit From faed18358f534ed7a743fcd168d649d06da092ab Mon Sep 17 00:00:00 2001 From: Arvind Somy Date: Fri, 17 Jun 2011 14:02:24 -0400 Subject: Fix for Bug lp:796813 - Adding support for distributed virtual portgroups on vmware ESX(i) --- Authors | 1 + nova/virt/vmwareapi/network_utils.py | 29 ++++++++++++++++++++++++----- nova/virt/vmwareapi/vm_util.py | 23 ++++++++++++++++++----- nova/virt/vmwareapi/vmops.py | 7 ++++--- 4 files changed, 47 insertions(+), 13 deletions(-) diff --git a/Authors b/Authors index 94fcf7f5b..0d79af2c7 100644 --- a/Authors +++ b/Authors @@ -6,6 +6,7 @@ Anne Gentle Anthony Young Antony Messerli Armando Migliaccio +Arvind Somya Bilal Akhtar Brian Lamar Brian Schott diff --git a/nova/virt/vmwareapi/network_utils.py b/nova/virt/vmwareapi/network_utils.py index e77842535..345cd2bd0 100644 --- a/nova/virt/vmwareapi/network_utils.py +++ b/nova/virt/vmwareapi/network_utils.py @@ -45,11 +45,30 @@ def get_network_with_the_name(session, network_name="vmnet0"): networks = session._call_method(vim_util, "get_properties_for_a_collection_of_objects", "Network", vm_networks, ["summary.name"]) - for network in networks: - if network.propSet[0].val == network_name: - return network.obj - return None - + network_obj = {} + for network in vm_networks: + # Get network properties + if network._type == 'DistributedVirtualPortgroup': + props = session._call_method(vim_util, + "get_dynamic_property",network, + "DistributedVirtualPortgroup","config") + # NOTE: This only works on ESXi if the port binding is + # set to ephemeral + if props.name == network_name: + network_obj['type'] = 'DistributedVirtualPortgroup' + network_obj['dvpg'] = props.key + network_obj['dvsw'] = props.distributedVirtualSwitch.value + else: + props = session._call_method(vim_util, + "get_dynamic_property",network, + "Network","summary.name") + if props == network_name: + network_obj['type'] = 'Network' + network_obj['name'] = network_name + if (len(network_obj) > 0): + return network_obj + else: + return None def get_vswitch_for_vlan_interface(session, vlan_interface): """ diff --git a/nova/virt/vmwareapi/vm_util.py b/nova/virt/vmwareapi/vm_util.py index a2fa7600c..d23472469 100644 --- a/nova/virt/vmwareapi/vm_util.py +++ b/nova/virt/vmwareapi/vm_util.py @@ -40,7 +40,7 @@ def split_datastore_path(datastore_path): def get_vm_create_spec(client_factory, instance, data_store_name, network_name="vmnet0", - os_type="otherGuest"): + os_type="otherGuest", network_ref=None): """Builds the VM Create spec.""" config_spec = client_factory.create('ns0:VirtualMachineConfigSpec') config_spec.name = instance.name @@ -89,7 +89,7 @@ def create_controller_spec(client_factory, key): return virtual_device_config -def create_network_spec(client_factory, network_name, mac_address): +def create_network_spec(client_factory, network_name, mac_address,network_ref=None): """ Builds a config spec for the addition of a new network adapter to the VM. @@ -101,9 +101,22 @@ def create_network_spec(client_factory, network_name, mac_address): # Get the recommended card type for the VM based on the guest OS of the VM net_device = client_factory.create('ns0:VirtualPCNet32') - backing = \ - client_factory.create('ns0:VirtualEthernetCardNetworkBackingInfo') - backing.deviceName = network_name + # NOTE: Only works on ESXi if the portgroup binding is set to + # ephemeral. Invalid configuration if set to static and the NIC does + # not come up on boot if set to dynamic. + backing = None + if (network_ref['type'] == "DistributedVirtualPortgroup"): + backing = \ + client_factory.create('ns0:VirtualEthernetCardDistributedVirtualPortBackingInfo') + portgroup = \ + client_factory.create('ns0:DistributedVirtualSwitchPortConnection') + portgroup.switchUuid = network_ref['dvsw'] + portgroup.portgroupKey = network_ref['dvpg'] + backing.port = portgroup + else: + backing = \ + client_factory.create('ns0:VirtualEthernetCardNetworkBackingInfo') + backing.deviceName = network_name connectable_spec = \ client_factory.create('ns0:VirtualDeviceConnectInfo') diff --git a/nova/virt/vmwareapi/vmops.py b/nova/virt/vmwareapi/vmops.py index 5f76b0df5..d23edbdf8 100644 --- a/nova/virt/vmwareapi/vmops.py +++ b/nova/virt/vmwareapi/vmops.py @@ -116,8 +116,9 @@ class VMWareVMOps(object): net_name) if network_ref is None: raise exception.NetworkNotFoundForBridge(bridge=net_name) - - _check_if_network_bridge_exists() + return network_ref + + network_obj = _check_if_network_bridge_exists() def _get_datastore_ref(): """Get the datastore list and choose the first local storage.""" @@ -176,7 +177,7 @@ class VMWareVMOps(object): # Get the create vm config spec config_spec = vm_util.get_vm_create_spec(client_factory, instance, - data_store_name, net_name, os_type) + data_store_name, net_name, os_type, network_obj) def _execute_create_vm(): """Create VM on ESX host.""" -- cgit From c3af5e65508fb325a4a8e350c9ed6d84d87e7cd8 Mon Sep 17 00:00:00 2001 From: Arvind Somy Date: Fri, 17 Jun 2011 15:12:01 -0400 Subject: Fix for lp:796834 - Fixes and enhancements to the ESX(i) guest_tool.py script. --- nova/virt/vmwareapi/vm_util.py | 4 ++-- nova/virt/vmwareapi/vmops.py | 5 ++++- tools/esx/guest_tool.py | 49 ++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 53 insertions(+), 5 deletions(-) diff --git a/nova/virt/vmwareapi/vm_util.py b/nova/virt/vmwareapi/vm_util.py index d23472469..c823ac710 100644 --- a/nova/virt/vmwareapi/vm_util.py +++ b/nova/virt/vmwareapi/vm_util.py @@ -287,9 +287,9 @@ def get_dummy_vm_create_spec(client_factory, name, data_store_name): return config_spec -def get_machine_id_change_spec(client_factory, mac, ip_addr, netmask, gateway): +def get_machine_id_change_spec(client_factory, mac, ip_addr, netmask, gateway, broadcast, dns): """Builds the machine id change config spec.""" - machine_id_str = "%s;%s;%s;%s" % (mac, ip_addr, netmask, gateway) + machine_id_str = "%s;%s;%s;%s;%s;%s" % (mac, ip_addr, netmask, gateway, broadcast, dns) virtual_machine_config_spec = \ client_factory.create('ns0:VirtualMachineConfigSpec') diff --git a/nova/virt/vmwareapi/vmops.py b/nova/virt/vmwareapi/vmops.py index d23edbdf8..9f2a36bf4 100644 --- a/nova/virt/vmwareapi/vmops.py +++ b/nova/virt/vmwareapi/vmops.py @@ -715,11 +715,14 @@ class VMWareVMOps(object): mac_addr = instance.mac_address net_mask = network["netmask"] gateway = network["gateway"] + broadcast = network["broadcast"] + dns = network["dns"] ip_addr = db.instance_get_fixed_address(context.get_admin_context(), instance['id']) machine_id_chanfge_spec = \ vm_util.get_machine_id_change_spec(client_factory, mac_addr, - ip_addr, net_mask, gateway) + ip_addr, net_mask, gateway, + broadcast, dns) LOG.debug(_("Reconfiguring VM instance %(name)s to set the machine id " "with ip - %(ip_addr)s") % ({'name': instance.name, diff --git a/tools/esx/guest_tool.py b/tools/esx/guest_tool.py index 13b0f8d33..e126721d5 100644 --- a/tools/esx/guest_tool.py +++ b/tools/esx/guest_tool.py @@ -21,6 +21,7 @@ On Windows we require pyWin32 installed on Python. """ import array +import gettext import logging import os import platform @@ -30,6 +31,8 @@ import subprocess import sys import time +_ = gettext.gettext + PLATFORM_WIN = 'win32' PLATFORM_LINUX = 'linux2' ARCH_32_BIT = '32bit' @@ -314,6 +317,43 @@ def _set_rhel_networking(network_details=[]): dns_file.close() _execute(['/sbin/service', 'network', 'restart']) +def _set_ubuntu_networking(network_details=[]): + """ Set IPv4 network settings for Ubuntu """ + all_dns_servers = [] + for network_detail in network_details: + mac_address, ip_address, subnet_mask, gateway, broadcast,\ + dns_servers = network_detail + all_dns_servers.extend(dns_servers) + adapter_name, current_ip_address = \ + _get_linux_adapter_name_and_ip_address(mac_address) + + if adapter_name and not ip_address == current_ip_address: + interface_file_name = \ + '/etc/network/interfaces' + # Remove file + os.remove(interface_file_name) + # Touch file + _execute(['touch', interface_file_name]) + interface_file = open(interface_file_name, 'w') + interface_file.write('\nauto %s' % adapter_name) + interface_file.write('\niface %s inet static' % adapter_name) + interface_file.write('\nbroadcast %s' % broadcast) + interface_file.write('\ngateway %s' % gateway) + interface_file.write('\nnetmask %s' % subnet_mask) + interface_file.write('\naddress %s' % ip_address) + interface_file.close() + if all_dns_servers: + dns_file_name = "/etc/resolv.conf" + os.remove(dns_file_name) + _execute(['touch', dns_file_name]) + dns_file = open(dns_file_name, 'w') + dns_file.write("; generated by OpenStack guest tools") + unique_entries = _filter_duplicates(all_dns_servers) + for dns_server in unique_entries: + dns_file.write("\nnameserver %s" % dns_server) + dns_file.close() + print "\nRestarting networking....\n" + _execute(['/etc/init.d/networking', 'restart']) def _linux_set_networking(): """Set IP address for the Linux VM.""" @@ -330,8 +370,13 @@ def _linux_set_networking(): cmd = [vmware_tools_bin, '--cmd', 'machine.id.get'] network_details = _parse_network_details(_execute(cmd, check_exit_code=False)) - # TODO(sateesh): For other distros like ubuntu, suse, debian, BSD, etc. - _set_rhel_networking(network_details) + # TODO(sateesh): For other distros like suse, debian, BSD, etc. + if(platform.dist()[0] == 'Ubuntu') : + _set_ubuntu_networking(network_details) + elif (platform.dist()[0] == 'redhat') : + _set_rhel_networking(network_details) + else: + logging.warn(_("Distro '%s' not supported") % platform.dist()[0]) else: logging.warn(_("VMware Tools is not installed")) -- cgit From 96fc985878cd52813aa07a4843e5928031b1501a Mon Sep 17 00:00:00 2001 From: Isaku Yamahata Date: Wed, 22 Jun 2011 12:40:06 +0900 Subject: db/migration: resolve version conflict --- .../versions/025_add_root_device_name.py | 47 ---------------------- .../versions/027_add_root_device_name.py | 47 ++++++++++++++++++++++ 2 files changed, 47 insertions(+), 47 deletions(-) delete mode 100644 nova/db/sqlalchemy/migrate_repo/versions/025_add_root_device_name.py create mode 100644 nova/db/sqlalchemy/migrate_repo/versions/027_add_root_device_name.py diff --git a/nova/db/sqlalchemy/migrate_repo/versions/025_add_root_device_name.py b/nova/db/sqlalchemy/migrate_repo/versions/025_add_root_device_name.py deleted file mode 100644 index 6b98b9890..000000000 --- a/nova/db/sqlalchemy/migrate_repo/versions/025_add_root_device_name.py +++ /dev/null @@ -1,47 +0,0 @@ -# Copyright 2011 OpenStack LLC. -# Copyright 2011 Isaku Yamahata -# -# 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 Column, Integer, MetaData, Table, String - -meta = MetaData() - - -# Just for the ForeignKey and column creation to succeed, these are not the -# actual definitions of instances or services. -instances = Table('instances', meta, - Column('id', Integer(), primary_key=True, nullable=False), - ) - -# -# New Column -# -root_device_name = Column( - 'root_device_name', - String(length=255, convert_unicode=False, assert_unicode=None, - unicode_error=None, _warn_on_bytestring=False), - nullable=True) - - -def upgrade(migrate_engine): - # Upgrade operations go here. Don't create your own engine; - # bind migrate_engine to your metadata - meta.bind = migrate_engine - instances.create_column(root_device_name) - - -def downgrade(migrate_engine): - # Operations to reverse the above upgrade go here. - meta.bind = migrate_engine - instances.drop_column('root_device_name') diff --git a/nova/db/sqlalchemy/migrate_repo/versions/027_add_root_device_name.py b/nova/db/sqlalchemy/migrate_repo/versions/027_add_root_device_name.py new file mode 100644 index 000000000..6b98b9890 --- /dev/null +++ b/nova/db/sqlalchemy/migrate_repo/versions/027_add_root_device_name.py @@ -0,0 +1,47 @@ +# Copyright 2011 OpenStack LLC. +# Copyright 2011 Isaku Yamahata +# +# 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 Column, Integer, MetaData, Table, String + +meta = MetaData() + + +# Just for the ForeignKey and column creation to succeed, these are not the +# actual definitions of instances or services. +instances = Table('instances', meta, + Column('id', Integer(), primary_key=True, nullable=False), + ) + +# +# New Column +# +root_device_name = Column( + 'root_device_name', + String(length=255, convert_unicode=False, assert_unicode=None, + unicode_error=None, _warn_on_bytestring=False), + nullable=True) + + +def upgrade(migrate_engine): + # Upgrade operations go here. Don't create your own engine; + # bind migrate_engine to your metadata + meta.bind = migrate_engine + instances.create_column(root_device_name) + + +def downgrade(migrate_engine): + # Operations to reverse the above upgrade go here. + meta.bind = migrate_engine + instances.drop_column('root_device_name') -- cgit From 4020e0dab41caf22de629c94cf94f5ea2101faee Mon Sep 17 00:00:00 2001 From: Isaku Yamahata Date: Wed, 22 Jun 2011 12:48:30 +0900 Subject: db/block_device_mapping/api: introduce update_or_create introduce db.block_device_mapping_udpate_or_create() which update the colume if exists. Create new column if not existed. This api will be used later for block device mapping tracking. --- nova/db/api.py | 8 +++++++- nova/db/sqlalchemy/api.py | 17 +++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/nova/db/api.py b/nova/db/api.py index 8f8e856b8..d77d0c352 100644 --- a/nova/db/api.py +++ b/nova/db/api.py @@ -936,10 +936,16 @@ def block_device_mapping_create(context, values): def block_device_mapping_update(context, bdm_id, values): - """Create an entry of block device mapping""" + """Update an entry of block device mapping""" return IMPL.block_device_mapping_update(context, bdm_id, values) +def block_device_mapping_update_or_create(context, values): + """Update an entry of block device mapping. + If not existed, create a new entry""" + return IMPL.block_device_mapping_update_or_create(context, values) + + def block_device_mapping_get_all_by_instance(context, instance_id): """Get all block device mapping belonging to a instance""" return IMPL.block_device_mapping_get_all_by_instance(context, instance_id) diff --git a/nova/db/sqlalchemy/api.py b/nova/db/sqlalchemy/api.py index a2500a38d..7108c7a7a 100644 --- a/nova/db/sqlalchemy/api.py +++ b/nova/db/sqlalchemy/api.py @@ -1932,6 +1932,23 @@ def block_device_mapping_update(context, bdm_id, values): update(values) +@require_context +def block_device_mapping_update_or_create(context, values): + session = get_session() + with session.begin(): + result = session.query(models.BlockDeviceMapping).\ + filter_by(instance_id=values['instance_id']).\ + filter_by(device_name=values['device_name']).\ + filter_by(deleted=False).\ + first() + if not result: + bdm_ref = models.BlockDeviceMapping() + bdm_ref.update(values) + bdm_ref.save(session=session) + else: + result.update(values) + + @require_context def block_device_mapping_get_all_by_instance(context, instance_id): session = get_session() -- cgit From 3a83471ec002127a84d319e397ce54e49bd696a1 Mon Sep 17 00:00:00 2001 From: Isaku Yamahata Date: Wed, 22 Jun 2011 12:53:47 +0900 Subject: api/ec2/image: make block device mapping pass unit tests. This patch makes pass unit tests which will follow later. --- nova/api/ec2/cloud.py | 30 +++++++++++++++++++++++------- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/nova/api/ec2/cloud.py b/nova/api/ec2/cloud.py index 8138567d0..703dd82b4 100644 --- a/nova/api/ec2/cloud.py +++ b/nova/api/ec2/cloud.py @@ -135,12 +135,29 @@ def _format_block_device_mapping(bdm): return item -def _format_mappings(mappings, result): +def _format_mappings(properties, result): """Format multiple BlockDeviceMappingItemType""" + mappings = [{'virtualName': m['virtual'], 'deviceName': m['device']} + for m in properties.get('mappings', []) + if (m['virtual'] == 'swap' or + m['virtual'].startswith('ephemeral'))] + block_device_mapping = [_format_block_device_mapping(bdm) for bdm in - mappings] - if block_device_mapping: - result['blockDeviceMapping'] = block_device_mapping + properties.get('block_device_mapping', [])] + + # NOTE(yamahata): overwrite mappings with block_device_mapping + for bdm in block_device_mapping: + for i in range(len(mappings)): + if bdm['deviceName'] == mappings[i]['deviceName']: + del mappings[i] + break + mappings.append(bdm) + + # NOTE(yamahata): trim ebs.no_device == true. Is this necessary? + mappings = [bdm for bdm in mappings if not (bdm.get('noDevice', False))] + + if mappings: + result['blockDeviceMapping'] = mappings class CloudController(object): @@ -1178,7 +1195,7 @@ class CloudController(object): i['rootDeviceName'] = (root_device_name or _DEFAULT_ROOT_DEVICE_NAME) i['rootDeviceType'] = root_device_type - _format_mappings(properties.get('block_device_mapping', []), i) + _format_mappings(properties, i) return i @@ -1232,8 +1249,7 @@ class CloudController(object): def describe_image_attribute(self, context, image_id, attribute, **kwargs): def _block_device_mapping_attribute(image, result): - _format_mappings( - image['properties'].get('block_device_mapping', []), result) + _format_mappings(image['properties'], result) def _launch_permission_attribute(image, result): result['launchPermission'] = [] -- cgit From 91cc2d5f974d67d91e1e783aaec105c489a47cce Mon Sep 17 00:00:00 2001 From: Isaku Yamahata Date: Wed, 22 Jun 2011 12:54:22 +0900 Subject: volume/api: introduce create_snapshot_force() Introduce create_snapshot_force() which create snapshot even when the volume is in in-use. This is needed for CreateImage with no_reboot=true. --- nova/compute/api.py | 24 +++++++++++------------- nova/volume/api.py | 13 +++++++++++-- 2 files changed, 22 insertions(+), 15 deletions(-) diff --git a/nova/compute/api.py b/nova/compute/api.py index e2692a42f..884ec9198 100644 --- a/nova/compute/api.py +++ b/nova/compute/api.py @@ -264,7 +264,7 @@ class API(base.Base): for bdm in mappings: LOG.debug(_("bdm %s"), bdm) - virtual_name = bdm['virtualName'] + virtual_name = bdm['virtual'] if virtual_name == 'ami' or virtual_name == 'root': continue @@ -272,9 +272,10 @@ class API(base.Base): virtual_name.startswith('ephemeral')) values = { 'instance_id': instance_id, - 'device_name': bdm['deviceName'], + 'device_name': bdm['device'], 'virtual_name': virtual_name, } - self.db.block_device_mapping_create(elevated_context, values) + self.db.block_device_mapping_update_or_create(elevated_context, + values) def _update_block_device_mapping(self, elevated_context, instance_id, block_device_mapping): @@ -285,15 +286,11 @@ class API(base.Base): LOG.debug(_('bdm %s'), bdm) assert 'device_name' in bdm - values = { - 'instance_id': instance_id, - 'device_name': bdm['device_name'], - 'delete_on_termination': bdm.get('delete_on_termination'), - 'virtual_name': bdm.get('virtual_name'), - 'snapshot_id': bdm.get('snapshot_id'), - 'volume_id': bdm.get('volume_id'), - 'volume_size': bdm.get('volume_size'), - 'no_device': bdm.get('no_device')} + values = {'instance_id': instance_id} + for key in ('device_name', 'delete_on_termination', 'virtual_name', + 'snapshot_id', 'volume_id', 'volume_size', + 'no_device'): + values[key] = bdm.get(key) # NOTE(yamahata): NoDevice eliminates devices defined in image # files by command line option. @@ -305,7 +302,8 @@ class API(base.Base): 'virtual_name'): values[k] = None - self.db.block_device_mapping_create(elevated_context, values) + self.db.block_device_mapping_update_or_create(elevated_context, + values) def create_db_entry_for_new_instance(self, context, image, base_options, security_groups, block_device_mapping, num=1): diff --git a/nova/volume/api.py b/nova/volume/api.py index 7d27abff9..cfc274c77 100644 --- a/nova/volume/api.py +++ b/nova/volume/api.py @@ -140,9 +140,10 @@ class API(base.Base): {"method": "remove_volume", "args": {'volume_id': volume_id}}) - def create_snapshot(self, context, volume_id, name, description): + def _create_snapshot(self, context, volume_id, name, description, + force=False): volume = self.get(context, volume_id) - if volume['status'] != "available": + if ((not force) and (volume['status'] != "available")): raise exception.ApiError(_("Volume status must be available")) options = { @@ -164,6 +165,14 @@ class API(base.Base): "snapshot_id": snapshot['id']}}) return snapshot + def create_snapshot(self, context, volume_id, name, description): + return self._create_snapshot(context, volume_id, name, description, + False) + + def create_snapshot_force(self, context, volume_id, name, description): + return self._create_snapshot(context, volume_id, name, description, + True) + def delete_snapshot(self, context, snapshot_id): snapshot = self.get_snapshot(context, snapshot_id) if snapshot['status'] != "available": -- cgit From d81d75bec04fe19492544e5bf7548dce5a2366ad Mon Sep 17 00:00:00 2001 From: Isaku Yamahata Date: Wed, 22 Jun 2011 12:54:22 +0900 Subject: api/ec2: make CreateImage pass unit tests --- nova/api/ec2/cloud.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/nova/api/ec2/cloud.py b/nova/api/ec2/cloud.py index 703dd82b4..c25db9014 100644 --- a/nova/api/ec2/cloud.py +++ b/nova/api/ec2/cloud.py @@ -1346,7 +1346,7 @@ class CloudController(object): state_description = instance['state_description'] # NOTE(yamahata): timeout and error? - src_image = self.image_service.show(context, instance['image_id']) + src_image = self._get_image(context, instance['image_ref']) properties = src_image['properties'] if instance['root_device_name']: properties['root_device_name'] = instance['root_device_name'] @@ -1372,7 +1372,7 @@ class CloudController(object): # NOTE(yamahata): Should we wait for snapshot creation? # Linux LVM snapshot creation completes in # short time, it doesn't matter for now. - snapshot = self.volume_api.create_snapshot( + snapshot = self.volume_api.create_snapshot_force( context, volume_id=volume_id, name=vol['display_name'], description=vol['display_description']) m['snapshot_id'] = snapshot['id'] @@ -1388,9 +1388,9 @@ class CloudController(object): assert (virtual_name == 'swap' or virtual_name.startswith('ephemeral')) - device_name = m['device_name'] - if device_name in [b.device_name for b in mapping - if not b.no_device]: + device_name = m['device'] + if device_name in [b['device_name'] for b in mapping + if not b.get('no_device', False)]: continue # NOTE(yamahata): swap and ephemeral devices are specified in @@ -1402,7 +1402,7 @@ class CloudController(object): properties['block_device_mapping'] = mapping for attr in ('status', 'location', 'id'): - del src_image[attr] + src_image.pop(attr, None) image_id = self._register_image(context, src_image) -- cgit From 181ae36fe34edd206c33e3a0b7e10800ced93e97 Mon Sep 17 00:00:00 2001 From: Isaku Yamahata Date: Wed, 22 Jun 2011 12:54:32 +0900 Subject: test_api: unit tests for ec2utils.id_to_ec2_{snap, vol}_id() --- nova/tests/test_api.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/nova/tests/test_api.py b/nova/tests/test_api.py index 20b20fcbf..e74ae839e 100644 --- a/nova/tests/test_api.py +++ b/nova/tests/test_api.py @@ -107,6 +107,8 @@ class Ec2utilsTestCase(test.TestCase): def test_ec2_id_to_id(self): self.assertEqual(ec2utils.ec2_id_to_id('i-0000001e'), 30) self.assertEqual(ec2utils.ec2_id_to_id('ami-1d'), 29) + self.assertEqual(ec2utils.ec2_id_to_id('snap-0000001c'), 28) + self.assertEqual(ec2utils.ec2_id_to_id('vol-0000001b'), 27) def test_bad_ec2_id(self): self.assertRaises(exception.InvalidEc2Id, @@ -116,7 +118,8 @@ class Ec2utilsTestCase(test.TestCase): def test_id_to_ec2_id(self): self.assertEqual(ec2utils.id_to_ec2_id(30), 'i-0000001e') self.assertEqual(ec2utils.id_to_ec2_id(29, 'ami-%08x'), 'ami-0000001d') - + self.assertEqual(ec2utils.id_to_ec2_snap_id(28), 'snap-0000001c') + self.assertEqual(ec2utils.id_to_ec2_vol_id(27), 'vol-0000001b') class ApiEc2TestCase(test.TestCase): """Unit test for the cloud controller on an EC2 API""" -- cgit From 79d97f7232c119496dde1dd2f0534520ab383962 Mon Sep 17 00:00:00 2001 From: Isaku Yamahata Date: Wed, 22 Jun 2011 12:54:39 +0900 Subject: ec2utils: add an unit test for dict_from_dotted_str() --- nova/tests/test_api.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/nova/tests/test_api.py b/nova/tests/test_api.py index e74ae839e..20d78687e 100644 --- a/nova/tests/test_api.py +++ b/nova/tests/test_api.py @@ -121,6 +121,26 @@ class Ec2utilsTestCase(test.TestCase): self.assertEqual(ec2utils.id_to_ec2_snap_id(28), 'snap-0000001c') self.assertEqual(ec2utils.id_to_ec2_vol_id(27), 'vol-0000001b') + def test_dict_from_dotted_str(self): + in_str = [('BlockDeviceMapping.1.DeviceName', '/dev/sda1'), + ('BlockDeviceMapping.1.Ebs.SnapshotId', 'snap-0000001c'), + ('BlockDeviceMapping.1.Ebs.VolumeSize', '80'), + ('BlockDeviceMapping.1.Ebs.DeleteOnTermination', 'false'), + ('BlockDeviceMapping.2.DeviceName', '/dev/sdc'), + ('BlockDeviceMapping.2.VirtualName', 'ephemeral0')] + expected_dict = { + 'block_device_mapping': { + '1': {'device_name': '/dev/sda1', + 'ebs': {'snapshot_id': 'snap-0000001c', + 'volume_size': 80, + 'delete_on_termination': False}}, + '2': {'device_name': '/dev/sdc', + 'virtual_name': 'ephemeral0'}}} + out_dict = ec2utils.dict_from_dotted_str(in_str) + + self.assertDictMatch(out_dict, expected_dict) + + class ApiEc2TestCase(test.TestCase): """Unit test for the cloud controller on an EC2 API""" def setUp(self): -- cgit From 776571b38fb898a4dafa80e8f3da34b214c948b8 Mon Sep 17 00:00:00 2001 From: Isaku Yamahata Date: Wed, 22 Jun 2011 12:54:43 +0900 Subject: ec2utils: unit tests for case insensitive true/false conversion --- nova/tests/test_api.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/nova/tests/test_api.py b/nova/tests/test_api.py index 20d78687e..bea4b4e9d 100644 --- a/nova/tests/test_api.py +++ b/nova/tests/test_api.py @@ -92,7 +92,9 @@ class XmlConversionTestCase(test.TestCase): conv = ec2utils._try_convert self.assertEqual(conv('None'), None) self.assertEqual(conv('True'), True) + self.assertEqual(conv('true'), True) self.assertEqual(conv('False'), False) + self.assertEqual(conv('false'), False) self.assertEqual(conv('0'), 0) self.assertEqual(conv('42'), 42) self.assertEqual(conv('3.14'), 3.14) -- cgit From ec515fa667e2954aa93a6954a541739e6e3aa221 Mon Sep 17 00:00:00 2001 From: Isaku Yamahata Date: Wed, 22 Jun 2011 12:54:49 +0900 Subject: image/s3: factor out _s3_create() for testability The unittest will come with later changeset. --- nova/image/s3.py | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/nova/image/s3.py b/nova/image/s3.py index b1afc6a81..86dd2e309 100644 --- a/nova/image/s3.py +++ b/nova/image/s3.py @@ -102,18 +102,7 @@ class S3ImageService(service.BaseImageService): key.get_contents_to_filename(local_filename) return local_filename - def _s3_create(self, context, metadata): - """Gets a manifext from s3 and makes an image.""" - - image_path = tempfile.mkdtemp(dir=FLAGS.image_decryption_dir) - - image_location = metadata['properties']['image_location'] - bucket_name = image_location.split('/')[0] - manifest_path = image_location[len(bucket_name) + 1:] - bucket = self._conn(context).get_bucket(bucket_name) - key = bucket.get_key(manifest_path) - manifest = key.get_contents_as_string() - + def _s3_parse_manifest(self, context, metadata, manifest): manifest = ElementTree.fromstring(manifest) image_format = 'ami' image_type = 'machine' @@ -183,6 +172,21 @@ class S3ImageService(service.BaseImageService): 'properties': properties}) metadata['properties']['image_state'] = 'pending' image = self.service.create(context, metadata) + return manifest, image + + def _s3_create(self, context, metadata): + """Gets a manifext from s3 and makes an image.""" + + image_path = tempfile.mkdtemp(dir=FLAGS.image_decryption_dir) + + image_location = metadata['properties']['image_location'] + bucket_name = image_location.split('/')[0] + manifest_path = image_location[len(bucket_name) + 1:] + bucket = self._conn(context).get_bucket(bucket_name) + key = bucket.get_key(manifest_path) + manifest = key.get_contents_as_string() + + manifest, image = self._s3_parse_manifest(metadata, context, manifest) image_id = image['id'] def delayed_create(): -- cgit From ac9ed64077eaad6b4df91fbf90af7933a6bddd5a Mon Sep 17 00:00:00 2001 From: Isaku Yamahata Date: Wed, 22 Jun 2011 12:54:49 +0900 Subject: unittest, image/s3: unit tests for s3 image handler --- nova/tests/image/test_s3.py | 122 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 122 insertions(+) create mode 100644 nova/tests/image/test_s3.py diff --git a/nova/tests/image/test_s3.py b/nova/tests/image/test_s3.py new file mode 100644 index 000000000..231e109f8 --- /dev/null +++ b/nova/tests/image/test_s3.py @@ -0,0 +1,122 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 Isaku Yamahata +# 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 nova import context +from nova import flags +from nova import test +from nova.image import s3 + +FLAGS = flags.FLAGS + + +ami_manifest_xml = """ + + 2011-06-17 + + test-s3 + 0 + 0 + + + x86_64 + + + ami + sda1 + + + root + /dev/sda1 + + + ephemeral0 + sda2 + + + swap + sda3 + + + + +""" + + +class TestS3ImageService(test.TestCase): + def setUp(self): + super(TestS3ImageService, self).setUp() + self.orig_image_service = FLAGS.image_service + FLAGS.image_service = 'nova.image.fake.FakeImageService' + self.image_service = s3.S3ImageService() + self.context = context.RequestContext(None, None) + + def tearDown(self): + super(TestS3ImageService, self).tearDown() + FLAGS.image_service = self.orig_image_service + + def _assertEqualList(self, list0, list1, keys): + self.assertEqual(len(list0), len(list1)) + key = keys[0] + for x in list0: + self.assertEqual(len(x), len(keys)) + self.assertTrue(key in x) + for y in list1: + self.assertTrue(key in y) + if x[key] == y[key]: + for k in keys: + self.assertEqual(x[k], y[k]) + + def test_s3_create(self): + metadata = {'properties': { + 'root_device_name': '/dev/sda1', + 'block_device_mapping': [ + {'device_name': '/dev/sda1', + 'snapshot_id': 'snap-12345678', + 'delete_on_termination': True}, + {'device_name': '/dev/sda2', + 'virutal_name': 'ephemeral0'}, + {'device_name': '/dev/sdb0', + 'no_device': True}]}} + _manifest, image = self.image_service._s3_parse_manifest( + self.context, metadata, ami_manifest_xml) + image_id = image['id'] + + ret_image = self.image_service.show(self.context, image_id) + self.assertTrue('properties' in ret_image) + properties = ret_image['properties'] + + self.assertTrue('mappings' in properties) + mappings = properties['mappings'] + expected_mappings = [ + {"device": "sda1", "virtual": "ami"}, + {"device": "/dev/sda1", "virtual": "root"}, + {"device": "sda2", "virtual": "ephemeral0"}, + {"device": "sda3", "virtual": "swap"}] + self._assertEqualList(mappings, expected_mappings, + ['device', 'virtual']) + + self.assertTrue('block_device_mapping', properties) + block_device_mapping = properties['block_device_mapping'] + expected_bdm = [ + {'device_name': '/dev/sda1', + 'snapshot_id': 'snap-12345678', + 'delete_on_termination': True}, + {'device_name': '/dev/sda2', + 'virutal_name': 'ephemeral0'}, + {'device_name': '/dev/sdb0', + 'no_device': True}] + self.assertEqual(block_device_mapping, expected_bdm) -- cgit From aadb9a0a8a9a0b947643c04f24b623412db7d48d Mon Sep 17 00:00:00 2001 From: Isaku Yamahata Date: Wed, 22 Jun 2011 12:54:56 +0900 Subject: ec2utils: an unit test for ec2utils.properties_root_defice_name. --- nova/tests/test_api.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/nova/tests/test_api.py b/nova/tests/test_api.py index bea4b4e9d..ebc5508cc 100644 --- a/nova/tests/test_api.py +++ b/nova/tests/test_api.py @@ -142,6 +142,17 @@ class Ec2utilsTestCase(test.TestCase): self.assertDictMatch(out_dict, expected_dict) + def test_properties_root_defice_name(self): + mappings = [{"device": "/dev/sda1", "virtual": "root"}] + properties0 = {'mappings': mappings} + properties1 = {'root_device_name': '/dev/sdb', 'mappings': mappings} + + root_device_name = ec2utils.properties_root_device_name(properties0) + self.assertEqual(root_device_name, '/dev/sda1') + + root_device_name = ec2utils.properties_root_device_name(properties1) + self.assertEqual(root_device_name, '/dev/sdb') + class ApiEc2TestCase(test.TestCase): """Unit test for the cloud controller on an EC2 API""" -- cgit From 1df8275883930c71ea4324b0d43b6508440e1d65 Mon Sep 17 00:00:00 2001 From: Isaku Yamahata Date: Wed, 22 Jun 2011 12:55:03 +0900 Subject: test_cloud: an unit test for describe image with block device mapping --- nova/tests/test_cloud.py | 133 ++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 131 insertions(+), 2 deletions(-) diff --git a/nova/tests/test_cloud.py b/nova/tests/test_cloud.py index fd1c21386..35660d4bb 100644 --- a/nova/tests/test_cloud.py +++ b/nova/tests/test_cloud.py @@ -323,6 +323,135 @@ class CloudTestCase(test.TestCase): self.assertRaises(exception.ImageNotFound, describe_images, self.context, ['ami-fake']) + def assertDictListUnorderedMatch(self, L1, L2, key): + self.assertEqual(len(L1), len(L2)) + for d1 in L1: + self.assertTrue(key in d1) + for d2 in L2: + self.assertTrue(key in d2) + if d1[key] == d2[key]: + self.assertDictMatch(d1, d2) + + def _assertImageSet(self, result, root_device_type, root_device_name): + self.assertEqual(1, len(result['imagesSet'])) + result = result['imagesSet'][0] + self.assertTrue('rootDeviceType' in result) + self.assertEqual(result['rootDeviceType'], root_device_type) + self.assertTrue('rootDeviceName' in result) + self.assertEqual(result['rootDeviceName'], root_device_name) + self.assertTrue('blockDeviceMapping' in result) + + return result + + # NOTE(yamahata): + # InstanceBlockDeviceMappingItemType + # rootDeviceType + # rootDeviceName + # blockDeviceMapping + # deviceName + # virtualName + # ebs + # snapshotId + # volumeSize + # deleteOnTermination + # noDevice + def test_describe_image_mapping(self): + """test for rootDeviceName and blockDeiceMapping""" + describe_images = self.cloud.describe_images + mappings1 = [ + {'device': '/dev/sda1', 'virtual': 'root'}, + + {'device': '/dev/sdb0', 'virtual': 'ephemeral0'}, + {'device': '/dev/sdb1', 'virtual': 'ephemeral1'}, + {'device': '/dev/sdb2', 'virtual': 'ephemeral2'}, + {'device': '/dev/sdb3', 'virtual': 'ephemeral3'}, + {'device': '/dev/sdb4', 'virtual': 'ephemeral4'}, + + {'device': '/dev/sdc0', 'virtual': 'swap'}, + {'device': '/dev/sdc1', 'virtual': 'swap'}, + {'device': '/dev/sdc2', 'virtual': 'swap'}, + {'device': '/dev/sdc3', 'virtual': 'swap'}, + {'device': '/dev/sdc4', 'virtual': 'swap'}] + block_device_mapping1 = [ + {'device_name': '/dev/sdb1', 'snapshot_id': 01234567}, + {'device_name': '/dev/sdb2', 'volume_id': 01234567}, + {'device_name': '/dev/sdb3', 'virtual_name': 'ephemeral5'}, + {'device_name': '/dev/sdb4', 'no_device': True}, + + {'device_name': '/dev/sdc1', 'snapshot_id': 12345678}, + {'device_name': '/dev/sdc2', 'volume_id': 12345678}, + {'device_name': '/dev/sdc3', 'virtual_name': 'ephemeral6'}, + {'device_name': '/dev/sdc4', 'no_device': True}] + image1 = { + 'id': 1, + 'properties': { + 'kernel_id': 1, + 'type': 'machine', + 'mappings': mappings1, + 'block_device_mapping': block_device_mapping1, + } + } + + mappings2 = [{'device': '/dev/sda1', 'virtual': 'root'}] + block_device_mapping2 = [{'device_name': '/dev/sdb1', + 'snapshot_id': 01234567}] + image2 = { + 'id': 2, + 'properties': { + 'kernel_id': 2, + 'type': 'machine', + 'root_device_name': '/dev/sdb1', + 'mappings': mappings2, + 'block_device_mapping': block_device_mapping2}} + + def fake_show(meh, context, image_id): + for i in [image1, image2]: + if i['id'] == image_id: + return i + raise exception.ImageNotFound(image_id=image_id) + + def fake_detail(meh, context): + return [image1, image2] + + self.stubs.Set(fake._FakeImageService, 'show', fake_show) + self.stubs.Set(fake._FakeImageService, 'detail', fake_detail) + + result = describe_images(self.context, ['ami-00000001']) + result = self._assertImageSet(result, 'instance-store', '/dev/sda1') + + # NOTE(yamahata): noDevice doesn't make sense when returning mapping + # It makes sense only when user overriding existing + # mapping. + expected_bdms = [ + {'deviceName': '/dev/sdb0', 'virtualName': 'ephemeral0'}, + {'deviceName': '/dev/sdb1', 'ebs': {'snapshotId': + 'snap-00053977'}}, + {'deviceName': '/dev/sdb2', 'ebs': {'snapshotId': + 'vol-00053977'}}, + {'deviceName': '/dev/sdb3', 'virtualName': 'ephemeral5'}, + # {'deviceName': '/dev/sdb4', 'noDevice': True}, + + {'deviceName': '/dev/sdc0', 'virtualName': 'swap'}, + {'deviceName': '/dev/sdc1', 'ebs': {'snapshotId': + 'snap-00bc614e'}}, + {'deviceName': '/dev/sdc2', 'ebs': {'snapshotId': + 'vol-00bc614e'}}, + {'deviceName': '/dev/sdc3', 'virtualName': 'ephemeral6'}, + # {'deviceName': '/dev/sdc4', 'noDevice': True} + ] + self.assertDictListUnorderedMatch(result['blockDeviceMapping'], + expected_bdms, 'deviceName') + + result = describe_images(self.context, ['ami-00000002']) + result = self._assertImageSet(result, 'ebs', '/dev/sdb1') + + expected_bdms = [{'deviceName': '/dev/sdb1', + 'ebs': {'snapshotId': 'snap-00053977'}}] + self.assertDictListUnorderedMatch(result['blockDeviceMapping'], + expected_bdms, 'deviceName') + + self.stubs.UnsetAll() + def test_describe_image_attribute(self): describe_image_attribute = self.cloud.describe_image_attribute @@ -679,10 +808,10 @@ class CloudTestCase(test.TestCase): 'max_count': 1, 'block_device_mapping': [{'device_name': '/dev/vdb', 'volume_id': vol1['id'], - 'delete_on_termination': False, }, + 'delete_on_termination': False}, {'device_name': '/dev/vdc', 'volume_id': vol2['id'], - 'delete_on_termination': True, }, + 'delete_on_termination': True}, ]} ec2_instance_id = self._run_instance_wait(**kwargs) instance_id = ec2utils.ec2_id_to_id(ec2_instance_id) -- cgit From c6792450aa745ef003b80999eae3283533a15521 Mon Sep 17 00:00:00 2001 From: Isaku Yamahata Date: Wed, 22 Jun 2011 12:55:08 +0900 Subject: unittest: an unit test for ec2 describe image attribute --- nova/tests/test_cloud.py | 141 +++++++++++++++++++++++++++++------------------ 1 file changed, 88 insertions(+), 53 deletions(-) diff --git a/nova/tests/test_cloud.py b/nova/tests/test_cloud.py index 35660d4bb..43bcdf703 100644 --- a/nova/tests/test_cloud.py +++ b/nova/tests/test_cloud.py @@ -332,32 +332,7 @@ class CloudTestCase(test.TestCase): if d1[key] == d2[key]: self.assertDictMatch(d1, d2) - def _assertImageSet(self, result, root_device_type, root_device_name): - self.assertEqual(1, len(result['imagesSet'])) - result = result['imagesSet'][0] - self.assertTrue('rootDeviceType' in result) - self.assertEqual(result['rootDeviceType'], root_device_type) - self.assertTrue('rootDeviceName' in result) - self.assertEqual(result['rootDeviceName'], root_device_name) - self.assertTrue('blockDeviceMapping' in result) - - return result - - # NOTE(yamahata): - # InstanceBlockDeviceMappingItemType - # rootDeviceType - # rootDeviceName - # blockDeviceMapping - # deviceName - # virtualName - # ebs - # snapshotId - # volumeSize - # deleteOnTermination - # noDevice - def test_describe_image_mapping(self): - """test for rootDeviceName and blockDeiceMapping""" - describe_images = self.cloud.describe_images + def _setUpImageSet(self): mappings1 = [ {'device': '/dev/sda1', 'virtual': 'root'}, @@ -416,39 +391,73 @@ class CloudTestCase(test.TestCase): self.stubs.Set(fake._FakeImageService, 'show', fake_show) self.stubs.Set(fake._FakeImageService, 'detail', fake_detail) + def _assertImageSet(self, result, root_device_type, root_device_name): + self.assertEqual(1, len(result['imagesSet'])) + result = result['imagesSet'][0] + self.assertTrue('rootDeviceType' in result) + self.assertEqual(result['rootDeviceType'], root_device_type) + self.assertTrue('rootDeviceName' in result) + self.assertEqual(result['rootDeviceName'], root_device_name) + self.assertTrue('blockDeviceMapping' in result) + + return result + + _expected_root_device_name1 = '/dev/sda1' + # NOTE(yamahata): noDevice doesn't make sense when returning mapping + # It makes sense only when user overriding existing + # mapping. + _expected_bdms1 = [ + {'deviceName': '/dev/sdb0', 'virtualName': 'ephemeral0'}, + {'deviceName': '/dev/sdb1', 'ebs': {'snapshotId': + 'snap-00053977'}}, + {'deviceName': '/dev/sdb2', 'ebs': {'snapshotId': + 'vol-00053977'}}, + {'deviceName': '/dev/sdb3', 'virtualName': 'ephemeral5'}, + # {'deviceName': '/dev/sdb4', 'noDevice': True}, + + {'deviceName': '/dev/sdc0', 'virtualName': 'swap'}, + {'deviceName': '/dev/sdc1', 'ebs': {'snapshotId': + 'snap-00bc614e'}}, + {'deviceName': '/dev/sdc2', 'ebs': {'snapshotId': + 'vol-00bc614e'}}, + {'deviceName': '/dev/sdc3', 'virtualName': 'ephemeral6'}, + # {'deviceName': '/dev/sdc4', 'noDevice': True} + ] + + _expected_root_device_name2 = '/dev/sdb1' + _expected_bdms2 = [{'deviceName': '/dev/sdb1', + 'ebs': {'snapshotId': 'snap-00053977'}}] + + # NOTE(yamahata): + # InstanceBlockDeviceMappingItemType + # rootDeviceType + # rootDeviceName + # blockDeviceMapping + # deviceName + # virtualName + # ebs + # snapshotId + # volumeSize + # deleteOnTermination + # noDevice + def test_describe_image_mapping(self): + """test for rootDeviceName and blockDeiceMapping""" + describe_images = self.cloud.describe_images + self._setUpImageSet() + result = describe_images(self.context, ['ami-00000001']) - result = self._assertImageSet(result, 'instance-store', '/dev/sda1') - - # NOTE(yamahata): noDevice doesn't make sense when returning mapping - # It makes sense only when user overriding existing - # mapping. - expected_bdms = [ - {'deviceName': '/dev/sdb0', 'virtualName': 'ephemeral0'}, - {'deviceName': '/dev/sdb1', 'ebs': {'snapshotId': - 'snap-00053977'}}, - {'deviceName': '/dev/sdb2', 'ebs': {'snapshotId': - 'vol-00053977'}}, - {'deviceName': '/dev/sdb3', 'virtualName': 'ephemeral5'}, - # {'deviceName': '/dev/sdb4', 'noDevice': True}, - - {'deviceName': '/dev/sdc0', 'virtualName': 'swap'}, - {'deviceName': '/dev/sdc1', 'ebs': {'snapshotId': - 'snap-00bc614e'}}, - {'deviceName': '/dev/sdc2', 'ebs': {'snapshotId': - 'vol-00bc614e'}}, - {'deviceName': '/dev/sdc3', 'virtualName': 'ephemeral6'}, - # {'deviceName': '/dev/sdc4', 'noDevice': True} - ] + result = self._assertImageSet(result, 'instance-store', + self._expected_root_device_name1) + self.assertDictListUnorderedMatch(result['blockDeviceMapping'], - expected_bdms, 'deviceName') + self._expected_bdms1, 'deviceName') result = describe_images(self.context, ['ami-00000002']) - result = self._assertImageSet(result, 'ebs', '/dev/sdb1') + result = self._assertImageSet(result, 'ebs', + self._expected_root_device_name2) - expected_bdms = [{'deviceName': '/dev/sdb1', - 'ebs': {'snapshotId': 'snap-00053977'}}] self.assertDictListUnorderedMatch(result['blockDeviceMapping'], - expected_bdms, 'deviceName') + self._expected_bdms2, 'deviceName') self.stubs.UnsetAll() @@ -465,6 +474,32 @@ class CloudTestCase(test.TestCase): 'launchPermission') self.assertEqual([{'group': 'all'}], result['launchPermission']) + def test_describe_image_attribute_root_device_name(self): + describe_image_attribute = self.cloud.describe_image_attribute + self._setUpImageSet() + + result = describe_image_attribute(self.context, 'ami-00000001', + 'rootDeviceName') + self.assertEqual(result['rootDeviceName'], + self._expected_root_device_name1) + result = describe_image_attribute(self.context, 'ami-00000002', + 'rootDeviceName') + self.assertEqual(result['rootDeviceName'], + self._expected_root_device_name2) + + def test_describe_image_attribute_block_device_mapping(self): + describe_image_attribute = self.cloud.describe_image_attribute + self._setUpImageSet() + + result = describe_image_attribute(self.context, 'ami-00000001', + 'blockDeviceMapping') + self.assertDictListUnorderedMatch(result['blockDeviceMapping'], + self._expected_bdms1, 'deviceName') + result = describe_image_attribute(self.context, 'ami-00000002', + 'blockDeviceMapping') + self.assertDictListUnorderedMatch(result['blockDeviceMapping'], + self._expected_bdms2, 'deviceName') + def test_modify_image_attribute(self): modify_image_attribute = self.cloud.modify_image_attribute -- cgit From 5276e80c403a2ae87d3c93979289331e286fd2a1 Mon Sep 17 00:00:00 2001 From: Isaku Yamahata Date: Wed, 22 Jun 2011 12:55:11 +0900 Subject: api/ec2, boot-from-volume: an unit test for describe instances --- nova/test.py | 12 ++++ nova/tests/test_cloud.py | 140 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 152 insertions(+) diff --git a/nova/test.py b/nova/test.py index 4a0a18fe7..99d4cec4f 100644 --- a/nova/test.py +++ b/nova/test.py @@ -252,3 +252,15 @@ class TestCase(unittest.TestCase): for d1, d2 in zip(L1, L2): self.assertDictMatch(d1, d2, approx_equal=approx_equal, tolerance=tolerance) + + def assertSubDictMatch(self, sub_dict, super_dict): + """Assert a sub_dict is subset of super_dict.""" + self.assertTrue(set(sub_dict.keys()).issubset(set(super_dict.keys()))) + for k, sub_value in sub_dict.items(): + super_value = super_dict[k] + if isinstance(sub_value, dict): + self.assertSubDictMatch(sub_value, super_value) + elif 'DONTCARE' in (sub_value, super_value): + continue + else: + self.assertEqual(sub_value, super_value) diff --git a/nova/tests/test_cloud.py b/nova/tests/test_cloud.py index 43bcdf703..f7326e66f 100644 --- a/nova/tests/test_cloud.py +++ b/nova/tests/test_cloud.py @@ -294,6 +294,146 @@ class CloudTestCase(test.TestCase): db.service_destroy(self.context, comp1['id']) db.service_destroy(self.context, comp2['id']) + def _assertInstance(self, instance_id): + ec2_instance_id = ec2utils.id_to_ec2_id(instance_id) + result = self.cloud.describe_instances(self.context, + instance_id=[ec2_instance_id]) + result = result['reservationSet'][0] + self.assertEqual(len(result['instancesSet']), 1) + result = result['instancesSet'][0] + self.assertEqual(result['instanceId'], ec2_instance_id) + return (ec2_instance_id, result) + + def _assertEqualBlockDeviceMapping(self, expected, result): + self.assertEqual(len(expected), len(result)) + for x in expected: + found = False + for y in result: + if x['deviceName'] == y['deviceName']: + self.assertSubDictMatch(x, y) + found = True + break + self.assertTrue(found) + + def test_describe_instances_bdm(self): + """Make sure describe_instances works with root_device_name and + block device mappings + """ + inst1 = db.instance_create(self.context, + {'image_ref': 1, + 'root_device_name': '/dev/sdb1'}) + inst2 = db.instance_create(self.context, + {'image_ref': 2, + 'root_device_name': '/dev/sdc1'}) + + instance_id = inst1['id'] + mappings = [ + {'instance_id': instance_id, + 'device_name': '/dev/sdb1', + 'snapshot_id': '1', + 'volume_id': '2'}, + {'instance_id': instance_id, + 'device_name': '/dev/sdb2', + 'volume_id': '3'}, + {'instance_id': instance_id, + 'device_name': '/dev/sdb3', + 'delete_on_termination': True, + 'snapshot_id': '4', + 'volume_id': '5'}, + {'instance_id': instance_id, + 'device_name': '/dev/sdb4', + 'delete_on_termination': False, + 'snapshot_id': '6', + 'volume_id': '7'}, + {'instance_id': instance_id, + 'device_name': '/dev/sdb5', + 'snapshot_id': '8', + 'volume_id': '9', + 'volume_size': 0}, + {'instance_id': instance_id, + 'device_name': '/dev/sdb6', + 'snapshot_id': '10', + 'volume_id': '11', + 'volume_size': 1}, + {'instance_id': instance_id, + 'device_name': '/dev/sdb7', + 'no_device': True}, + {'instance_id': instance_id, + 'device_name': '/dev/sdb8', + 'virtual_name': 'swap'}, + {'instance_id': instance_id, + 'device_name': '/dev/sdb9', + 'virtual_name': 'ephemeral3'}] + + volumes = [] + for bdm in mappings: + db.block_device_mapping_create(self.context, bdm) + if bdm.get('volume_id'): + values = {'volume_id': bdm['volume_id']} + for bdm_key, vol_key in [('snapshot_id', 'snapshot_id'), + ('snapshot_size', 'volume_size'), + ('delete_on_termination', + 'delete_on_termination')]: + if bdm.get(bdm_key): + values[vol_key] = bdm[bdm_key] + vol = db.volume_create(self.context, values) + db.volume_attached(self.context, vol['id'], + instance_id, bdm['device_name']) + volumes.append(vol) + + ec2_instance_id, result = self._assertInstance(instance_id) + expected_result = {'instanceId': ec2_instance_id, + 'rootDeviceName': '/dev/sdb1', + 'rootDeviceType': 'ebs'} + expected_block_device_mapping = [ + {'deviceName': '/dev/sdb1', + 'ebs': {'status': 'in-use', + 'deleteOnTermination': False, + 'volumeId': 2, + }}, + {'deviceName': '/dev/sdb2', + 'ebs': {'status': 'in-use', + 'deleteOnTermination': False, + 'volumeId': 3, + }}, + {'deviceName': '/dev/sdb3', + 'ebs': {'status': 'in-use', + 'deleteOnTermination': True, + 'volumeId': 5, + }}, + {'deviceName': '/dev/sdb4', + 'ebs': {'status': 'in-use', + 'deleteOnTermination': False, + 'volumeId': 7, + }}, + {'deviceName': '/dev/sdb5', + 'ebs': {'status': 'in-use', + 'deleteOnTermination': False, + 'volumeId': 9, + }}, + {'deviceName': '/dev/sdb6', + 'ebs': {'status': 'in-use', + 'deleteOnTermination': False, + 'volumeId': 11, }}] + # NOTE(yamahata): swap/ephemeral device case isn't supported yet. + self.assertSubDictMatch(expected_result, result) + self._assertEqualBlockDeviceMapping(expected_block_device_mapping, + result['blockDeviceMapping']) + + ec2_instance_id, result = self._assertInstance(inst2['id']) + expected_result = {'instanceId': ec2_instance_id, + 'rootDeviceName': '/dev/sdc1', + 'rootDeviceType': 'instance-store'} + self.assertSubDictMatch(expected_result, result) + + for vol in volumes: + db.volume_destroy(self.context, vol['id']) + for bdm in db.block_device_mapping_get_all_by_instance(self.context, + instance_id): + db.block_device_mapping_destroy(self.context, bdm['id']) + db.instance_destroy(self.context, inst2['id']) + db.instance_destroy(self.context, inst1['id']) + def test_describe_images(self): describe_images = self.cloud.describe_images -- cgit From 97a710fa191b0abd94fef25d7110448c41c4e259 Mon Sep 17 00:00:00 2001 From: Isaku Yamahata Date: Wed, 22 Jun 2011 12:55:14 +0900 Subject: api/ec2: an unit test for create image unit test for ec2 create image. This is incomplete as there is no unit test for register image. --- nova/tests/test_cloud.py | 90 ++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 72 insertions(+), 18 deletions(-) diff --git a/nova/tests/test_cloud.py b/nova/tests/test_cloud.py index f7326e66f..aed5aff6a 100644 --- a/nova/tests/test_cloud.py +++ b/nova/tests/test_cloud.py @@ -294,6 +294,24 @@ class CloudTestCase(test.TestCase): db.service_destroy(self.context, comp1['id']) db.service_destroy(self.context, comp2['id']) + def _block_device_mapping_create(self, instance_id, mappings): + volumes = [] + for bdm in mappings: + db.block_device_mapping_create(self.context, bdm) + if 'volume_id' in bdm: + values = {'id': bdm['volume_id']} + for bdm_key, vol_key in [('snapshot_id', 'snapshot_id'), + ('snapshot_size', 'volume_size'), + ('delete_on_termination', + 'delete_on_termination')]: + if bdm_key in bdm: + values[vol_key] = bdm[bdm_key] + vol = db.volume_create(self.context, values) + db.volume_attached(self.context, vol['id'], + instance_id, bdm['device_name']) + volumes.append(vol) + return volumes + def _assertInstance(self, instance_id): ec2_instance_id = ec2utils.id_to_ec2_id(instance_id) result = self.cloud.describe_instances(self.context, @@ -334,7 +352,8 @@ class CloudTestCase(test.TestCase): 'volume_id': '2'}, {'instance_id': instance_id, 'device_name': '/dev/sdb2', - 'volume_id': '3'}, + 'volume_id': '3', + 'volume_size': 1}, {'instance_id': instance_id, 'device_name': '/dev/sdb3', 'delete_on_termination': True, @@ -365,21 +384,7 @@ class CloudTestCase(test.TestCase): 'device_name': '/dev/sdb9', 'virtual_name': 'ephemeral3'}] - volumes = [] - for bdm in mappings: - db.block_device_mapping_create(self.context, bdm) - if bdm.get('volume_id'): - values = {'volume_id': bdm['volume_id']} - for bdm_key, vol_key in [('snapshot_id', 'snapshot_id'), - ('snapshot_size', 'volume_size'), - ('delete_on_termination', - 'delete_on_termination')]: - if bdm.get(bdm_key): - values[vol_key] = bdm[bdm_key] - vol = db.volume_create(self.context, values) - db.volume_attached(self.context, vol['id'], - instance_id, bdm['device_name']) - volumes.append(vol) + volumes = self._block_device_mapping_create(instance_id, mappings) ec2_instance_id, result = self._assertInstance(instance_id) expected_result = {'instanceId': ec2_instance_id, @@ -472,7 +477,7 @@ class CloudTestCase(test.TestCase): if d1[key] == d2[key]: self.assertDictMatch(d1, d2) - def _setUpImageSet(self): + def _setUpImageSet(self, create_volumes_and_snapshots=False): mappings1 = [ {'device': '/dev/sda1', 'virtual': 'root'}, @@ -502,6 +507,7 @@ class CloudTestCase(test.TestCase): 'properties': { 'kernel_id': 1, 'type': 'machine', + 'image_state': 'available', 'mappings': mappings1, 'block_device_mapping': block_device_mapping1, } @@ -531,6 +537,22 @@ class CloudTestCase(test.TestCase): self.stubs.Set(fake._FakeImageService, 'show', fake_show) self.stubs.Set(fake._FakeImageService, 'detail', fake_detail) + volumes = [] + snapshots = [] + if create_volumes_and_snapshots: + for bdm in block_device_mapping1: + if 'volume_id' in bdm: + vol = self._volume_create(bdm['volume_id']) + volumes.append(vol['id']) + if 'snapshot_id' in bdm: + snap = db.snapshot_create(self.context, + {'id': bdm['snapshot_id'], + 'volume_id': 76543210, + 'status': "available", + 'volume_size': 1}) + snapshots.append(snap['id']) + return (volumes, snapshots) + def _assertImageSet(self, result, root_device_type, root_device_name): self.assertEqual(1, len(result['imagesSet'])) result = result['imagesSet'][0] @@ -951,11 +973,13 @@ class CloudTestCase(test.TestCase): self._restart_compute_service() - def _volume_create(self): + def _volume_create(self, volume_id=None): kwargs = {'status': 'available', 'host': self.volume.host, 'size': 1, 'attach_status': 'detached', } + if volume_id: + kwargs['id'] = volume_id return db.volume_create(self.context, kwargs) def _assert_volume_attached(self, vol, instance_id, mountpoint): @@ -1175,3 +1199,33 @@ class CloudTestCase(test.TestCase): self.cloud.delete_snapshot(self.context, snapshot_id) greenthread.sleep(0.3) db.volume_destroy(self.context, vol['id']) + + def test_create_image(self): + """Make sure that CreateImage works""" + # enforce periodic tasks run in short time to avoid wait for 60s. + self._restart_compute_service(periodic_interval=0.3) + + (volumes, snapshots) = self._setUpImageSet( + create_volumes_and_snapshots=True) + + kwargs = {'image_id': 'ami-1', + 'instance_type': FLAGS.default_instance_type, + 'max_count': 1} + ec2_instance_id = self._run_instance_wait(**kwargs) + + # TODO(yamahata): s3._s3_create() can't be tested easily by unit test + # as there is no unit test for s3.create() + ## result = self.cloud.create_image(self.context, ec2_instance_id, + ## no_reboot=True) + ## ec2_image_id = result['imageId'] + ## created_image = self.cloud.describe_images(self.context, + ## [ec2_image_id]) + + self.cloud.terminate_instances(self.context, [ec2_instance_id]) + for vol in volumes: + db.volume_destroy(self.context, vol) + for snap in snapshots: + db.snapshot_destroy(self.context, snap) + # TODO(yamahata): clean up snapshot created by CreateImage. + + self._restart_compute_service() -- cgit From c24b30c9a060e50c7bd953a7d68c409416f4f752 Mon Sep 17 00:00:00 2001 From: Isaku Yamahata Date: Thu, 23 Jun 2011 19:51:00 +0900 Subject: volume/api: an unit test for create_snapshot_force() --- nova/tests/test_volume.py | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/nova/tests/test_volume.py b/nova/tests/test_volume.py index 4f10ee6af..5230cef0e 100644 --- a/nova/tests/test_volume.py +++ b/nova/tests/test_volume.py @@ -27,8 +27,10 @@ from nova import exception from nova import db from nova import flags from nova import log as logging +from nova import rpc from nova import test from nova import utils +from nova import volume FLAGS = flags.FLAGS LOG = logging.getLogger('nova.tests.volume') @@ -43,6 +45,11 @@ class VolumeTestCase(test.TestCase): self.flags(connection_type='fake') self.volume = utils.import_object(FLAGS.volume_manager) self.context = context.get_admin_context() + self.instance_id = db.instance_create(self.context, {})['id'] + + def tearDown(self): + db.instance_destroy(self.context, self.instance_id) + super(VolumeTestCase, self).tearDown() @staticmethod def _create_volume(size='0', snapshot_id=None): @@ -224,6 +231,30 @@ class VolumeTestCase(test.TestCase): snapshot_id) self.volume.delete_volume(self.context, volume_id) + def test_create_snapshot_force(self): + """Test snapshot in use can be created forcibly.""" + + def fake_cast(ctxt, topic, msg): + pass + self.stubs.Set(rpc, 'cast', fake_cast) + + volume_id = self._create_volume() + self.volume.create_volume(self.context, volume_id) + db.volume_attached(self.context, volume_id, self.instance_id, + '/dev/sda1') + + volume_api = volume.api.API() + self.assertRaises(exception.ApiError, + volume_api.create_snapshot, + self.context, volume_id, + 'fake_name', 'fake_description') + snapshot_ref = volume_api.create_snapshot_force(self.context, + volume_id, + 'fake_name', + 'fake_description') + db.snapshot_destroy(self.context, snapshot_ref['id']) + db.volume_destroy(self.context, volume_id) + class DriverTestCase(test.TestCase): """Base Test class for Drivers.""" -- cgit From 1c4a3e14a0cef6938c477908d5c3bfe5ddf0e07b Mon Sep 17 00:00:00 2001 From: Isaku Yamahata Date: Thu, 23 Jun 2011 19:51:00 +0900 Subject: ec2utils: introduce helper function to prepend '/dev/' in mappings Introduce a helper function to prepend /dev/ to device name in block device mapping of bundle --- nova/api/ec2/ec2utils.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/nova/api/ec2/ec2utils.py b/nova/api/ec2/ec2utils.py index 9839f8604..bae1e0ee5 100644 --- a/nova/api/ec2/ec2utils.py +++ b/nova/api/ec2/ec2utils.py @@ -154,3 +154,13 @@ def properties_root_device_name(properties): root_device_name = properties['root_device_name'] return root_device_name + + +def mappings_prepend_dev(mappings): + """Prepend '/dev/' to 'device' entry of swap/ephemeral virtual type""" + for m in mappings: + virtual = m['virtual'] + if ((virtual == 'swap' or virtual.startswith('ephemeral')) and + (not m['device'].startswith('/'))): + m['device'] = '/dev/' + m['device'] + return mappings -- cgit From 4b5fdb2ee109960be6b3ff1fa8068ab3ec428283 Mon Sep 17 00:00:00 2001 From: Isaku Yamahata Date: Thu, 23 Jun 2011 19:51:00 +0900 Subject: ec2: bundle block device mapping device name in block device mapping of bundle doesn't necessary carry "/dev/". So prepend it before processing. --- nova/api/ec2/cloud.py | 25 +++++++++++++++---------- nova/compute/api.py | 2 +- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/nova/api/ec2/cloud.py b/nova/api/ec2/cloud.py index c25db9014..8bf0950b2 100644 --- a/nova/api/ec2/cloud.py +++ b/nova/api/ec2/cloud.py @@ -92,20 +92,25 @@ def _parse_block_device_mapping(bdm): """ ebs = bdm.pop('ebs', None) if ebs: - ec2_id = ebs.pop('snapshot_id') - id = ec2utils.ec2_id_to_id(ec2_id) - if ec2_id.startswith('snap-'): - bdm['snapshot_id'] = id - elif ec2_id.startswith('vol-'): - bdm['volume_id'] = id - ebs.setdefault('delete_on_termination', True) + ec2_id = ebs.pop('snapshot_id', None) + if ec2_id: + id = ec2utils.ec2_id_to_id(ec2_id) + if ec2_id.startswith('snap-'): + bdm['snapshot_id'] = id + elif ec2_id.startswith('vol-'): + bdm['volume_id'] = id + ebs.setdefault('delete_on_termination', True) bdm.update(ebs) return bdm +def _properties_get_mappings(properties): + return ec2utils.mappings_prepend_dev(properties.get('mappings', [])) + + def _format_block_device_mapping(bdm): """Contruct BlockDeviceMappingItemType - {'device_name': '...', 'Snapshot_Id': , ...} + {'device_name': '...', 'snapshot_id': , ...} => BlockDeviceMappingItemType """ keys = (('deviceName', 'device_name'), @@ -138,7 +143,7 @@ def _format_block_device_mapping(bdm): def _format_mappings(properties, result): """Format multiple BlockDeviceMappingItemType""" mappings = [{'virtualName': m['virtual'], 'deviceName': m['device']} - for m in properties.get('mappings', []) + for m in _properties_get_mappings(properties) if (m['virtual'] == 'swap' or m['virtual'].startswith('ephemeral'))] @@ -1381,7 +1386,7 @@ class CloudController(object): if m: mapping.append(m) - for m in properties.get('mappings', []): + for m in _properties_get_mappings(properties): virtual_name = m['virtual'] if virtual_name in ('ami', 'root'): continue diff --git a/nova/compute/api.py b/nova/compute/api.py index 884ec9198..b3635d71f 100644 --- a/nova/compute/api.py +++ b/nova/compute/api.py @@ -261,7 +261,7 @@ class API(base.Base): """tell vm driver to create ephemeral/swap device at boot time by updating BlockDeviceMapping """ - for bdm in mappings: + for bdm in ec2utils.mappings_prepend_dev(mappings): LOG.debug(_("bdm %s"), bdm) virtual_name = bdm['virtual'] -- cgit From 8e3da07f2af1fb4c0d5fcb58cb6747afaa6b76d8 Mon Sep 17 00:00:00 2001 From: Isaku Yamahata Date: Thu, 23 Jun 2011 19:51:00 +0900 Subject: ec2utils: an unit test for mapping_prepend_dev() --- nova/tests/test_api.py | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/nova/tests/test_api.py b/nova/tests/test_api.py index ebc5508cc..26ac5ff24 100644 --- a/nova/tests/test_api.py +++ b/nova/tests/test_api.py @@ -153,6 +153,40 @@ class Ec2utilsTestCase(test.TestCase): root_device_name = ec2utils.properties_root_device_name(properties1) self.assertEqual(root_device_name, '/dev/sdb') + def test_mapping_prepend_dev(self): + mappings = [ + {'virtual': 'ami', + 'device': 'sda1'}, + {'virtual': 'root', + 'device': '/dev/sda1'}, + + {'virtual': 'swap', + 'device': 'sdb1'}, + {'virtual': 'swap', + 'device': '/dev/sdb2'}, + + {'virtual': 'ephemeral0', + 'device': 'sdc1'}, + {'virtual': 'ephemeral1', + 'device': '/dev/sdc1'}] + expected_result = [ + {'virtual': 'ami', + 'device': 'sda1'}, + {'virtual': 'root', + 'device': '/dev/sda1'}, + + {'virtual': 'swap', + 'device': '/dev/sdb1'}, + {'virtual': 'swap', + 'device': '/dev/sdb2'}, + + {'virtual': 'ephemeral0', + 'device': '/dev/sdc1'}, + {'virtual': 'ephemeral1', + 'device': '/dev/sdc1'}] + self.assertDictListMatch(ec2utils.mappings_prepend_dev(mappings), + expected_result) + class ApiEc2TestCase(test.TestCase): """Unit test for the cloud controller on an EC2 API""" -- cgit From c5761c6e983e539e5bb24ae6c0f3ea88faea676f Mon Sep 17 00:00:00 2001 From: Isaku Yamahata Date: Thu, 23 Jun 2011 19:51:01 +0900 Subject: ec2/cloud: an unit test for _format_instance_bdm() --- nova/tests/test_cloud.py | 206 +++++++++++++++++++++++++++-------------------- 1 file changed, 120 insertions(+), 86 deletions(-) diff --git a/nova/tests/test_cloud.py b/nova/tests/test_cloud.py index aed5aff6a..a179899ca 100644 --- a/nova/tests/test_cloud.py +++ b/nova/tests/test_cloud.py @@ -312,31 +312,7 @@ class CloudTestCase(test.TestCase): volumes.append(vol) return volumes - def _assertInstance(self, instance_id): - ec2_instance_id = ec2utils.id_to_ec2_id(instance_id) - result = self.cloud.describe_instances(self.context, - instance_id=[ec2_instance_id]) - result = result['reservationSet'][0] - self.assertEqual(len(result['instancesSet']), 1) - result = result['instancesSet'][0] - self.assertEqual(result['instanceId'], ec2_instance_id) - return (ec2_instance_id, result) - - def _assertEqualBlockDeviceMapping(self, expected, result): - self.assertEqual(len(expected), len(result)) - for x in expected: - found = False - for y in result: - if x['deviceName'] == y['deviceName']: - self.assertSubDictMatch(x, y) - found = True - break - self.assertTrue(found) - - def test_describe_instances_bdm(self): - """Make sure describe_instances works with root_device_name and - block device mappings - """ + def _setUpBlockDeviceMapping(self): inst1 = db.instance_create(self.context, {'image_ref': 1, 'root_device_name': '/dev/sdb1'}) @@ -345,7 +321,7 @@ class CloudTestCase(test.TestCase): 'root_device_name': '/dev/sdc1'}) instance_id = inst1['id'] - mappings = [ + mappings0 = [ {'instance_id': instance_id, 'device_name': '/dev/sdb1', 'snapshot_id': '1', @@ -384,61 +360,119 @@ class CloudTestCase(test.TestCase): 'device_name': '/dev/sdb9', 'virtual_name': 'ephemeral3'}] - volumes = self._block_device_mapping_create(instance_id, mappings) - - ec2_instance_id, result = self._assertInstance(instance_id) - expected_result = {'instanceId': ec2_instance_id, - 'rootDeviceName': '/dev/sdb1', - 'rootDeviceType': 'ebs'} - expected_block_device_mapping = [ - {'deviceName': '/dev/sdb1', - 'ebs': {'status': 'in-use', - 'deleteOnTermination': False, - 'volumeId': 2, - }}, - {'deviceName': '/dev/sdb2', - 'ebs': {'status': 'in-use', - 'deleteOnTermination': False, - 'volumeId': 3, - }}, - {'deviceName': '/dev/sdb3', - 'ebs': {'status': 'in-use', - 'deleteOnTermination': True, - 'volumeId': 5, - }}, - {'deviceName': '/dev/sdb4', - 'ebs': {'status': 'in-use', - 'deleteOnTermination': False, - 'volumeId': 7, - }}, - {'deviceName': '/dev/sdb5', - 'ebs': {'status': 'in-use', - 'deleteOnTermination': False, - 'volumeId': 9, - }}, - {'deviceName': '/dev/sdb6', - 'ebs': {'status': 'in-use', - 'deleteOnTermination': False, - 'volumeId': 11, }}] - # NOTE(yamahata): swap/ephemeral device case isn't supported yet. - self.assertSubDictMatch(expected_result, result) - self._assertEqualBlockDeviceMapping(expected_block_device_mapping, - result['blockDeviceMapping']) - - ec2_instance_id, result = self._assertInstance(inst2['id']) - expected_result = {'instanceId': ec2_instance_id, - 'rootDeviceName': '/dev/sdc1', - 'rootDeviceType': 'instance-store'} - self.assertSubDictMatch(expected_result, result) + volumes = self._block_device_mapping_create(instance_id, mappings0) + return (inst1, inst2, volumes) + def _tearDownBlockDeviceMapping(self, inst1, inst2, volumes): for vol in volumes: db.volume_destroy(self.context, vol['id']) - for bdm in db.block_device_mapping_get_all_by_instance(self.context, - instance_id): - db.block_device_mapping_destroy(self.context, bdm['id']) + for id in (inst1['id'], inst2['id']): + for bdm in db.block_device_mapping_get_all_by_instance( + self.context, id): + db.block_device_mapping_destroy(self.context, bdm['id']) db.instance_destroy(self.context, inst2['id']) db.instance_destroy(self.context, inst1['id']) + _expected_instance_bdm1 = { + 'instanceId': 'i-00000001', + 'rootDeviceName': '/dev/sdb1', + 'rootDeviceType': 'ebs'} + + _expected_block_device_mapping0 = [ + {'deviceName': '/dev/sdb1', + 'ebs': {'status': 'in-use', + 'deleteOnTermination': False, + 'volumeId': 2, + }}, + {'deviceName': '/dev/sdb2', + 'ebs': {'status': 'in-use', + 'deleteOnTermination': False, + 'volumeId': 3, + }}, + {'deviceName': '/dev/sdb3', + 'ebs': {'status': 'in-use', + 'deleteOnTermination': True, + 'volumeId': 5, + }}, + {'deviceName': '/dev/sdb4', + 'ebs': {'status': 'in-use', + 'deleteOnTermination': False, + 'volumeId': 7, + }}, + {'deviceName': '/dev/sdb5', + 'ebs': {'status': 'in-use', + 'deleteOnTermination': False, + 'volumeId': 9, + }}, + {'deviceName': '/dev/sdb6', + 'ebs': {'status': 'in-use', + 'deleteOnTermination': False, + 'volumeId': 11, }}] + # NOTE(yamahata): swap/ephemeral device case isn't supported yet. + + _expected_instance_bdm2 = { + 'instanceId': 'i-00000002', + 'rootDeviceName': '/dev/sdc1', + 'rootDeviceType': 'instance-store'} + + def test_format_instance_bdm(self): + (inst1, inst2, volumes) = self._setUpBlockDeviceMapping() + + result = {} + self.cloud._format_instance_bdm(self.context, inst1['id'], '/dev/sdb1', + result) + self.assertSubDictMatch( + {'rootDeviceType': self._expected_instance_bdm1['rootDeviceType']}, + result) + self._assertEqualBlockDeviceMapping( + self._expected_block_device_mapping0, result['blockDeviceMapping']) + + result = {} + self.cloud._format_instance_bdm(self.context, inst2['id'], '/dev/sdc1', + result) + self.assertSubDictMatch( + {'rootDeviceType': self._expected_instance_bdm2['rootDeviceType']}, + result) + + self._tearDownBlockDeviceMapping(inst1, inst2, volumes) + + def _assertInstance(self, instance_id): + ec2_instance_id = ec2utils.id_to_ec2_id(instance_id) + result = self.cloud.describe_instances(self.context, + instance_id=[ec2_instance_id]) + result = result['reservationSet'][0] + self.assertEqual(len(result['instancesSet']), 1) + result = result['instancesSet'][0] + self.assertEqual(result['instanceId'], ec2_instance_id) + return result + + def _assertEqualBlockDeviceMapping(self, expected, result): + self.assertEqual(len(expected), len(result)) + for x in expected: + found = False + for y in result: + if x['deviceName'] == y['deviceName']: + self.assertSubDictMatch(x, y) + found = True + break + self.assertTrue(found) + + def test_describe_instances_bdm(self): + """Make sure describe_instances works with root_device_name and + block device mappings + """ + (inst1, inst2, volumes) = self._setUpBlockDeviceMapping() + + result = self._assertInstance(inst1['id']) + self.assertSubDictMatch(self._expected_instance_bdm1, result) + self._assertEqualBlockDeviceMapping( + self._expected_block_device_mapping0, result['blockDeviceMapping']) + + result = self._assertInstance(inst2['id']) + self.assertSubDictMatch(self._expected_instance_bdm2, result) + + self._tearDownBlockDeviceMapping(inst1, inst2, volumes) + def test_describe_images(self): describe_images = self.cloud.describe_images @@ -481,17 +515,17 @@ class CloudTestCase(test.TestCase): mappings1 = [ {'device': '/dev/sda1', 'virtual': 'root'}, - {'device': '/dev/sdb0', 'virtual': 'ephemeral0'}, - {'device': '/dev/sdb1', 'virtual': 'ephemeral1'}, - {'device': '/dev/sdb2', 'virtual': 'ephemeral2'}, - {'device': '/dev/sdb3', 'virtual': 'ephemeral3'}, - {'device': '/dev/sdb4', 'virtual': 'ephemeral4'}, - - {'device': '/dev/sdc0', 'virtual': 'swap'}, - {'device': '/dev/sdc1', 'virtual': 'swap'}, - {'device': '/dev/sdc2', 'virtual': 'swap'}, - {'device': '/dev/sdc3', 'virtual': 'swap'}, - {'device': '/dev/sdc4', 'virtual': 'swap'}] + {'device': 'sdb0', 'virtual': 'ephemeral0'}, + {'device': 'sdb1', 'virtual': 'ephemeral1'}, + {'device': 'sdb2', 'virtual': 'ephemeral2'}, + {'device': 'sdb3', 'virtual': 'ephemeral3'}, + {'device': 'sdb4', 'virtual': 'ephemeral4'}, + + {'device': 'sdc0', 'virtual': 'swap'}, + {'device': 'sdc1', 'virtual': 'swap'}, + {'device': 'sdc2', 'virtual': 'swap'}, + {'device': 'sdc3', 'virtual': 'swap'}, + {'device': 'sdc4', 'virtual': 'swap'}] block_device_mapping1 = [ {'device_name': '/dev/sdb1', 'snapshot_id': 01234567}, {'device_name': '/dev/sdb2', 'volume_id': 01234567}, -- cgit From c13bb7c3bf2400c45d1b93141e67916c81296e38 Mon Sep 17 00:00:00 2001 From: Isaku Yamahata Date: Thu, 23 Jun 2011 19:51:01 +0900 Subject: ec2/cloud: unit tests for parser/formatter of block device mapping This patch adds several unit tests for private functions in ec2/cloud.py. Which are used to parse/format block device mapping. _parse_block_device_mapping(), _format_block_device_mapping() and _format_mappings() --- nova/tests/test_bdm.py | 233 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 233 insertions(+) create mode 100644 nova/tests/test_bdm.py diff --git a/nova/tests/test_bdm.py b/nova/tests/test_bdm.py new file mode 100644 index 000000000..b258f6a75 --- /dev/null +++ b/nova/tests/test_bdm.py @@ -0,0 +1,233 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 Isaku Yamahata +# 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. + +""" +Tests for Block Device Mapping Code. +""" + +from nova.api.ec2 import cloud +from nova import test + + +class BlockDeviceMappingEc2CloudTestCase(test.TestCase): + """Test Case for Block Device Mapping""" + + def setUp(self): + super(BlockDeviceMappingEc2CloudTestCase, self).setUp() + + def tearDown(self): + super(BlockDeviceMappingEc2CloudTestCase, self).tearDown() + + def _assertApply(self, action, bdm_list): + for bdm, expected_result in bdm_list: + self.assertDictMatch(action(bdm), expected_result) + + def test_parse_block_device_mapping(self): + bdm_list = [ + ({'device_name': '/dev/fake0', + 'ebs': {'snapshot_id': 'snap-12345678', + 'volume_size': 1}}, + {'device_name': '/dev/fake0', + 'snapshot_id': 0x12345678, + 'volume_size': 1, + 'delete_on_termination': True}), + + ({'device_name': '/dev/fake1', + 'ebs': {'snapshot_id': 'snap-23456789', + 'delete_on_termination': False}}, + {'device_name': '/dev/fake1', + 'snapshot_id': 0x23456789, + 'delete_on_termination': False}), + + ({'device_name': '/dev/fake2', + 'ebs': {'snapshot_id': 'vol-87654321', + 'volume_size': 2}}, + {'device_name': '/dev/fake2', + 'volume_id': 0x87654321, + 'volume_size': 2, + 'delete_on_termination': True}), + + ({'device_name': '/dev/fake3', + 'ebs': {'snapshot_id': 'vol-98765432', + 'delete_on_termination': False}}, + {'device_name': '/dev/fake3', + 'volume_id': 0x98765432, + 'delete_on_termination': False}), + + ({'device_name': '/dev/fake4', + 'ebs': {'no_device': True}}, + {'device_name': '/dev/fake4', + 'no_device': True}), + + ({'device_name': '/dev/fake5', + 'virtual_name': 'ephemeral0'}, + {'device_name': '/dev/fake5', + 'virtual_name': 'ephemeral0'}), + + ({'device_name': '/dev/fake6', + 'virtual_name': 'swap'}, + {'device_name': '/dev/fake6', + 'virtual_name': 'swap'}), + ] + self._assertApply(cloud._parse_block_device_mapping, bdm_list) + + def test_format_block_device_mapping(self): + bdm_list = [ + ({'device_name': '/dev/fake0', + 'snapshot_id': 0x12345678, + 'volume_size': 1, + 'delete_on_termination': True}, + {'deviceName': '/dev/fake0', + 'ebs': {'snapshotId': 'snap-12345678', + 'volumeSize': 1, + 'deleteOnTermination': True}}), + + ({'device_name': '/dev/fake1', + 'snapshot_id': 0x23456789}, + {'deviceName': '/dev/fake1', + 'ebs': {'snapshotId': 'snap-23456789'}}), + + ({'device_name': '/dev/fake2', + 'snapshot_id': 0x23456789, + 'delete_on_termination': False}, + {'deviceName': '/dev/fake2', + 'ebs': {'snapshotId': 'snap-23456789', + 'deleteOnTermination': False}}), + + ({'device_name': '/dev/fake3', + 'volume_id': 0x12345678, + 'volume_size': 1, + 'delete_on_termination': True}, + {'deviceName': '/dev/fake3', + 'ebs': {'snapshotId': 'vol-12345678', + 'volumeSize': 1, + 'deleteOnTermination': True}}), + + ({'device_name': '/dev/fake4', + 'volume_id': 0x23456789}, + {'deviceName': '/dev/fake4', + 'ebs': {'snapshotId': 'vol-23456789'}}), + + ({'device_name': '/dev/fake5', + 'volume_id': 0x23456789, + 'delete_on_termination': False}, + {'deviceName': '/dev/fake5', + 'ebs': {'snapshotId': 'vol-23456789', + 'deleteOnTermination': False}}), + ] + self._assertApply(cloud._format_block_device_mapping, bdm_list) + + def test_format_mapping(self): + properties = { + 'mappings': [ + {'virtual': 'ami', + 'device': 'sda1'}, + {'virtual': 'root', + 'device': '/dev/sda1'}, + + {'virtual': 'swap', + 'device': 'sdb1'}, + {'virtual': 'swap', + 'device': 'sdb2'}, + {'virtual': 'swap', + 'device': 'sdb3'}, + {'virtual': 'swap', + 'device': 'sdb4'}, + + {'virtual': 'ephemeral0', + 'device': 'sdc1'}, + {'virtual': 'ephemeral1', + 'device': 'sdc2'}, + {'virtual': 'ephemeral2', + 'device': 'sdc3'}, + ], + + 'block_device_mapping': [ + # root + {'device_name': '/dev/sda1', + 'snapshot_id': 0x12345678, + 'delete_on_termination': False}, + + + # overwrite swap + {'device_name': '/dev/sdb2', + 'snapshot_id': 0x23456789, + 'delete_on_termination': False}, + {'device_name': '/dev/sdb3', + 'snapshot_id': 0x3456789A}, + {'device_name': '/dev/sdb4', + 'no_device': True}, + + # overwrite ephemeral + {'device_name': '/dev/sdc2', + 'snapshot_id': 0x3456789A, + 'delete_on_termination': False}, + {'device_name': '/dev/sdc3', + 'snapshot_id': 0x456789AB}, + {'device_name': '/dev/sdc4', + 'no_device': True}, + + # volume + {'device_name': '/dev/sdd1', + 'snapshot_id': 0x87654321, + 'delete_on_termination': False}, + {'device_name': '/dev/sdd2', + 'snapshot_id': 0x98765432}, + {'device_name': '/dev/sdd3', + 'snapshot_id': 0xA9875463}, + {'device_name': '/dev/sdd4', + 'no_device': True}]} + + expected_result = { + 'blockDeviceMapping': [ + # root + {'deviceName': '/dev/sda1', + 'ebs': {'snapshotId': 'snap-12345678', + 'deleteOnTermination': False}}, + + # swap + {'deviceName': '/dev/sdb1', + 'virtualName': 'swap'}, + {'deviceName': '/dev/sdb2', + 'ebs': {'snapshotId': 'snap-23456789', + 'deleteOnTermination': False}}, + {'deviceName': '/dev/sdb3', + 'ebs': {'snapshotId': 'snap-3456789a'}}, + + # ephemeral + {'deviceName': '/dev/sdc1', + 'virtualName': 'ephemeral0'}, + {'deviceName': '/dev/sdc2', + 'ebs': {'snapshotId': 'snap-3456789a', + 'deleteOnTermination': False}}, + {'deviceName': '/dev/sdc3', + 'ebs': {'snapshotId': 'snap-456789ab'}}, + + # volume + {'deviceName': '/dev/sdd1', + 'ebs': {'snapshotId': 'snap-87654321', + 'deleteOnTermination': False}}, + {'deviceName': '/dev/sdd2', + 'ebs': {'snapshotId': 'snap-98765432'}}, + {'deviceName': '/dev/sdd3', + 'ebs': {'snapshotId': 'snap-a9875463'}}]} + + result = {} + cloud._format_mappings(properties, result) + print result + self.assertEqual(result['blockDeviceMapping'].sort(), + expected_result['blockDeviceMapping'].sort()) -- cgit From fd577b786c3a929300ae744858b57ccfed4fb2fc Mon Sep 17 00:00:00 2001 From: Isaku Yamahata Date: Thu, 23 Jun 2011 19:51:01 +0900 Subject: compute/api: an unit test for _update_{image_}bdm an unit test for _update_image_block_device_mapping() and _update_block_device_mapping() --- nova/tests/test_compute.py | 111 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 111 insertions(+) diff --git a/nova/tests/test_compute.py b/nova/tests/test_compute.py index 439508b27..783261127 100644 --- a/nova/tests/test_compute.py +++ b/nova/tests/test_compute.py @@ -721,3 +721,114 @@ class ComputeTestCase(test.TestCase): LOG.info(_("After force-killing instances: %s"), instances) self.assertEqual(len(instances), 1) self.assertEqual(power_state.SHUTOFF, instances[0]['state']) + + @staticmethod + def _parse_db_block_device_mapping(bdm_ref): + attr_list = ('delete_on_termination', 'device_name', 'no_device', + 'virtual_name', 'volume_id', 'volume_size', 'snapshot_id') + bdm = {} + for attr in attr_list: + val = bdm_ref.get(attr, None) + if val: + bdm[attr] = val + + return bdm + + def test_update_block_device_mapping(self): + instance_id = self._create_instance() + mappings = [ + {'virtual': 'ami', 'device': 'sda1'}, + {'virtual': 'root', 'device': '/dev/sda1'}, + + {'virtual': 'swap', 'device': 'sdb1'}, + {'virtual': 'swap', 'device': 'sdb2'}, + {'virtual': 'swap', 'device': 'sdb3'}, + {'virtual': 'swap', 'device': 'sdb4'}, + + {'virtual': 'ephemeral0', 'device': 'sdc1'}, + {'virtual': 'ephemeral1', 'device': 'sdc2'}, + {'virtual': 'ephemeral2', 'device': 'sdc3'}] + block_device_mapping = [ + # root + {'device_name': '/dev/sda1', + 'snapshot_id': 0x12345678, + 'delete_on_termination': False}, + + + # overwrite swap + {'device_name': '/dev/sdb2', + 'snapshot_id': 0x23456789, + 'delete_on_termination': False}, + {'device_name': '/dev/sdb3', + 'snapshot_id': 0x3456789A}, + {'device_name': '/dev/sdb4', + 'no_device': True}, + + # overwrite ephemeral + {'device_name': '/dev/sdc2', + 'snapshot_id': 0x456789AB, + 'delete_on_termination': False}, + {'device_name': '/dev/sdc3', + 'snapshot_id': 0x56789ABC}, + {'device_name': '/dev/sdc4', + 'no_device': True}, + + # volume + {'device_name': '/dev/sdd1', + 'snapshot_id': 0x87654321, + 'delete_on_termination': False}, + {'device_name': '/dev/sdd2', + 'snapshot_id': 0x98765432}, + {'device_name': '/dev/sdd3', + 'snapshot_id': 0xA9875463}, + {'device_name': '/dev/sdd4', + 'no_device': True}] + + self.compute_api._update_image_block_device_mapping( + self.context, instance_id, mappings) + + bdms = [self._parse_db_block_device_mapping(bdm_ref) + for bdm_ref in db.block_device_mapping_get_all_by_instance( + self.context, instance_id)] + expected_result = [ + {'virtual_name': 'swap', 'device_name': '/dev/sdb1'}, + {'virtual_name': 'swap', 'device_name': '/dev/sdb2'}, + {'virtual_name': 'swap', 'device_name': '/dev/sdb3'}, + {'virtual_name': 'swap', 'device_name': '/dev/sdb4'}, + {'virtual_name': 'ephemeral0', 'device_name': '/dev/sdc1'}, + {'virtual_name': 'ephemeral1', 'device_name': '/dev/sdc2'}, + {'virtual_name': 'ephemeral2', 'device_name': '/dev/sdc3'}] + bdms.sort() + expected_result.sort() + self.assertDictListMatch(bdms, expected_result) + + self.compute_api._update_block_device_mapping( + self.context, instance_id, block_device_mapping) + bdms = [self._parse_db_block_device_mapping(bdm_ref) + for bdm_ref in db.block_device_mapping_get_all_by_instance( + self.context, instance_id)] + expected_result = [ + {'snapshot_id': 0x12345678, 'device_name': '/dev/sda1'}, + + {'virtual_name': 'swap', 'device_name': '/dev/sdb1'}, + {'snapshot_id': 0x23456789, 'device_name': '/dev/sdb2'}, + {'snapshot_id': 0x3456789A, 'device_name': '/dev/sdb3'}, + {'no_device': True, 'device_name': '/dev/sdb4'}, + + {'virtual_name': 'ephemeral0', 'device_name': '/dev/sdc1'}, + {'snapshot_id': 0x456789AB, 'device_name': '/dev/sdc2'}, + {'snapshot_id': 0x56789ABC, 'device_name': '/dev/sdc3'}, + {'no_device': True, 'device_name': '/dev/sdc4'}, + + {'snapshot_id': 0x87654321, 'device_name': '/dev/sdd1'}, + {'snapshot_id': 0x98765432, 'device_name': '/dev/sdd2'}, + {'snapshot_id': 0xA9875463, 'device_name': '/dev/sdd3'}, + {'no_device': True, 'device_name': '/dev/sdd4'}] + bdms.sort() + expected_result.sort() + self.assertDictListMatch(bdms, expected_result) + + for bdm in db.block_device_mapping_get_all_by_instance( + self.context, instance_id): + db.block_device_mapping_destroy(self.context, bdm['id']) + self.compute.terminate_instance(self.context, instance_id) -- cgit From 8a884121e6a7c5f03f51266632bb671603c9c9a0 Mon Sep 17 00:00:00 2001 From: Isaku Yamahata Date: Thu, 23 Jun 2011 19:51:01 +0900 Subject: ec2/cloud: address review. - eliminated commented out lines in create_image() - added time out to create_image() , --- nova/api/ec2/cloud.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/nova/api/ec2/cloud.py b/nova/api/ec2/cloud.py index 8bf0950b2..b6274df0d 100644 --- a/nova/api/ec2/cloud.py +++ b/nova/api/ec2/cloud.py @@ -1323,8 +1323,6 @@ class CloudController(object): def create_image(self, context, instance_id, **kwargs): # NOTE(yamahata): name/description are ignored by register_image(), # do so here - #description = kwargs.get('name') - #description = kwargs.get('description') no_reboot = kwargs.get('no_reboot', False) ec2_instance_id = instance_id @@ -1345,11 +1343,18 @@ class CloudController(object): self.compute_api.stop(context, instance_id=instance_id) # wait instance for really stopped + start_time = time.time() while state_description != 'stopped': time.sleep(1) instance = self.compute_api.get(context, instance_id) state_description = instance['state_description'] - # NOTE(yamahata): timeout and error? + # NOTE(yamahata): timeout and error. 1 hour for now for safety. + # Is it too short/long? + # Or is there any better way? + timeout = 1 * 60 * 60 * 60 + if time.time() > start_time + timeout: + raise exception.ApiError( + _('Couldn\'t stop instance with in %d sec') % timeout) src_image = self._get_image(context, instance['image_ref']) properties = src_image['properties'] -- cgit From c0a750757b2b6121bb04c6355a01cb31967c15e2 Mon Sep 17 00:00:00 2001 From: Isaku Yamahata Date: Fri, 24 Jun 2011 19:08:26 +0900 Subject: image/s3: typo --- nova/image/s3.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nova/image/s3.py b/nova/image/s3.py index 86dd2e309..4a3df98ba 100644 --- a/nova/image/s3.py +++ b/nova/image/s3.py @@ -186,7 +186,7 @@ class S3ImageService(service.BaseImageService): key = bucket.get_key(manifest_path) manifest = key.get_contents_as_string() - manifest, image = self._s3_parse_manifest(metadata, context, manifest) + manifest, image = self._s3_parse_manifest(context, metadata, manifest) image_id = image['id'] def delayed_create(): -- cgit From ce3b1ffec9bab02e2988b69e7e361d76e56ec002 Mon Sep 17 00:00:00 2001 From: Isaku Yamahata Date: Fri, 24 Jun 2011 19:08:26 +0900 Subject: ec2/cloud: typo --- nova/api/ec2/cloud.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nova/api/ec2/cloud.py b/nova/api/ec2/cloud.py index b6274df0d..a0a7db1f2 100644 --- a/nova/api/ec2/cloud.py +++ b/nova/api/ec2/cloud.py @@ -943,7 +943,7 @@ class CloudController(object): i['amiLaunchIndex'] = instance['launch_index'] i['displayName'] = instance['display_name'] i['displayDescription'] = instance['display_description'] - i['rootDeviceName'] = (instance['root_device_name'] or + i['rootDeviceName'] = (instance.get('root_device_name') or _DEFAULT_ROOT_DEVICE_NAME) self._format_instance_bdm(context, instance_id, i['rootDeviceName'], i) -- cgit From 03db1b862f38fa864316530c0a0b22ef74f25c81 Mon Sep 17 00:00:00 2001 From: "Arvind Somya asomya@cisco.com" <> Date: Fri, 24 Jun 2011 12:15:13 -0400 Subject: Fixing PEP8 compliance issues. --- nova/virt/vmwareapi/network_utils.py | 11 ++++++----- nova/virt/vmwareapi/vm_util.py | 5 +++-- nova/virt/vmwareapi/vmops.py | 8 +++++--- tools/esx/guest_tool.py | 6 ++++-- 4 files changed, 18 insertions(+), 12 deletions(-) diff --git a/nova/virt/vmwareapi/network_utils.py b/nova/virt/vmwareapi/network_utils.py index 345cd2bd0..08e3bf0b1 100644 --- a/nova/virt/vmwareapi/network_utils.py +++ b/nova/virt/vmwareapi/network_utils.py @@ -50,9 +50,9 @@ def get_network_with_the_name(session, network_name="vmnet0"): # Get network properties if network._type == 'DistributedVirtualPortgroup': props = session._call_method(vim_util, - "get_dynamic_property",network, - "DistributedVirtualPortgroup","config") - # NOTE: This only works on ESXi if the port binding is + "get_dynamic_property", network, + "DistributedVirtualPortgroup", "config") + # NOTE(asomya): This only works on ESXi if the port binding is # set to ephemeral if props.name == network_name: network_obj['type'] = 'DistributedVirtualPortgroup' @@ -60,8 +60,8 @@ def get_network_with_the_name(session, network_name="vmnet0"): network_obj['dvsw'] = props.distributedVirtualSwitch.value else: props = session._call_method(vim_util, - "get_dynamic_property",network, - "Network","summary.name") + "get_dynamic_property", network, + "Network", "summary.name") if props == network_name: network_obj['type'] = 'Network' network_obj['name'] = network_name @@ -70,6 +70,7 @@ def get_network_with_the_name(session, network_name="vmnet0"): else: return None + def get_vswitch_for_vlan_interface(session, vlan_interface): """ Gets the vswitch associated with the physical network adapter diff --git a/nova/virt/vmwareapi/vm_util.py b/nova/virt/vmwareapi/vm_util.py index c823ac710..411305081 100644 --- a/nova/virt/vmwareapi/vm_util.py +++ b/nova/virt/vmwareapi/vm_util.py @@ -89,7 +89,8 @@ def create_controller_spec(client_factory, key): return virtual_device_config -def create_network_spec(client_factory, network_name, mac_address,network_ref=None): +def create_network_spec(client_factory, network_name, mac_address, + network_ref=None): """ Builds a config spec for the addition of a new network adapter to the VM. @@ -101,7 +102,7 @@ def create_network_spec(client_factory, network_name, mac_address,network_ref=No # Get the recommended card type for the VM based on the guest OS of the VM net_device = client_factory.create('ns0:VirtualPCNet32') - # NOTE: Only works on ESXi if the portgroup binding is set to + # NOTE: Only works on ESXi if the portgroup binding is set to # ephemeral. Invalid configuration if set to static and the NIC does # not come up on boot if set to dynamic. backing = None diff --git a/nova/virt/vmwareapi/vmops.py b/nova/virt/vmwareapi/vmops.py index 9f2a36bf4..6f8b823c5 100644 --- a/nova/virt/vmwareapi/vmops.py +++ b/nova/virt/vmwareapi/vmops.py @@ -117,7 +117,7 @@ class VMWareVMOps(object): if network_ref is None: raise exception.NetworkNotFoundForBridge(bridge=net_name) return network_ref - + network_obj = _check_if_network_bridge_exists() def _get_datastore_ref(): @@ -176,8 +176,10 @@ class VMWareVMOps(object): vm_folder_mor, res_pool_mor = _get_vmfolder_and_res_pool_mors() # Get the create vm config spec - config_spec = vm_util.get_vm_create_spec(client_factory, instance, - data_store_name, net_name, os_type, network_obj) + config_spec = vm_util.get_vm_create_spec( + client_factory, instance, + data_store_name, net_name, os_type, + network_obj) def _execute_create_vm(): """Create VM on ESX host.""" diff --git a/tools/esx/guest_tool.py b/tools/esx/guest_tool.py index e126721d5..3463be5dd 100644 --- a/tools/esx/guest_tool.py +++ b/tools/esx/guest_tool.py @@ -317,6 +317,7 @@ def _set_rhel_networking(network_details=[]): dns_file.close() _execute(['/sbin/service', 'network', 'restart']) + def _set_ubuntu_networking(network_details=[]): """ Set IPv4 network settings for Ubuntu """ all_dns_servers = [] @@ -355,6 +356,7 @@ def _set_ubuntu_networking(network_details=[]): print "\nRestarting networking....\n" _execute(['/etc/init.d/networking', 'restart']) + def _linux_set_networking(): """Set IP address for the Linux VM.""" vmware_tools_bin = None @@ -371,9 +373,9 @@ def _linux_set_networking(): network_details = _parse_network_details(_execute(cmd, check_exit_code=False)) # TODO(sateesh): For other distros like suse, debian, BSD, etc. - if(platform.dist()[0] == 'Ubuntu') : + if(platform.dist()[0] == 'Ubuntu'): _set_ubuntu_networking(network_details) - elif (platform.dist()[0] == 'redhat') : + elif (platform.dist()[0] == 'redhat'): _set_rhel_networking(network_details) else: logging.warn(_("Distro '%s' not supported") % platform.dist()[0]) -- cgit From 8ec448d2bcf4afce7d0486d9cf6b07e0e30bd6be Mon Sep 17 00:00:00 2001 From: "Arvind Somya asomya@cisco.com" <> Date: Fri, 24 Jun 2011 12:22:14 -0400 Subject: Fixed the default arguments to None instead of an empty list. --- tools/esx/guest_tool.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tools/esx/guest_tool.py b/tools/esx/guest_tool.py index 3463be5dd..60ecfcd20 100644 --- a/tools/esx/guest_tool.py +++ b/tools/esx/guest_tool.py @@ -278,7 +278,8 @@ def _filter_duplicates(all_entries): return final_list -def _set_rhel_networking(network_details=[]): +def _set_rhel_networking(network_details=None): + network_details = network_details or [] all_dns_servers = [] for network_detail in network_details: mac_address, ip_address, subnet_mask, gateway, broadcast,\ @@ -318,7 +319,8 @@ def _set_rhel_networking(network_details=[]): _execute(['/sbin/service', 'network', 'restart']) -def _set_ubuntu_networking(network_details=[]): +def _set_ubuntu_networking(network_details=None): + network_details = network_details or [] """ Set IPv4 network settings for Ubuntu """ all_dns_servers = [] for network_detail in network_details: -- cgit From 02c0bf3b242395e63baf582b1f9c279eef4282d6 Mon Sep 17 00:00:00 2001 From: Isaku Yamahata Date: Mon, 27 Jun 2011 20:21:40 +0900 Subject: sqlalchmey/migration: resolved version conflict --- .../versions/027_add_root_device_name.py | 47 ---------------------- .../versions/028_add_root_device_name.py | 47 ++++++++++++++++++++++ 2 files changed, 47 insertions(+), 47 deletions(-) delete mode 100644 nova/db/sqlalchemy/migrate_repo/versions/027_add_root_device_name.py create mode 100644 nova/db/sqlalchemy/migrate_repo/versions/028_add_root_device_name.py diff --git a/nova/db/sqlalchemy/migrate_repo/versions/027_add_root_device_name.py b/nova/db/sqlalchemy/migrate_repo/versions/027_add_root_device_name.py deleted file mode 100644 index 6b98b9890..000000000 --- a/nova/db/sqlalchemy/migrate_repo/versions/027_add_root_device_name.py +++ /dev/null @@ -1,47 +0,0 @@ -# Copyright 2011 OpenStack LLC. -# Copyright 2011 Isaku Yamahata -# -# 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 Column, Integer, MetaData, Table, String - -meta = MetaData() - - -# Just for the ForeignKey and column creation to succeed, these are not the -# actual definitions of instances or services. -instances = Table('instances', meta, - Column('id', Integer(), primary_key=True, nullable=False), - ) - -# -# New Column -# -root_device_name = Column( - 'root_device_name', - String(length=255, convert_unicode=False, assert_unicode=None, - unicode_error=None, _warn_on_bytestring=False), - nullable=True) - - -def upgrade(migrate_engine): - # Upgrade operations go here. Don't create your own engine; - # bind migrate_engine to your metadata - meta.bind = migrate_engine - instances.create_column(root_device_name) - - -def downgrade(migrate_engine): - # Operations to reverse the above upgrade go here. - meta.bind = migrate_engine - instances.drop_column('root_device_name') diff --git a/nova/db/sqlalchemy/migrate_repo/versions/028_add_root_device_name.py b/nova/db/sqlalchemy/migrate_repo/versions/028_add_root_device_name.py new file mode 100644 index 000000000..6b98b9890 --- /dev/null +++ b/nova/db/sqlalchemy/migrate_repo/versions/028_add_root_device_name.py @@ -0,0 +1,47 @@ +# Copyright 2011 OpenStack LLC. +# Copyright 2011 Isaku Yamahata +# +# 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 Column, Integer, MetaData, Table, String + +meta = MetaData() + + +# Just for the ForeignKey and column creation to succeed, these are not the +# actual definitions of instances or services. +instances = Table('instances', meta, + Column('id', Integer(), primary_key=True, nullable=False), + ) + +# +# New Column +# +root_device_name = Column( + 'root_device_name', + String(length=255, convert_unicode=False, assert_unicode=None, + unicode_error=None, _warn_on_bytestring=False), + nullable=True) + + +def upgrade(migrate_engine): + # Upgrade operations go here. Don't create your own engine; + # bind migrate_engine to your metadata + meta.bind = migrate_engine + instances.create_column(root_device_name) + + +def downgrade(migrate_engine): + # Operations to reverse the above upgrade go here. + meta.bind = migrate_engine + instances.drop_column('root_device_name') -- cgit From 43713a2e45862219c538ede60363053d36bb0f1b Mon Sep 17 00:00:00 2001 From: Arvind Somy Date: Mon, 27 Jun 2011 14:41:07 -0400 Subject: - Modified NOTE in vm_util.py - Changed gettext line to nova default in guest_tool.py --- nova/virt/vmwareapi/vm_util.py | 2 +- tools/esx/guest_tool.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/nova/virt/vmwareapi/vm_util.py b/nova/virt/vmwareapi/vm_util.py index 411305081..b93c1f81b 100644 --- a/nova/virt/vmwareapi/vm_util.py +++ b/nova/virt/vmwareapi/vm_util.py @@ -102,7 +102,7 @@ def create_network_spec(client_factory, network_name, mac_address, # Get the recommended card type for the VM based on the guest OS of the VM net_device = client_factory.create('ns0:VirtualPCNet32') - # NOTE: Only works on ESXi if the portgroup binding is set to + # NOTE(asomya): Only works on ESXi if the portgroup binding is set to # ephemeral. Invalid configuration if set to static and the NIC does # not come up on boot if set to dynamic. backing = None diff --git a/tools/esx/guest_tool.py b/tools/esx/guest_tool.py index 60ecfcd20..97b5302ba 100644 --- a/tools/esx/guest_tool.py +++ b/tools/esx/guest_tool.py @@ -31,7 +31,7 @@ import subprocess import sys import time -_ = gettext.gettext +gettext.install('nova', unicode=1) PLATFORM_WIN = 'win32' PLATFORM_LINUX = 'linux2' -- cgit From ff195162d97f4a1ffaa6127f92cca102705b023b Mon Sep 17 00:00:00 2001 From: Lvov Maxim Date: Tue, 28 Jun 2011 16:46:50 +0400 Subject: remove network to project bind --- bin/nova-manage | 5 ++--- nova/network/manager.py | 2 -- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/bin/nova-manage b/bin/nova-manage index 67cf0bd51..a4b221b8c 100755 --- a/bin/nova-manage +++ b/bin/nova-manage @@ -619,7 +619,7 @@ class NetworkCommands(object): @args('--project', dest="project_id", metavar='', help='Project for network') def create(self, fixed_range=None, num_networks=None, network_size=None, vlan_start=None, vpn_start=None, fixed_range_v6=None, - gateway_v6=None, label='public', project_id=None): + gateway_v6=None, label='public'): """Creates fixed ips for host by range""" if not fixed_range: @@ -649,8 +649,7 @@ class NetworkCommands(object): vpn_start=int(vpn_start), cidr_v6=fixed_range_v6, gateway_v6=gateway_v6, - label=label, - project_id=project_id) + label=label) except ValueError, e: print e raise e diff --git a/nova/network/manager.py b/nova/network/manager.py index a7f5aac60..b5352ca0f 100644 --- a/nova/network/manager.py +++ b/nova/network/manager.py @@ -571,8 +571,6 @@ class VlanManager(NetworkManager): net['dhcp_start'] = str(project_net[3]) net['vlan'] = vlan net['bridge'] = 'br%s' % vlan - if 'project_id' in kwargs: - net['project_id'] = kwargs['project_id'] if(FLAGS.use_ipv6): cidr_v6 = '%s/%s' % (fixed_net_v6[start_v6], significant_bits_v6) -- cgit From 639717b1eadb769f1d77a4ddcdb6618da4defbea Mon Sep 17 00:00:00 2001 From: William Wolf Date: Thu, 30 Jun 2011 10:06:06 -0400 Subject: added FlavorRef exception handling on create instance --- nova/api/openstack/create_instance_helper.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/nova/api/openstack/create_instance_helper.py b/nova/api/openstack/create_instance_helper.py index 436e524c1..94ae3bb85 100644 --- a/nova/api/openstack/create_instance_helper.py +++ b/nova/api/openstack/create_instance_helper.py @@ -143,6 +143,10 @@ class CreateInstanceHelper(object): except exception.ImageNotFound as error: msg = _("Can not find requested image") raise faults.Fault(exc.HTTPBadRequest(explanation=msg)) + except exception.FlavorNotFound as error: + msg = _("Invalid flavorRef provided.") + raise faults.Fault(exc.HTTPBadRequest(explanation=msg)) + # Let the caller deal with unhandled exceptions. -- cgit -- cgit From 3a65ea2f29ca169779cbd09acf4f7ac50314c969 Mon Sep 17 00:00:00 2001 From: Kevin Bringard Date: Fri, 1 Jul 2011 11:14:30 -0600 Subject: Changed fixed_ip.network to be fixed_ips.network, which is the correct DB field --- nova/db/sqlalchemy/api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nova/db/sqlalchemy/api.py b/nova/db/sqlalchemy/api.py index ffd009513..7278ad1f5 100644 --- a/nova/db/sqlalchemy/api.py +++ b/nova/db/sqlalchemy/api.py @@ -1237,7 +1237,7 @@ def instance_get_project_vpn(context, project_id): options(joinedload_all('fixed_ips.floating_ips')).\ options(joinedload('virtual_interfaces')).\ options(joinedload('security_groups')).\ - options(joinedload_all('fixed_ip.network')).\ + options(joinedload_all('fixed_ips.network')).\ options(joinedload('metadata')).\ options(joinedload('instance_type')).\ filter_by(project_id=project_id).\ -- cgit From 42ef4e0adb7b0ec939f40d5356d4a3d2d03dec9f Mon Sep 17 00:00:00 2001 From: Kevin Bringard Date: Fri, 1 Jul 2011 12:32:26 -0600 Subject: Found some additional fixed_ip. entries in the Intance model contest that needed to be updated --- nova/db/sqlalchemy/api.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nova/db/sqlalchemy/api.py b/nova/db/sqlalchemy/api.py index 7278ad1f5..fd7ace4c7 100644 --- a/nova/db/sqlalchemy/api.py +++ b/nova/db/sqlalchemy/api.py @@ -1141,9 +1141,9 @@ def instance_get_active_by_window(context, begin, end=None): """Return instances that were continuously active over the given window""" session = get_session() query = session.query(models.Instance).\ - options(joinedload_all('fixed_ip.floating_ips')).\ + options(joinedload_all('fixed_ips.floating_ips')).\ options(joinedload('security_groups')).\ - options(joinedload_all('fixed_ip.network')).\ + options(joinedload_all('fixed_ips.network')).\ options(joinedload('instance_type')).\ filter(models.Instance.launched_at < begin) if end: -- cgit From b8b96769bfc49e75d2eee3ae561e4e9ee7615473 Mon Sep 17 00:00:00 2001 From: Yoshiaki Tamura Date: Mon, 4 Jul 2011 13:53:17 +0900 Subject: Fix boot from volume failure for network block devices. This patch looks up the device_path and swithes between 'block' and 'network' when creating libvirt.xml. --- nova/virt/libvirt.xml.template | 6 +++++- nova/virt/libvirt/connection.py | 11 +++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/nova/virt/libvirt.xml.template b/nova/virt/libvirt.xml.template index e1a683da8..36d8c5aaa 100644 --- a/nova/virt/libvirt.xml.template +++ b/nova/virt/libvirt.xml.template @@ -82,9 +82,13 @@ #end if #for $vol in $volumes - + + #if $vol.type == 'network' + + #else + #end if #end for diff --git a/nova/virt/libvirt/connection.py b/nova/virt/libvirt/connection.py index 0c6eaab84..3168bb9df 100644 --- a/nova/virt/libvirt/connection.py +++ b/nova/virt/libvirt/connection.py @@ -971,6 +971,7 @@ class LibvirtConnection(driver.ComputeDriver): return True return False + @exception.wrap_exception def _prepare_xml_info(self, instance, rescue=False, network_info=None, block_device_mapping=None): block_device_mapping = block_device_mapping or [] @@ -993,6 +994,16 @@ class LibvirtConnection(driver.ComputeDriver): for vol in block_device_mapping: vol['mount_device'] = _strip_dev(vol['mount_device']) + if vol['device_path'].startswith('/dev/'): + vol['type'] = 'block' + elif ':' in vol['device_path']: + (protocol, name) = vol['device_path'].split(':') + vol['type'] = 'network' + vol['protocol'] = protocol + vol['device_path'] = name + else: + raise exception.InvalidDevicePath(path=vol['device_path']) + ebs_root = self._volume_in_mapping(self.root_mount_device, block_device_mapping) if self._volume_in_mapping(self.local_mount_device, -- cgit From 0834f3d64b2cc37407c24a9b717e218d758adf79 Mon Sep 17 00:00:00 2001 From: Brian Waldon Date: Mon, 4 Jul 2011 15:14:36 -0400 Subject: properly displays addresses in each network, not just public/private; adding addresses attribute to server entities --- nova/api/openstack/__init__.py | 11 +-- nova/api/openstack/ips.py | 79 ++++++++++++++---- nova/api/openstack/servers.py | 31 ++++--- nova/api/openstack/views/addresses.py | 38 +++++++-- nova/api/openstack/views/servers.py | 13 ++- nova/db/sqlalchemy/api.py | 23 +++-- nova/tests/api/openstack/test_servers.py | 139 ++++++++++++++++++++++++++++--- 7 files changed, 272 insertions(+), 62 deletions(-) diff --git a/nova/api/openstack/__init__.py b/nova/api/openstack/__init__.py index f24017df0..3e4d87ee0 100644 --- a/nova/api/openstack/__init__.py +++ b/nova/api/openstack/__init__.py @@ -144,9 +144,6 @@ class APIRouterV10(APIRouter): def _setup_routes(self, mapper): super(APIRouterV10, self)._setup_routes(mapper, '1.0') - mapper.resource("image", "images", - controller=images.create_resource('1.0'), - collection={'detail': 'GET'}) mapper.resource("shared_ip_group", "shared_ip_groups", collection={'detail': 'GET'}, @@ -157,8 +154,8 @@ class APIRouterV10(APIRouter): parent_resource=dict(member_name='server', collection_name='servers')) - mapper.resource("ip", "ips", controller=ips.create_resource(), - collection=dict(public='GET', private='GET'), + mapper.resource("ip", "ips", controller=ips.create_resource('1.0'), + member=dict(public='GET', private='GET'), parent_resource=dict(member_name='server', collection_name='servers')) @@ -177,3 +174,7 @@ class APIRouterV11(APIRouter): controller=server_metadata.create_resource(), parent_resource=dict(member_name='server', collection_name='servers')) + + mapper.resource("ip", "ips", controller=ips.create_resource('1.1'), + parent_resource=dict(member_name='server', + collection_name='servers')) diff --git a/nova/api/openstack/ips.py b/nova/api/openstack/ips.py index 71646b6d3..b83e2f9b3 100644 --- a/nova/api/openstack/ips.py +++ b/nova/api/openstack/ips.py @@ -23,6 +23,7 @@ import nova from nova.api.openstack import faults import nova.api.openstack.views.addresses from nova.api.openstack import wsgi +from nova import db class Controller(object): @@ -30,7 +31,6 @@ class Controller(object): def __init__(self): self.compute_api = nova.compute.API() - self.builder = nova.api.openstack.views.addresses.ViewBuilderV10() def _get_instance(self, req, server_id): try: @@ -40,29 +40,78 @@ class Controller(object): return faults.Fault(exc.HTTPNotFound()) return instance + def create(self, req, server_id, body): + return faults.Fault(exc.HTTPNotImplemented()) + + def delete(self, req, server_id, id): + return faults.Fault(exc.HTTPNotImplemented()) + + +class ControllerV10(Controller): + def index(self, req, server_id): instance = self._get_instance(req, server_id) - return {'addresses': self.builder.build(instance)} + builder = nova.api.openstack.views.addresses.ViewBuilderV10() + return {'addresses': builder.build(instance)} - def public(self, req, server_id): + def show(self, req, server_id, id): instance = self._get_instance(req, server_id) - return {'public': self.builder.build_public_parts(instance)} + builder = self._get_view_builder(req) + if id == 'private': + view = builder.build_private_parts(instance) + elif id == 'public': + view = builder.build_public_parts(instance) + else: + msg = _("Only private and public networks available") + return faults.Fault(exc.HTTPNotFound(explanation=msg)) - def private(self, req, server_id): - instance = self._get_instance(req, server_id) - return {'private': self.builder.build_private_parts(instance)} + return {id: view} + + def _get_view_builder(self, req): + return nova.api.openstack.views.addresses.ViewBuilderV10() + + +class ControllerV11(Controller): + + def index(self, req, server_id): + context = req.environ['nova.context'] + interfaces = self._get_virtual_interfaces(context, server_id) + networks = self._get_view_builder(req).build(interfaces) + return {'addresses': networks} def show(self, req, server_id, id): - return faults.Fault(exc.HTTPNotImplemented()) + context = req.environ['nova.context'] + interfaces = self._get_virtual_interfaces(context, server_id) + network = self._get_view_builder(req).build_network(interfaces, id) - def create(self, req, server_id, body): - return faults.Fault(exc.HTTPNotImplemented()) + if network is None: + msg = _("Instance is not a member of specified network") + return faults.Fault(exc.HTTPNotFound(explanation=msg)) - def delete(self, req, server_id, id): - return faults.Fault(exc.HTTPNotImplemented()) + return network + + def _get_virtual_interfaces(self, context, server_id): + try: + return db.api.virtual_interface_get_by_instance(context, server_id) + except exception.InstanceNotFound: + msg = _("Instance does not exist") + raise exc.HTTPNotFound(explanation=msg) + + def _get_view_builder(self, req): + return nova.api.openstack.views.addresses.ViewBuilderV11() + + +def create_resource(version): + controller = { + '1.0': ControllerV10, + '1.1': ControllerV11, + }[version]() + xmlns = { + '1.0': wsgi.XMLNS_V10, + '1.1': wsgi.XMLNS_V11, + }[version] -def create_resource(): metadata = { 'list_collections': { 'public': {'item_name': 'ip', 'item_key': 'addr'}, @@ -72,7 +121,7 @@ def create_resource(): serializers = { 'application/xml': wsgi.XMLDictSerializer(metadata=metadata, - xmlns=wsgi.XMLNS_V10), + xmlns=xmlns), } - return wsgi.Resource(Controller(), serializers=serializers) + return wsgi.Resource(controller, serializers=serializers) diff --git a/nova/api/openstack/servers.py b/nova/api/openstack/servers.py index fc1ab8d46..3c29c2606 100644 --- a/nova/api/openstack/servers.py +++ b/nova/api/openstack/servers.py @@ -19,6 +19,7 @@ import traceback from webob import exc from nova import compute +from nova import db from nova import exception from nova import flags from nova import log as logging @@ -62,7 +63,7 @@ class Controller(object): return exc.HTTPBadRequest(explanation=str(err)) return servers - def _get_view_builder(self, req): + def _build_view(self, req, instance, is_detail=False): raise NotImplementedError() def _limit_items(self, items, req): @@ -88,8 +89,7 @@ class Controller(object): fixed_ip=fixed_ip, recurse_zones=recurse_zones) limited_list = self._limit_items(instance_list, req) - builder = self._get_view_builder(req) - servers = [builder.build(inst, is_detail)['server'] + servers = [self._build_view(req, inst, is_detail)['server'] for inst in limited_list] return dict(servers=servers) @@ -99,8 +99,7 @@ class Controller(object): try: instance = self.compute_api.routing_get( req.environ['nova.context'], id) - builder = self._get_view_builder(req) - return builder.build(instance, is_detail=True) + return self._build_view(req, instance, is_detail=True) except exception.NotFound: return faults.Fault(exc.HTTPNotFound()) @@ -130,8 +129,7 @@ class Controller(object): for key in ['instance_type', 'image_ref']: inst[key] = extra_values[key] - builder = self._get_view_builder(req) - server = builder.build(inst, is_detail=True) + server = self._build_view(req, inst, is_detail=True) server['server']['adminPass'] = extra_values['password'] return server @@ -418,10 +416,10 @@ class ControllerV10(Controller): def _flavor_id_from_req_data(self, data): return data['server']['flavorId'] - def _get_view_builder(self, req): - addresses_builder = nova.api.openstack.views.addresses.ViewBuilderV10() - return nova.api.openstack.views.servers.ViewBuilderV10( - addresses_builder) + def _build_view(self, req, instance, is_detail=False): + addresses = nova.api.openstack.views.addresses.ViewBuilderV10() + builder = nova.api.openstack.views.servers.ViewBuilderV10(addresses) + return builder.build(instance, is_detail=is_detail) def _limit_items(self, items, req): return common.limited(items, req) @@ -481,16 +479,23 @@ class ControllerV11(Controller): href = data['server']['flavorRef'] return common.get_id_from_href(href) - def _get_view_builder(self, req): + def _build_view(self, req, instance, is_detail=False): base_url = req.application_url flavor_builder = nova.api.openstack.views.flavors.ViewBuilderV11( base_url) image_builder = nova.api.openstack.views.images.ViewBuilderV11( base_url) addresses_builder = nova.api.openstack.views.addresses.ViewBuilderV11() - return nova.api.openstack.views.servers.ViewBuilderV11( + builder = nova.api.openstack.views.servers.ViewBuilderV11( addresses_builder, flavor_builder, image_builder, base_url) + context = req.environ['nova.context'] + interfaces = db.api.virtual_interface_get_by_instance(context, + instance['id']) + instance['virtual_interfaces'] = interfaces + + return builder.build(instance, is_detail=is_detail) + def _action_change_password(self, input_dict, req, id): context = req.environ['nova.context'] if (not 'changePassword' in input_dict diff --git a/nova/api/openstack/views/addresses.py b/nova/api/openstack/views/addresses.py index b59eb4751..c7464a7c5 100644 --- a/nova/api/openstack/views/addresses.py +++ b/nova/api/openstack/views/addresses.py @@ -20,13 +20,14 @@ from nova.api.openstack import common class ViewBuilder(object): - ''' Models a server addresses response as a python dictionary.''' + """Models a server addresses response as a python dictionary.""" def build(self, inst): raise NotImplementedError() class ViewBuilderV10(ViewBuilder): + def build(self, inst): private_ips = self.build_private_parts(inst) public_ips = self.build_public_parts(inst) @@ -40,11 +41,30 @@ class ViewBuilderV10(ViewBuilder): class ViewBuilderV11(ViewBuilder): - def build(self, inst): - # TODO(tr3buchet) - this shouldn't be hard coded to 4... - private_ips = utils.get_from_path(inst, 'fixed_ips/address') - private_ips = [dict(version=4, addr=a) for a in private_ips] - public_ips = utils.get_from_path(inst, - 'fixed_ips/floating_ips/address') - public_ips = [dict(version=4, addr=a) for a in public_ips] - return dict(public=public_ips, private=private_ips) + + def build(self, interfaces): + networks = {} + for interface in interfaces: + network_label = interface['network']['label'] + if network_label not in networks: + networks[network_label] = [] + + for fixed_ip in interface['fixed_ips']: + ip = {'addr': fixed_ip['address'], 'version': 4} + networks[network_label].append(ip) + return networks + + def build_network(self, interfaces, network_label): + for interface in interfaces: + if interface['network']['label'] == network_label: + ips = self._extract_fixed_ips(interface) + return {network_label: ips} + return None + + def _extract_fixed_ips(self, interface): + fixed_ips = [] + for fixed_ip in interface['fixed_ips']: + ip = {'addr': fixed_ip['address'], 'version': 4} + fixed_ips.append(ip) + return fixed_ips + diff --git a/nova/api/openstack/views/servers.py b/nova/api/openstack/views/servers.py index cbfa5aae7..691cc48ca 100644 --- a/nova/api/openstack/views/servers.py +++ b/nova/api/openstack/views/servers.py @@ -77,7 +77,6 @@ class ViewBuilder(object): inst_dict = { 'id': inst['id'], 'name': inst['display_name'], - 'addresses': self.addresses_builder.build(inst), 'status': power_mapping[inst.get('state')]} ctxt = nova.context.get_admin_context() @@ -98,10 +97,15 @@ class ViewBuilder(object): self._build_image(inst_dict, inst) self._build_flavor(inst_dict, inst) + self._build_addresses(inst_dict, inst) inst_dict['uuid'] = inst['uuid'] return dict(server=inst_dict) + def _build_addresses(self, response, inst): + """Return the addresses sub-resource of a server.""" + raise NotImplementedError() + def _build_image(self, response, inst): """Return the image sub-resource of a server.""" raise NotImplementedError() @@ -128,6 +132,9 @@ class ViewBuilderV10(ViewBuilder): if 'instance_type' in dict(inst): response['flavorId'] = inst['instance_type']['flavorid'] + def _build_addresses(self, response, inst): + response['addresses'] = self.addresses_builder.build(inst) + class ViewBuilderV11(ViewBuilder): """Model an Openstack API V1.0 server response.""" @@ -151,6 +158,10 @@ class ViewBuilderV11(ViewBuilder): flavor_ref = self.flavor_builder.generate_href(flavor_id) response["flavorRef"] = flavor_ref + def _build_addresses(self, response, inst): + interfaces = inst['virtual_interfaces'] + response['addresses'] = self.addresses_builder.build(interfaces) + def _build_extra(self, response, inst): self._build_links(response, inst) diff --git a/nova/db/sqlalchemy/api.py b/nova/db/sqlalchemy/api.py index ffd009513..854b7dea7 100644 --- a/nova/db/sqlalchemy/api.py +++ b/nova/db/sqlalchemy/api.py @@ -118,6 +118,20 @@ def require_context(f): return wrapper +def require_instance_exists(f): + """Decorator to require the specified instance to exist. + + Requres the wrapped function to use context and instance_id as + their first two arguments. + """ + + def wrapper(context, instance_id, *args, **kwargs): + db.api.instance_get(context, instance_id) + return f(context, instance_id, *args, **kwargs) + return wrapper + + + ################### @require_admin_context @@ -921,6 +935,7 @@ def virtual_interface_get_by_fixed_ip(context, fixed_ip_id): @require_context +@require_instance_exists def virtual_interface_get_by_instance(context, instance_id): """Gets all virtual interfaces for instance. @@ -3071,14 +3086,6 @@ def zone_get_all(context): #################### -def require_instance_exists(func): - def new_func(context, instance_id, *args, **kwargs): - db.api.instance_get(context, instance_id) - return func(context, instance_id, *args, **kwargs) - new_func.__name__ = func.__name__ - return new_func - - @require_context @require_instance_exists def instance_metadata_get(context, instance_id): diff --git a/nova/tests/api/openstack/test_servers.py b/nova/tests/api/openstack/test_servers.py index c3ca1431b..6ee87830a 100644 --- a/nova/tests/api/openstack/test_servers.py +++ b/nova/tests/api/openstack/test_servers.py @@ -65,6 +65,12 @@ def return_server_by_uuid(context, uuid): return stub_instance(id, uuid=uuid) +def return_virtual_interface_by_instance(interfaces): + def _return_virtual_interface_by_instance(context, instance_id): + return interfaces + return _return_virtual_interface_by_instance + + def return_server_with_addresses(private, public): def _return_server(context, id): return stub_instance(id, private_address=private, @@ -417,23 +423,134 @@ class ServersTest(test.TestCase): self.assertEquals(ip.getAttribute('addr'), private) def test_get_server_by_id_with_addresses_v1_1(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) + interfaces = [ + { + 'network': {'label': 'network_1'}, + 'fixed_ips': [ + {'address': '192.168.0.3'}, + {'address': '192.168.0.4'}, + ], + }, + { + 'network': {'label': 'network_2'}, + 'fixed_ips': [ + {'address': '172.19.0.1'}, + {'address': '172.19.0.2'}, + ], + }, + ] + + _return_vifs = return_virtual_interface_by_instance(interfaces) + self.stubs.Set(nova.db.api, + 'virtual_interface_get_by_instance', + _return_vifs) + + req = webob.Request.blank('/v1.1/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'] - # RM(4047): Figure otu what is up with the 1.1 api and multi-nic - #self.assertEqual(len(addresses["public"]), len(public)) - #self.assertEqual(addresses["public"][0], - # {"version": 4, "addr": public[0]}) - #self.assertEqual(len(addresses["private"]), 1) - #self.assertEqual(addresses["private"][0], - # {"version": 4, "addr": private}) + expected = { + 'network_1': [ + {'addr': '192.168.0.3', 'version': 4}, + {'addr': '192.168.0.4', 'version': 4}, + ], + 'network_2': [ + {'addr': '172.19.0.1', 'version': 4}, + {'addr': '172.19.0.2', 'version': 4}, + ], + } + + self.assertEqual(addresses, expected) + + def test_get_server_addresses_v1_1(self): + interfaces = [ + { + 'network': {'label': 'network_1'}, + 'fixed_ips': [ + {'address': '192.168.0.3'}, + {'address': '192.168.0.4'}, + ], + }, + { + 'network': {'label': 'network_2'}, + 'fixed_ips': [ + {'address': '172.19.0.1'}, + {'address': '172.19.0.2'}, + ], + }, + ] + + _return_vifs = return_virtual_interface_by_instance(interfaces) + self.stubs.Set(nova.db.api, + 'virtual_interface_get_by_instance', + _return_vifs) + + req = webob.Request.blank('/v1.1/servers/1/ips') + res = req.get_response(fakes.wsgi_app()) + res_dict = json.loads(res.body) + + expected = { + 'addresses': { + 'network_1': [ + {'version': 4, 'addr': '192.168.0.3'}, + {'version': 4, 'addr': '192.168.0.4'}, + ], + 'network_2': [ + {'version': 4, 'addr': '172.19.0.1'}, + {'version': 4, 'addr': '172.19.0.2'}, + ], + }, + } + + self.assertEqual(res_dict, expected) + + def test_get_server_addresses_single_network_v1_1(self): + interfaces = [ + { + 'network': {'label': 'network_1'}, + 'fixed_ips': [ + {'address': '192.168.0.3'}, + {'address': '192.168.0.4'}, + ], + }, + { + 'network': {'label': 'network_2'}, + 'fixed_ips': [ + {'address': '172.19.0.1'}, + {'address': '172.19.0.2'}, + ], + }, + ] + _return_vifs = return_virtual_interface_by_instance(interfaces) + self.stubs.Set(nova.db.api, + 'virtual_interface_get_by_instance', + _return_vifs) + + req = webob.Request.blank('/v1.1/servers/1/ips/network_2') + res = req.get_response(fakes.wsgi_app()) + self.assertEqual(res.status_int, 200) + res_dict = json.loads(res.body) + expected = { + 'network_2': [ + {'version': 4, 'addr': '172.19.0.1'}, + {'version': 4, 'addr': '172.19.0.2'}, + ], + } + self.assertEqual(res_dict, expected) + + def test_get_server_addresses_nonexistant_network_v1_1(self): + _return_vifs = return_virtual_interface_by_instance([]) + self.stubs.Set(nova.db.api, + 'virtual_interface_get_by_instance', + _return_vifs) + + req = webob.Request.blank('/v1.1/servers/1/ips/network_0') + res = req.get_response(fakes.wsgi_app()) + self.assertEqual(res.status_int, 404) def test_get_server_list(self): req = webob.Request.blank('/v1.0/servers') -- cgit From 01b9d211e606ee0be221b27edae8aab1d35096ff Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Tue, 5 Jul 2011 11:51:46 -0700 Subject: First round of changes for ha-flatdhcp. * added 'host' column to fixed_ips to allow associating with a host * added 'multi_host' column to network for multi_host possibility * moved extra db access from linux_net to manager * added host parameter to network calls --- bin/nova-manage | 2 +- nova/db/api.py | 19 ++++-- nova/db/sqlalchemy/api.py | 29 ++++++--- .../migrate_repo/versions/032_ha_network.py | 37 +++++++++++ nova/db/sqlalchemy/models.py | 1 + nova/exception.py | 5 ++ nova/network/api.py | 13 +++- nova/network/linux_net.py | 15 ++--- nova/network/manager.py | 73 +++++++++++++++------- 9 files changed, 144 insertions(+), 50 deletions(-) create mode 100644 nova/db/sqlalchemy/migrate_repo/versions/032_ha_network.py diff --git a/bin/nova-manage b/bin/nova-manage index 7dfe91698..71c99138f 100755 --- a/bin/nova-manage +++ b/bin/nova-manage @@ -494,7 +494,7 @@ class FixedIpCommands(object): if host is None: fixed_ips = db.fixed_ip_get_all(ctxt) else: - fixed_ips = db.fixed_ip_get_all_by_host(ctxt, host) + fixed_ips = db.fixed_ip_get_all_by_instance_host(ctxt, host) except exception.NotFound as ex: print "error: %s" % ex sys.exit(2) diff --git a/nova/db/api.py b/nova/db/api.py index b7c5700e5..febe33374 100644 --- a/nova/db/api.py +++ b/nova/db/api.py @@ -332,13 +332,15 @@ def fixed_ip_associate(context, address, instance_id): return IMPL.fixed_ip_associate(context, address, instance_id) -def fixed_ip_associate_pool(context, network_id, instance_id): - """Find free ip in network and associate it to instance. +def fixed_ip_associate_pool(context, network_id, instance_id=None, host=None): + """Find free ip in network and associate it to instance or host. + If reserved is true, it will pull the ip from the reserved pool. Raises if one is not available. """ - return IMPL.fixed_ip_associate_pool(context, network_id, instance_id) + return IMPL.fixed_ip_associate_pool(context, network_id, + instance_id, host, reserved) def fixed_ip_create(context, values): @@ -361,9 +363,9 @@ def fixed_ip_get_all(context): return IMPL.fixed_ip_get_all(context) -def fixed_ip_get_all_by_host(context, host): - """Get all defined fixed ips used by a host.""" - return IMPL.fixed_ip_get_all_by_host(context, host) +def fixed_ip_get_all_by_instance_host(context, host): + """Get all allocated fixed ips filtered by instance host.""" + return IMPL.fixed_ip_get_all_instance_by_host(context, host) def fixed_ip_get_by_address(context, address): @@ -376,6 +378,11 @@ def fixed_ip_get_by_instance(context, instance_id): return IMPL.fixed_ip_get_by_instance(context, instance_id) +def fixed_ip_get_by_network_host(context, network_id, host): + """Get fixed ip for a host in a network.""" + return IMPL.fixed_ip_get_by_network_host(context, network_id, host) + + def fixed_ip_get_by_virtual_interface(context, vif_id): """Get fixed ips by virtual interface or raise if none exist.""" return IMPL.fixed_ip_get_by_virtual_interface(context, vif_id) diff --git a/nova/db/sqlalchemy/api.py b/nova/db/sqlalchemy/api.py index ffd009513..6db142276 100644 --- a/nova/db/sqlalchemy/api.py +++ b/nova/db/sqlalchemy/api.py @@ -18,7 +18,6 @@ """ Implementation of SQLAlchemy backend. """ -import traceback import warnings from nova import db @@ -33,7 +32,6 @@ from sqlalchemy import or_ from sqlalchemy.exc import IntegrityError from sqlalchemy.orm import joinedload from sqlalchemy.orm import joinedload_all -from sqlalchemy.sql import exists from sqlalchemy.sql import func from sqlalchemy.sql.expression import literal_column @@ -657,7 +655,7 @@ def fixed_ip_associate(context, address, instance_id): @require_admin_context -def fixed_ip_associate_pool(context, network_id, instance_id): +def fixed_ip_associate_pool(context, network_id, instance_id=None, host=None): session = get_session() with session.begin(): network_or_none = or_(models.FixedIp.network_id == network_id, @@ -677,9 +675,12 @@ def fixed_ip_associate_pool(context, network_id, instance_id): fixed_ip_ref.network = network_get(context, network_id, session=session) - fixed_ip_ref.instance = instance_get(context, - instance_id, - session=session) + if instance_id: + fixed_ip_ref.instance = instance_get(context, + instance_id, + session=session) + if host: + fixed_ip_ref.host = host session.add(fixed_ip_ref) return fixed_ip_ref['address'] @@ -735,7 +736,7 @@ def fixed_ip_get_all(context, session=None): @require_admin_context -def fixed_ip_get_all_by_host(context, host=None): +def fixed_ip_get_all_by_instance_host(context, host=None): session = get_session() result = session.query(models.FixedIp).\ @@ -784,6 +785,20 @@ def fixed_ip_get_by_instance(context, instance_id): return rv +@require_context +def fixed_ip_get_by_network_host(context, network_id, host): + session = get_session() + rv = session.query(models.FixedIp).\ + filter_by(network_id=network_id).\ + filter_by(host=host).\ + filter_by(deleted=False).\ + all() + if not rv: + raise exception.FixedIpNotFoundForNetworkHost(network_id=network_id, + host=host) + return rv + + @require_context def fixed_ip_get_by_virtual_interface(context, vif_id): session = get_session() diff --git a/nova/db/sqlalchemy/migrate_repo/versions/032_ha_network.py b/nova/db/sqlalchemy/migrate_repo/versions/032_ha_network.py new file mode 100644 index 000000000..316c36cbc --- /dev/null +++ b/nova/db/sqlalchemy/migrate_repo/versions/032_ha_network.py @@ -0,0 +1,37 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 MORITA Kazutaka. +# 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 Column, Table, MetaData, Boolean, String + +meta = MetaData() + +fixed_ips_host = Column('host', String(255)) + +networks_multi_host = Column('multi_host', Boolean, default=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 + + # Add columns to existing tables + fixed_ips = Table('fixed_ips', meta, autoload=True) + fixed_ips.create_column(fixed_ips_host) + + networks = Table('networks', meta, autoload=True) + networks.create_column(networks_multi_host) diff --git a/nova/db/sqlalchemy/models.py b/nova/db/sqlalchemy/models.py index d29d3d6f1..8c86870f0 100644 --- a/nova/db/sqlalchemy/models.py +++ b/nova/db/sqlalchemy/models.py @@ -543,6 +543,7 @@ class Network(BASE, NovaBase): injected = Column(Boolean, default=False) cidr = Column(String(255), unique=True) cidr_v6 = Column(String(255), unique=True) + multi_host = Column(Boolean, default=False) gateway_v6 = Column(String(255)) netmask_v6 = Column(String(255)) diff --git a/nova/exception.py b/nova/exception.py index a6776b64f..29a209c3e 100644 --- a/nova/exception.py +++ b/nova/exception.py @@ -377,6 +377,11 @@ class FixedIpNotFoundForInstance(FixedIpNotFound): message = _("Instance %(instance_id)s has zero fixed ips.") +class FixedIpNotFoundForNetworkHost(FixedIpNotFound): + message = _("Network host %(host)s has zero fixed ips " + "in network %(network_id)s.") + + class FixedIpNotFoundForVirtualInterface(FixedIpNotFound): message = _("Virtual interface %(vif_id)s has zero associated fixed ips.") diff --git a/nova/network/api.py b/nova/network/api.py index b2b96082b..c57394204 100644 --- a/nova/network/api.py +++ b/nova/network/api.py @@ -18,7 +18,6 @@ """Handles all requests relating to instances (guest vms).""" -from nova import db from nova import exception from nova import flags from nova import log as logging @@ -105,7 +104,10 @@ class API(base.Base): '(%(project)s)') % {'address': floating_ip['address'], 'project': context.project_id}) - host = fixed_ip['network']['host'] + if fixed_ip['network']['multi_gateway']: + host = fixed_ip['instance']['host'] + else: + host = fixed_ip['network']['host'] rpc.cast(context, self.db.queue_get_for(context, FLAGS.network_topic, host), {'method': 'associate_floating_ip', @@ -120,7 +122,10 @@ class API(base.Base): return if not floating_ip.get('fixed_ip'): raise exception.ApiError('Address is not associated.') - host = floating_ip['fixed_ip']['network']['host'] + if floating_ip['fixed_ip']['network']['multi_gateway']: + host = floating_ip['fixed_ip']['instance']['host'] + else: + host = floating_ip['fixed_ip']['network']['host'] rpc.call(context, self.db.queue_get_for(context, FLAGS.network_topic, host), {'method': 'disassociate_floating_ip', @@ -134,7 +139,9 @@ class API(base.Base): args = kwargs args['instance_id'] = instance['id'] args['project_id'] = instance['project_id'] + args['host'] = instance['host'] args['instance_type_id'] = instance['instance_type_id'] + return rpc.call(context, FLAGS.network_topic, {'method': 'allocate_for_instance', 'args': args}) diff --git a/nova/network/linux_net.py b/nova/network/linux_net.py index 283a5aca1..8d2bc5c0b 100644 --- a/nova/network/linux_net.py +++ b/nova/network/linux_net.py @@ -573,18 +573,16 @@ def get_dhcp_hosts(context, network_id): # configuration options (like dchp-range, vlan, ...) # aren't reloaded. @utils.synchronized('dnsmasq_start') -def update_dhcp(context, network_id): +def update_dhcp(context, network_ref): """(Re)starts a dnsmasq server for a given network. If a dnsmasq instance is already running then send a HUP signal causing it to reload, otherwise spawn a new instance. """ - network_ref = db.network_get(context, network_id) - conffile = _dhcp_file(network_ref['bridge'], 'conf') with open(conffile, 'w') as f: - f.write(get_dhcp_hosts(context, network_id)) + f.write(get_dhcp_hosts(context, network_ref['id'])) # Make sure dnsmasq can actually read it (it setuid()s to "nobody") os.chmod(conffile, 0644) @@ -612,9 +610,7 @@ def update_dhcp(context, network_id): @utils.synchronized('radvd_start') -def update_ra(context, network_id): - network_ref = db.network_get(context, network_id) - +def update_ra(context, network_ref): conffile = _ra_file(network_ref['bridge'], 'conf') with open(conffile, 'w') as f: conf_str = """ @@ -650,9 +646,6 @@ interface %s LOG.debug(_('Pid %d is stale, relaunching radvd'), pid) command = _ra_cmd(network_ref) _execute(*command) - db.network_update(context, network_id, - {'gateway_v6': - utils.get_my_linklocal(network_ref['bridge'])}) def _host_lease(fixed_ip_ref): @@ -704,7 +697,7 @@ def _dnsmasq_cmd(net): '--conf-file=%s' % FLAGS.dnsmasq_config_file, '--domain=%s' % FLAGS.dhcp_domain, '--pid-file=%s' % _dhcp_file(net['bridge'], 'pid'), - '--listen-address=%s' % net['gateway'], + '--listen-address=%s' % net['dhcp_listen'], '--except-interface=lo', '--dhcp-range=%s,static,120s' % net['dhcp_start'], '--dhcp-lease-max=%s' % len(netaddr.IPNetwork(net['cidr'])), diff --git a/nova/network/manager.py b/nova/network/manager.py index d42bc8c4e..f60763eba 100644 --- a/nova/network/manager.py +++ b/nova/network/manager.py @@ -124,15 +124,17 @@ class RPCAllocateFixedIP(object): used since they share code to RPC.call allocate_fixed_ip on the correct network host to configure dnsmasq """ - def _allocate_fixed_ips(self, context, instance_id, networks): + def _allocate_fixed_ips(self, context, instance_id, host, networks): """Calls allocate_fixed_ip once for each network.""" green_pool = greenpool.GreenPool() for network in networks: - if network['host'] != self.host: + # NOTE(vish): if we are multi_gateway pass to the specified host + if not network['multi_gateway']: + host = network['host'] + if host != self.host: # need to call allocate_fixed_ip to correct network host - topic = self.db.queue_get_for(context, FLAGS.network_topic, - network['host']) + topic = self.db.queue_get_for(context, FLAGS.network_topic, host) args = {} args['instance_id'] = instance_id args['network_id'] = network['id'] @@ -298,6 +300,21 @@ class NetworkManager(manager.SchedulerDependentManager): super(NetworkManager, self).__init__(service_name='network', *args, **kwargs) + def _update_dchp(self, context, network_ref): + """Sets the listen address before sending update to the driver.""" + network_ref['dhcp_listen'] = self._get_dhcp_ip() + return self.driver.update_dhcp(context, network_ref) + + def _get_dhcp_ip(self, context, network_ref): + """Get the proper dhcp address to listen on. + + If it is a multi_host network, get the ip assigned to this host, + otherwise, assume that dhcp is listening on the gateway.""" + if network_ref['multi_host']: + return self.db.network_get_host_ip(context, FLAGS.host) + else: + return network_ref['gateway'] + def init_host(self): """Do any initialization that needs to be run if this is a standalone service. @@ -360,6 +377,7 @@ class NetworkManager(manager.SchedulerDependentManager): rpc.called by network_api """ instance_id = kwargs.pop('instance_id') + host = kwargs.pop('host') project_id = kwargs.pop('project_id') type_id = kwargs.pop('instance_type_id') admin_context = context.elevated() @@ -368,7 +386,7 @@ class NetworkManager(manager.SchedulerDependentManager): networks = self._get_networks_for_instance(admin_context, instance_id, project_id) self._allocate_mac_addresses(context, instance_id, networks) - self._allocate_fixed_ips(admin_context, instance_id, networks) + self._allocate_fixed_ips(admin_context, instance_id, host, networks) return self.get_instance_nw_info(context, instance_id, type_id) def deallocate_for_instance(self, context, **kwargs): @@ -475,10 +493,10 @@ class NetworkManager(manager.SchedulerDependentManager): random.randint(0x00, 0xff)] return ':'.join(map(lambda x: "%02x" % x, mac)) - def add_fixed_ip_to_instance(self, context, instance_id, network_id): + def add_fixed_ip_to_instance(self, context, instance_id, host, network_id): """Adds a fixed ip to an instance from specified network.""" networks = [self.db.network_get(context, network_id)] - self._allocate_fixed_ips(context, instance_id, networks) + self._allocate_fixed_ips(context, instance_id, host, networks) def allocate_fixed_ip(self, context, instance_id, network, **kwargs): """Gets a fixed ip from the pool.""" @@ -540,8 +558,9 @@ class NetworkManager(manager.SchedulerDependentManager): # means there will stale entries in the conf file # the code below will update the file if necessary if FLAGS.update_dhcp_on_disassociate: - network = self.db.fixed_ip_get_network(context, address) - self.driver.update_dhcp(context, network['id']) + network_ref = self.db.fixed_ip_get_network(context, address) + network_ref['dhcp_listen'] = self._get_dhcp_ip(context, network_ref) + self._update_dhcp(context, network_ref) def create_networks(self, context, label, cidr, num_networks, network_size, cidr_v6, gateway_v6, bridge, @@ -637,7 +656,7 @@ class NetworkManager(manager.SchedulerDependentManager): 'address': address, 'reserved': reserved}) - def _allocate_fixed_ips(self, context, instance_id, networks): + def _allocate_fixed_ips(self, context, instance_id, host, networks): """Calls allocate_fixed_ip once for each network.""" raise NotImplementedError() @@ -684,7 +703,7 @@ class FlatManager(NetworkManager): timeout_fixed_ips = False - def _allocate_fixed_ips(self, context, instance_id, networks): + def _allocate_fixed_ips(self, context, instance_id, host, networks): """Calls allocate_fixed_ip once for each network.""" for network in networks: self.allocate_fixed_ip(context, instance_id, network) @@ -738,8 +757,9 @@ class FlatDHCPManager(FloatingIP, RPCAllocateFixedIP, NetworkManager): """ networks = db.network_get_all_by_instance(context, instance_id) for network in networks: - self.driver.ensure_bridge(network['bridge'], - network['bridge_interface']) + if not network['multi_host']: + self.driver.ensure_bridge(network['bridge'], + network['bridge_interface']) def allocate_fixed_ip(self, context, instance_id, network): """Allocate flat_network fixed_ip, then setup dhcp for this network.""" @@ -747,7 +767,7 @@ class FlatDHCPManager(FloatingIP, RPCAllocateFixedIP, NetworkManager): instance_id, network) if not FLAGS.fake_network: - self.driver.update_dhcp(context, network['id']) + self._update_dhcp(context, network) def _on_set_network_host(self, context, network_id): """Called when this host becomes the host for a project.""" @@ -759,9 +779,13 @@ class FlatDHCPManager(FloatingIP, RPCAllocateFixedIP, NetworkManager): network['bridge_interface'], network) if not FLAGS.fake_network: - self.driver.update_dhcp(context, network_id) + network_ref = self.db.network_get(context, network_id) + self._update_dhcp(context, network_ref) if(FLAGS.use_ipv6): - self.driver.update_ra(context, network_id) + self.driver.update_ra(context, network_ref) + gateway = utils.get_my_linklocal(network_ref['bridge']) + self.db.network_update(context, network_id, + {'gateway_v6': gateway}) class VlanManager(RPCAllocateFixedIP, FloatingIP, NetworkManager): @@ -810,7 +834,7 @@ class VlanManager(RPCAllocateFixedIP, FloatingIP, NetworkManager): 'virtual_interface_id': vif['id']} self.db.fixed_ip_update(context, address, values) if not FLAGS.fake_network: - self.driver.update_dhcp(context, network['id']) + self._update_dhcp(context, network) def add_network_to_project(self, context, project_id): """Force adds another network to a project.""" @@ -822,9 +846,10 @@ class VlanManager(RPCAllocateFixedIP, FloatingIP, NetworkManager): """ networks = self.db.network_get_all_by_instance(context, instance_id) for network in networks: - self.driver.ensure_vlan_bridge(network['vlan'], - network['bridge'], - network['bridge_interface']) + if not network['multi_host']: + self.driver.ensure_vlan_bridge(network['vlan'], + network['bridge'], + network['bridge_interface']) def _get_networks_for_instance(self, context, instance_id, project_id): """Determine which networks an instance should connect to.""" @@ -874,9 +899,13 @@ class VlanManager(RPCAllocateFixedIP, FloatingIP, NetworkManager): network['vpn_public_port'], network['vpn_private_address']) if not FLAGS.fake_network: - self.driver.update_dhcp(context, network_id) + network_ref = self.db.network_get(context, network_id) + self._update_dhcp(context, network_ref) if(FLAGS.use_ipv6): - self.driver.update_ra(context, network_id) + self.driver.update_ra(context, network_ref) + gateway = utils.get_my_linklocal(network_ref['bridge']) + self.db.network_update(context, network_id, + {'gateway_v6': gateway}) @property def _bottom_reserved_ips(self): -- cgit From 9a8254ef2751e1b7502107a9c6afe05ea1e2efd4 Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Tue, 5 Jul 2011 12:00:11 -0700 Subject: it is multi_host not multi_gateway --- nova/network/api.py | 6 ++++-- nova/network/manager.py | 5 ++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/nova/network/api.py b/nova/network/api.py index c57394204..c0f0fb8eb 100644 --- a/nova/network/api.py +++ b/nova/network/api.py @@ -104,7 +104,8 @@ class API(base.Base): '(%(project)s)') % {'address': floating_ip['address'], 'project': context.project_id}) - if fixed_ip['network']['multi_gateway']: + # NOTE(vish): if we are multi_host, send to the instances host + if fixed_ip['network']['multi_host']: host = fixed_ip['instance']['host'] else: host = fixed_ip['network']['host'] @@ -122,7 +123,8 @@ class API(base.Base): return if not floating_ip.get('fixed_ip'): raise exception.ApiError('Address is not associated.') - if floating_ip['fixed_ip']['network']['multi_gateway']: + # NOTE(vish): if we are multi_host, send to the instances host + if floating_ip['fixed_ip']['network']['multi_host']: host = floating_ip['fixed_ip']['instance']['host'] else: host = floating_ip['fixed_ip']['network']['host'] diff --git a/nova/network/manager.py b/nova/network/manager.py index f60763eba..84d2cbbcb 100644 --- a/nova/network/manager.py +++ b/nova/network/manager.py @@ -49,7 +49,6 @@ import datetime import math import netaddr import socket -import pickle from eventlet import greenpool from nova import context @@ -129,8 +128,8 @@ class RPCAllocateFixedIP(object): green_pool = greenpool.GreenPool() for network in networks: - # NOTE(vish): if we are multi_gateway pass to the specified host - if not network['multi_gateway']: + # NOTE(vish): if we are not multi_host pass to the network host + if not network['multi_host']: host = network['host'] if host != self.host: # need to call allocate_fixed_ip to correct network host -- cgit From 8d2f3f26e8089020616312334689f1c594a67b4f Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Tue, 5 Jul 2011 12:16:46 -0700 Subject: make sure to filter out ips associated by host and add some sync for allocating ip to host --- nova/db/sqlalchemy/api.py | 1 + nova/network/manager.py | 13 ++++++++++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/nova/db/sqlalchemy/api.py b/nova/db/sqlalchemy/api.py index 6db142276..7065de00f 100644 --- a/nova/db/sqlalchemy/api.py +++ b/nova/db/sqlalchemy/api.py @@ -665,6 +665,7 @@ def fixed_ip_associate_pool(context, network_id, instance_id=None, host=None): filter_by(reserved=False).\ filter_by(deleted=False).\ filter_by(instance=None).\ + filter_by(host=None).\ with_lockmode('update').\ first() # NOTE(vish): if with_lockmode isn't supported, as in sqlite, diff --git a/nova/network/manager.py b/nova/network/manager.py index 84d2cbbcb..55f81da9e 100644 --- a/nova/network/manager.py +++ b/nova/network/manager.py @@ -310,7 +310,18 @@ class NetworkManager(manager.SchedulerDependentManager): If it is a multi_host network, get the ip assigned to this host, otherwise, assume that dhcp is listening on the gateway.""" if network_ref['multi_host']: - return self.db.network_get_host_ip(context, FLAGS.host) + + @utils.synchronized('get_dhcp') + def _sync_get_dhcp_ip(): + try: + return self.db.fixed_ip_get_by_network_host(context, + FLAGS.host) + except exception.FixedIpNotFoundForNetworkHost: + return self.db.fixed_ip_associate_pool(context.elevated(), + network_ref['id'], + host=FLAGS.host) + return _sync_get_dhcp_ip() + else: return network_ref['gateway'] -- cgit From 9b5adcbe92a4f7e0f9b1592be123c58f743def34 Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Tue, 5 Jul 2011 15:55:16 -0700 Subject: pass in dhcp server address, fix a bunch of bugs --- nova/compute/manager.py | 17 ++++++++------- nova/db/api.py | 2 +- nova/db/sqlalchemy/api.py | 3 ++- nova/db/sqlalchemy/models.py | 1 + nova/network/linux_net.py | 4 ++-- nova/network/manager.py | 47 ++++++++++++++++++++++++++++------------- nova/virt/libvirt/connection.py | 3 +-- 7 files changed, 48 insertions(+), 29 deletions(-) diff --git a/nova/compute/manager.py b/nova/compute/manager.py index bbbddde0a..b198adc97 100644 --- a/nova/compute/manager.py +++ b/nova/compute/manager.py @@ -77,8 +77,6 @@ flags.DEFINE_integer('live_migration_retry_count', 30, flags.DEFINE_integer("rescue_timeout", 0, "Automatically unrescue an instance after N seconds." " Set to 0 to disable.") -flags.DEFINE_bool('auto_assign_floating_ip', False, - 'Autoassigning floating ip to VM') flags.DEFINE_integer('host_state_interval', 120, 'Interval in seconds for querying the host status') @@ -268,16 +266,19 @@ class ComputeManager(manager.SchedulerDependentManager): """Launch a new instance with specified options.""" context = context.elevated() instance = self.db.instance_get(context, instance_id) - instance.injected_files = kwargs.get('injected_files', []) - instance.admin_pass = kwargs.get('admin_password', None) if instance['name'] in self.driver.list_instances(): raise exception.Error(_("Instance has already been created")) LOG.audit(_("instance %s: starting..."), instance_id, context=context) - self.db.instance_update(context, - instance_id, - {'host': self.host, 'launched_on': self.host}) - + updates = {} + updates['host'] = self.host + updates['launched_on'] = self.host + # NOTE(vish): used by virt but not in database + updates['injected_files'] = kwargs.get('injected_files', []) + updates['admin_pass'] = kwargs.get('admin_password', None) + instance = self.db.instance_update(context, + instance_id, + updates) self.db.instance_set_state(context, instance_id, power_state.NOSTATE, diff --git a/nova/db/api.py b/nova/db/api.py index febe33374..ee4cbad57 100644 --- a/nova/db/api.py +++ b/nova/db/api.py @@ -340,7 +340,7 @@ def fixed_ip_associate_pool(context, network_id, instance_id=None, host=None): """ return IMPL.fixed_ip_associate_pool(context, network_id, - instance_id, host, reserved) + instance_id, host) def fixed_ip_create(context, values): diff --git a/nova/db/sqlalchemy/api.py b/nova/db/sqlalchemy/api.py index 7065de00f..9c138d2fd 100644 --- a/nova/db/sqlalchemy/api.py +++ b/nova/db/sqlalchemy/api.py @@ -683,6 +683,7 @@ def fixed_ip_associate_pool(context, network_id, instance_id=None, host=None): if host: fixed_ip_ref.host = host session.add(fixed_ip_ref) + LOG.warn("gave address %s to %s, %s", fixed_ip_ref['address'], instance_id, host) return fixed_ip_ref['address'] @@ -793,7 +794,7 @@ def fixed_ip_get_by_network_host(context, network_id, host): filter_by(network_id=network_id).\ filter_by(host=host).\ filter_by(deleted=False).\ - all() + first() if not rv: raise exception.FixedIpNotFoundForNetworkHost(network_id=network_id, host=host) diff --git a/nova/db/sqlalchemy/models.py b/nova/db/sqlalchemy/models.py index 8c86870f0..639941dc8 100644 --- a/nova/db/sqlalchemy/models.py +++ b/nova/db/sqlalchemy/models.py @@ -602,6 +602,7 @@ class FixedIp(BASE, NovaBase): # leased means dhcp bridge has leased the ip leased = Column(Boolean, default=False) reserved = Column(Boolean, default=False) + host = Column(String(255)) class FloatingIp(BASE, NovaBase): diff --git a/nova/network/linux_net.py b/nova/network/linux_net.py index 8d2bc5c0b..0a7a62510 100644 --- a/nova/network/linux_net.py +++ b/nova/network/linux_net.py @@ -497,7 +497,7 @@ def ensure_bridge(bridge, interface, net_attrs=None): suffix = net_attrs['cidr'].rpartition('/')[2] out, err = _execute('sudo', 'ip', 'addr', 'add', '%s/%s' % - (net_attrs['gateway'], suffix), + (net_attrs['dhcp_server'], suffix), 'brd', net_attrs['broadcast'], 'dev', @@ -697,7 +697,7 @@ def _dnsmasq_cmd(net): '--conf-file=%s' % FLAGS.dnsmasq_config_file, '--domain=%s' % FLAGS.dhcp_domain, '--pid-file=%s' % _dhcp_file(net['bridge'], 'pid'), - '--listen-address=%s' % net['dhcp_listen'], + '--listen-address=%s' % net['dhcp_server'], '--except-interface=lo', '--dhcp-range=%s,static,120s' % net['dhcp_start'], '--dhcp-lease-max=%s' % len(netaddr.IPNetwork(net['cidr'])), diff --git a/nova/network/manager.py b/nova/network/manager.py index 55f81da9e..ce631ce75 100644 --- a/nova/network/manager.py +++ b/nova/network/manager.py @@ -103,7 +103,8 @@ flags.DEFINE_integer('fixed_ip_disassociate_timeout', 600, 'Seconds after which a deallocated ip is disassociated') flags.DEFINE_integer('create_unique_mac_address_attempts', 5, 'Number of attempts to create unique mac address') - +flags.DEFINE_bool('auto_assign_floating_ip', False, + 'Autoassigning floating ip to VM') flags.DEFINE_bool('use_ipv6', False, 'use the ipv6') flags.DEFINE_string('network_host', socket.gethostname(), @@ -192,7 +193,7 @@ class FloatingIP(object): # which is currently the NetworkManager version # do this first so fixed ip is already allocated ips = super(FloatingIP, self).allocate_for_instance(context, **kwargs) - if hasattr(FLAGS, 'auto_assign_floating_ip'): + if FLAGS.auto_assign_floating_ip: # allocate a floating ip (public_ip is just the address string) public_ip = self.allocate_floating_ip(context, project_id) # set auto_assigned column to true for the floating ip @@ -299,9 +300,9 @@ class NetworkManager(manager.SchedulerDependentManager): super(NetworkManager, self).__init__(service_name='network', *args, **kwargs) - def _update_dchp(self, context, network_ref): + def _update_dhcp(self, context, network_ref): """Sets the listen address before sending update to the driver.""" - network_ref['dhcp_listen'] = self._get_dhcp_ip() + network_ref['dhcp_server'] = self._get_dhcp_ip(context, network_ref) return self.driver.update_dhcp(context, network_ref) def _get_dhcp_ip(self, context, network_ref): @@ -313,13 +314,18 @@ class NetworkManager(manager.SchedulerDependentManager): @utils.synchronized('get_dhcp') def _sync_get_dhcp_ip(): + network_id = network_ref['id'] try: - return self.db.fixed_ip_get_by_network_host(context, - FLAGS.host) + fip = self.db.fixed_ip_get_by_network_host(context, + network_id, + self.host) + return fip['address'] except exception.FixedIpNotFoundForNetworkHost: - return self.db.fixed_ip_associate_pool(context.elevated(), - network_ref['id'], - host=FLAGS.host) + addr = self.db.fixed_ip_associate_pool(context.elevated(), + network_id, + host=self.host) + return addr + return _sync_get_dhcp_ip() else: @@ -334,6 +340,11 @@ class NetworkManager(manager.SchedulerDependentManager): ctxt = context.get_admin_context() for network in self.db.network_get_all_by_host(ctxt, self.host): self._on_set_network_host(ctxt, network['id']) + for network in self.db.network_get_all(ctxt): + # NOTE(vish): multi_host networks need to be set on all boxes + if network['multi_host']: + self._on_set_network_host(ctxt, network['id']) + def periodic_tasks(self, context=None): """Tasks to be run at a periodic interval.""" @@ -349,7 +360,7 @@ class NetworkManager(manager.SchedulerDependentManager): LOG.debug(_('Dissassociated %s stale fixed ip(s)'), num) # setup any new networks which have been created - self.set_network_hosts(context) + #self.set_network_hosts(context) def set_network_host(self, context, network_id): """Safely sets the host of the network.""" @@ -378,8 +389,8 @@ class NetworkManager(manager.SchedulerDependentManager): networks = self.db.network_get_all(context) # return only networks which are not vlan networks and have host set - return [network for network in networks if - not network['vlan'] and network['host']] + return [network for network in networks if not network['vlan'] + and network['host'] or network['multi_host']] def allocate_for_instance(self, context, **kwargs): """Handles allocating the various network resources for an instance. @@ -395,6 +406,7 @@ class NetworkManager(manager.SchedulerDependentManager): context=context) networks = self._get_networks_for_instance(admin_context, instance_id, project_id) + LOG.warn(networks) self._allocate_mac_addresses(context, instance_id, networks) self._allocate_fixed_ips(admin_context, instance_id, host, networks) return self.get_instance_nw_info(context, instance_id, type_id) @@ -464,6 +476,7 @@ class NetworkManager(manager.SchedulerDependentManager): info = { 'label': network['label'], 'gateway': network['gateway'], + 'dhcp_server': self._get_dhcp_ip(context, network), 'broadcast': network['broadcast'], 'mac': vif['address'], 'rxtx_cap': flavor['rxtx_cap'], @@ -569,7 +582,7 @@ class NetworkManager(manager.SchedulerDependentManager): # the code below will update the file if necessary if FLAGS.update_dhcp_on_disassociate: network_ref = self.db.fixed_ip_get_network(context, address) - network_ref['dhcp_listen'] = self._get_dhcp_ip(context, network_ref) + network_ref['dhcp_server'] = self._get_dhcp_ip(context, network_ref) self._update_dhcp(context, network_ref) def create_networks(self, context, label, cidr, num_networks, @@ -628,6 +641,7 @@ class NetworkManager(manager.SchedulerDependentManager): # robust solution would be to make them uniq per ip net['vpn_public_port'] = kwargs['vpn_start'] + index + net['multi_host'] = True # None if network with cidr or cidr_v6 already exists network = self.db.network_create_safe(context, net) @@ -785,6 +799,7 @@ class FlatDHCPManager(FloatingIP, RPCAllocateFixedIP, NetworkManager): net['dhcp_start'] = FLAGS.flat_network_dhcp_start self.db.network_update(context, network_id, net) network = db.network_get(context, network_id) + network['dhcp_server'] = self._get_dhcp_ip(context, network) self.driver.ensure_bridge(network['bridge'], network['bridge_interface'], network) @@ -866,8 +881,9 @@ class VlanManager(RPCAllocateFixedIP, FloatingIP, NetworkManager): # get networks associated with project networks = self.db.project_get_networks(context, project_id) - # return only networks which have host set - return [network for network in networks if network['host']] + # return only networks which have host set or are multi_host + return [network for network in networks + if network['host'] or network['multi_host']] def create_networks(self, context, **kwargs): """Create networks based on parameters.""" @@ -896,6 +912,7 @@ class VlanManager(RPCAllocateFixedIP, FloatingIP, NetworkManager): db.network_update(context, network_id, net) else: address = network['vpn_public_address'] + network['dhcp_server'] = self._get_dhcp_ip(context, network) self.driver.ensure_vlan_bridge(network['vlan'], network['bridge'], network['bridge_interface'], diff --git a/nova/virt/libvirt/connection.py b/nova/virt/libvirt/connection.py index 0c6eaab84..5e8fcdd2f 100644 --- a/nova/virt/libvirt/connection.py +++ b/nova/virt/libvirt/connection.py @@ -928,7 +928,6 @@ class LibvirtConnection(driver.ComputeDriver): def _get_nic_for_xml(self, network, mapping): # Assume that the gateway also acts as the dhcp server. - dhcp_server = mapping['gateway'] gateway6 = mapping.get('gateway6') mac_id = mapping['mac'].replace(':', '') @@ -951,7 +950,7 @@ class LibvirtConnection(driver.ComputeDriver): 'bridge_name': network['bridge'], 'mac_address': mapping['mac'], 'ip_address': mapping['ips'][0]['ip'], - 'dhcp_server': dhcp_server, + 'dhcp_server': mapping['dhcp_server'], 'extra_params': extra_params, } -- cgit From 8e1a74e5604e1569e314af67b72966122940330b Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Tue, 5 Jul 2011 16:06:20 -0700 Subject: filter the dhcp to only respond to requests from this host --- bin/nova-dhcpbridge | 2 +- nova/network/linux_net.py | 24 +++++++++++++++--------- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/bin/nova-dhcpbridge b/bin/nova-dhcpbridge index 6d9d85896..325642d52 100755 --- a/bin/nova-dhcpbridge +++ b/bin/nova-dhcpbridge @@ -91,7 +91,7 @@ def init_leases(interface): """Get the list of hosts for an interface.""" ctxt = context.get_admin_context() network_ref = db.network_get_by_bridge(ctxt, interface) - return linux_net.get_dhcp_leases(ctxt, network_ref['id']) + return linux_net.get_dhcp_leases(ctxt, network_ref) def main(): diff --git a/nova/network/linux_net.py b/nova/network/linux_net.py index 0a7a62510..848e40643 100644 --- a/nova/network/linux_net.py +++ b/nova/network/linux_net.py @@ -551,21 +551,27 @@ def ensure_bridge(bridge, interface, net_attrs=None): bridge) -def get_dhcp_leases(context, network_id): +def get_dhcp_leases(context, network_ref): """Return a network's hosts config in dnsmasq leasefile format.""" hosts = [] - for fixed_ip_ref in db.network_get_associated_fixed_ips(context, - network_id): - hosts.append(_host_lease(fixed_ip_ref)) + for fixed_ref in db.network_get_associated_fixed_ips(context, + network_ref['id']): + host = fixed_ref['instance']['host'] + if network_ref['multi_host'] and FLAGS.host != host: + continue + hosts.append(_host_lease(fixed_ref)) return '\n'.join(hosts) -def get_dhcp_hosts(context, network_id): +def get_dhcp_hosts(context, network_ref): """Get network's hosts config in dhcp-host format.""" hosts = [] - for fixed_ip_ref in db.network_get_associated_fixed_ips(context, - network_id): - hosts.append(_host_dhcp(fixed_ip_ref)) + for fixed_ref in db.network_get_associated_fixed_ips(context, + network_ref['id']): + host = fixed_ref['instance']['host'] + if network_ref['multi_host'] and FLAGS.host != host: + continue + hosts.append(_host_dhcp(fixed_ref)) return '\n'.join(hosts) @@ -582,7 +588,7 @@ def update_dhcp(context, network_ref): """ conffile = _dhcp_file(network_ref['bridge'], 'conf') with open(conffile, 'w') as f: - f.write(get_dhcp_hosts(context, network_ref['id'])) + f.write(get_dhcp_hosts(context, network_ref)) # Make sure dnsmasq can actually read it (it setuid()s to "nobody") os.chmod(conffile, 0644) -- cgit From 53213dc4cd0f6f940d707c5d5932f4af7e5f988a Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Tue, 5 Jul 2011 16:22:41 -0700 Subject: add ability to set multi_host in nova-manage and remove debugging issues --- bin/nova-manage | 9 +++++++-- nova/db/sqlalchemy/api.py | 1 - nova/network/manager.py | 8 +++++--- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/bin/nova-manage b/bin/nova-manage index 71c99138f..cd43f734f 100755 --- a/bin/nova-manage +++ b/bin/nova-manage @@ -558,12 +558,12 @@ class NetworkCommands(object): """Class for managing networks.""" def create(self, label=None, fixed_range=None, num_networks=None, - network_size=None, vlan_start=None, + network_size=None, multi_host=None, vlan_start=None, vpn_start=None, fixed_range_v6=None, gateway_v6=None, flat_network_bridge=None, bridge_interface=None): """Creates fixed ips for host by range arguments: label, fixed_range, [num_networks=FLAG], - [network_size=FLAG], [vlan_start=FLAG], + [network_size=FLAG], [multi_host=FLAG], [vlan_start=FLAG], [vpn_start=FLAG], [fixed_range_v6=FLAG], [gateway_v6=FLAG], [flat_network_bridge=FLAG], [bridge_interface=FLAG] If you wish to use a later argument fill in the gaps with 0s @@ -583,6 +583,10 @@ class NetworkCommands(object): num_networks = FLAGS.num_networks if not network_size: network_size = FLAGS.network_size + if not multi_host: + multi_host = FLAGS.multi_host + else: + multi_host = multi_host == 'T' if not vlan_start: vlan_start = FLAGS.vlan_start if not vpn_start: @@ -601,6 +605,7 @@ class NetworkCommands(object): net_manager.create_networks(context.get_admin_context(), label=label, cidr=fixed_range, + multi_host=multi_host, num_networks=int(num_networks), network_size=int(network_size), vlan_start=int(vlan_start), diff --git a/nova/db/sqlalchemy/api.py b/nova/db/sqlalchemy/api.py index 9c138d2fd..472513329 100644 --- a/nova/db/sqlalchemy/api.py +++ b/nova/db/sqlalchemy/api.py @@ -683,7 +683,6 @@ def fixed_ip_associate_pool(context, network_id, instance_id=None, host=None): if host: fixed_ip_ref.host = host session.add(fixed_ip_ref) - LOG.warn("gave address %s to %s, %s", fixed_ip_ref['address'], instance_id, host) return fixed_ip_ref['address'] diff --git a/nova/network/manager.py b/nova/network/manager.py index ce631ce75..d799d5346 100644 --- a/nova/network/manager.py +++ b/nova/network/manager.py @@ -86,6 +86,8 @@ flags.DEFINE_integer('num_networks', 1, 'Number of networks to support') flags.DEFINE_string('vpn_ip', '$my_ip', 'Public IP for the cloudpipe VPN servers') flags.DEFINE_integer('vpn_start', 1000, 'First Vpn port for private networks') +flags.DEFINE_bool('multi_host', False, + 'Default value for multi_host in networks') flags.DEFINE_integer('network_size', 256, 'Number of addresses in each private subnet') flags.DEFINE_string('floating_range', '4.4.4.0/24', @@ -360,7 +362,7 @@ class NetworkManager(manager.SchedulerDependentManager): LOG.debug(_('Dissassociated %s stale fixed ip(s)'), num) # setup any new networks which have been created - #self.set_network_hosts(context) + self.set_network_hosts(context) def set_network_host(self, context, network_id): """Safely sets the host of the network.""" @@ -585,7 +587,7 @@ class NetworkManager(manager.SchedulerDependentManager): network_ref['dhcp_server'] = self._get_dhcp_ip(context, network_ref) self._update_dhcp(context, network_ref) - def create_networks(self, context, label, cidr, num_networks, + def create_networks(self, context, label, cidr, multi_host, num_networks, network_size, cidr_v6, gateway_v6, bridge, bridge_interface, **kwargs): """Create networks based on parameters.""" @@ -604,6 +606,7 @@ class NetworkManager(manager.SchedulerDependentManager): net['bridge_interface'] = bridge_interface net['dns'] = FLAGS.flat_network_dns net['cidr'] = cidr + net['multi_host'] = multi_host net['netmask'] = str(project_net.netmask) net['gateway'] = str(project_net[1]) net['broadcast'] = str(project_net.broadcast) @@ -641,7 +644,6 @@ class NetworkManager(manager.SchedulerDependentManager): # robust solution would be to make them uniq per ip net['vpn_public_port'] = kwargs['vpn_start'] + index - net['multi_host'] = True # None if network with cidr or cidr_v6 already exists network = self.db.network_create_safe(context, net) -- cgit From 11dfe937bcfa542c985a977e9ff855f717e80e69 Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Tue, 5 Jul 2011 16:24:43 -0700 Subject: don't set network host for multi_host networks --- nova/network/manager.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/nova/network/manager.py b/nova/network/manager.py index d799d5346..83ab4dd72 100644 --- a/nova/network/manager.py +++ b/nova/network/manager.py @@ -378,8 +378,7 @@ class NetworkManager(manager.SchedulerDependentManager): """Set the network hosts for any networks which are unset.""" networks = self.db.network_get_all(context) for network in networks: - host = network['host'] - if not host: + if not network['multi_host'] and not network['host']: # return so worker will only grab 1 (to help scale flatter) return self.set_network_host(context, network['id']) -- cgit From 77b655cd16a265eb2b8fc369941d19890766e712 Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Tue, 5 Jul 2011 17:01:19 -0700 Subject: update tests --- nova/tests/__init__.py | 1 + nova/tests/test_network.py | 3 +++ 2 files changed, 4 insertions(+) diff --git a/nova/tests/__init__.py b/nova/tests/__init__.py index e4ed75d37..b885000e1 100644 --- a/nova/tests/__init__.py +++ b/nova/tests/__init__.py @@ -59,6 +59,7 @@ def setup(): network.create_networks(ctxt, label='test', cidr=FLAGS.fixed_range, + multi_host=FLAGS.multi_host, num_networks=FLAGS.num_networks, network_size=FLAGS.network_size, cidr_v6=FLAGS.fixed_range_v6, diff --git a/nova/tests/test_network.py b/nova/tests/test_network.py index 6d5166019..390cdc003 100644 --- a/nova/tests/test_network.py +++ b/nova/tests/test_network.py @@ -44,6 +44,7 @@ class FakeModel(dict): networks = [{'id': 0, 'label': 'test0', 'injected': False, + 'multi_host': False, 'cidr': '192.168.0.0/24', 'cidr_v6': '2001:db8::/64', 'gateway_v6': '2001:db8::1', @@ -61,6 +62,7 @@ networks = [{'id': 0, {'id': 1, 'label': 'test1', 'injected': False, + 'multi_host': False, 'cidr': '192.168.1.0/24', 'cidr_v6': '2001:db9::/64', 'gateway_v6': '2001:db9::1', @@ -163,6 +165,7 @@ class FlatNetworkTestCase(test.TestCase): self.assertDictMatch(nw[0], check) check = {'broadcast': '192.168.%s.255' % i, + 'dhcp_server': '192.168.%s.1' % i, 'dns': 'DONTCARE', 'gateway': '192.168.%s.1' % i, 'gateway6': '2001:db%s::1' % i8, -- cgit From e24c1d998331444235480be241484b0408cdaf9e Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Tue, 5 Jul 2011 17:07:44 -0700 Subject: fix libvirt test --- nova/tests/test_libvirt.py | 1 + 1 file changed, 1 insertion(+) diff --git a/nova/tests/test_libvirt.py b/nova/tests/test_libvirt.py index f99e1713d..6e2ec7ed6 100644 --- a/nova/tests/test_libvirt.py +++ b/nova/tests/test_libvirt.py @@ -58,6 +58,7 @@ def _create_network_info(count=1, ipv6=None): 'cidr': fake_ip, 'cidr_v6': fake_ip} mapping = {'mac': fake, + 'dhcp_server': fake, 'gateway': fake, 'gateway6': fake, 'ips': [{'ip': fake_ip}, {'ip': fake_ip}]} -- cgit From f568d9345d19064c0024523195b17eb9e7d036ef Mon Sep 17 00:00:00 2001 From: Ryu Ishimoto Date: Thu, 7 Jul 2011 01:03:00 +0900 Subject: Added a L2 network driver for bridge/vlan creation --- Authors | 1 + nova/network/l2_drivers.py | 56 ++++++++++++++++++++++++++++++++++++++++++++++ nova/network/manager.py | 32 ++++++++++++++++---------- 3 files changed, 77 insertions(+), 12 deletions(-) create mode 100644 nova/network/l2_drivers.py diff --git a/Authors b/Authors index c3a65f1b4..e0119d63e 100644 --- a/Authors +++ b/Authors @@ -77,6 +77,7 @@ Rick Harris Rob Kost Ryan Lane Ryan Lucio +Ryu Ishimoto Salvatore Orlando Sandy Walsh Sateesh Chodapuneedi diff --git a/nova/network/l2_drivers.py b/nova/network/l2_drivers.py new file mode 100644 index 000000000..1e66dcd26 --- /dev/null +++ b/nova/network/l2_drivers.py @@ -0,0 +1,56 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright (C) 2011 Midokura KK +# 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 responsible for managing L2 connectivity for Nova.""" + +from nova.network import linux_net + +class L2Driver(object): + """Base class that defines interfaces for L2 drivers.""" + + def ensure_bridge(bridge, interface, net_attrs=None): + """Create a bridge unless it already exists.""" + raise NotImplementedError() + + def ensure_vlan(vlan_num, bridge_interface): + """Create a vlan unless it already exists.""" + raise NotImplementedError() + + +class LinuxBridgeDriver(L2Driver): + """L2 driver based on Linux Bridge.""" + + def ensure_bridge(bridge, interface, net_attrs=None): + """Create a Linux bridge unless it already eixsts.""" + linux_net.ensure_bridge(bridge, interface, net_attrs) + + def ensure_vlan(vlan_num, bridge_interface): + """Create a vlan unless it already exists.""" + return linux_net.ensure_vlan(vlan_num, bridge_interface) + + +class QuantumDriver(L2Driver): + """L2 driver based on Quantum network service.""" + + def ensure_bridge(bridge, interface, net_attrs=None): + """Do nothing.""" + pass + + def ensure_vlan(vlan_num, bridge_interface): + """Return None.""" + return None + diff --git a/nova/network/manager.py b/nova/network/manager.py index d42bc8c4e..3d6388259 100644 --- a/nova/network/manager.py +++ b/nova/network/manager.py @@ -111,6 +111,8 @@ flags.DEFINE_string('network_host', socket.gethostname(), 'Network host to use for ip allocation in flat modes') flags.DEFINE_bool('fake_call', False, 'If True, skip using the queue and make local calls') +flags.DEFINE_string('l2_driver', 'nova.network.l2_drivers.LinuxBridgeDriver', + 'L2 network connectivity driver.') class AddressAlreadyAllocated(exception.Error): @@ -294,6 +296,7 @@ class NetworkManager(manager.SchedulerDependentManager): if not network_driver: network_driver = FLAGS.network_driver self.driver = utils.import_object(network_driver) + self.l2_driver = utils.import_object(FLAGS.l2_driver) self.network_api = network_api.API() super(NetworkManager, self).__init__(service_name='network', *args, **kwargs) @@ -738,8 +741,8 @@ class FlatDHCPManager(FloatingIP, RPCAllocateFixedIP, NetworkManager): """ networks = db.network_get_all_by_instance(context, instance_id) for network in networks: - self.driver.ensure_bridge(network['bridge'], - network['bridge_interface']) + self.l2_driver.ensure_bridge(network['bridge'], + network['bridge_interface']) def allocate_fixed_ip(self, context, instance_id, network): """Allocate flat_network fixed_ip, then setup dhcp for this network.""" @@ -755,9 +758,9 @@ class FlatDHCPManager(FloatingIP, RPCAllocateFixedIP, NetworkManager): net['dhcp_start'] = FLAGS.flat_network_dhcp_start self.db.network_update(context, network_id, net) network = db.network_get(context, network_id) - self.driver.ensure_bridge(network['bridge'], - network['bridge_interface'], - network) + self.l2_driver.ensure_bridge(network['bridge'], + network['bridge_interface'], + network) if not FLAGS.fake_network: self.driver.update_dhcp(context, network_id) if(FLAGS.use_ipv6): @@ -822,9 +825,11 @@ class VlanManager(RPCAllocateFixedIP, FloatingIP, NetworkManager): """ networks = self.db.network_get_all_by_instance(context, instance_id) for network in networks: - self.driver.ensure_vlan_bridge(network['vlan'], - network['bridge'], - network['bridge_interface']) + interface = self.l2_driver.ensure_vlan(network['vlan'], + network['bridge_interface']) + if interface: + self.l2_driver.ensure_bridge(network['bridge'], interface, + network['bridge_interface']) def _get_networks_for_instance(self, context, instance_id, project_id): """Determine which networks an instance should connect to.""" @@ -861,10 +866,13 @@ class VlanManager(RPCAllocateFixedIP, FloatingIP, NetworkManager): db.network_update(context, network_id, net) else: address = network['vpn_public_address'] - self.driver.ensure_vlan_bridge(network['vlan'], - network['bridge'], - network['bridge_interface'], - network) + + interface = self.l2_driver.ensure_vlan(network['vlan'], + network['bridge_interface']) + if interface: + self.l2_driver.ensure_bridge(network['bridge'], interface, + network['bridge_interface'], + network) # NOTE(vish): only ensure this forward if the address hasn't been set # manually. -- cgit From 2b553a89b34d7a3691c0952c5debda6b5ea1bb79 Mon Sep 17 00:00:00 2001 From: Ryu Ishimoto Date: Thu, 7 Jul 2011 01:04:56 +0900 Subject: Added net_attrs argument for ensure_bridge/vlan methods --- nova/network/l2_drivers.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/nova/network/l2_drivers.py b/nova/network/l2_drivers.py index 1e66dcd26..e1de5cc5b 100644 --- a/nova/network/l2_drivers.py +++ b/nova/network/l2_drivers.py @@ -26,7 +26,7 @@ class L2Driver(object): """Create a bridge unless it already exists.""" raise NotImplementedError() - def ensure_vlan(vlan_num, bridge_interface): + def ensure_vlan(vlan_num, bridge_interface, net_attrs=None): """Create a vlan unless it already exists.""" raise NotImplementedError() @@ -38,9 +38,9 @@ class LinuxBridgeDriver(L2Driver): """Create a Linux bridge unless it already eixsts.""" linux_net.ensure_bridge(bridge, interface, net_attrs) - def ensure_vlan(vlan_num, bridge_interface): + def ensure_vlan(vlan_num, bridge_interface, net_attrs=None): """Create a vlan unless it already exists.""" - return linux_net.ensure_vlan(vlan_num, bridge_interface) + return linux_net.ensure_vlan(vlan_num, bridge_interface, net_attrs) class QuantumDriver(L2Driver): @@ -50,7 +50,7 @@ class QuantumDriver(L2Driver): """Do nothing.""" pass - def ensure_vlan(vlan_num, bridge_interface): + def ensure_vlan(vlan_num, bridge_interface, net_attrs=None): """Return None.""" return None -- cgit From 23a5775d6dbd5f11ff0adb67dd6b5bceb96b8030 Mon Sep 17 00:00:00 2001 From: Ryu Ishimoto Date: Thu, 7 Jul 2011 02:12:03 +0900 Subject: Added the missing 'self' parameter --- nova/network/l2_drivers.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/nova/network/l2_drivers.py b/nova/network/l2_drivers.py index e1de5cc5b..23d05cda4 100644 --- a/nova/network/l2_drivers.py +++ b/nova/network/l2_drivers.py @@ -22,11 +22,11 @@ from nova.network import linux_net class L2Driver(object): """Base class that defines interfaces for L2 drivers.""" - def ensure_bridge(bridge, interface, net_attrs=None): + def ensure_bridge(self, bridge, interface, net_attrs=None): """Create a bridge unless it already exists.""" raise NotImplementedError() - def ensure_vlan(vlan_num, bridge_interface, net_attrs=None): + def ensure_vlan(self, vlan_num, bridge_interface, net_attrs=None): """Create a vlan unless it already exists.""" raise NotImplementedError() @@ -34,11 +34,11 @@ class L2Driver(object): class LinuxBridgeDriver(L2Driver): """L2 driver based on Linux Bridge.""" - def ensure_bridge(bridge, interface, net_attrs=None): + def ensure_bridge(self, bridge, interface, net_attrs=None): """Create a Linux bridge unless it already eixsts.""" linux_net.ensure_bridge(bridge, interface, net_attrs) - def ensure_vlan(vlan_num, bridge_interface, net_attrs=None): + def ensure_vlan(self, vlan_num, bridge_interface, net_attrs=None): """Create a vlan unless it already exists.""" return linux_net.ensure_vlan(vlan_num, bridge_interface, net_attrs) @@ -46,11 +46,11 @@ class LinuxBridgeDriver(L2Driver): class QuantumDriver(L2Driver): """L2 driver based on Quantum network service.""" - def ensure_bridge(bridge, interface, net_attrs=None): + def ensure_bridge(self, bridge, interface, net_attrs=None): """Do nothing.""" pass - def ensure_vlan(vlan_num, bridge_interface, net_attrs=None): + def ensure_vlan(self, vlan_num, bridge_interface, net_attrs=None): """Return None.""" return None -- cgit From a05b0325a76dea16a2d5f7d1931a5cbc922e0364 Mon Sep 17 00:00:00 2001 From: Ryu Ishimoto Date: Thu, 7 Jul 2011 02:13:18 +0900 Subject: Added VIF driver concept --- nova/virt/libvirt.xml.template | 18 ++++++++++ nova/virt/libvirt/connection.py | 48 +++++-------------------- nova/virt/libvirt/vif_drivers.py | 76 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 103 insertions(+), 39 deletions(-) create mode 100644 nova/virt/libvirt/vif_drivers.py diff --git a/nova/virt/libvirt.xml.template b/nova/virt/libvirt.xml.template index e1a683da8..ea27e5fd7 100644 --- a/nova/virt/libvirt.xml.template +++ b/nova/virt/libvirt.xml.template @@ -92,6 +92,22 @@ #end if #for $nic in $nics + #if $vif_type='ethernet' + + + +