diff options
89 files changed, 1712 insertions, 470 deletions
diff --git a/HACKING.rst b/HACKING.rst index be894f072..35493e55b 100644 --- a/HACKING.rst +++ b/HACKING.rst @@ -9,6 +9,7 @@ Nova Style Commandments General ------- - Put two newlines between top-level code (funcs, classes, etc) +- Use only UNIX style newlines ("\n"), not Windows style ("\r\n") - Put one newline between methods in classes and anywhere else - Long lines should be wrapped in parentheses in preference to using a backslash for line continuation. diff --git a/bin/nova-novncproxy b/bin/nova-novncproxy index beee143f5..8562acc53 100755 --- a/bin/nova-novncproxy +++ b/bin/nova-novncproxy @@ -21,20 +21,12 @@ Websocket proxy that is compatible with OpenStack Nova noVNC consoles. Leverages websockify.py by Joel Martin ''' -import Cookie import os -import socket import sys -import websockify - from nova import config -from nova.consoleauth import rpcapi as consoleauth_rpcapi -from nova import context +from nova.console import websocketproxy as ws from nova.openstack.common import cfg -from nova.openstack.common import log as logging -from nova.openstack.common import rpc -from nova import utils opts = [ @@ -69,64 +61,6 @@ opts = [ CONF = cfg.CONF CONF.register_cli_opts(opts) -LOG = logging.getLogger(__name__) - - -class NovaWebSocketProxy(websockify.WebSocketProxy): - def __init__(self, *args, **kwargs): - websockify.WebSocketProxy.__init__(self, unix_target=None, - target_cfg=None, - ssl_target=None, *args, **kwargs) - - def new_client(self): - """ - Called after a new WebSocket connection has been established. - """ - cookie = Cookie.SimpleCookie() - cookie.load(self.headers.getheader('cookie')) - token = cookie['token'].value - ctxt = context.get_admin_context() - rpcapi = consoleauth_rpcapi.ConsoleAuthAPI() - connect_info = rpcapi.check_token(ctxt, token=token) - - if not connect_info: - LOG.audit("Invalid Token: %s", token) - raise Exception(_("Invalid Token")) - - host = connect_info['host'] - port = int(connect_info['port']) - - # Connect to the target - self.msg("connecting to: %s:%s" % (host, port)) - LOG.audit("connecting to: %s:%s" % (host, port)) - tsock = self.socket(host, port, connect=True) - - # Handshake as necessary - if connect_info.get('internal_access_path'): - tsock.send("CONNECT %s HTTP/1.1\r\n\r\n" % - connect_info['internal_access_path']) - while True: - data = tsock.recv(4096, socket.MSG_PEEK) - if data.find("\r\n\r\n") != -1: - if not data.split("\r\n")[0].find("200"): - LOG.audit("Invalid Connection Info %s", token) - raise Exception(_("Invalid Connection Info")) - tsock.recv(len(data)) - break - - if self.verbose and not self.daemon: - print(self.traffic_legend) - - # Start proxying - try: - self.do_proxy(tsock) - except Exception: - if tsock: - tsock.shutdown(socket.SHUT_RDWR) - tsock.close() - self.vmsg("%s:%s: Target closed" % (host, port)) - LOG.audit("%s:%s: Target closed" % (host, port)) - raise if __name__ == '__main__': @@ -142,18 +76,18 @@ if __name__ == '__main__': sys.exit(-1) # Create and start the NovaWebSockets proxy - server = NovaWebSocketProxy(listen_host=CONF.novncproxy_host, - listen_port=CONF.novncproxy_port, - source_is_ipv6=CONF.source_is_ipv6, - verbose=CONF.verbose, - cert=CONF.cert, - key=CONF.key, - ssl_only=CONF.ssl_only, - daemon=CONF.daemon, - record=CONF.record, - web=CONF.web, - target_host='ignore', - target_port='ignore', - wrap_mode='exit', - wrap_cmd=None) + server = ws.NovaWebSocketProxy(listen_host=CONF.novncproxy_host, + listen_port=CONF.novncproxy_port, + source_is_ipv6=CONF.source_is_ipv6, + verbose=CONF.verbose, + cert=CONF.cert, + key=CONF.key, + ssl_only=CONF.ssl_only, + daemon=CONF.daemon, + record=CONF.record, + web=CONF.web, + target_host='ignore', + target_port='ignore', + wrap_mode='exit', + wrap_cmd=None) server.start_server() diff --git a/bin/nova-spicehtml5proxy b/bin/nova-spicehtml5proxy new file mode 100755 index 000000000..b1882bbea --- /dev/null +++ b/bin/nova-spicehtml5proxy @@ -0,0 +1,93 @@ +#!/usr/bin/env python +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright (c) 2012 OpenStack, LLC. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +''' +Websocket proxy that is compatible with OpenStack Nova +SPICE HTML5 consoles. Leverages websockify.py by Joel Martin +''' + +import os +import sys + +from nova import config +from nova.console import websocketproxy as ws +from nova.openstack.common import cfg + + +opts = [ + cfg.BoolOpt('record', + default=False, + help='Record sessions to FILE.[session_number]'), + cfg.BoolOpt('daemon', + default=False, + help='Become a daemon (background process)'), + cfg.BoolOpt('ssl_only', + default=False, + help='Disallow non-encrypted connections'), + cfg.BoolOpt('source_is_ipv6', + default=False, + help='Source is ipv6'), + cfg.StrOpt('cert', + default='self.pem', + help='SSL certificate file'), + cfg.StrOpt('key', + default=None, + help='SSL key file (if separate from cert)'), + cfg.StrOpt('web', + default='/usr/share/spice-html5', + help='Run webserver on same port. Serve files from DIR.'), + cfg.StrOpt('spicehtml5proxy_host', + default='0.0.0.0', + help='Host on which to listen for incoming requests'), + cfg.IntOpt('spicehtml5proxy_port', + default=6082, + help='Port on which to listen for incoming requests'), + ] + +CONF = cfg.CONF +CONF.register_cli_opts(opts) + + +if __name__ == '__main__': + if CONF.ssl_only and not os.path.exists(CONF.cert): + parser.error("SSL only and %s not found" % CONF.cert) + + # Setup flags + config.parse_args(sys.argv) + + # Check to see if spice html/js/css files are present + if not os.path.exists(CONF.web): + print "Can not find spice html/js/css files at %s." % CONF.web + sys.exit(-1) + + # Create and start the NovaWebSockets proxy + server = ws.NovaWebSocketProxy(listen_host=CONF.spicehtml5proxy_host, + listen_port=CONF.spicehtml5proxy_port, + source_is_ipv6=CONF.source_is_ipv6, + verbose=CONF.verbose, + cert=CONF.cert, + key=CONF.key, + ssl_only=CONF.ssl_only, + daemon=CONF.daemon, + record=CONF.record, + web=CONF.web, + target_host='ignore', + target_port='ignore', + wrap_mode='exit', + wrap_cmd=None) + server.start_server() diff --git a/doc/api_samples/os-consoles/get-spice-console-post-req.json b/doc/api_samples/os-consoles/get-spice-console-post-req.json new file mode 100644 index 000000000..d04f7c7ae --- /dev/null +++ b/doc/api_samples/os-consoles/get-spice-console-post-req.json @@ -0,0 +1,5 @@ +{ + "os-getSPICEConsole": { + "type": "spice-html5" + } +} diff --git a/doc/api_samples/os-consoles/get-spice-console-post-req.xml b/doc/api_samples/os-consoles/get-spice-console-post-req.xml new file mode 100644 index 000000000..59052abea --- /dev/null +++ b/doc/api_samples/os-consoles/get-spice-console-post-req.xml @@ -0,0 +1,2 @@ +<?xml version="1.0" encoding="UTF-8"?> +<os-getSPICEConsole type="spice-html5" /> diff --git a/doc/api_samples/os-consoles/get-spice-console-post-resp.json b/doc/api_samples/os-consoles/get-spice-console-post-resp.json new file mode 100644 index 000000000..f4999e1ba --- /dev/null +++ b/doc/api_samples/os-consoles/get-spice-console-post-resp.json @@ -0,0 +1,6 @@ +{ + "console": { + "type": "spice-html5", + "url": "http://example.com:6080/spice_auto.html?token=f9906a48-b71e-4f18-baca-c987da3ebdb3&title=dafa(75ecef58-3b8e-4659-ab3b-5501454188e9)" + } +} diff --git a/doc/api_samples/os-consoles/get-spice-console-post-resp.xml b/doc/api_samples/os-consoles/get-spice-console-post-resp.xml new file mode 100644 index 000000000..acba8b1f0 --- /dev/null +++ b/doc/api_samples/os-consoles/get-spice-console-post-resp.xml @@ -0,0 +1,5 @@ +<?xml version='1.0' encoding='UTF-8'?> +<console> + <type>spice-html5</type> + <url>http://example.com:6080/spice_auto.html?token=f9906a48-b71e-4f18-baca-c987da3ebdb3</url> +</console> diff --git a/doc/source/conf.py b/doc/source/conf.py index 804080e79..0bdaeb08e 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -145,6 +145,8 @@ man_pages = [ [u'OpenStack'], 1), ('man/nova-novncproxy', 'nova-novncproxy', u'Cloud controller fabric', [u'OpenStack'], 1), + ('man/nova-spicehtml5proxy', 'nova-spicehtml5proxy', u'Cloud controller fabric', + [u'OpenStack'], 1), ('man/nova-objectstore', 'nova-objectstore', u'Cloud controller fabric', [u'OpenStack'], 1), ('man/nova-rootwrap', 'nova-rootwrap', u'Cloud controller fabric', diff --git a/doc/source/man/nova-spicehtml5proxy.rst b/doc/source/man/nova-spicehtml5proxy.rst new file mode 100644 index 000000000..4d0aaa202 --- /dev/null +++ b/doc/source/man/nova-spicehtml5proxy.rst @@ -0,0 +1,48 @@ +==================== +nova-spicehtml5proxy +==================== + +-------------------------------------------------------- +Websocket Proxy for OpenStack Nova SPICE HTML5 consoles. +-------------------------------------------------------- + +:Author: openstack@lists.launchpad.net +:Date: 2012-09-27 +:Copyright: OpenStack LLC +:Version: 2012.1 +:Manual section: 1 +:Manual group: cloud computing + +SYNOPSIS +======== + + nova-spicehtml5proxy [options] + +DESCRIPTION +=========== + +Websocket proxy that is compatible with OpenStack Nova +SPICE HTML5 consoles. + +OPTIONS +======= + + **General options** + +FILES +======== + +* /etc/nova/nova.conf +* /etc/nova/policy.json +* /etc/nova/rootwrap.conf +* /etc/nova/rootwrap.d/ + +SEE ALSO +======== + +* `OpenStack Nova <http://nova.openstack.org>`__ + +BUGS +==== + +* Nova is sourced in Launchpad so you can view current bugs at `OpenStack Nova <http://nova.openstack.org>`__ diff --git a/etc/nova/nova.conf.sample b/etc/nova/nova.conf.sample index 96118eb76..36a7b0d9c 100644 --- a/etc/nova/nova.conf.sample +++ b/etc/nova/nova.conf.sample @@ -1756,7 +1756,7 @@ # value) #hyperv_attaching_volume_retry_count=10 -# The seconds to wait between an volume attachment attempt +# The seconds to wait between a volume attachment attempt # (integer value) #hyperv_wait_between_attach_retry=5 @@ -2282,6 +2282,10 @@ # value) #cinder_http_retries=3 +# Allow to perform insecure SSL (https) requests to cinder +# (boolean value) +#cinder_api_insecure=false + [conductor] @@ -2476,7 +2480,7 @@ # # Do not set this out of dev/test environments. If a node does -# not have an fixed PXE IP address, volumes are exported with +# not have a fixed PXE IP address, volumes are exported with # globally opened ACL (boolean value) #use_unsafe_iscsi=false @@ -2546,4 +2550,4 @@ #keymap=en-us -# Total option count: 519 +# Total option count: 520 diff --git a/etc/nova/rootwrap.d/baremetal-compute-pxe.filters b/etc/nova/rootwrap.d/baremetal-compute-pxe.filters deleted file mode 100644 index 35fa61723..000000000 --- a/etc/nova/rootwrap.d/baremetal-compute-pxe.filters +++ /dev/null @@ -1,11 +0,0 @@ -# nova-rootwrap command filters for compute nodes -# This file should be owned by (and only-writeable by) the root user - -[Filters] - -# nova/virt/baremetal/pxe.py: 'dnsmasq', ... -dnsmasq: CommandFilter, /usr/sbin/dnsmasq, root - -# nova/virt/baremetal/pxe.py: 'kill', '-TERM', str(dnsmasq_pid) -kill_dnsmasq: KillFilter, root, /usr/sbin/dnsmasq, -15, -TERM - diff --git a/nova/api/openstack/compute/contrib/admin_actions.py b/nova/api/openstack/compute/contrib/admin_actions.py index fa7836b37..1c053ea59 100644 --- a/nova/api/openstack/compute/contrib/admin_actions.py +++ b/nova/api/openstack/compute/contrib/admin_actions.py @@ -130,7 +130,7 @@ class AdminActionsController(wsgi.Controller): @wsgi.action('resetNetwork') def _reset_network(self, req, id, body): - """Permit admins to reset networking on an server.""" + """Permit admins to reset networking on a server.""" context = req.environ['nova.context'] authorize(context, 'resetNetwork') try: diff --git a/nova/api/openstack/compute/contrib/consoles.py b/nova/api/openstack/compute/contrib/consoles.py index 4f88d033c..4895a9e7b 100644 --- a/nova/api/openstack/compute/contrib/consoles.py +++ b/nova/api/openstack/compute/contrib/consoles.py @@ -53,10 +53,33 @@ class ConsolesController(wsgi.Controller): return {'console': {'type': console_type, 'url': output['url']}} + @wsgi.action('os-getSPICEConsole') + def get_spice_console(self, req, id, body): + """Get text console output.""" + context = req.environ['nova.context'] + authorize(context) + + # If type is not supplied or unknown, get_spice_console below will cope + console_type = body['os-getSPICEConsole'].get('type') + + try: + instance = self.compute_api.get(context, id) + output = self.compute_api.get_spice_console(context, + instance, + console_type) + except exception.InstanceNotFound as e: + raise webob.exc.HTTPNotFound(explanation=unicode(e)) + except exception.InstanceNotReady as e: + raise webob.exc.HTTPConflict(explanation=unicode(e)) + + return {'console': {'type': console_type, 'url': output['url']}} + def get_actions(self): """Return the actions the extension adds, as required by contract.""" actions = [extensions.ActionExtension("servers", "os-getVNCConsole", - self.get_vnc_console)] + self.get_vnc_console), + extensions.ActionExtension("servers", "os-getSPICEConsole", + self.get_spice_console)] return actions diff --git a/nova/api/openstack/compute/servers.py b/nova/api/openstack/compute/servers.py index f0fdb5a15..308530b8e 100644 --- a/nova/api/openstack/compute/servers.py +++ b/nova/api/openstack/compute/servers.py @@ -561,17 +561,28 @@ class Controller(wsgi.Controller): req.cache_db_instance(instance) return instance - def _validate_server_name(self, value): + def _check_string_length(self, value, name, max_length=None): if not isinstance(value, basestring): - msg = _("Server name is not a string or unicode") + msg = _("%s is not a string or unicode") % name raise exc.HTTPBadRequest(explanation=msg) if not value.strip(): - msg = _("Server name is an empty string") + msg = _("%s is an empty string") % name + raise exc.HTTPBadRequest(explanation=msg) + + if max_length and len(value) > max_length: + msg = _("%(name)s can be at most %(max_length)s " + "characters.") % locals() raise exc.HTTPBadRequest(explanation=msg) - if not len(value) < 256: - msg = _("Server name must be less than 256 characters.") + def _validate_server_name(self, value): + self._check_string_length(value, 'Server name', max_length=255) + + def _validate_device_name(self, value): + self._check_string_length(value, 'Device name', max_length=255) + + if ' ' in value: + msg = _("Device name cannot include spaces.") raise exc.HTTPBadRequest(explanation=msg) def _get_injected_files(self, personality): @@ -809,6 +820,7 @@ class Controller(wsgi.Controller): if self.ext_mgr.is_loaded('os-volumes'): block_device_mapping = server_dict.get('block_device_mapping', []) for bdm in block_device_mapping: + self._validate_device_name(bdm["device_name"]) if 'delete_on_termination' in bdm: bdm['delete_on_termination'] = utils.bool_from_str( bdm['delete_on_termination']) diff --git a/nova/api/openstack/wsgi.py b/nova/api/openstack/wsgi.py index 519669134..733685b14 100644 --- a/nova/api/openstack/wsgi.py +++ b/nova/api/openstack/wsgi.py @@ -919,6 +919,10 @@ class Resource(wsgi.Application): msg = _("Malformed request body") return Fault(webob.exc.HTTPBadRequest(explanation=msg)) + if body: + LOG.info(_("Action: '%(action)s', body: %(body)s") % locals()) + LOG.debug(_("Calling method %s") % meth) + # Now, deserialize the request body... try: if content_type: diff --git a/nova/availability_zones.py b/nova/availability_zones.py index cb5cce591..7493ce5c5 100644 --- a/nova/availability_zones.py +++ b/nova/availability_zones.py @@ -13,7 +13,7 @@ # License for the specific language governing permissions and limitations # under the License. -"""utilities for multiple APIs.""" +"""Availability zone helper functions.""" from nova import db from nova.openstack.common import cfg diff --git a/nova/compute/api.py b/nova/compute/api.py index 8ba6b97aa..4b15a3e27 100644 --- a/nova/compute/api.py +++ b/nova/compute/api.py @@ -946,11 +946,10 @@ class API(base.Base): if (old['vm_state'] != vm_states.SOFT_DELETED and old['task_state'] not in (task_states.DELETING, task_states.SOFT_DELETING)): - reservations = QUOTAS.reserve(context, - project_id=project_id, - instances=-1, - cores=-instance['vcpus'], - ram=-instance['memory_mb']) + reservations = self._create_reservations(context, + old, + updated, + project_id) if not host: # Just update database, nothing else we can do @@ -1026,6 +1025,45 @@ class API(base.Base): reservations, project_id=project_id) + def _create_reservations(self, context, old_instance, new_instance, + project_id): + instance_vcpus = old_instance['vcpus'] + instance_memory_mb = old_instance['memory_mb'] + # NOTE(wangpan): if the instance is resizing, and the resources + # are updated to new instance type, we should use + # the old instance type to create reservation. + # see https://bugs.launchpad.net/nova/+bug/1099729 for more details + if old_instance['task_state'] in (task_states.RESIZE_MIGRATED, + task_states.RESIZE_FINISH): + get_migration = self.db.migration_get_by_instance_and_status + try: + migration_ref = get_migration(context.elevated(), + old_instance['uuid'], 'post-migrating') + except exception.MigrationNotFoundByStatus: + migration_ref = None + if (migration_ref and + new_instance['instance_type_id'] == + migration_ref['new_instance_type_id']): + old_inst_type_id = migration_ref['old_instance_type_id'] + get_inst_type_by_id = instance_types.get_instance_type + try: + old_inst_type = get_inst_type_by_id(old_inst_type_id) + except exception.InstanceTypeNotFound: + LOG.warning(_("instance type %(old_inst_type_id)d " + "not found") % locals()) + pass + else: + instance_vcpus = old_inst_type['vcpus'] + instance_memory_mb = old_inst_type['memory_mb'] + LOG.debug(_("going to delete a resizing instance")) + + reservations = QUOTAS.reserve(context, + project_id=project_id, + instances=-1, + cores=-instance_vcpus, + ram=-instance_memory_mb) + return reservations + def _local_delete(self, context, instance, bdms): LOG.warning(_("instance's host %s is down, deleting from " "database") % instance['host'], instance=instance) @@ -1182,8 +1220,10 @@ class API(base.Base): # NOTE(ameade): we still need to support integer ids for ec2 if uuidutils.is_uuid_like(instance_id): instance = self.db.instance_get_by_uuid(context, instance_id) - else: + elif utils.is_int_like(instance_id): instance = self.db.instance_get(context, instance_id) + else: + raise exception.InstanceNotFound(instance_id=instance_id) check_policy(context, 'get', instance) @@ -2031,6 +2071,29 @@ class API(base.Base): return connect_info @wrap_check_policy + def get_spice_console(self, context, instance, console_type): + """Get a url to an instance Console.""" + if not instance['host']: + raise exception.InstanceNotReady(instance_id=instance['uuid']) + + connect_info = self.compute_rpcapi.get_spice_console(context, + instance=instance, console_type=console_type) + + self.consoleauth_rpcapi.authorize_console(context, + connect_info['token'], console_type, connect_info['host'], + connect_info['port'], connect_info['internal_access_path']) + + return {'url': connect_info['access_url']} + + def get_spice_connect_info(self, context, instance, console_type): + """Used in a child cell to get console info.""" + if not instance['host']: + raise exception.InstanceNotReady(instance_id=instance['uuid']) + connect_info = self.compute_rpcapi.get_spice_console(context, + instance=instance, console_type=console_type) + return connect_info + + @wrap_check_policy def get_console_output(self, context, instance, tail_length=None): """Get console output for an instance.""" return self.compute_rpcapi.get_console_output(context, diff --git a/nova/compute/cells_api.py b/nova/compute/cells_api.py index d1d9a11d2..d5427a04b 100644 --- a/nova/compute/cells_api.py +++ b/nova/compute/cells_api.py @@ -439,6 +439,21 @@ class ComputeCellsAPI(compute_api.API): connect_info['port'], connect_info['internal_access_path']) return {'url': connect_info['access_url']} + @wrap_check_policy + @validate_cell + def get_spice_console(self, context, instance, console_type): + """Get a url to a SPICE Console.""" + if not instance['host']: + raise exception.InstanceNotReady(instance_id=instance['uuid']) + + connect_info = self._call_to_cells(context, instance, + 'get_spice_connect_info', console_type) + + self.consoleauth_rpcapi.authorize_console(context, + connect_info['token'], console_type, connect_info['host'], + connect_info['port'], connect_info['internal_access_path']) + return {'url': connect_info['access_url']} + @validate_cell def get_console_output(self, context, instance, *args, **kwargs): """Get console output for an an instance.""" diff --git a/nova/compute/manager.py b/nova/compute/manager.py index 3bf8e61ef..03c54a363 100644 --- a/nova/compute/manager.py +++ b/nova/compute/manager.py @@ -230,7 +230,7 @@ def wrap_instance_fault(function): with excutils.save_and_reraise_exception(): compute_utils.add_instance_fault_from_exc(context, - kwargs['instance']['uuid'], e, sys.exc_info()) + kwargs['instance'], e, sys.exc_info()) return decorated_function @@ -293,7 +293,7 @@ class ComputeVirtAPI(virtapi.VirtAPI): class ComputeManager(manager.SchedulerDependentManager): """Manages the running instances from creation to destruction.""" - RPC_API_VERSION = '2.23' + RPC_API_VERSION = '2.24' def __init__(self, compute_driver=None, *args, **kwargs): """Load configuration options and connect to the hypervisor.""" @@ -463,6 +463,11 @@ class ComputeManager(manager.SchedulerDependentManager): except NotImplementedError: LOG.warning(_('Hypervisor driver does not support ' 'resume guests'), instance=instance) + except Exception: + # NOTE(vish): The instance failed to resume, so we set the + # instance to error and attempt to continue. + LOG.warning(_('Failed to resume instance'), instance=instance) + self._set_instance_error_state(context, instance['uuid']) elif drv_state == power_state.RUNNING: # VMwareAPI drivers will raise an exception @@ -730,8 +735,8 @@ class ComputeManager(manager.SchedulerDependentManager): instance_uuid = instance['uuid'] rescheduled = False - compute_utils.add_instance_fault_from_exc(context, instance_uuid, - exc_info[0], exc_info=exc_info) + compute_utils.add_instance_fault_from_exc(context, instance, + exc_info[1], exc_info=exc_info) try: self._deallocate_network(context, instance) @@ -1464,7 +1469,7 @@ class ComputeManager(manager.SchedulerDependentManager): LOG.error(_('Cannot reboot instance: %(exc)s'), locals(), context=context, instance=instance) compute_utils.add_instance_fault_from_exc(context, - instance['uuid'], exc, sys.exc_info()) + instance, exc, sys.exc_info()) # Fall through and reset task_state to None current_power_state = self._get_power_state(context, instance) @@ -1995,7 +2000,7 @@ class ComputeManager(manager.SchedulerDependentManager): rescheduled = False instance_uuid = instance['uuid'] - compute_utils.add_instance_fault_from_exc(context, instance_uuid, + compute_utils.add_instance_fault_from_exc(context, instance, exc_info[0], exc_info=exc_info) try: @@ -2387,6 +2392,9 @@ class ComputeManager(manager.SchedulerDependentManager): LOG.debug(_("Getting vnc console"), instance=instance) token = str(uuid.uuid4()) + if not CONF.vnc_enabled: + raise exception.ConsoleTypeInvalid(console_type=console_type) + if console_type == 'novnc': # For essex, novncproxy_base_url must include the full path # including the html file (like http://myhost/vnc_auto.html) @@ -2404,6 +2412,33 @@ class ComputeManager(manager.SchedulerDependentManager): return connect_info + @exception.wrap_exception(notifier=notifier, publisher_id=publisher_id()) + @wrap_instance_fault + def get_spice_console(self, context, console_type, instance): + """Return connection information for a spice console.""" + context = context.elevated() + LOG.debug(_("Getting spice console"), instance=instance) + token = str(uuid.uuid4()) + + if not CONF.spice.enabled: + raise exception.ConsoleTypeInvalid(console_type=console_type) + + if console_type == 'spice-html5': + # For essex, spicehtml5proxy_base_url must include the full path + # including the html file (like http://myhost/spice_auto.html) + access_url = '%s?token=%s' % (CONF.spice.html5proxy_base_url, + token) + else: + raise exception.ConsoleTypeInvalid(console_type=console_type) + + # Retrieve connect info from driver, and then decorate with our + # access info token + connect_info = self.driver.get_spice_console(instance) + connect_info['token'] = token + connect_info['access_url'] = access_url + + return connect_info + def _attach_volume_boot(self, context, instance, volume, mountpoint): """Attach a volume to an instance at boot time. So actual attach is done by instance creation""" @@ -2429,8 +2464,11 @@ class ComputeManager(manager.SchedulerDependentManager): @lockutils.synchronized(instance['uuid'], 'nova-') def do_reserve(): + bdms = self.conductor_api.block_device_mapping_get_all_by_instance( + context, instance) result = compute_utils.get_device_name_for_instance(context, instance, + bdms, device) # NOTE(vish): create bdm here to avoid race condition values = {'instance_uuid': instance['uuid'], diff --git a/nova/compute/resource_tracker.py b/nova/compute/resource_tracker.py index 055963873..be0360185 100644 --- a/nova/compute/resource_tracker.py +++ b/nova/compute/resource_tracker.py @@ -25,7 +25,6 @@ from nova.compute import task_states from nova.compute import vm_states from nova import conductor from nova import context -from nova import db from nova import exception from nova.openstack.common import cfg from nova.openstack.common import importutils @@ -304,8 +303,8 @@ class ResourceTracker(object): def _create(self, context, values): """Create the compute node in the DB.""" # initialize load stats from existing instances: - compute_node = db.compute_node_create(context, values) - self.compute_node = dict(compute_node) + self.compute_node = self.conductor_api.compute_node_create(context, + values) def _get_service(self, context): try: @@ -349,9 +348,10 @@ class ResourceTracker(object): def _update(self, context, values, prune_stats=False): """Persist the compute node updates to the DB.""" - compute_node = db.compute_node_update(context, - self.compute_node['id'], values, prune_stats) - self.compute_node = dict(compute_node) + if "service" in self.compute_node: + del self.compute_node['service'] + self.compute_node = self.conductor_api.compute_node_update( + context, self.compute_node, values, prune_stats) def confirm_resize(self, context, migration, status='confirmed'): """Cleanup usage for a confirmed resize.""" diff --git a/nova/compute/rpcapi.py b/nova/compute/rpcapi.py index 3e7ed1cfd..525d1adc7 100644 --- a/nova/compute/rpcapi.py +++ b/nova/compute/rpcapi.py @@ -158,6 +158,7 @@ class ComputeAPI(nova.openstack.common.rpc.proxy.RpcProxy): 2.22 - Add recreate, on_shared_storage and host arguments to rebuild_instance() 2.23 - Remove network_info from reboot_instance + 2.24 - Added get_spice_console method ''' # @@ -295,6 +296,13 @@ class ComputeAPI(nova.openstack.common.rpc.proxy.RpcProxy): instance=instance_p, console_type=console_type), topic=_compute_topic(self.topic, ctxt, None, instance)) + def get_spice_console(self, ctxt, instance, console_type): + instance_p = jsonutils.to_primitive(instance) + return self.call(ctxt, self.make_msg('get_spice_console', + instance=instance_p, console_type=console_type), + topic=_compute_topic(self.topic, ctxt, None, instance), + version='2.24') + def host_maintenance_mode(self, ctxt, host_param, mode, host): '''Set host maintenance mode diff --git a/nova/compute/utils.py b/nova/compute/utils.py index 0c475d082..2b1286e16 100644 --- a/nova/compute/utils.py +++ b/nova/compute/utils.py @@ -44,7 +44,7 @@ def metadata_to_dict(metadata): return result -def add_instance_fault_from_exc(context, instance_uuid, fault, exc_info=None): +def add_instance_fault_from_exc(context, instance, fault, exc_info=None): """Adds the specified fault to the database.""" code = 500 @@ -62,15 +62,16 @@ def add_instance_fault_from_exc(context, instance_uuid, fault, exc_info=None): details += '\n' + ''.join(traceback.format_tb(tb)) values = { - 'instance_uuid': instance_uuid, + 'instance_uuid': instance['uuid'], 'code': code, 'message': unicode(message), 'details': unicode(details), + 'host': CONF.host } db.instance_fault_create(context, values) -def get_device_name_for_instance(context, instance, device): +def get_device_name_for_instance(context, instance, bdms, device): """Validates (or generates) a device name for instance. If device is not set, it will generate a unique device appropriate @@ -87,8 +88,6 @@ def get_device_name_for_instance(context, instance, device): req_prefix, req_letters = block_device.match_device(device) except (TypeError, AttributeError, ValueError): raise exception.InvalidDevicePath(path=device) - bdms = db.block_device_mapping_get_all_by_instance(context, - instance['uuid']) mappings = block_device.instance_block_mapping(instance, bdms) try: prefix = block_device.match_device(mappings['root'])[0] diff --git a/nova/conductor/api.py b/nova/conductor/api.py index c805f403c..138e72f70 100644 --- a/nova/conductor/api.py +++ b/nova/conductor/api.py @@ -278,6 +278,16 @@ class LocalAPI(object): def service_destroy(self, context, service_id): return self._manager.service_destroy(context, service_id) + def compute_node_create(self, context, values): + return self._manager.compute_node_create(context, values) + + def compute_node_update(self, context, node, values, prune_stats=False): + return self._manager.compute_node_update(context, node, values, + prune_stats) + + def service_update(self, context, service, values): + return self._manager.service_update(context, service, values) + class API(object): """Conductor API that does updates via RPC to the ConductorManager.""" @@ -534,3 +544,13 @@ class API(object): def service_destroy(self, context, service_id): return self.conductor_rpcapi.service_destroy(context, service_id) + + def compute_node_create(self, context, values): + return self.conductor_rpcapi.compute_node_create(context, values) + + def compute_node_update(self, context, node, values, prune_stats=False): + return self.conductor_rpcapi.compute_node_update(context, node, + values, prune_stats) + + def service_update(self, context, service, values): + return self.conductor_rpcapi.service_update(context, service, values) diff --git a/nova/conductor/manager.py b/nova/conductor/manager.py index 4a82989ec..0ff2e1400 100644 --- a/nova/conductor/manager.py +++ b/nova/conductor/manager.py @@ -43,7 +43,7 @@ datetime_fields = ['launched_at', 'terminated_at'] class ConductorManager(manager.SchedulerDependentManager): """Mission: TBD.""" - RPC_API_VERSION = '1.32' + RPC_API_VERSION = '1.34' def __init__(self, *args, **kwargs): super(ConductorManager, self).__init__(service_name='conductor', @@ -301,3 +301,17 @@ class ConductorManager(manager.SchedulerDependentManager): @rpc_common.client_exceptions(exception.ServiceNotFound) def service_destroy(self, context, service_id): self.db.service_destroy(context, service_id) + + def compute_node_create(self, context, values): + result = self.db.compute_node_create(context, values) + return jsonutils.to_primitive(result) + + def compute_node_update(self, context, node, values, prune_stats=False): + result = self.db.compute_node_update(context, node['id'], values, + prune_stats) + return jsonutils.to_primitive(result) + + @rpc_common.client_exceptions(exception.ServiceNotFound) + def service_update(self, context, service, values): + svc = self.db.service_update(context, service['id'], values) + return jsonutils.to_primitive(svc) diff --git a/nova/conductor/rpcapi.py b/nova/conductor/rpcapi.py index e5f901289..6dc8aef04 100644 --- a/nova/conductor/rpcapi.py +++ b/nova/conductor/rpcapi.py @@ -65,6 +65,8 @@ class ConductorAPI(nova.openstack.common.rpc.proxy.RpcProxy): 1.30 - Added migration_create 1.31 - Added migration_get_in_progress_by_host_and_node 1.32 - Added optional node to instance_get_all_by_host + 1.33 - Added compute_node_create and compute_node_update + 1.34 - Added service_update """ BASE_RPC_API_VERSION = '1.0' @@ -305,3 +307,18 @@ class ConductorAPI(nova.openstack.common.rpc.proxy.RpcProxy): def service_destroy(self, context, service_id): msg = self.make_msg('service_destroy', service_id=service_id) return self.call(context, msg, version='1.29') + + def compute_node_create(self, context, values): + msg = self.make_msg('compute_node_create', values=values) + return self.call(context, msg, version='1.33') + + def compute_node_update(self, context, node, values, prune_stats=False): + node_p = jsonutils.to_primitive(node) + msg = self.make_msg('compute_node_update', node=node_p, values=values, + prune_stats=prune_stats) + return self.call(context, msg, version='1.33') + + def service_update(self, context, service, values): + service_p = jsonutils.to_primitive(service) + msg = self.make_msg('service_update', service=service_p, values=values) + return self.call(context, msg, version='1.34') diff --git a/nova/console/websocketproxy.py b/nova/console/websocketproxy.py new file mode 100644 index 000000000..ce9243d46 --- /dev/null +++ b/nova/console/websocketproxy.py @@ -0,0 +1,89 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright (c) 2012 OpenStack, LLC. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +''' +Websocket proxy that is compatible with OpenStack Nova. +Leverages websockify.py by Joel Martin +''' + +import Cookie +import socket + +import websockify + +from nova.consoleauth import rpcapi as consoleauth_rpcapi +from nova import context +from nova.openstack.common import log as logging + +LOG = logging.getLogger(__name__) + + +class NovaWebSocketProxy(websockify.WebSocketProxy): + def __init__(self, *args, **kwargs): + websockify.WebSocketProxy.__init__(self, unix_target=None, + target_cfg=None, + ssl_target=None, *args, **kwargs) + + def new_client(self): + """ + Called after a new WebSocket connection has been established. + """ + cookie = Cookie.SimpleCookie() + cookie.load(self.headers.getheader('cookie')) + token = cookie['token'].value + ctxt = context.get_admin_context() + rpcapi = consoleauth_rpcapi.ConsoleAuthAPI() + connect_info = rpcapi.check_token(ctxt, token=token) + + if not connect_info: + LOG.audit("Invalid Token: %s", token) + raise Exception(_("Invalid Token")) + + host = connect_info['host'] + port = int(connect_info['port']) + + # Connect to the target + self.msg("connecting to: %s:%s" % (host, port)) + LOG.audit("connecting to: %s:%s" % (host, port)) + tsock = self.socket(host, port, connect=True) + + # Handshake as necessary + if connect_info.get('internal_access_path'): + tsock.send("CONNECT %s HTTP/1.1\r\n\r\n" % + connect_info['internal_access_path']) + while True: + data = tsock.recv(4096, socket.MSG_PEEK) + if data.find("\r\n\r\n") != -1: + if not data.split("\r\n")[0].find("200"): + LOG.audit("Invalid Connection Info %s", token) + raise Exception(_("Invalid Connection Info")) + tsock.recv(len(data)) + break + + if self.verbose and not self.daemon: + print(self.traffic_legend) + + # Start proxying + try: + self.do_proxy(tsock) + except Exception: + if tsock: + tsock.shutdown(socket.SHUT_RDWR) + tsock.close() + self.vmsg("%s:%s: Target closed" % (host, port)) + LOG.audit("%s:%s: Target closed" % (host, port)) + raise diff --git a/nova/crypto.py b/nova/crypto.py index ff76a54d0..68d25e650 100644 --- a/nova/crypto.py +++ b/nova/crypto.py @@ -171,13 +171,44 @@ def decrypt_text(project_id, text): raise exception.ProjectNotFound(project_id=project_id) try: dec, _err = utils.execute('openssl', - 'rsautl', - '-decrypt', - '-inkey', '%s' % private_key, - process_input=text) + 'rsautl', + '-decrypt', + '-inkey', '%s' % private_key, + process_input=text) return dec - except exception.ProcessExecutionError: - raise exception.DecryptionFailure() + except exception.ProcessExecutionError as exc: + raise exception.DecryptionFailure(reason=exc.stderr) + + +def ssh_encrypt_text(ssh_public_key, text): + """Encrypt text with an ssh public key. + + Requires recent ssh-keygen binary in addition to openssl binary. + """ + with utils.tempdir() as tmpdir: + sshkey = os.path.abspath(os.path.join(tmpdir, 'ssh.key')) + with open(sshkey, 'w') as f: + f.write(ssh_public_key) + sslkey = os.path.abspath(os.path.join(tmpdir, 'ssl.key')) + try: + # NOTE(vish): -P is to skip prompt on bad keys + out, _err = utils.execute('ssh-keygen', + '-P', '', + '-e', + '-f', sshkey, + '-m', 'PKCS8') + with open(sslkey, 'w') as f: + f.write(out) + enc, _err = utils.execute('openssl', + 'rsautl', + '-encrypt', + '-pubin', + '-inkey', sslkey, + '-keyform', 'PEM', + process_input=text) + return enc + except exception.ProcessExecutionError as exc: + raise exception.EncryptionFailure(reason=exc.stderr) def revoke_cert(project_id, file_name): diff --git a/nova/db/api.py b/nova/db/api.py index ecfcfab15..d8a16c52d 100644 --- a/nova/db/api.py +++ b/nova/db/api.py @@ -497,6 +497,11 @@ def fixed_ip_get_by_address_detailed(context, address): return IMPL.fixed_ip_get_by_address_detailed(context, address) +def fixed_ip_get_by_floating_address(context, floating_address): + """Get a fixed ip by a floating address.""" + return IMPL.fixed_ip_get_by_floating_address(context, floating_address) + + def fixed_ip_get_by_instance(context, instance_uuid): """Get fixed ips by instance or raise if none exist.""" return IMPL.fixed_ip_get_by_instance(context, instance_uuid) @@ -754,12 +759,13 @@ def instance_info_cache_update(context, instance_uuid, values, :param values: = dict containing column values to update """ rv = IMPL.instance_info_cache_update(context, instance_uuid, values) - try: - cells_rpcapi.CellsAPI().instance_info_cache_update_at_top(context, - rv) - except Exception: - LOG.exception(_("Failed to notify cells of instance info cache " - "update")) + if update_cells: + try: + cells_rpcapi.CellsAPI().instance_info_cache_update_at_top( + context, rv) + except Exception: + LOG.exception(_("Failed to notify cells of instance info " + "cache update")) return rv diff --git a/nova/db/sqlalchemy/api.py b/nova/db/sqlalchemy/api.py index 038a47ca1..cc83ec4f5 100644 --- a/nova/db/sqlalchemy/api.py +++ b/nova/db/sqlalchemy/api.py @@ -253,6 +253,12 @@ def exact_filter(query, model, filters, legal_keys): return query +def convert_datetimes(values, *datetime_keys): + for key in values: + if key in datetime_keys and isinstance(values[key], basestring): + values[key] = timeutils.parse_strtime(values[key]) + return values + ################### @@ -442,6 +448,7 @@ def service_update(context, service_id, values): service_ref = service_get(context, service_id, session=session) service_ref.update(values) service_ref.save(session=session) + return service_ref ################### @@ -497,6 +504,7 @@ def compute_node_create(context, values): """Creates a new ComputeNode and populates the capacity fields with the most recent data.""" _prep_stats_dict(values) + convert_datetimes(values, 'created_at', 'deleted_at', 'updated_at') compute_node_ref = models.ComputeNode() compute_node_ref.update(values) @@ -545,9 +553,10 @@ def compute_node_update(context, compute_id, values, prune_stats=False): stats = values.pop('stats', {}) session = get_session() - with session.begin(subtransactions=True): + with session.begin(): _update_stats(context, stats, compute_id, session, prune_stats) compute_ref = _compute_node_get(context, compute_id, session=session) + convert_datetimes(values, 'created_at', 'deleted_at', 'updated_at') compute_ref.update(values) return compute_ref @@ -1187,6 +1196,20 @@ def fixed_ip_get_by_address_detailed(context, address, session=None): @require_context +def fixed_ip_get_by_floating_address(context, floating_address): + subq = model_query(context, models.FloatingIp.fixed_ip_id, + read_deleted="no").\ + filter_by(address=floating_address).\ + limit(1).\ + subquery() + return model_query(context, models.FixedIp, read_deleted="no").\ + filter_by(id=subq.as_scalar()).\ + first() + + # NOTE(tr3buchet) please don't invent an exception here, empty list is fine + + +@require_context def fixed_ip_get_by_instance(context, instance_uuid): if not uuidutils.is_uuid_like(instance_uuid): raise exception.InvalidUUID(uuid=instance_uuid) @@ -4691,49 +4714,44 @@ def _ec2_instance_get_query(context, session=None): @require_admin_context -def task_log_get(context, task_name, period_beginning, - period_ending, host, state=None, session=None): +def _task_log_get_query(context, task_name, period_beginning, + period_ending, host=None, state=None, session=None): query = model_query(context, models.TaskLog, session=session).\ filter_by(task_name=task_name).\ filter_by(period_beginning=period_beginning).\ - filter_by(period_ending=period_ending).\ - filter_by(host=host) + filter_by(period_ending=period_ending) + if host is not None: + query = query.filter_by(host=host) if state is not None: query = query.filter_by(state=state) + return query - return query.first() + +@require_admin_context +def task_log_get(context, task_name, period_beginning, period_ending, host, + state=None): + return _task_log_get_query(task_name, period_beginning, period_ending, + host, state).first() @require_admin_context -def task_log_get_all(context, task_name, period_beginning, - period_ending, host=None, state=None, session=None): - query = model_query(context, models.TaskLog, session=session).\ - filter_by(task_name=task_name).\ - filter_by(period_beginning=period_beginning).\ - filter_by(period_ending=period_ending) - if host is not None: - query = query.filter_by(host=host) - if state is not None: - query = query.filter_by(state=state) - return query.all() +def task_log_get_all(context, task_name, period_beginning, period_ending, + host=None, state=None): + return _task_log_get_query(task_name, period_beginning, period_ending, + host, state).all() @require_admin_context -def task_log_begin_task(context, task_name, - period_beginning, - period_ending, - host, - task_items=None, - message=None, - session=None): - session = session or get_session() +def task_log_begin_task(context, task_name, period_beginning, period_ending, + host, task_items=None, message=None): + # NOTE(boris-42): This method has a race condition and will be rewritten + # after bp/db-unique-keys implementation. + session = get_session() with session.begin(): - task = task_log_get(context, task_name, - period_beginning, - period_ending, - host, - session=session) - if task: + task_ref = _task_log_get_query(context, task_name, period_beginning, + period_ending, host, session=session).\ + first() + if task_ref: #It's already run(ning)! raise exception.TaskAlreadyRunning(task_name=task_name, host=host) task = models.TaskLog() @@ -4747,30 +4765,20 @@ def task_log_begin_task(context, task_name, if task_items: task.task_items = task_items task.save(session=session) - return task @require_admin_context -def task_log_end_task(context, task_name, - period_beginning, - period_ending, - host, - errors, - message=None, - session=None): - session = session or get_session() +def task_log_end_task(context, task_name, period_beginning, period_ending, + host, errors, message=None): + values = dict(state="DONE", errors=errors) + if message: + values["message"] = message + + session = get_session() with session.begin(): - task = task_log_get(context, task_name, - period_beginning, - period_ending, - host, - session=session) - if not task: + rows = _task_log_get_query(context, task_name, period_beginning, + period_ending, host, session=session).\ + update(values) + if rows == 0: #It's not running! raise exception.TaskNotRunning(task_name=task_name, host=host) - task.state = "DONE" - if message: - task.message = message - task.errors = errors - task.save(session=session) - return task diff --git a/nova/db/sqlalchemy/migrate_repo/versions/150_add_host_to_instance_faults.py b/nova/db/sqlalchemy/migrate_repo/versions/150_add_host_to_instance_faults.py new file mode 100644 index 000000000..3fd87e1e1 --- /dev/null +++ b/nova/db/sqlalchemy/migrate_repo/versions/150_add_host_to_instance_faults.py @@ -0,0 +1,36 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2013 OpenStack LLC. +# +# 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, Index, MetaData, String, Table + + +def upgrade(migrate_engine): + meta = MetaData() + meta.bind = migrate_engine + + instance_faults = Table('instance_faults', meta, autoload=True) + host = Column('host', String(length=255)) + instance_faults.create_column(host) + Index('instance_faults_host_idx', instance_faults.c.host).create( + migrate_engine) + + +def downgrade(migrate_engine): + meta = MetaData() + meta.bind = migrate_engine + + instance_faults = Table('instance_faults', meta, autoload=True) + instance_faults.drop_column('host') diff --git a/nova/db/sqlalchemy/models.py b/nova/db/sqlalchemy/models.py index 56a4d944a..5050cb77e 100644 --- a/nova/db/sqlalchemy/models.py +++ b/nova/db/sqlalchemy/models.py @@ -992,6 +992,7 @@ class InstanceFault(BASE, NovaBase): code = Column(Integer(), nullable=False) message = Column(String(255)) details = Column(Text) + host = Column(String(255)) class InstanceAction(BASE, NovaBase): diff --git a/nova/exception.py b/nova/exception.py index dcd75bf4e..c15fc1e43 100644 --- a/nova/exception.py +++ b/nova/exception.py @@ -179,8 +179,12 @@ class DBDuplicateEntry(DBError): super(DBDuplicateEntry, self).__init__(inner_exception) +class EncryptionFailure(NovaException): + message = _("Failed to encrypt text: %(reason)s") + + class DecryptionFailure(NovaException): - message = _("Failed to decrypt text") + message = _("Failed to decrypt text: %(reason)s") class VirtualInterfaceCreateException(NovaException): @@ -526,6 +530,10 @@ class PortNotUsable(NovaException): message = _("Port %(port_id)s not usable for instance %(instance)s.") +class PortNotFree(NovaException): + message = _("No free port available for instance %(instance)s.") + + class FixedIpNotFound(NotFound): message = _("No fixed IP associated with id %(id)s.") diff --git a/nova/network/linux_net.py b/nova/network/linux_net.py index e6abde609..1be06f66a 100644 --- a/nova/network/linux_net.py +++ b/nova/network/linux_net.py @@ -371,19 +371,32 @@ class IptablesManager(object): s += [('ip6tables', self.ipv6)] for cmd, tables in s: + all_tables, _err = self.execute('%s-save' % (cmd,), '-c', + run_as_root=True, + attempts=5) + all_lines = all_tables.split('\n') for table in tables: - current_table, _err = self.execute('%s-save' % (cmd,), '-c', - '-t', '%s' % (table,), - run_as_root=True, - attempts=5) - current_lines = current_table.split('\n') - new_filter = self._modify_rules(current_lines, - tables[table]) - self.execute('%s-restore' % (cmd,), '-c', run_as_root=True, - process_input='\n'.join(new_filter), - attempts=5) + start, end = self._find_table(all_lines, table) + all_lines[start:end] = self._modify_rules( + all_lines[start:end], tables[table]) + self.execute('%s-restore' % (cmd,), '-c', run_as_root=True, + process_input='\n'.join(all_lines), + attempts=5) LOG.debug(_("IPTablesManager.apply completed with success")) + def _find_table(self, lines, table_name): + if len(lines) < 3: + # length only <2 when fake iptables + return (0, 0) + try: + start = lines.index('*%s' % table_name) - 1 + except ValueError: + # Couldn't find table_name + # For Unit Tests + return (0, 0) + end = lines[start:].index('COMMIT') + start + 2 + return (start, end) + def _modify_rules(self, current_lines, table, binary=None): unwrapped_chains = table.unwrapped_chains chains = table.chains diff --git a/nova/network/manager.py b/nova/network/manager.py index 7b69c7a36..e4a97f162 100644 --- a/nova/network/manager.py +++ b/nova/network/manager.py @@ -147,9 +147,6 @@ network_opts = [ cfg.BoolOpt('auto_assign_floating_ip', default=False, help='Autoassigning floating ip to VM'), - cfg.StrOpt('network_host', - default=socket.getfqdn(), - help='Network host to use for ip allocation in flat modes'), cfg.BoolOpt('fake_network', default=False, help='If passed, use fake network devices and addresses'), @@ -482,7 +479,7 @@ class FloatingIP(object): @wrap_check_policy def deallocate_floating_ip(self, context, address, affect_auto_assigned=False): - """Returns an floating ip to the pool.""" + """Returns a floating ip to the pool.""" floating_ip = self.db.floating_ip_get_by_address(context, address) # handle auto_assigned @@ -1926,21 +1923,11 @@ class NetworkManager(manager.SchedulerDependentManager): def get_instance_id_by_floating_address(self, context, address): """Returns the instance id a floating ip's fixed ip is allocated to.""" - floating_ip = self.db.floating_ip_get_by_address(context, address) - if floating_ip['fixed_ip_id'] is None: + fixed_ip = self.db.fixed_ip_get_by_floating_address(context, address) + if fixed_ip is None: return None - - fixed_ip = self.db.fixed_ip_get(context, floating_ip['fixed_ip_id']) - - # NOTE(tr3buchet): this can be None - # NOTE(mikal): we need to return the instance id here because its used - # by ec2 (and possibly others) - uuid = fixed_ip['instance_uuid'] - if not uuid: - return uuid - - instance = self.db.instance_get_by_uuid(context, uuid) - return instance['id'] + else: + return fixed_ip['instance_uuid'] @wrap_check_policy def get_network(self, context, network_uuid): diff --git a/nova/network/quantumv2/api.py b/nova/network/quantumv2/api.py index 0deb3a4bb..29e5e2f06 100644 --- a/nova/network/quantumv2/api.py +++ b/nova/network/quantumv2/api.py @@ -111,9 +111,19 @@ class API(base.Base): :param macs: None or a set of MAC addresses that the instance should use. macs is supplied by the hypervisor driver (contrast with requested_networks which is user supplied). - NB: QuantumV2 does not yet honour mac address limits. + NB: QuantumV2 currently assigns hypervisor supplied MAC addresses + to arbitrary networks, which requires openflow switches to + function correctly if more than one network is being used with + the bare metal hypervisor (which is the only one known to limit + MAC addresses). """ hypervisor_macs = kwargs.get('macs', None) + available_macs = None + if hypervisor_macs is not None: + # Make a copy we can mutate: records macs that have not been used + # to create a port on a network. If we find a mac with a + # pre-allocated port we also remove it from this set. + available_macs = set(hypervisor_macs) quantum = quantumv2.get_client(context) LOG.debug(_('allocate_for_instance() for %s'), instance['display_name']) @@ -133,6 +143,12 @@ class API(base.Base): if port['mac_address'] not in hypervisor_macs: raise exception.PortNotUsable(port_id=port_id, instance=instance['display_name']) + else: + # Don't try to use this MAC if we need to create a + # port on the fly later. Identical MACs may be + # configured by users into multiple ports so we + # discard rather than popping. + available_macs.discard(port['mac_address']) network_id = port['network_id'] ports[network_id] = port elif fixed_ip: @@ -141,7 +157,6 @@ class API(base.Base): nets = self._get_available_networks(context, instance['project_id'], net_ids) - touched_port_ids = [] created_port_ids = [] for network in nets: @@ -161,6 +176,12 @@ class API(base.Base): port_req_body['port']['network_id'] = network_id port_req_body['port']['admin_state_up'] = True port_req_body['port']['tenant_id'] = instance['project_id'] + if available_macs is not None: + if not available_macs: + raise exception.PortNotFree( + instance=instance['display_name']) + mac_address = available_macs.pop() + port_req_body['port']['mac_address'] = mac_address created_port_ids.append( quantum.create_port(port_req_body)['port']['id']) except Exception: @@ -217,11 +238,62 @@ class API(base.Base): def add_fixed_ip_to_instance(self, context, instance, network_id): """Add a fixed ip to the instance from specified network.""" - raise NotImplementedError() + search_opts = {'network_id': network_id} + data = quantumv2.get_client(context).list_subnets(**search_opts) + ipam_subnets = data.get('subnets', []) + if not ipam_subnets: + raise exception.NetworkNotFoundForInstance( + instance_id=instance['uuid']) + + zone = 'compute:%s' % instance['availability_zone'] + search_opts = {'device_id': instance['uuid'], + 'device_owner': zone, + 'network_id': network_id} + data = quantumv2.get_client(context).list_ports(**search_opts) + ports = data['ports'] + for p in ports: + fixed_ips = p['fixed_ips'] + for subnet in ipam_subnets: + fixed_ip = {'subnet_id': subnet['id']} + fixed_ips.append(fixed_ip) + port_req_body = {'port': {'fixed_ips': fixed_ips}} + try: + quantumv2.get_client(context).update_port(p['id'], + port_req_body) + except Exception as ex: + msg = _("Unable to update port %(portid)s with" + " failure: %(exception)s") + LOG.debug(msg, {'portid': p['id'], 'exception': ex}) + return + raise exception.NetworkNotFoundForInstance( + instance_id=instance['uuid']) def remove_fixed_ip_from_instance(self, context, instance, address): """Remove a fixed ip from the instance.""" - raise NotImplementedError() + zone = 'compute:%s' % instance['availability_zone'] + search_opts = {'device_id': instance['uuid'], + 'device_owner': zone, + 'fixed_ips': 'ip_address=%s' % address} + data = quantumv2.get_client(context).list_ports(**search_opts) + ports = data['ports'] + for p in ports: + fixed_ips = p['fixed_ips'] + new_fixed_ips = [] + for fixed_ip in fixed_ips: + if fixed_ip['ip_address'] != address: + new_fixed_ips.append(fixed_ip) + port_req_body = {'port': {'fixed_ips': new_fixed_ips}} + try: + quantumv2.get_client(context).update_port(p['id'], + port_req_body) + except Exception as ex: + msg = _("Unable to update port %(portid)s with" + " failure: %(exception)s") + LOG.debug(msg, {'portid': p['id'], 'exception': ex}) + return + + raise exception.FixedIpNotFoundForSpecificInstance( + instance_uuid=instance['uuid'], ip=address) def validate_networks(self, context, requested_networks): """Validate that the tenant can use the requested networks.""" diff --git a/nova/openstack/common/eventlet_backdoor.py b/nova/openstack/common/eventlet_backdoor.py index f18e84f6d..118385427 100644 --- a/nova/openstack/common/eventlet_backdoor.py +++ b/nova/openstack/common/eventlet_backdoor.py @@ -46,7 +46,7 @@ def _find_objects(t): def _print_greenthreads(): - for i, gt in enumerate(find_objects(greenlet.greenlet)): + for i, gt in enumerate(_find_objects(greenlet.greenlet)): print i, gt traceback.print_stack(gt.gr_frame) print diff --git a/nova/openstack/common/lockutils.py b/nova/openstack/common/lockutils.py index ba390dc69..6f80a1f67 100644 --- a/nova/openstack/common/lockutils.py +++ b/nova/openstack/common/lockutils.py @@ -28,6 +28,7 @@ from eventlet import semaphore from nova.openstack.common import cfg from nova.openstack.common import fileutils +from nova.openstack.common.gettextutils import _ from nova.openstack.common import log as logging @@ -219,6 +220,11 @@ def synchronized(name, lock_file_prefix, external=False, lock_path=None): 'method': f.__name__}) retval = f(*args, **kwargs) finally: + LOG.debug(_('Released file lock "%(lock)s" at %(path)s' + ' for method "%(method)s"...'), + {'lock': name, + 'path': lock_file_path, + 'method': f.__name__}) # NOTE(vish): This removes the tempdir if we needed # to create one. This is used to cleanup # the locks left behind by unit tests. diff --git a/nova/openstack/common/log.py b/nova/openstack/common/log.py index 6e25bb597..5c6dbcf14 100644 --- a/nova/openstack/common/log.py +++ b/nova/openstack/common/log.py @@ -49,19 +49,19 @@ from nova.openstack.common import notifier log_opts = [ cfg.StrOpt('logging_context_format_string', - default='%(asctime)s.%(msecs)d %(levelname)s %(name)s ' + default='%(asctime)s.%(msecs)03d %(levelname)s %(name)s ' '[%(request_id)s %(user)s %(tenant)s] %(instance)s' '%(message)s', help='format string to use for log messages with context'), cfg.StrOpt('logging_default_format_string', - default='%(asctime)s.%(msecs)d %(process)d %(levelname)s ' + default='%(asctime)s.%(msecs)03d %(process)d %(levelname)s ' '%(name)s [-] %(instance)s%(message)s', help='format string to use for log messages without context'), cfg.StrOpt('logging_debug_format_suffix', default='%(funcName)s %(pathname)s:%(lineno)d', help='data to append to log format when level is DEBUG'), cfg.StrOpt('logging_exception_prefix', - default='%(asctime)s.%(msecs)d %(process)d TRACE %(name)s ' + default='%(asctime)s.%(msecs)03d %(process)d TRACE %(name)s ' '%(instance)s', help='prefix each line of exception output with this format'), cfg.ListOpt('default_log_levels', @@ -259,7 +259,7 @@ class JSONFormatter(logging.Formatter): class PublishErrorsHandler(logging.Handler): def emit(self, record): if ('nova.openstack.common.notifier.log_notifier' in - CONF.notification_driver): + CONF.notification_driver): return notifier.api.notify(None, 'error.publisher', 'error_notification', @@ -361,10 +361,12 @@ def _setup_logging_from_conf(product_name): datefmt=datefmt)) handler.setFormatter(LegacyFormatter(datefmt=datefmt)) - if CONF.verbose or CONF.debug: + if CONF.debug: log_root.setLevel(logging.DEBUG) - else: + elif CONF.verbose: log_root.setLevel(logging.INFO) + else: + log_root.setLevel(logging.WARNING) level = logging.NOTSET for pair in CONF.default_log_levels: @@ -425,7 +427,7 @@ class LegacyFormatter(logging.Formatter): self._fmt = CONF.logging_default_format_string if (record.levelno == logging.DEBUG and - CONF.logging_debug_format_suffix): + CONF.logging_debug_format_suffix): self._fmt += " " + CONF.logging_debug_format_suffix # Cache this on the record, Logger will respect our formated copy diff --git a/nova/scheduler/driver.py b/nova/scheduler/driver.py index d1ae1cd6e..09de10388 100644 --- a/nova/scheduler/driver.py +++ b/nova/scheduler/driver.py @@ -56,8 +56,6 @@ CONF.register_opts(scheduler_driver_opts) def handle_schedule_error(context, ex, instance_uuid, request_spec): if not isinstance(ex, exception.NoValidHost): LOG.exception(_("Exception during scheduler.run_instance")) - compute_utils.add_instance_fault_from_exc(context, - instance_uuid, ex, sys.exc_info()) state = vm_states.ERROR.upper() LOG.warning(_('Setting instance to %(state)s state.'), locals(), instance_uuid=instance_uuid) @@ -68,6 +66,8 @@ def handle_schedule_error(context, ex, instance_uuid, request_spec): 'task_state': None}) notifications.send_update(context, old_ref, new_ref, service="scheduler") + compute_utils.add_instance_fault_from_exc(context, + new_ref, ex, sys.exc_info()) properties = request_spec.get('instance_properties', {}) payload = dict(request_spec=request_spec, diff --git a/nova/scheduler/manager.py b/nova/scheduler/manager.py index 84bdcddb5..23e64cd7c 100644 --- a/nova/scheduler/manager.py +++ b/nova/scheduler/manager.py @@ -180,8 +180,6 @@ class SchedulerManager(manager.Manager): uuids = [properties.get('uuid')] for instance_uuid in request_spec.get('instance_uuids') or uuids: if instance_uuid: - compute_utils.add_instance_fault_from_exc(context, - instance_uuid, ex, sys.exc_info()) state = vm_state.upper() LOG.warning(_('Setting instance to %(state)s state.'), locals(), instance_uuid=instance_uuid) @@ -191,6 +189,8 @@ class SchedulerManager(manager.Manager): context, instance_uuid, updates) notifications.send_update(context, old_ref, new_ref, service="scheduler") + compute_utils.add_instance_fault_from_exc(context, + new_ref, ex, sys.exc_info()) payload = dict(request_spec=request_spec, instance_properties=properties, diff --git a/nova/service.py b/nova/service.py index 0fde14baa..df8cf020f 100644 --- a/nova/service.py +++ b/nova/service.py @@ -411,7 +411,7 @@ class Service(object): self.db_allowed = db_allowed self.conductor_api = conductor.API(use_local=db_allowed) self.conductor_api.wait_until_ready(context.get_admin_context()) - self.servicegroup_api = servicegroup.API() + self.servicegroup_api = servicegroup.API(db_allowed=db_allowed) def start(self): verstr = version.version_string_with_package() @@ -421,12 +421,11 @@ class Service(object): self.model_disconnected = False ctxt = context.get_admin_context() try: - service_ref = self.conductor_api.service_get_by_args(ctxt, - self.host, - self.binary) - self.service_id = service_ref['id'] + self.service_ref = self.conductor_api.service_get_by_args(ctxt, + self.host, self.binary) + self.service_id = self.service_ref['id'] except exception.NotFound: - self._create_service_ref(ctxt) + self.service_ref = self._create_service_ref(ctxt) if self.backdoor_port is not None: self.manager.backdoor_port = self.backdoor_port @@ -479,6 +478,7 @@ class Service(object): } service = self.conductor_api.service_create(context, svc_values) self.service_id = service['id'] + return service def __getattr__(self, key): manager = self.__dict__.get('manager', None) diff --git a/nova/servicegroup/api.py b/nova/servicegroup/api.py index ebd0ee6ac..358b7dcbc 100644 --- a/nova/servicegroup/api.py +++ b/nova/servicegroup/api.py @@ -45,6 +45,15 @@ class API(object): @lockutils.synchronized('nova.servicegroup.api.new', 'nova-') def __new__(cls, *args, **kwargs): + '''Create an instance of the servicegroup API. + + args and kwargs are passed down to the servicegroup driver when it gets + created. No args currently exist, though. Valid kwargs are: + + db_allowed - Boolean. False if direct db access is not allowed and + alternative data access (conductor) should be used + instead. + ''' if not cls._driver: LOG.debug(_('ServiceGroup driver defined as an instance of %s'), @@ -55,7 +64,8 @@ class API(object): except KeyError: raise TypeError(_("unknown ServiceGroup driver name: %s") % driver_name) - cls._driver = importutils.import_object(driver_class) + cls._driver = importutils.import_object(driver_class, + *args, **kwargs) utils.check_isinstance(cls._driver, ServiceGroupDriver) # we don't have to check that cls._driver is not NONE, # check_isinstance does it diff --git a/nova/servicegroup/drivers/db.py b/nova/servicegroup/drivers/db.py index 075db3ed8..686ee728b 100644 --- a/nova/servicegroup/drivers/db.py +++ b/nova/servicegroup/drivers/db.py @@ -14,8 +14,8 @@ # See the License for the specific language governing permissions and # limitations under the License. +from nova import conductor from nova import context -from nova import db from nova import exception from nova.openstack.common import cfg from nova.openstack.common import log as logging @@ -32,6 +32,10 @@ LOG = logging.getLogger(__name__) class DbDriver(api.ServiceGroupDriver): + def __init__(self, *args, **kwargs): + self.db_allowed = kwargs.get('db_allowed', True) + self.conductor_api = conductor.API(use_local=self.db_allowed) + def join(self, member_id, group_id, service=None): """Join the given service with it's group.""" @@ -53,6 +57,11 @@ class DbDriver(api.ServiceGroupDriver): Check whether a service is up based on last heartbeat. """ last_heartbeat = service_ref['updated_at'] or service_ref['created_at'] + if isinstance(last_heartbeat, basestring): + # NOTE(russellb) If this service_ref came in over rpc via + # conductor, then the timestamp will be a string and needs to be + # converted back to a datetime. + last_heartbeat = timeutils.parse_strtime(last_heartbeat) # Timestamps in DB are UTC. elapsed = utils.total_seconds(timeutils.utcnow() - last_heartbeat) LOG.debug('DB_Driver.is_up last_heartbeat = %(lhb)s elapsed = %(el)s', @@ -66,7 +75,8 @@ class DbDriver(api.ServiceGroupDriver): LOG.debug(_('DB_Driver: get_all members of the %s group') % group_id) rs = [] ctxt = context.get_admin_context() - for service in db.service_get_all_by_topic(ctxt, group_id): + services = self.conductor_api.service_get_all_by_topic(ctxt, group_id) + for service in services: if self.is_up(service): rs.append(service['host']) return rs @@ -76,18 +86,11 @@ class DbDriver(api.ServiceGroupDriver): ctxt = context.get_admin_context() state_catalog = {} try: - try: - service_ref = db.service_get(ctxt, service.service_id) - except exception.NotFound: - LOG.debug(_('The service database object disappeared, ' - 'Recreating it.')) - service._create_service_ref(ctxt) - service_ref = db.service_get(ctxt, service.service_id) - - state_catalog['report_count'] = service_ref['report_count'] + 1 + report_count = service.service_ref['report_count'] + 1 + state_catalog['report_count'] = report_count - db.service_update(ctxt, - service.service_id, state_catalog) + service.service_ref = self.conductor_api.service_update(ctxt, + service.service_ref, state_catalog) # TODO(termie): make this pattern be more elegant. if getattr(service, 'model_disconnected', False): diff --git a/nova/tests/api/openstack/compute/contrib/test_consoles.py b/nova/tests/api/openstack/compute/contrib/test_consoles.py index d251c6b75..cf044dfcd 100644 --- a/nova/tests/api/openstack/compute/contrib/test_consoles.py +++ b/nova/tests/api/openstack/compute/contrib/test_consoles.py @@ -26,19 +26,36 @@ def fake_get_vnc_console(self, _context, _instance, _console_type): return {'url': 'http://fake'} +def fake_get_spice_console(self, _context, _instance, _console_type): + return {'url': 'http://fake'} + + def fake_get_vnc_console_invalid_type(self, _context, _instance, _console_type): raise exception.ConsoleTypeInvalid(console_type=_console_type) +def fake_get_spice_console_invalid_type(self, _context, + _instance, _console_type): + raise exception.ConsoleTypeInvalid(console_type=_console_type) + + def fake_get_vnc_console_not_ready(self, _context, instance, _console_type): raise exception.InstanceNotReady(instance_id=instance["uuid"]) +def fake_get_spice_console_not_ready(self, _context, instance, _console_type): + raise exception.InstanceNotReady(instance_id=instance["uuid"]) + + def fake_get_vnc_console_not_found(self, _context, instance, _console_type): raise exception.InstanceNotFound(instance_id=instance["uuid"]) +def fake_get_spice_console_not_found(self, _context, instance, _console_type): + raise exception.InstanceNotFound(instance_id=instance["uuid"]) + + def fake_get(self, context, instance_uuid): return {'uuid': instance_uuid} @@ -53,6 +70,8 @@ class ConsolesExtensionTest(test.TestCase): super(ConsolesExtensionTest, self).setUp() self.stubs.Set(compute_api.API, 'get_vnc_console', fake_get_vnc_console) + self.stubs.Set(compute_api.API, 'get_spice_console', + fake_get_spice_console) self.stubs.Set(compute_api.API, 'get', fake_get) self.flags( osapi_compute_extension=[ @@ -132,3 +151,76 @@ class ConsolesExtensionTest(test.TestCase): res = req.get_response(self.app) self.assertEqual(res.status_int, 400) + + def test_get_spice_console(self): + body = {'os-getSPICEConsole': {'type': 'spice-html5'}} + req = webob.Request.blank('/v2/fake/servers/1/action') + req.method = "POST" + req.body = jsonutils.dumps(body) + req.headers["content-type"] = "application/json" + + res = req.get_response(self.app) + output = jsonutils.loads(res.body) + self.assertEqual(res.status_int, 200) + self.assertEqual(output, + {u'console': {u'url': u'http://fake', u'type': u'spice-html5'}}) + + def test_get_spice_console_not_ready(self): + self.stubs.Set(compute_api.API, 'get_spice_console', + fake_get_spice_console_not_ready) + body = {'os-getSPICEConsole': {'type': 'spice-html5'}} + req = webob.Request.blank('/v2/fake/servers/1/action') + req.method = "POST" + req.body = jsonutils.dumps(body) + req.headers["content-type"] = "application/json" + + res = req.get_response(self.app) + output = jsonutils.loads(res.body) + self.assertEqual(res.status_int, 409) + + def test_get_spice_console_no_type(self): + self.stubs.Set(compute_api.API, 'get_spice_console', + fake_get_spice_console_invalid_type) + body = {'os-getSPICEConsole': {}} + req = webob.Request.blank('/v2/fake/servers/1/action') + req.method = "POST" + req.body = jsonutils.dumps(body) + req.headers["content-type"] = "application/json" + + res = req.get_response(self.app) + self.assertEqual(res.status_int, 400) + + def test_get_spice_console_no_instance(self): + self.stubs.Set(compute_api.API, 'get', fake_get_not_found) + body = {'os-getSPICEConsole': {'type': 'spice-html5'}} + req = webob.Request.blank('/v2/fake/servers/1/action') + req.method = "POST" + req.body = jsonutils.dumps(body) + req.headers["content-type"] = "application/json" + + res = req.get_response(self.app) + self.assertEqual(res.status_int, 404) + + def test_get_spice_console_no_instance_on_console_get(self): + self.stubs.Set(compute_api.API, 'get_spice_console', + fake_get_spice_console_not_found) + body = {'os-getSPICEConsole': {'type': 'spice-html5'}} + req = webob.Request.blank('/v2/fake/servers/1/action') + req.method = "POST" + req.body = jsonutils.dumps(body) + req.headers["content-type"] = "application/json" + + res = req.get_response(self.app) + self.assertEqual(res.status_int, 404) + + def test_get_spice_console_invalid_type(self): + body = {'os-getSPICEConsole': {'type': 'invalid'}} + self.stubs.Set(compute_api.API, 'get_spice_console', + fake_get_spice_console_invalid_type) + req = webob.Request.blank('/v2/fake/servers/1/action') + req.method = "POST" + req.body = jsonutils.dumps(body) + req.headers["content-type"] = "application/json" + + res = req.get_response(self.app) + self.assertEqual(res.status_int, 400) diff --git a/nova/tests/api/openstack/compute/test_servers.py b/nova/tests/api/openstack/compute/test_servers.py index b69268d2a..43746223a 100644 --- a/nova/tests/api/openstack/compute/test_servers.py +++ b/nova/tests/api/openstack/compute/test_servers.py @@ -2197,6 +2197,74 @@ class ServersControllerCreateTest(test.TestCase): self.stubs.Set(compute_api.API, 'create', create) self._test_create_extra(params) + def test_create_instance_with_device_name_not_string(self): + self.ext_mgr.extensions = {'os-volumes': 'fake'} + bdm = [{'delete_on_termination': 1, + 'device_name': 123, + 'volume_size': 1, + 'volume_id': '11111111-1111-1111-1111-111111111111'}] + params = {'block_device_mapping': bdm} + old_create = compute_api.API.create + + def create(*args, **kwargs): + self.assertEqual(kwargs['block_device_mapping'], bdm) + return old_create(*args, **kwargs) + + self.stubs.Set(compute_api.API, 'create', create) + self.assertRaises(webob.exc.HTTPBadRequest, + self._test_create_extra, params) + + def test_create_instance_with_device_name_empty(self): + self.ext_mgr.extensions = {'os-volumes': 'fake'} + bdm = [{'delete_on_termination': 1, + 'device_name': '', + 'volume_size': 1, + 'volume_id': '11111111-1111-1111-1111-111111111111'}] + params = {'block_device_mapping': bdm} + old_create = compute_api.API.create + + def create(*args, **kwargs): + self.assertEqual(kwargs['block_device_mapping'], bdm) + return old_create(*args, **kwargs) + + self.stubs.Set(compute_api.API, 'create', create) + self.assertRaises(webob.exc.HTTPBadRequest, + self._test_create_extra, params) + + def test_create_instance_with_device_name_too_long(self): + self.ext_mgr.extensions = {'os-volumes': 'fake'} + bdm = [{'delete_on_termination': 1, + 'device_name': 'a' * 256, + 'volume_size': 1, + 'volume_id': '11111111-1111-1111-1111-111111111111'}] + params = {'block_device_mapping': bdm} + old_create = compute_api.API.create + + def create(*args, **kwargs): + self.assertEqual(kwargs['block_device_mapping'], bdm) + return old_create(*args, **kwargs) + + self.stubs.Set(compute_api.API, 'create', create) + self.assertRaises(webob.exc.HTTPBadRequest, + self._test_create_extra, params) + + def test_create_instance_with_space_in_device_name(self): + self.ext_mgr.extensions = {'os-volumes': 'fake'} + bdm = [{'delete_on_termination': 1, + 'device_name': 'vd a', + 'volume_size': 1, + 'volume_id': '11111111-1111-1111-1111-111111111111'}] + params = {'block_device_mapping': bdm} + old_create = compute_api.API.create + + def create(*args, **kwargs): + self.assertEqual(kwargs['block_device_mapping'], bdm) + return old_create(*args, **kwargs) + + self.stubs.Set(compute_api.API, 'create', create) + self.assertRaises(webob.exc.HTTPBadRequest, + self._test_create_extra, params) + def test_create_instance_with_bdm_delete_on_termination(self): self.ext_mgr.extensions = {'os-volumes': 'fake'} bdm = [{'device_name': 'foo1', 'delete_on_termination': 1}, diff --git a/nova/tests/baremetal/test_pxe.py b/nova/tests/baremetal/test_pxe.py index 45c9ede43..ad4975849 100644 --- a/nova/tests/baremetal/test_pxe.py +++ b/nova/tests/baremetal/test_pxe.py @@ -147,12 +147,6 @@ class PXEClassMethodsTestCase(BareMetalPXETestCase): config = pxe.build_network_config(net) self.assertIn('eth0', config) self.assertNotIn('eth1', config) - self.assertIn('hwaddress ether fake', config) - self.assertNotIn('hwaddress ether aa:bb:cc:dd', config) - - net[0][1]['mac'] = 'aa:bb:cc:dd' - config = pxe.build_network_config(net) - self.assertIn('hwaddress ether aa:bb:cc:dd', config) net = utils.get_test_network_info(2) config = pxe.build_network_config(net) @@ -306,15 +300,6 @@ class PXEPrivateMethodsTestCase(BareMetalPXETestCase): macs = self.driver._collect_mac_addresses(self.context, self.node) self.assertEqual(macs, address_list) - def test_generate_udev_rules(self): - self._create_node() - address_list = [nic['address'] for nic in self.nic_info] - address_list.append(self.node_info['prov_mac_address']) - - rules = self.driver._generate_udev_rules(self.context, self.node) - for address in address_list: - self.assertIn('ATTR{address}=="%s"' % address, rules) - def test_cache_tftp_images(self): self.instance['kernel_id'] = 'aaaa' self.instance['ramdisk_id'] = 'bbbb' @@ -357,8 +342,6 @@ class PXEPrivateMethodsTestCase(BareMetalPXETestCase): # nova.virt.disk.api._inject_*_into_fs self._create_node() files = [] - files.append(('/etc/udev/rules.d/70-persistent-net.rules', - self.driver._generate_udev_rules(self.context, self.node))) self.instance['hostname'] = 'fake hostname' files.append(('/etc/hostname', 'fake hostname')) self.instance['key_data'] = 'fake ssh key' diff --git a/nova/tests/compute/test_compute.py b/nova/tests/compute/test_compute.py index 0d9f67231..092fd940a 100644 --- a/nova/tests/compute/test_compute.py +++ b/nova/tests/compute/test_compute.py @@ -1335,6 +1335,9 @@ class ComputeTestCase(BaseTestCase): def test_novnc_vnc_console(self): # Make sure we can a vnc console for an instance. + self.flags(vnc_enabled=True) + self.flags(enabled=False, group='spice') + instance = jsonutils.to_primitive(self._create_fake_instance()) self.compute.run_instance(self.context, instance=instance) @@ -1347,6 +1350,9 @@ class ComputeTestCase(BaseTestCase): def test_xvpvnc_vnc_console(self): # Make sure we can a vnc console for an instance. + self.flags(vnc_enabled=True) + self.flags(enabled=False, group='spice') + instance = jsonutils.to_primitive(self._create_fake_instance()) self.compute.run_instance(self.context, instance=instance) @@ -1357,6 +1363,9 @@ class ComputeTestCase(BaseTestCase): def test_invalid_vnc_console_type(self): # Raise useful error if console type is an unrecognised string. + self.flags(vnc_enabled=True) + self.flags(enabled=False, group='spice') + instance = jsonutils.to_primitive(self._create_fake_instance()) self.compute.run_instance(self.context, instance=instance) @@ -1367,6 +1376,9 @@ class ComputeTestCase(BaseTestCase): def test_missing_vnc_console_type(self): # Raise useful error is console type is None. + self.flags(vnc_enabled=True) + self.flags(enabled=False, group='spice') + instance = jsonutils.to_primitive(self._create_fake_instance()) self.compute.run_instance(self.context, instance=instance) @@ -1375,6 +1387,47 @@ class ComputeTestCase(BaseTestCase): self.context, None, instance=instance) self.compute.terminate_instance(self.context, instance=instance) + def test_spicehtml5_spice_console(self): + # Make sure we can a spice console for an instance. + self.flags(vnc_enabled=False) + self.flags(enabled=True, group='spice') + + instance = jsonutils.to_primitive(self._create_fake_instance()) + self.compute.run_instance(self.context, instance=instance) + + # Try with the full instance + console = self.compute.get_spice_console(self.context, 'spice-html5', + instance=instance) + self.assert_(console) + + self.compute.terminate_instance(self.context, instance=instance) + + def test_invalid_spice_console_type(self): + # Raise useful error if console type is an unrecognised string + self.flags(vnc_enabled=False) + self.flags(enabled=True, group='spice') + + instance = jsonutils.to_primitive(self._create_fake_instance()) + self.compute.run_instance(self.context, instance=instance) + + self.assertRaises(exception.ConsoleTypeInvalid, + self.compute.get_spice_console, + self.context, 'invalid', instance=instance) + self.compute.terminate_instance(self.context, instance=instance) + + def test_missing_spice_console_type(self): + # Raise useful error is console type is None + self.flags(vnc_enabled=False) + self.flags(enabled=True, group='spice') + + instance = jsonutils.to_primitive(self._create_fake_instance()) + self.compute.run_instance(self.context, instance=instance) + + self.assertRaises(exception.ConsoleTypeInvalid, + self.compute.get_spice_console, + self.context, None, instance=instance) + self.compute.terminate_instance(self.context, instance=instance) + def test_diagnostics(self): # Make sure we can get diagnostics for an instance. expected_diagnostic = {'cpu0_time': 17300000000, @@ -2645,8 +2698,8 @@ class ComputeTestCase(BaseTestCase): self.assertEqual(task_states.POWERING_OFF, instances[0]['task_state']) def test_add_instance_fault(self): + instance = self._create_fake_instance() exc_info = None - instance_uuid = str(uuid.uuid4()) def fake_db_fault_create(ctxt, values): self.assertTrue(values['details'].startswith('test')) @@ -2656,7 +2709,8 @@ class ComputeTestCase(BaseTestCase): expected = { 'code': 500, 'message': 'NotImplementedError', - 'instance_uuid': instance_uuid, + 'instance_uuid': instance['uuid'], + 'host': self.compute.host } self.assertEquals(expected, values) @@ -2668,13 +2722,12 @@ class ComputeTestCase(BaseTestCase): self.stubs.Set(nova.db, 'instance_fault_create', fake_db_fault_create) ctxt = context.get_admin_context() - compute_utils.add_instance_fault_from_exc(ctxt, instance_uuid, - NotImplementedError('test'), - exc_info) + compute_utils.add_instance_fault_from_exc(ctxt, instance, + NotImplementedError('test'), exc_info) def test_add_instance_fault_with_remote_error(self): + instance = self._create_fake_instance() exc_info = None - instance_uuid = str(uuid.uuid4()) def fake_db_fault_create(ctxt, values): self.assertTrue(values['details'].startswith('Remote error')) @@ -2684,8 +2737,9 @@ class ComputeTestCase(BaseTestCase): expected = { 'code': 500, - 'instance_uuid': instance_uuid, - 'message': 'My Test Message' + 'instance_uuid': instance['uuid'], + 'message': 'My Test Message', + 'host': self.compute.host } self.assertEquals(expected, values) @@ -2697,13 +2751,12 @@ class ComputeTestCase(BaseTestCase): self.stubs.Set(nova.db, 'instance_fault_create', fake_db_fault_create) ctxt = context.get_admin_context() - compute_utils.add_instance_fault_from_exc(ctxt, instance_uuid, - exc, - exc_info) + compute_utils.add_instance_fault_from_exc(ctxt, instance, exc, + exc_info) def test_add_instance_fault_user_error(self): + instance = self._create_fake_instance() exc_info = None - instance_uuid = str(uuid.uuid4()) def fake_db_fault_create(ctxt, values): @@ -2711,7 +2764,8 @@ class ComputeTestCase(BaseTestCase): 'code': 400, 'message': 'Invalid', 'details': 'fake details', - 'instance_uuid': instance_uuid, + 'instance_uuid': instance['uuid'], + 'host': self.compute.host } self.assertEquals(expected, values) @@ -2725,26 +2779,27 @@ class ComputeTestCase(BaseTestCase): self.stubs.Set(nova.db, 'instance_fault_create', fake_db_fault_create) ctxt = context.get_admin_context() - compute_utils.add_instance_fault_from_exc(ctxt, instance_uuid, - user_exc, exc_info) + compute_utils.add_instance_fault_from_exc(ctxt, instance, user_exc, + exc_info) def test_add_instance_fault_no_exc_info(self): - instance_uuid = str(uuid.uuid4()) + instance = self._create_fake_instance() def fake_db_fault_create(ctxt, values): expected = { 'code': 500, 'message': 'NotImplementedError', 'details': 'test', - 'instance_uuid': instance_uuid, + 'instance_uuid': instance['uuid'], + 'host': self.compute.host } self.assertEquals(expected, values) self.stubs.Set(nova.db, 'instance_fault_create', fake_db_fault_create) ctxt = context.get_admin_context() - compute_utils.add_instance_fault_from_exc(ctxt, instance_uuid, - NotImplementedError('test')) + compute_utils.add_instance_fault_from_exc(ctxt, instance, + NotImplementedError('test')) def test_cleanup_running_deleted_instances(self): admin_context = context.get_admin_context() @@ -3251,6 +3306,35 @@ class ComputeTestCase(BaseTestCase): self.mox.VerifyAll() self.mox.UnsetStubs() + def test_init_instance_failed_resume_sets_error(self): + instance = { + 'uuid': 'fake-uuid', + 'info_cache': None, + 'power_state': power_state.RUNNING, + 'vm_state': vm_states.ACTIVE, + } + self.flags(resume_guests_state_on_host_boot=True) + self.mox.StubOutWithMock(self.compute, '_get_power_state') + self.mox.StubOutWithMock(self.compute.driver, 'plug_vifs') + self.mox.StubOutWithMock(self.compute.driver, + 'resume_state_on_host_boot') + self.mox.StubOutWithMock(self.compute, + '_get_instance_volume_block_device_info') + self.mox.StubOutWithMock(self.compute, + '_set_instance_error_state') + self.compute._get_power_state(mox.IgnoreArg(), + instance).AndReturn(power_state.SHUTDOWN) + self.compute.driver.plug_vifs(instance, mox.IgnoreArg()) + self.compute._get_instance_volume_block_device_info(mox.IgnoreArg(), + instance['uuid']).AndReturn('fake-bdm') + self.compute.driver.resume_state_on_host_boot(mox.IgnoreArg(), + instance, mox.IgnoreArg(), + 'fake-bdm').AndRaise(test.TestingException) + self.compute._set_instance_error_state(mox.IgnoreArg(), + instance['uuid']) + self.mox.ReplayAll() + self.compute._init_instance('fake-context', instance) + def test_get_instances_on_driver(self): fake_context = context.get_admin_context() @@ -3903,6 +3987,38 @@ class ComputeAPITestCase(BaseTestCase): db.instance_destroy(self.context, instance['uuid']) + def test_delete_in_resizing(self): + def fake_quotas_reserve(context, expire=None, project_id=None, + **deltas): + old_type = instance_types.get_instance_type_by_name('m1.tiny') + # ensure using old instance type to create reservations + self.assertEqual(deltas['cores'], -old_type['vcpus']) + self.assertEqual(deltas['ram'], -old_type['memory_mb']) + + self.stubs.Set(QUOTAS, 'reserve', fake_quotas_reserve) + + instance, instance_uuid = self._run_instance(params={ + 'host': CONF.host}) + + # create a fake migration record (manager does this) + new_inst_type = instance_types.get_instance_type_by_name('m1.small') + db.migration_create(self.context.elevated(), + {'instance_uuid': instance['uuid'], + 'old_instance_type_id': instance['instance_type_id'], + 'new_instance_type_id': new_inst_type['id'], + 'status': 'post-migrating'}) + + # update instance type to resized one + db.instance_update(self.context, instance['uuid'], + {'instance_type_id': new_inst_type['id'], + 'vcpus': new_inst_type['vcpus'], + 'memory_mb': new_inst_type['memory_mb'], + 'task_state': task_states.RESIZE_FINISH}) + + self.compute_api.delete(self.context, instance) + + db.instance_destroy(self.context, instance['uuid']) + def test_delete_in_resized(self): instance, instance_uuid = self._run_instance(params={ 'host': CONF.host}) @@ -5512,6 +5628,50 @@ class ComputeAPITestCase(BaseTestCase): db.instance_destroy(self.context, instance['uuid']) + def test_spice_console(self): + # Make sure we can a spice console for an instance. + + fake_instance = {'uuid': 'fake_uuid', + 'host': 'fake_compute_host'} + fake_console_type = "spice-html5" + fake_connect_info = {'token': 'fake_token', + 'console_type': fake_console_type, + 'host': 'fake_console_host', + 'port': 'fake_console_port', + 'internal_access_path': 'fake_access_path'} + fake_connect_info2 = copy.deepcopy(fake_connect_info) + fake_connect_info2['access_url'] = 'fake_console_url' + + self.mox.StubOutWithMock(rpc, 'call') + + rpc_msg1 = {'method': 'get_spice_console', + 'args': {'instance': fake_instance, + 'console_type': fake_console_type}, + 'version': '2.24'} + rpc_msg2 = {'method': 'authorize_console', + 'args': fake_connect_info, + 'version': '1.0'} + + rpc.call(self.context, 'compute.%s' % fake_instance['host'], + rpc_msg1, None).AndReturn(fake_connect_info2) + rpc.call(self.context, CONF.consoleauth_topic, + rpc_msg2, None).AndReturn(None) + + self.mox.ReplayAll() + + console = self.compute_api.get_spice_console(self.context, + fake_instance, fake_console_type) + self.assertEqual(console, {'url': 'fake_console_url'}) + + def test_get_spice_console_no_host(self): + instance = self._create_fake_instance(params={'host': ''}) + + self.assertRaises(exception.InstanceNotReady, + self.compute_api.get_spice_console, + self.context, instance, 'spice') + + db.instance_destroy(self.context, instance['uuid']) + def test_get_backdoor_port(self): # Test api call to get backdoor_port. fake_backdoor_port = 59697 @@ -6520,7 +6680,7 @@ class ComputeRescheduleOrReraiseTestCase(BaseTestCase): exc_info = sys.exc_info() compute_utils.add_instance_fault_from_exc(self.context, - instance_uuid, exc_info[0], exc_info=exc_info) + self.instance, exc_info[0], exc_info=exc_info) self.compute._deallocate_network(self.context, self.instance).AndRaise(InnerTestingException("Error")) self.compute._log_original_error(exc_info, instance_uuid) @@ -6570,7 +6730,7 @@ class ComputeRescheduleOrReraiseTestCase(BaseTestCase): except Exception: exc_info = sys.exc_info() compute_utils.add_instance_fault_from_exc(self.context, - instance_uuid, exc_info[0], exc_info=exc_info) + self.instance, exc_info[0], exc_info=exc_info) self.compute._deallocate_network(self.context, self.instance) self.compute._reschedule(self.context, None, {}, instance_uuid, @@ -6598,7 +6758,7 @@ class ComputeRescheduleOrReraiseTestCase(BaseTestCase): exc_info = sys.exc_info() compute_utils.add_instance_fault_from_exc(self.context, - instance_uuid, exc_info[0], exc_info=exc_info) + self.instance, exc_info[0], exc_info=exc_info) self.compute._deallocate_network(self.context, self.instance) self.compute._reschedule(self.context, None, {}, instance_uuid, diff --git a/nova/tests/compute/test_compute_utils.py b/nova/tests/compute/test_compute_utils.py index f29c68627..6e7227d4c 100644 --- a/nova/tests/compute/test_compute_utils.py +++ b/nova/tests/compute/test_compute_utils.py @@ -69,8 +69,11 @@ class ComputeValidateDeviceTestCase(test.TestCase): lambda context, instance: self.data) def _validate_device(self, device=None): + bdms = db.block_device_mapping_get_all_by_instance( + self.context, self.instance['uuid']) return compute_utils.get_device_name_for_instance(self.context, self.instance, + bdms, device) @staticmethod diff --git a/nova/tests/compute/test_rpcapi.py b/nova/tests/compute/test_rpcapi.py index 00b90ea65..b81e049bf 100644 --- a/nova/tests/compute/test_rpcapi.py +++ b/nova/tests/compute/test_rpcapi.py @@ -165,6 +165,11 @@ class ComputeRpcAPITestCase(test.TestCase): self._test_compute_api('get_vnc_console', 'call', instance=self.fake_instance, console_type='type') + def test_get_spice_console(self): + self._test_compute_api('get_spice_console', 'call', + instance=self.fake_instance, console_type='type', + version='2.24') + def test_host_maintenance_mode(self): self._test_compute_api('host_maintenance_mode', 'call', host_param='param', mode='mode', host='host') diff --git a/nova/tests/conductor/test_conductor.py b/nova/tests/conductor/test_conductor.py index 9f6940607..d010b454f 100644 --- a/nova/tests/conductor/test_conductor.py +++ b/nova/tests/conductor/test_conductor.py @@ -398,6 +398,25 @@ class _BaseTestCase(object): result = self.conductor.ping(self.context, 'foo') self.assertEqual(result, {'service': 'conductor', 'arg': 'foo'}) + def test_compute_node_create(self): + self.mox.StubOutWithMock(db, 'compute_node_create') + db.compute_node_create(self.context, 'fake-values').AndReturn( + 'fake-result') + self.mox.ReplayAll() + result = self.conductor.compute_node_create(self.context, + 'fake-values') + self.assertEqual(result, 'fake-result') + + def test_compute_node_update(self): + node = {'id': 'fake-id'} + self.mox.StubOutWithMock(db, 'compute_node_update') + db.compute_node_update(self.context, node['id'], 'fake-values', + False).AndReturn('fake-result') + self.mox.ReplayAll() + result = self.conductor.compute_node_update(self.context, node, + 'fake-values', False) + self.assertEqual(result, 'fake-result') + class ConductorTestCase(_BaseTestCase, test.TestCase): """Conductor Manager Tests.""" @@ -728,6 +747,14 @@ class ConductorAPITestCase(_BaseTestCase, test.TestCase): def test_service_destroy(self): self._test_stubbed('service_destroy', '', returns=False) + def test_service_update(self): + ctxt = self.context + self.mox.StubOutWithMock(db, 'service_update') + db.service_update(ctxt, '', {}).AndReturn('fake-result') + self.mox.ReplayAll() + result = self.conductor.service_update(self.context, {'id': ''}, {}) + self.assertEqual(result, 'fake-result') + def test_instance_get_all_by_host(self): self._test_stubbed('instance_get_all_by_host', self.context.elevated(), 'host') diff --git a/nova/tests/fake_policy.py b/nova/tests/fake_policy.py index 15890cdcd..acefa856c 100644 --- a/nova/tests/fake_policy.py +++ b/nova/tests/fake_policy.py @@ -42,6 +42,7 @@ policy_data = """ "compute:unlock": "", "compute:get_vnc_console": "", + "compute:get_spice_console": "", "compute:get_console_output": "", "compute:associate_floating_ip": "", diff --git a/nova/tests/fakelibvirt.py b/nova/tests/fakelibvirt.py index 8d9561c7e..a573b7d1c 100644 --- a/nova/tests/fakelibvirt.py +++ b/nova/tests/fakelibvirt.py @@ -414,6 +414,7 @@ class Domain(object): <input type='tablet' bus='usb'/> <input type='mouse' bus='ps2'/> <graphics type='vnc' port='-1' autoport='yes'/> + <graphics type='spice' port='-1' autoport='yes'/> <video> <model type='cirrus' vram='9216' heads='1'/> <address type='pci' domain='0x0000' bus='0x00' slot='0x02' diff --git a/nova/tests/integrated/api_samples/os-consoles/get-spice-console-post-req.json.tpl b/nova/tests/integrated/api_samples/os-consoles/get-spice-console-post-req.json.tpl new file mode 100644 index 000000000..d04f7c7ae --- /dev/null +++ b/nova/tests/integrated/api_samples/os-consoles/get-spice-console-post-req.json.tpl @@ -0,0 +1,5 @@ +{ + "os-getSPICEConsole": { + "type": "spice-html5" + } +} diff --git a/nova/tests/integrated/api_samples/os-consoles/get-spice-console-post-req.xml.tpl b/nova/tests/integrated/api_samples/os-consoles/get-spice-console-post-req.xml.tpl new file mode 100644 index 000000000..c8cd2df9f --- /dev/null +++ b/nova/tests/integrated/api_samples/os-consoles/get-spice-console-post-req.xml.tpl @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="UTF-8"?> +<os-getSPICEConsole> + <type>spice-html5</type> +</os-getSPICEConsole> diff --git a/nova/tests/integrated/api_samples/os-consoles/get-spice-console-post-resp.json.tpl b/nova/tests/integrated/api_samples/os-consoles/get-spice-console-post-resp.json.tpl new file mode 100644 index 000000000..20e260e9e --- /dev/null +++ b/nova/tests/integrated/api_samples/os-consoles/get-spice-console-post-resp.json.tpl @@ -0,0 +1,6 @@ +{ + "console": { + "type": "spice-html5", + "url":"%(url)s" + } +} diff --git a/nova/tests/integrated/api_samples/os-consoles/get-spice-console-post-resp.xml.tpl b/nova/tests/integrated/api_samples/os-consoles/get-spice-console-post-resp.xml.tpl new file mode 100644 index 000000000..77e35ae5b --- /dev/null +++ b/nova/tests/integrated/api_samples/os-consoles/get-spice-console-post-resp.xml.tpl @@ -0,0 +1,5 @@ +<?xml version='1.0' encoding='UTF-8'?> +<console> + <type>spice-html5</type> + <url>%(url)s</url> +</console> diff --git a/nova/tests/integrated/test_api_samples.py b/nova/tests/integrated/test_api_samples.py index 7c3157872..aa41a8259 100644 --- a/nova/tests/integrated/test_api_samples.py +++ b/nova/tests/integrated/test_api_samples.py @@ -2116,6 +2116,11 @@ class ConsolesSampleJsonTests(ServersSampleBase): extension_name = ("nova.api.openstack.compute.contrib" ".consoles.Consoles") + def setUp(self): + super(ConsolesSampleJsonTests, self).setUp() + self.flags(vnc_enabled=True) + self.flags(enabled=True, group='spice') + def test_get_vnc_console(self): uuid = self._post_server() response = self._do_post('servers/%s/action' % uuid, @@ -2128,6 +2133,18 @@ class ConsolesSampleJsonTests(ServersSampleBase): return self._verify_response('get-vnc-console-post-resp', subs, response) + def test_get_spice_console(self): + uuid = self._post_server() + response = self._do_post('servers/%s/action' % uuid, + 'get-spice-console-post-req', + {'action': 'os-getSPICEConsole'}) + self.assertEqual(response.status, 200) + subs = self._get_regexes() + subs["url"] = \ + "((https?):((//)|(\\\\))+([\w\d:#@%/;$()~_?\+-=\\\.&](#!)?)*)" + return self._verify_response('get-spice-console-post-resp', + subs, response) + class ConsolesSampleXmlTests(ConsolesSampleJsonTests): ctype = 'xml' diff --git a/nova/tests/network/test_linux_net.py b/nova/tests/network/test_linux_net.py index c0770902d..8a7865b83 100644 --- a/nova/tests/network/test_linux_net.py +++ b/nova/tests/network/test_linux_net.py @@ -469,13 +469,9 @@ class LinuxNetworkTestCase(test.TestCase): '--arp-ip-src', dhcp, '-j', 'DROP'), ('ebtables', '-I', 'OUTPUT', '-p', 'ARP', '-o', iface, '--arp-ip-src', dhcp, '-j', 'DROP'), - ('iptables-save', '-c', '-t', 'filter'), + ('iptables-save', '-c'), ('iptables-restore', '-c'), - ('iptables-save', '-c', '-t', 'mangle'), - ('iptables-restore', '-c'), - ('iptables-save', '-c', '-t', 'nat'), - ('iptables-restore', '-c'), - ('ip6tables-save', '-c', '-t', 'filter'), + ('ip6tables-save', '-c'), ('ip6tables-restore', '-c'), ] self.assertEqual(executes, expected) @@ -508,13 +504,9 @@ class LinuxNetworkTestCase(test.TestCase): '--arp-ip-dst', dhcp, '-j', 'DROP'), ('ebtables', '-D', 'OUTPUT', '-p', 'ARP', '-o', iface, '--arp-ip-src', dhcp, '-j', 'DROP'), - ('iptables-save', '-c', '-t', 'filter'), - ('iptables-restore', '-c'), - ('iptables-save', '-c', '-t', 'mangle'), - ('iptables-restore', '-c'), - ('iptables-save', '-c', '-t', 'nat'), + ('iptables-save', '-c'), ('iptables-restore', '-c'), - ('ip6tables-save', '-c', '-t', 'filter'), + ('ip6tables-save', '-c'), ('ip6tables-restore', '-c'), ] self.assertEqual(executes, expected) diff --git a/nova/tests/network/test_quantumv2.py b/nova/tests/network/test_quantumv2.py index 876bce90d..c9b2e43b3 100644 --- a/nova/tests/network/test_quantumv2.py +++ b/nova/tests/network/test_quantumv2.py @@ -349,6 +349,9 @@ class TestQuantumv2(test.TestCase): nets = self.nets[net_idx - 1] ports = {} fixed_ips = {} + macs = kwargs.get('macs') + if macs: + macs = set(macs) req_net_ids = [] if 'requested_networks' in kwargs: for id, fixed_ip, port_id in kwargs['requested_networks']: @@ -359,13 +362,15 @@ class TestQuantumv2(test.TestCase): 'mac_address': 'my_mac1'}}) ports['my_netid1'] = self.port_data1[0] id = 'my_netid1' + if macs is not None: + macs.discard('my_mac1') else: fixed_ips[id] = fixed_ip req_net_ids.append(id) expected_network_order = req_net_ids else: expected_network_order = [n['id'] for n in nets] - if kwargs.get('_break_list_networks'): + if kwargs.get('_break') == 'pre_list_networks': self.mox.ReplayAll() return api search_ids = [net['id'] for net in nets if net['id'] in req_net_ids] @@ -382,8 +387,10 @@ class TestQuantumv2(test.TestCase): mox_list_network_params['id'] = mox.SameElementsAs(search_ids) self.moxed_client.list_networks( **mox_list_network_params).AndReturn({'networks': []}) - for net_id in expected_network_order: + if kwargs.get('_break') == 'net_id2': + self.mox.ReplayAll() + return api port_req_body = { 'port': { 'device_id': self.instance['uuid'], @@ -406,10 +413,15 @@ class TestQuantumv2(test.TestCase): port_req_body['port']['admin_state_up'] = True port_req_body['port']['tenant_id'] = \ self.instance['project_id'] + if macs: + port_req_body['port']['mac_address'] = macs.pop() res_port = {'port': {'id': 'fake'}} self.moxed_client.create_port( MyComparator(port_req_body)).AndReturn(res_port) + if kwargs.get('_break') == 'pre_get_instance_nw_info': + self.mox.ReplayAll() + return api api.get_instance_nw_info(mox.IgnoreArg(), self.instance, networks=nets).AndReturn(None) @@ -433,16 +445,63 @@ class TestQuantumv2(test.TestCase): self._allocate_for_instance(1, macs=None) def test_allocate_for_instance_accepts_macs_kwargs_set(self): - # The macs kwarg should be accepted, as a set. + # The macs kwarg should be accepted, as a set, the + # _allocate_for_instance helper checks that the mac is used to create a + # port. self._allocate_for_instance(1, macs=set(['ab:cd:ef:01:23:45'])) + def test_allocate_for_instance_not_enough_macs_via_ports(self): + # using a hypervisor MAC via a pre-created port will stop it being + # used to dynamically create a port on a network. We put the network + # first in requested_networks so that if the code were to not pre-check + # requested ports, it would incorrectly assign the mac and not fail. + requested_networks = [ + (self.nets2[1]['id'], None, None), + (None, None, 'my_portid1')] + api = self._stub_allocate_for_instance( + net_idx=2, requested_networks=requested_networks, + macs=set(['my_mac1']), + _break='net_id2') + self.assertRaises(exception.PortNotFree, + api.allocate_for_instance, self.context, + self.instance, requested_networks=requested_networks, + macs=set(['my_mac1'])) + + def test_allocate_for_instance_not_enough_macs(self): + # If not enough MAC addresses are available to allocate to networks, an + # error should be raised. + # We could pass in macs=set(), but that wouldn't tell us that + # allocate_for_instance tracks used macs properly, so we pass in one + # mac, and ask for two networks. + requested_networks = [ + (self.nets2[1]['id'], None, None), + (self.nets2[0]['id'], None, None)] + api = self._stub_allocate_for_instance( + net_idx=2, requested_networks=requested_networks, + macs=set(['my_mac2']), + _break='pre_get_instance_nw_info') + self.assertRaises(exception.PortNotFree, + api.allocate_for_instance, self.context, + self.instance, requested_networks=requested_networks, + macs=set(['my_mac2'])) + + def test_allocate_for_instance_two_macs_two_networks(self): + # If two MACs are available and two networks requested, two new ports + # get made and no exceptions raised. + requested_networks = [ + (self.nets2[1]['id'], None, None), + (self.nets2[0]['id'], None, None)] + self._allocate_for_instance( + net_idx=2, requested_networks=requested_networks, + macs=set(['my_mac2', 'my_mac1'])) + def test_allocate_for_instance_mac_conflicting_requested_port(self): # specify only first and last network requested_networks = [(None, None, 'my_portid1')] api = self._stub_allocate_for_instance( net_idx=1, requested_networks=requested_networks, macs=set(['unknown:mac']), - _break_list_networks=True) + _break='pre_list_networks') self.assertRaises(exception.PortNotUsable, api.allocate_for_instance, self.context, self.instance, requested_networks=requested_networks, @@ -943,6 +1002,54 @@ class TestQuantumv2(test.TestCase): self.mox.ReplayAll() api.disassociate_floating_ip(self.context, self.instance, address) + def test_add_fixed_ip_to_instance(self): + api = quantumapi.API() + network_id = 'my_netid1' + search_opts = {'network_id': network_id} + self.moxed_client.list_subnets( + **search_opts).AndReturn({'subnets': self.subnet_data1}) + + zone = 'compute:%s' % self.instance['availability_zone'] + search_opts = {'device_id': self.instance['uuid'], + 'device_owner': 'compute:nova', + 'network_id': network_id} + self.moxed_client.list_ports( + **search_opts).AndReturn({'ports': self.port_data1}) + port_req_body = { + 'port': { + 'fixed_ips': [{'subnet_id': 'my_subid1'}], + }, + } + port = self.port_data1[0] + port['fixed_ips'] = [{'subnet_id': 'my_subid1'}] + self.moxed_client.update_port('my_portid1', + MyComparator(port_req_body)).AndReturn({'port': port}) + + self.mox.ReplayAll() + api.add_fixed_ip_to_instance(self.context, self.instance, network_id) + + def test_remove_fixed_ip_from_instance(self): + api = quantumapi.API() + address = '10.0.0.3' + zone = 'compute:%s' % self.instance['availability_zone'] + search_opts = {'device_id': self.instance['uuid'], + 'device_owner': zone, + 'fixed_ips': 'ip_address=%s' % address} + self.moxed_client.list_ports( + **search_opts).AndReturn({'ports': self.port_data1}) + port_req_body = { + 'port': { + 'fixed_ips': [], + }, + } + port = self.port_data1[0] + port['fixed_ips'] = [] + self.moxed_client.update_port('my_portid1', + MyComparator(port_req_body)).AndReturn({'port': port}) + + self.mox.ReplayAll() + api.remove_fixed_ip_from_instance(self.context, self.instance, address) + class TestQuantumv2ModuleMethods(test.TestCase): def test_ensure_requested_network_ordering_no_preference(self): diff --git a/nova/tests/scheduler/test_chance_scheduler.py b/nova/tests/scheduler/test_chance_scheduler.py index 26cde055b..76fba900d 100644 --- a/nova/tests/scheduler/test_chance_scheduler.py +++ b/nova/tests/scheduler/test_chance_scheduler.py @@ -130,11 +130,11 @@ class ChanceSchedulerTestCase(test_scheduler.SchedulerTestCase): # instance 1 ctxt.elevated().AndReturn(ctxt_elevated) self.driver.hosts_up(ctxt_elevated, 'compute').AndReturn([]) - compute_utils.add_instance_fault_from_exc(ctxt, - uuid, mox.IsA(exception.NoValidHost), mox.IgnoreArg()) - db.instance_update_and_get_original(ctxt, uuid, + old_ref, new_ref = db.instance_update_and_get_original(ctxt, uuid, {'vm_state': vm_states.ERROR, 'task_state': None}).AndReturn(({}, {})) + compute_utils.add_instance_fault_from_exc(ctxt, + new_ref, mox.IsA(exception.NoValidHost), mox.IgnoreArg()) self.mox.ReplayAll() self.driver.schedule_run_instance( diff --git a/nova/tests/scheduler/test_filter_scheduler.py b/nova/tests/scheduler/test_filter_scheduler.py index 5d8e8236b..2bd2cb85b 100644 --- a/nova/tests/scheduler/test_filter_scheduler.py +++ b/nova/tests/scheduler/test_filter_scheduler.py @@ -58,11 +58,11 @@ class FilterSchedulerTestCase(test_scheduler.SchedulerTestCase): self.mox.StubOutWithMock(compute_utils, 'add_instance_fault_from_exc') self.mox.StubOutWithMock(db, 'instance_update_and_get_original') + old_ref, new_ref = db.instance_update_and_get_original(fake_context, + uuid, {'vm_state': vm_states.ERROR, 'task_state': + None}).AndReturn(({}, {})) compute_utils.add_instance_fault_from_exc(fake_context, - uuid, mox.IsA(exception.NoValidHost), mox.IgnoreArg()) - db.instance_update_and_get_original(fake_context, uuid, - {'vm_state': vm_states.ERROR, - 'task_state': None}).AndReturn(({}, {})) + new_ref, mox.IsA(exception.NoValidHost), mox.IgnoreArg()) self.mox.ReplayAll() sched.schedule_run_instance( fake_context, request_spec, None, None, None, None, {}) @@ -88,11 +88,11 @@ class FilterSchedulerTestCase(test_scheduler.SchedulerTestCase): 'instance_uuids': [uuid]} self.mox.StubOutWithMock(compute_utils, 'add_instance_fault_from_exc') self.mox.StubOutWithMock(db, 'instance_update_and_get_original') + old_ref, new_ref = db.instance_update_and_get_original(fake_context, + uuid, {'vm_state': vm_states.ERROR, 'task_state': + None}).AndReturn(({}, {})) compute_utils.add_instance_fault_from_exc(fake_context, - uuid, mox.IsA(exception.NoValidHost), mox.IgnoreArg()) - db.instance_update_and_get_original(fake_context, uuid, - {'vm_state': vm_states.ERROR, - 'task_state': None}).AndReturn(({}, {})) + new_ref, mox.IsA(exception.NoValidHost), mox.IgnoreArg()) self.mox.ReplayAll() sched.schedule_run_instance( fake_context, request_spec, None, None, None, None, {}) diff --git a/nova/tests/scheduler/test_scheduler.py b/nova/tests/scheduler/test_scheduler.py index dd5b0ae32..eb4c3864f 100644 --- a/nova/tests/scheduler/test_scheduler.py +++ b/nova/tests/scheduler/test_scheduler.py @@ -183,12 +183,12 @@ class SchedulerManagerTestCase(test.TestCase): self.manager.driver.schedule_run_instance(self.context, request_spec, None, None, None, None, {}).AndRaise( exception.NoValidHost(reason="")) - db.instance_update_and_get_original(self.context, fake_instance_uuid, + old, new_ref = db.instance_update_and_get_original(self.context, + fake_instance_uuid, {"vm_state": vm_states.ERROR, "task_state": None}).AndReturn((inst, inst)) - compute_utils.add_instance_fault_from_exc(self.context, - fake_instance_uuid, mox.IsA(exception.NoValidHost), - mox.IgnoreArg()) + compute_utils.add_instance_fault_from_exc(self.context, new_ref, + mox.IsA(exception.NoValidHost), mox.IgnoreArg()) self.mox.ReplayAll() self.manager.run_instance(self.context, request_spec, @@ -217,12 +217,12 @@ class SchedulerManagerTestCase(test.TestCase): } self.manager.driver.schedule_prep_resize(**kwargs).AndRaise( exception.NoValidHost(reason="")) - db.instance_update_and_get_original(self.context, fake_instance_uuid, + old_ref, new_ref = db.instance_update_and_get_original(self.context, + fake_instance_uuid, {"vm_state": vm_states.ACTIVE, "task_state": None}).AndReturn( (inst, inst)) - compute_utils.add_instance_fault_from_exc(self.context, - fake_instance_uuid, mox.IsA(exception.NoValidHost), - mox.IgnoreArg()) + compute_utils.add_instance_fault_from_exc(self.context, new_ref, + mox.IsA(exception.NoValidHost), mox.IgnoreArg()) self.mox.ReplayAll() self.manager.prep_resize(**kwargs) @@ -254,12 +254,12 @@ class SchedulerManagerTestCase(test.TestCase): "vm_state": "", "task_state": "", } - db.instance_update_and_get_original(self.context, fake_instance_uuid, + old_ref, new_ref = db.instance_update_and_get_original(self.context, + fake_instance_uuid, {"vm_state": vm_states.ERROR, "task_state": None}).AndReturn((inst, inst)) - compute_utils.add_instance_fault_from_exc(self.context, - fake_instance_uuid, mox.IsA(test.TestingException), - mox.IgnoreArg()) + compute_utils.add_instance_fault_from_exc(self.context, new_ref, + mox.IsA(test.TestingException), mox.IgnoreArg()) self.mox.ReplayAll() diff --git a/nova/tests/test_cinder.py b/nova/tests/test_cinder.py index 29e2e978b..79b5ae66a 100644 --- a/nova/tests/test_cinder.py +++ b/nova/tests/test_cinder.py @@ -98,13 +98,14 @@ class FakeHTTPClient(cinder.cinder_client.client.HTTPClient): class FakeCinderClient(cinder.cinder_client.Client): def __init__(self, username, password, project_id=None, auth_url=None, - retries=None): + insecure=False, retries=None): super(FakeCinderClient, self).__init__(username, password, project_id=project_id, auth_url=auth_url, + insecure=insecure, retries=retries) self.client = FakeHTTPClient(username, password, project_id, auth_url, - retries=retries) + insecure=insecure, retries=retries) # keep a ref to the clients callstack for factory's assert_called self.callstack = self.client.callstack = [] @@ -177,6 +178,15 @@ class CinderTestCase(test.TestCase): self.assertTrue('volume_image_metadata' in volume) self.assertEqual(volume['volume_image_metadata'], _image_metadata) + def test_cinder_api_insecure(self): + # The True/False negation is awkward, but better for the client + # to pass us insecure=True and we check verify_cert == False + self.flags(cinder_api_insecure=True) + volume = self.api.get(self.context, '1234') + self.assert_called('GET', '/volumes/1234') + self.assertEquals( + self.fake_client_factory.client.client.verify_cert, False) + def test_cinder_http_retries(self): retries = 42 self.flags(cinder_http_retries=retries) diff --git a/nova/tests/test_crypto.py b/nova/tests/test_crypto.py index 83010cee2..25df336fb 100644 --- a/nova/tests/test_crypto.py +++ b/nova/tests/test_crypto.py @@ -149,3 +149,66 @@ class CertExceptionTests(test.TestCase): self.assertRaises(exception.CryptoCRLFileNotFound, crypto.fetch_crl, project_id='fake') + + +class EncryptionTests(test.TestCase): + pubkey = ("ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDArtgrfBu/g2o28o+H2ng/crv" + "zgES91i/NNPPFTOutXelrJ9QiPTPTm+B8yspLsXifmbsmXztNOlBQgQXs6usxb4" + "fnJKNUZ84Vkp5esbqK/L7eyRqwPvqo7btKBMoAMVX/kUyojMpxb7Ssh6M6Y8cpi" + "goi+MSDPD7+5yRJ9z4mH9h7MCY6Ejv8KTcNYmVHvRhsFUcVhWcIISlNWUGiG7rf" + "oki060F5myQN3AXcL8gHG5/Qb1RVkQFUKZ5geQ39/wSyYA1Q65QTba/5G2QNbl2" + "0eAIBTyKZhN6g88ak+yARa6BLLDkrlP7L4WctHQMLsuXHohQsUO9AcOlVMARgrg" + "uF test@test") + prikey = """-----BEGIN RSA PRIVATE KEY----- +MIIEpQIBAAKCAQEAwK7YK3wbv4NqNvKPh9p4P3K784BEvdYvzTTzxUzrrV3payfU +Ij0z05vgfMrKS7F4n5m7Jl87TTpQUIEF7OrrMW+H5ySjVGfOFZKeXrG6ivy+3ska +sD76qO27SgTKADFV/5FMqIzKcW+0rIejOmPHKYoKIvjEgzw+/uckSfc+Jh/YezAm +OhI7/Ck3DWJlR70YbBVHFYVnCCEpTVlBohu636JItOtBeZskDdwF3C/IBxuf0G9U +VZEBVCmeYHkN/f8EsmANUOuUE22v+RtkDW5dtHgCAU8imYTeoPPGpPsgEWugSyw5 +K5T+y+FnLR0DC7Llx6IULFDvQHDpVTAEYK4LhQIDAQABAoIBAF9ibrrgHnBpItx+ +qVUMbriiGK8LUXxUmqdQTljeolDZi6KzPc2RVKWtpazBSvG7skX3+XCediHd+0JP +DNri1HlNiA6B0aUIGjoNsf6YpwsE4YwyK9cR5k5YGX4j7se3pKX2jOdngxQyw1Mh +dkmCeWZz4l67nbSFz32qeQlwrsB56THJjgHB7elDoGCXTX/9VJyjFlCbfxVCsIng +inrNgT0uMSYMNpAjTNOjguJt/DtXpwzei5eVpsERe0TRRVH23ycS0fuq/ancYwI/ +MDr9KSB8r+OVGeVGj3popCxECxYLBxhqS1dAQyJjhQXKwajJdHFzidjXO09hLBBz +FiutpYUCgYEA6OFikTrPlCMGMJjSj+R9woDAOPfvCDbVZWfNo8iupiECvei88W28 +RYFnvUQRjSC0pHe//mfUSmiEaE+SjkNCdnNR+vsq9q+htfrADm84jl1mfeWatg/g +zuGz2hAcZnux3kQMI7ufOwZNNpM2bf5B4yKamvG8tZRRxSkkAL1NV48CgYEA08/Z +Ty9g9XPKoLnUWStDh1zwG+c0q14l2giegxzaUAG5DOgOXbXcw0VQ++uOWD5ARELG +g9wZcbBsXxJrRpUqx+GAlv2Y1bkgiPQS1JIyhsWEUtwfAC/G+uZhCX53aI3Pbsjh +QmkPCSp5DuOuW2PybMaw+wVe+CaI/gwAWMYDAasCgYEA4Fzkvc7PVoU33XIeywr0 +LoQkrb4QyPUrOvt7H6SkvuFm5thn0KJMlRpLfAksb69m2l2U1+HooZd4mZawN+eN +DNmlzgxWJDypq83dYwq8jkxmBj1DhMxfZnIE+L403nelseIVYAfPLOqxUTcbZXVk +vRQFp+nmSXqQHUe5rAy1ivkCgYEAqLu7cclchCxqDv/6mc5NTVhMLu5QlvO5U6fq +HqitgW7d69oxF5X499YQXZ+ZFdMBf19ypTiBTIAu1M3nh6LtIa4SsjXzus5vjKpj +FdQhTBus/hU83Pkymk1MoDOPDEtsI+UDDdSDldmv9pyKGWPVi7H86vusXCLWnwsQ +e6fCXWECgYEAqgpGvva5kJ1ISgNwnJbwiNw0sOT9BMOsdNZBElf0kJIIy6FMPvap +6S1ziw+XWfdQ83VIUOCL5DrwmcYzLIogS0agmnx/monfDx0Nl9+OZRxy6+AI9vkK +86A1+DXdo+IgX3grFK1l1gPhAZPRWJZ+anrEkyR4iLq6ZoPZ3BQn97U= +-----END RSA PRIVATE KEY-----""" + text = "Some text! %$*" + + def _ssh_decrypt_text(self, ssh_private_key, text): + with utils.tempdir() as tmpdir: + sshkey = os.path.abspath(os.path.join(tmpdir, 'ssh.key')) + with open(sshkey, 'w') as f: + f.write(ssh_private_key) + try: + dec, _err = utils.execute('openssl', + 'rsautl', + '-decrypt', + '-inkey', sshkey, + process_input=text) + return dec + except exception.ProcessExecutionError as exc: + raise exception.DecryptionFailure(reason=exc.stderr) + + def test_ssh_encrypt_decrypt_text(self): + enc = crypto.ssh_encrypt_text(self.pubkey, self.text) + self.assertNotEqual(enc, self.text) + result = self._ssh_decrypt_text(self.prikey, enc) + self.assertEqual(result, self.text) + + def test_ssh_encrypt_failure(self): + self.assertRaises(exception.EncryptionFailure, + crypto.ssh_encrypt_text, '', self.text) diff --git a/nova/tests/test_fakelibvirt.py b/nova/tests/test_fakelibvirt.py index fea666f36..32c85a95a 100644 --- a/nova/tests/test_fakelibvirt.py +++ b/nova/tests/test_fakelibvirt.py @@ -53,6 +53,7 @@ def get_vm_xml(name="testname", uuid=None, source_type='file', </interface> <input type='mouse' bus='ps2'/> <graphics type='vnc' port='5901' autoport='yes' keymap='en-us'/> + <graphics type='spice' port='5901' autoport='yes' keymap='en-us'/> </devices> </domain>''' % {'name': name, 'uuid_tag': uuid_tag, diff --git a/nova/tests/test_libvirt.py b/nova/tests/test_libvirt.py index 0abf16801..c4816d202 100644 --- a/nova/tests/test_libvirt.py +++ b/nova/tests/test_libvirt.py @@ -2854,11 +2854,11 @@ class LibvirtConnTestCase(test.TestCase): conn = libvirt_driver.LibvirtDriver(fake.FakeVirtAPI(), False) self.mox.StubOutWithMock(conn, "_wrapped_conn") - self.mox.StubOutWithMock(conn._wrapped_conn, "getCapabilities") + self.mox.StubOutWithMock(conn._wrapped_conn, "getLibVersion") self.mox.StubOutWithMock(libvirt.libvirtError, "get_error_code") self.mox.StubOutWithMock(libvirt.libvirtError, "get_error_domain") - conn._wrapped_conn.getCapabilities().AndRaise( + conn._wrapped_conn.getLibVersion().AndRaise( libvirt.libvirtError("fake failure")) libvirt.libvirtError.get_error_code().AndReturn(error) @@ -3693,30 +3693,25 @@ class IptablesFirewallTestCase(test.TestCase): fake.FakeVirtAPI(), get_connection=lambda: self.fake_libvirt_connection) - in_nat_rules = [ + in_rules = [ '# Generated by iptables-save v1.4.10 on Sat Feb 19 00:03:19 2011', '*nat', ':PREROUTING ACCEPT [1170:189210]', ':INPUT ACCEPT [844:71028]', ':OUTPUT ACCEPT [5149:405186]', ':POSTROUTING ACCEPT [5063:386098]', - ] - - in_mangle_rules = [ - '# Generated by iptables-save v1.4.12 on Tue Dec 18 15:50:25 201;', - '*mangle', - ':PREROUTING ACCEPT [241:39722]', - ':INPUT ACCEPT [230:39282]', - ':FORWARD ACCEPT [0:0]', - ':OUTPUT ACCEPT [266:26558]', - ':POSTROUTING ACCEPT [267:26590]', - '-A POSTROUTING -o virbr0 -p udp -m udp --dport 68 -j CHECKSUM ' - '--checksum-fill', - 'COMMIT', - '# Completed on Tue Dec 18 15:50:25 2012', - ] - - in_filter_rules = [ + '# Completed on Tue Dec 18 15:50:25 2012', + '# Generated by iptables-save v1.4.12 on Tue Dec 18 15:50:25 201;', + '*mangle', + ':PREROUTING ACCEPT [241:39722]', + ':INPUT ACCEPT [230:39282]', + ':FORWARD ACCEPT [0:0]', + ':OUTPUT ACCEPT [266:26558]', + ':POSTROUTING ACCEPT [267:26590]', + '-A POSTROUTING -o virbr0 -p udp -m udp --dport 68 -j CHECKSUM ' + '--checksum-fill', + 'COMMIT', + '# Completed on Tue Dec 18 15:50:25 2012', '# Generated by iptables-save v1.4.4 on Mon Dec 6 11:54:13 2010', '*filter', ':INPUT ACCEPT [969615:281627771]', @@ -3811,15 +3806,11 @@ class IptablesFirewallTestCase(test.TestCase): # self.fw.add_instance(instance_ref) def fake_iptables_execute(*cmd, **kwargs): process_input = kwargs.get('process_input', None) - if cmd == ('ip6tables-save', '-c', '-t', 'filter'): + if cmd == ('ip6tables-save', '-c'): return '\n'.join(self.in6_filter_rules), None - if cmd == ('iptables-save', '-c', '-t', 'filter'): - return '\n'.join(self.in_filter_rules), None - if cmd == ('iptables-save', '-c', '-t', 'nat'): - return '\n'.join(self.in_nat_rules), None - if cmd == ('iptables-save', '-c', '-t', 'mangle'): - return '\n'.join(self.in_mangle_rules), None - if cmd == ('iptables-restore', '-c',): + if cmd == ('iptables-save', '-c'): + return '\n'.join(self.in_rules), None + if cmd == ('iptables-restore', '-c'): lines = process_input.split('\n') if '*filter' in lines: self.out_rules = lines @@ -3843,7 +3834,7 @@ class IptablesFirewallTestCase(test.TestCase): self.fw.apply_instance_filter(instance_ref, network_info) in_rules = filter(lambda l: not l.startswith('#'), - self.in_filter_rules) + self.in_rules) for rule in in_rules: if not 'nova' in rule: self.assertTrue(rule in self.out_rules, diff --git a/nova/tests/test_utils.py b/nova/tests/test_utils.py index 2c46b27bd..9eab72c5b 100644 --- a/nova/tests/test_utils.py +++ b/nova/tests/test_utils.py @@ -757,3 +757,24 @@ class LastBytesTestCase(test.TestCase): content = '1234567890' flo.write(content) self.assertEqual((content, 0), utils.last_bytes(flo, 1000)) + + +class IntLikeTestCase(test.TestCase): + + def test_is_int_like(self): + self.assertTrue(utils.is_int_like(1)) + self.assertTrue(utils.is_int_like("1")) + self.assertTrue(utils.is_int_like("514")) + self.assertTrue(utils.is_int_like("0")) + + self.assertFalse(utils.is_int_like(1.1)) + self.assertFalse(utils.is_int_like("1.1")) + self.assertFalse(utils.is_int_like("1.1.1")) + self.assertFalse(utils.is_int_like(None)) + self.assertFalse(utils.is_int_like("0.")) + self.assertFalse(utils.is_int_like("aaaaaa")) + self.assertFalse(utils.is_int_like("....")) + self.assertFalse(utils.is_int_like("1g")) + self.assertFalse( + utils.is_int_like("0cc3346e-9fef-4445-abe6-5d2b2690ec64")) + self.assertFalse(utils.is_int_like("a1")) diff --git a/nova/tests/test_virt_drivers.py b/nova/tests/test_virt_drivers.py index 199ae30b1..9747ecccd 100644 --- a/nova/tests/test_virt_drivers.py +++ b/nova/tests/test_virt_drivers.py @@ -446,6 +446,15 @@ class _VirtDriverTestCase(_FakeDriverBackendTestCase): self.assertIn('port', vnc_console) @catch_notimplementederror + def test_get_spice_console(self): + instance_ref, network_info = self._get_running_instance() + spice_console = self.connection.get_spice_console(instance_ref) + self.assertIn('internal_access_path', spice_console) + self.assertIn('host', spice_console) + self.assertIn('port', spice_console) + self.assertIn('tlsPort', spice_console) + + @catch_notimplementederror def test_get_console_pool_info(self): instance_ref, network_info = self._get_running_instance() console_pool = self.connection.get_console_pool_info(instance_ref) diff --git a/nova/tests/test_xenapi.py b/nova/tests/test_xenapi.py index 0b1c5d0e7..067e28a13 100644 --- a/nova/tests/test_xenapi.py +++ b/nova/tests/test_xenapi.py @@ -1822,16 +1822,31 @@ class XenAPIBWCountersTestCase(stubs.XenAPITestBase): # Consider abstracting common code in a base class for firewall driver testing. class XenAPIDom0IptablesFirewallTestCase(stubs.XenAPITestBase): - _in_nat_rules = [ + _in_rules = [ '# Generated by iptables-save v1.4.10 on Sat Feb 19 00:03:19 2011', '*nat', ':PREROUTING ACCEPT [1170:189210]', ':INPUT ACCEPT [844:71028]', ':OUTPUT ACCEPT [5149:405186]', ':POSTROUTING ACCEPT [5063:386098]', - ] - - _in_filter_rules = [ + '# Completed on Mon Dec 6 11:54:13 2010', + '# Generated by iptables-save v1.4.4 on Mon Dec 6 11:54:13 2010', + '*mangle', + ':INPUT ACCEPT [969615:281627771]', + ':FORWARD ACCEPT [0:0]', + ':OUTPUT ACCEPT [915599:63811649]', + ':nova-block-ipv4 - [0:0]', + '[0:0] -A INPUT -i virbr0 -p tcp -m tcp --dport 67 -j ACCEPT ', + '[0:0] -A FORWARD -d 192.168.122.0/24 -o virbr0 -m state --state RELATED' + ',ESTABLISHED -j ACCEPT ', + '[0:0] -A FORWARD -s 192.168.122.0/24 -i virbr0 -j ACCEPT ', + '[0:0] -A FORWARD -i virbr0 -o virbr0 -j ACCEPT ', + '[0:0] -A FORWARD -o virbr0 -j REJECT ' + '--reject-with icmp-port-unreachable ', + '[0:0] -A FORWARD -i virbr0 -j REJECT ' + '--reject-with icmp-port-unreachable ', + 'COMMIT', + '# Completed on Mon Dec 6 11:54:13 2010', '# Generated by iptables-save v1.4.4 on Mon Dec 6 11:54:13 2010', '*filter', ':INPUT ACCEPT [969615:281627771]', @@ -1916,7 +1931,7 @@ class XenAPIDom0IptablesFirewallTestCase(stubs.XenAPITestBase): def _validate_security_group(self): in_rules = filter(lambda l: not l.startswith('#'), - self._in_filter_rules) + self._in_rules) for rule in in_rules: if not 'nova' in rule: self.assertTrue(rule in self._out_rules, diff --git a/nova/tests/xenapi/stubs.py b/nova/tests/xenapi/stubs.py index 85c85b5e2..fa214b23e 100644 --- a/nova/tests/xenapi/stubs.py +++ b/nova/tests/xenapi/stubs.py @@ -208,12 +208,10 @@ class FakeSessionForFirewallTests(FakeSessionForVMTests): def __init__(self, uri, test_case=None): super(FakeSessionForFirewallTests, self).__init__(uri) - if hasattr(test_case, '_in_filter_rules'): - self._in_filter_rules = test_case._in_filter_rules + if hasattr(test_case, '_in_rules'): + self._in_rules = test_case._in_rules if hasattr(test_case, '_in6_filter_rules'): self._in6_filter_rules = test_case._in6_filter_rules - if hasattr(test_case, '_in_nat_rules'): - self._in_nat_rules = test_case._in_nat_rules self._test_case = test_case def host_call_plugin(self, _1, _2, plugin, method, args): @@ -230,12 +228,10 @@ class FakeSessionForFirewallTests(FakeSessionForVMTests): else: output = '' process_input = args.get('process_input', None) - if cmd == ['ip6tables-save', '-c', '-t', 'filter']: + if cmd == ['ip6tables-save', '-c']: output = '\n'.join(self._in6_filter_rules) - if cmd == ['iptables-save', '-c', '-t', 'filter']: - output = '\n'.join(self._in_filter_rules) - if cmd == ['iptables-save', '-c', '-t', 'nat']: - output = '\n'.join(self._in_nat_rules) + if cmd == ['iptables-save', '-c']: + output = '\n'.join(self._in_rules) if cmd == ['iptables-restore', '-c', ]: lines = process_input.split('\n') if '*filter' in lines: diff --git a/nova/utils.py b/nova/utils.py index 115791b64..75cba0a7c 100644 --- a/nova/utils.py +++ b/nova/utils.py @@ -859,6 +859,14 @@ def bool_from_str(val): val.lower() == 'y' +def is_int_like(val): + """Check if a value looks like an int.""" + try: + return str(int(val)) == str(val) + except Exception: + return False + + def is_valid_boolstr(val): """Check if the provided string is a valid bool string or not.""" val = str(val).lower() diff --git a/nova/virt/baremetal/net-dhcp.ubuntu.template b/nova/virt/baremetal/net-dhcp.ubuntu.template index e8824a88d..34a9e8be7 100644 --- a/nova/virt/baremetal/net-dhcp.ubuntu.template +++ b/nova/virt/baremetal/net-dhcp.ubuntu.template @@ -10,9 +10,6 @@ iface lo inet loopback #for $ifc in $interfaces auto ${ifc.name} iface ${ifc.name} inet dhcp -#if $ifc.hwaddress - hwaddress ether ${ifc.hwaddress} -#end if #if $use_ipv6 iface ${ifc.name} inet6 dhcp diff --git a/nova/virt/baremetal/net-static.ubuntu.template b/nova/virt/baremetal/net-static.ubuntu.template index f14f0ce8c..1fe5a1ab8 100644 --- a/nova/virt/baremetal/net-static.ubuntu.template +++ b/nova/virt/baremetal/net-static.ubuntu.template @@ -16,9 +16,6 @@ iface ${ifc.name} inet static #if $ifc.dns dns-nameservers ${ifc.dns} #end if -#if $ifc.hwaddress - hwaddress ether ${ifc.hwaddress} -#end if #if $use_ipv6 iface ${ifc.name} inet6 static diff --git a/nova/virt/baremetal/pxe.py b/nova/virt/baremetal/pxe.py index b94ac9032..47bfc55af 100644 --- a/nova/virt/baremetal/pxe.py +++ b/nova/virt/baremetal/pxe.py @@ -121,7 +121,6 @@ def build_network_config(network_info): gateway_v6 = mapping['gateway_v6'] interface = { 'name': 'eth%d' % id, - 'hwaddress': mapping['mac'], 'address': mapping['ips'][0]['ip'], 'gateway': mapping['gateway'], 'netmask': mapping['ips'][0]['netmask'], @@ -238,27 +237,12 @@ class PXE(base.NodeDriver): super(PXE, self).__init__() def _collect_mac_addresses(self, context, node): - macs = [] - macs.append(db.bm_node_get(context, node['id'])['prov_mac_address']) + macs = set() + macs.add(db.bm_node_get(context, node['id'])['prov_mac_address']) for nic in db.bm_interface_get_all_by_bm_node_id(context, node['id']): if nic['address']: - macs.append(nic['address']) - macs.sort() - return macs - - def _generate_udev_rules(self, context, node): - # TODO(deva): fix assumption that device names begin with "eth" - # and fix assumption of ordering - macs = self._collect_mac_addresses(context, node) - rules = '' - for (i, mac) in enumerate(macs): - rules += 'SUBSYSTEM=="net", ACTION=="add", DRIVERS=="?*", ' \ - 'ATTR{address}=="%(mac)s", ATTR{dev_id}=="0x0", ' \ - 'ATTR{type}=="1", KERNEL=="eth*", NAME="%(name)s"\n' \ - % {'mac': mac.lower(), - 'name': 'eth%d' % i, - } - return rules + macs.add(nic['address']) + return sorted(macs) def _cache_tftp_images(self, context, instance, image_info): """Fetch the necessary kernels and ramdisks for the instance.""" @@ -330,9 +314,6 @@ class PXE(base.NodeDriver): injected_files = [] net_config = build_network_config(network_info) - udev_rules = self._generate_udev_rules(context, node) - injected_files.append( - ('/etc/udev/rules.d/70-persistent-net.rules', udev_rules)) if instance['hostname']: injected_files.append(('/etc/hostname', instance['hostname'])) @@ -385,7 +366,6 @@ class PXE(base.NodeDriver): config ./pxelinux.cfg/ {mac} -> ../{uuid}/config - """ image_info = get_tftp_image_info(instance) (root_mb, swap_mb) = get_partition_sizes(instance) diff --git a/nova/virt/baremetal/volume_driver.py b/nova/virt/baremetal/volume_driver.py index 2e6f82b93..0a05dfedd 100644 --- a/nova/virt/baremetal/volume_driver.py +++ b/nova/virt/baremetal/volume_driver.py @@ -31,7 +31,7 @@ opts = [ cfg.BoolOpt('use_unsafe_iscsi', default=False, help='Do not set this out of dev/test environments. ' - 'If a node does not have an fixed PXE IP address, ' + 'If a node does not have a fixed PXE IP address, ' 'volumes are exported with globally opened ACL'), cfg.StrOpt('iscsi_iqn_prefix', default='iqn.2010-10.org.openstack.baremetal', diff --git a/nova/virt/driver.py b/nova/virt/driver.py index a8f779e66..aa0439e74 100644 --- a/nova/virt/driver.py +++ b/nova/virt/driver.py @@ -258,6 +258,10 @@ class ComputeDriver(object): # TODO(Vek): Need to pass context in for access to auth_token raise NotImplementedError() + def get_spice_console(self, instance): + # TODO(Vek): Need to pass context in for access to auth_token + raise NotImplementedError() + def get_diagnostics(self, instance): """Return data about VM diagnostics.""" # TODO(Vek): Need to pass context in for access to auth_token diff --git a/nova/virt/fake.py b/nova/virt/fake.py index 0a29a6d67..338d1dec1 100644 --- a/nova/virt/fake.py +++ b/nova/virt/fake.py @@ -271,6 +271,12 @@ class FakeDriver(driver.ComputeDriver): 'host': 'fakevncconsole.com', 'port': 6969} + def get_spice_console(self, instance): + return {'internal_access_path': 'FAKE', + 'host': 'fakespiceconsole.com', + 'port': 6969, + 'tlsPort': 6970} + def get_console_pool_info(self, console_type): return {'address': '127.0.0.1', 'username': 'fakeuser', diff --git a/nova/virt/hyperv/volumeops.py b/nova/virt/hyperv/volumeops.py index 200236233..192d6834c 100644 --- a/nova/virt/hyperv/volumeops.py +++ b/nova/virt/hyperv/volumeops.py @@ -37,7 +37,7 @@ hyper_volumeops_opts = [ help='The number of times we retry on attaching volume '), cfg.IntOpt('hyperv_wait_between_attach_retry', default=5, - help='The seconds to wait between an volume attachment attempt'), + help='The seconds to wait between a volume attachment attempt'), cfg.BoolOpt('force_volumeutils_v1', default=False, help='Force volumeutils v1'), diff --git a/nova/virt/libvirt/driver.py b/nova/virt/libvirt/driver.py index 557818a99..115c6cd02 100644 --- a/nova/virt/libvirt/driver.py +++ b/nova/virt/libvirt/driver.py @@ -277,6 +277,7 @@ class LibvirtDriver(driver.ComputeDriver): self._host_state = None self._initiator = None self._wrapped_conn = None + self._caps = None self.read_only = read_only self.firewall_driver = firewall.load_driver( DEFAULT_FIREWALL_DRIVER, @@ -362,7 +363,7 @@ class LibvirtDriver(driver.ComputeDriver): def _test_connection(self): try: - self._wrapped_conn.getCapabilities() + self._wrapped_conn.getLibVersion() return True except libvirt.libvirtError as e: if (e.get_error_code() == libvirt.VIR_ERR_SYSTEM_ERROR and @@ -1146,6 +1147,27 @@ class LibvirtDriver(driver.ComputeDriver): return {'host': host, 'port': port, 'internal_access_path': None} + @exception.wrap_exception() + def get_spice_console(self, instance): + def get_spice_ports_for_instance(instance_name): + virt_dom = self._lookup_by_name(instance_name) + xml = virt_dom.XMLDesc(0) + # TODO(sleepsonthefloor): use etree instead of minidom + dom = minidom.parseString(xml) + + for graphic in dom.getElementsByTagName('graphics'): + if graphic.getAttribute('type') == 'spice': + return (graphic.getAttribute('port'), + graphic.getAttribute('tlsPort')) + + return (None, None) + + ports = get_spice_ports_for_instance(instance['name']) + host = CONF.spice.server_proxyclient_address + + return {'host': host, 'port': ports[0], + 'tlsPort': ports[1], 'internal_access_path': None} + @staticmethod def _supports_direct_io(dirpath): @@ -1422,11 +1444,11 @@ class LibvirtDriver(driver.ComputeDriver): def get_host_capabilities(self): """Returns an instance of config.LibvirtConfigCaps representing the capabilities of the host""" - xmlstr = self._conn.getCapabilities() - - caps = vconfig.LibvirtConfigCaps() - caps.parse_str(xmlstr) - return caps + if not self._caps: + xmlstr = self._conn.getCapabilities() + self._caps = vconfig.LibvirtConfigCaps() + self._caps.parse_str(xmlstr) + return self._caps def get_host_uuid(self): """Returns a UUID representing the host.""" @@ -1973,7 +1995,7 @@ class LibvirtDriver(driver.ComputeDriver): def get_interfaces(self, xml): """ - Note that this function takes an domain xml. + Note that this function takes a domain xml. Returns a list of all network interfaces for this instance. """ diff --git a/nova/virt/libvirt/vif.py b/nova/virt/libvirt/vif.py index 54de9da2d..83d43a6db 100644 --- a/nova/virt/libvirt/vif.py +++ b/nova/virt/libvirt/vif.py @@ -32,6 +32,11 @@ from nova.virt import netutils LOG = logging.getLogger(__name__) libvirt_vif_opts = [ + # quantum_ovs_bridge is used, if Quantum provides Nova + # the 'vif_type' portbinding field + cfg.StrOpt('libvirt_ovs_bridge', + default='br-int', + help='Name of Integration Bridge used by Open vSwitch'), cfg.BoolOpt('libvirt_use_virtio_for_bridges', default=True, help='Use virtio for bridge interfaces with KVM/QEMU'), @@ -71,6 +76,9 @@ class LibvirtBaseVIFDriver(object): class LibvirtBridgeDriver(LibvirtBaseVIFDriver): """VIF driver for Linux bridge.""" + def get_bridge_name(self, network): + return network['bridge'] + def get_config(self, instance, network, mapping): """Get VIF configurations for bridge type.""" @@ -82,7 +90,8 @@ class LibvirtBridgeDriver(LibvirtBaseVIFDriver): mapping) designer.set_vif_host_backend_bridge_config( - conf, network['bridge'], self.get_vif_devname(mapping)) + conf, self.get_bridge_name(network), + self.get_vif_devname(mapping)) name = "nova-instance-" + instance['name'] + "-" + mac_id primary_addr = mapping['ips'][0]['ip'] @@ -112,18 +121,18 @@ class LibvirtBridgeDriver(LibvirtBaseVIFDriver): iface = CONF.vlan_interface or network['bridge_interface'] LOG.debug(_('Ensuring vlan %(vlan)s and bridge %(bridge)s'), {'vlan': network['vlan'], - 'bridge': network['bridge']}, + 'bridge': self.get_bridge_name(network)}, instance=instance) linux_net.LinuxBridgeInterfaceDriver.ensure_vlan_bridge( network['vlan'], - network['bridge'], + self.get_bridge_name(network), iface) else: iface = CONF.flat_interface or network['bridge_interface'] - LOG.debug(_("Ensuring bridge %s"), network['bridge'], - instance=instance) + LOG.debug(_("Ensuring bridge %s"), + self.get_bridge_name(network), instance=instance) linux_net.LinuxBridgeInterfaceDriver.ensure_bridge( - network['bridge'], + self.get_bridge_name(network), iface) def unplug(self, instance, vif): @@ -138,6 +147,9 @@ class LibvirtOpenVswitchDriver(LibvirtBaseVIFDriver): OVS virtual port XML (0.9.10 or earlier). """ + def get_bridge_name(self, network): + return network.get('bridge') or CONF.libvirt_ovs_bridge + def get_config(self, instance, network, mapping): dev = self.get_vif_devname(mapping) @@ -183,7 +195,7 @@ class LibvirtOpenVswitchDriver(LibvirtBaseVIFDriver): utils.execute('tunctl', '-b', '-t', dev, run_as_root=True) utils.execute('ip', 'link', 'set', dev, 'up', run_as_root=True) - self.create_ovs_vif_port(network['bridge'], + self.create_ovs_vif_port(self.get_bridge_name(network), dev, iface_id, mapping['mac'], instance['uuid']) @@ -191,7 +203,7 @@ class LibvirtOpenVswitchDriver(LibvirtBaseVIFDriver): """Unplug the VIF by deleting the port from the bridge.""" try: network, mapping = vif - self.delete_ovs_vif_port(network['bridge'], + self.delete_ovs_vif_port(self.get_bridge_name(network), self.get_vif_devname(mapping)) except exception.ProcessExecutionError: LOG.exception(_("Failed while unplugging vif"), instance=instance) @@ -214,6 +226,9 @@ class LibvirtHybridOVSBridgeDriver(LibvirtBridgeDriver, return (("qvb%s" % iface_id)[:network_model.NIC_NAME_LEN], ("qvo%s" % iface_id)[:network_model.NIC_NAME_LEN]) + def get_bridge_name(self, network): + return network.get('bridge') or CONF.libvirt_ovs_bridge + def get_config(self, instance, network, mapping): br_name = self.get_br_name(mapping['vif_uuid']) network['bridge'] = br_name @@ -243,7 +258,7 @@ class LibvirtHybridOVSBridgeDriver(LibvirtBridgeDriver, linux_net._create_veth_pair(v1_name, v2_name) utils.execute('ip', 'link', 'set', br_name, 'up', run_as_root=True) utils.execute('brctl', 'addif', br_name, v1_name, run_as_root=True) - self.create_ovs_vif_port(network['bridge'], + self.create_ovs_vif_port(self.get_bridge_name(network), v2_name, iface_id, mapping['mac'], instance['uuid']) @@ -264,7 +279,7 @@ class LibvirtHybridOVSBridgeDriver(LibvirtBridgeDriver, run_as_root=True) utils.execute('brctl', 'delbr', br_name, run_as_root=True) - self.delete_ovs_vif_port(network['bridge'], v2_name) + self.delete_ovs_vif_port(self.get_bridge_name(network), v2_name) except exception.ProcessExecutionError: LOG.exception(_("Failed while unplugging vif"), instance=instance) @@ -273,6 +288,9 @@ class LibvirtOpenVswitchVirtualPortDriver(LibvirtBaseVIFDriver): """VIF driver for Open vSwitch that uses integrated libvirt OVS virtual port XML (introduced in libvirt 0.9.11).""" + def get_bridge_name(self, network): + return network.get('bridge') or CONF.libvirt_ovs_bridge + def get_config(self, instance, network, mapping): """Pass data required to create OVS virtual port element.""" conf = super(LibvirtOpenVswitchVirtualPortDriver, @@ -281,7 +299,7 @@ class LibvirtOpenVswitchVirtualPortDriver(LibvirtBaseVIFDriver): mapping) designer.set_vif_host_backend_ovs_config( - conf, network['bridge'], mapping['vif_uuid'], + conf, self.get_bridge_name(network), mapping['vif_uuid'], self.get_vif_devname(mapping)) return conf @@ -297,10 +315,15 @@ class LibvirtOpenVswitchVirtualPortDriver(LibvirtBaseVIFDriver): class QuantumLinuxBridgeVIFDriver(LibvirtBaseVIFDriver): """VIF driver for Linux Bridge when running Quantum.""" + def get_bridge_name(self, network): + def_bridge = ("brq" + network['id'])[:network_model.NIC_NAME_LEN] + return network.get('bridge') or def_bridge + def get_config(self, instance, network, mapping): - linux_net.LinuxBridgeInterfaceDriver.ensure_bridge(network['bridge'], - None, - filtering=False) + linux_net.LinuxBridgeInterfaceDriver.ensure_bridge( + self.get_bridge_name(network), + None, + filtering=False) conf = super(QuantumLinuxBridgeVIFDriver, self).get_config(instance, @@ -308,7 +331,8 @@ class QuantumLinuxBridgeVIFDriver(LibvirtBaseVIFDriver): mapping) designer.set_vif_host_backend_bridge_config( - conf, network['bridge'], self.get_vif_devname(mapping)) + conf, self.get_bridge_name(network), + self.get_vif_devname(mapping)) return conf diff --git a/nova/virt/xenapi/agent.py b/nova/virt/xenapi/agent.py index 61cfa9631..ef08edbc1 100644 --- a/nova/virt/xenapi/agent.py +++ b/nova/virt/xenapi/agent.py @@ -21,6 +21,9 @@ import os import time import uuid +from nova.api.metadata import password +from nova import context +from nova import crypto from nova.openstack.common import cfg from nova.openstack.common import jsonutils from nova.openstack.common import log as logging @@ -207,6 +210,12 @@ class XenAPIBasedAgent(object): LOG.error(msg, instance=self.instance) raise Exception(msg) + sshkey = self.instance.get('key_data') + if sshkey: + enc = crypto.ssh_encrypt_text(sshkey, new_pass) + password.set_password(context.get_admin_context(), + self.instance['uuid'], base64.b64encode(enc)) + return resp['message'] def inject_file(self, path, contents): diff --git a/nova/virt/xenapi/pool_states.py b/nova/virt/xenapi/pool_states.py index 5bf326117..138f84831 100644 --- a/nova/virt/xenapi/pool_states.py +++ b/nova/virt/xenapi/pool_states.py @@ -19,10 +19,10 @@ A pool may be 'created', in which case the admin has triggered its creation, but the underlying hypervisor pool has not actually being set up -yet. An pool may be 'changing', meaning that the underlying hypervisor -pool is being setup. An pool may be 'active', in which case the underlying -hypervisor pool is up and running. An pool may be 'dismissed' when it has -no hosts and it has been deleted. An pool may be in 'error' in all other +yet. A pool may be 'changing', meaning that the underlying hypervisor +pool is being setup. A pool may be 'active', in which case the underlying +hypervisor pool is up and running. A pool may be 'dismissed' when it has +no hosts and it has been deleted. A pool may be in 'error' in all other cases. A 'created' pool becomes 'changing' during the first request of adding a host. During a 'changing' status no other requests will be accepted; @@ -34,7 +34,7 @@ All other operations (e.g. add/remove hosts) that succeed will keep the pool in the 'active' state. If a number of continuous requests fail, an 'active' pool goes into an 'error' state. To recover from such a state, admin intervention is required. Currently an error state is irreversible, -that is, in order to recover from it an pool must be deleted. +that is, in order to recover from it a pool must be deleted. """ CREATED = 'created' diff --git a/nova/volume/cinder.py b/nova/volume/cinder.py index fccdedac8..3e1ccc66b 100644 --- a/nova/volume/cinder.py +++ b/nova/volume/cinder.py @@ -48,6 +48,9 @@ cinder_opts = [ cfg.IntOpt('cinder_http_retries', default=3, help='Number of cinderclient retries on failed http calls'), + cfg.BoolOpt('cinder_api_insecure', + default=False, + help='Allow to perform insecure SSL requests to cinder'), ] CONF = cfg.CONF @@ -88,6 +91,7 @@ def cinderclient(context): context.auth_token, project_id=context.project_id, auth_url=url, + insecure=CONF.cinder_api_insecure, retries=CONF.cinder_http_retries) # noauth extracts user_id:project_id from auth_token c.client.auth_token = context.auth_token or '%s:%s' % (context.user_id, diff --git a/run_tests.sh b/run_tests.sh index 3a579ca36..39176d78b 100755 --- a/run_tests.sh +++ b/run_tests.sh @@ -121,7 +121,7 @@ function run_pep8 { srcfiles+=" setup.py" # Until all these issues get fixed, ignore. - ignore='--ignore=E12,E711,E721,E712' + ignore='--ignore=E12,E711,E721,E712,N403,N404' # First run the hacking selftest, to make sure it's right echo "Running hacking.py self test" @@ -66,6 +66,7 @@ setuptools.setup(name='nova', 'bin/nova-objectstore', 'bin/nova-rootwrap', 'bin/nova-scheduler', + 'bin/nova-spicehtml5proxy', 'bin/nova-xvpvncproxy', ], py_modules=[]) diff --git a/tools/hacking.py b/tools/hacking.py index ed22956eb..56f6694bd 100755 --- a/tools/hacking.py +++ b/tools/hacking.py @@ -21,7 +21,6 @@ built on top of pep8.py """ -import fnmatch import inspect import logging import os @@ -46,16 +45,15 @@ logging.disable('LOG') #N8xx git commit messages IMPORT_EXCEPTIONS = ['sqlalchemy', 'migrate', 'nova.db.sqlalchemy.session'] -DOCSTRING_TRIPLE = ['"""', "'''"] +START_DOCSTRING_TRIPLE = ['u"""', 'r"""', '"""', "u'''", "r'''", "'''"] +END_DOCSTRING_TRIPLE = ['"""', "'''"] VERBOSE_MISSING_IMPORT = os.getenv('HACKING_VERBOSE_MISSING_IMPORT', 'False') # Monkey patch broken excluded filter in pep8 # See https://github.com/jcrocholl/pep8/pull/111 def excluded(self, filename): - """ - Check if options.exclude contains a pattern that matches filename. - """ + """Check if options.exclude contains a pattern that matches filename.""" basename = os.path.basename(filename) return any((pep8.filename_match(filename, self.options.exclude, default=False), @@ -120,7 +118,7 @@ def nova_todo_format(physical_line): pos2 = physical_line.find('#') # make sure it's a comment # TODO(sdague): should be smarter on this test this_test = physical_line.find('N101: #TODO fail') - if (pos != pos1 and pos2 >= 0 and pos2 < pos and this_test == -1): + if pos != pos1 and pos2 >= 0 and pos2 < pos and this_test == -1: return pos, "N101: Use TODO(NAME)" @@ -187,7 +185,8 @@ def nova_import_module_only(logical_line): # TODO(sdague) actually get these tests working def importModuleCheck(mod, parent=None, added=False): - """ + """Import Module helper function. + If can't find module on first try, recursively check for relative imports """ @@ -258,8 +257,7 @@ def nova_import_module_only(logical_line): def nova_import_alphabetical(logical_line, blank_lines, previous_logical, indent_level, previous_indent_level): - r""" - Check for imports in alphabetical order. + r"""Check for imports in alphabetical order. nova HACKING guide recommendation for imports: imports in human alphabetical order @@ -294,6 +292,11 @@ def nova_import_no_db_in_virt(logical_line, filename): yield (0, "N307: nova.db import not allowed in nova/virt/*") +def in_docstring_position(previous_logical): + return (previous_logical.startswith("def ") or + previous_logical.startswith("class ")) + + def nova_docstring_start_space(physical_line, previous_logical): r"""Check for docstring not start with space. @@ -311,11 +314,10 @@ def nova_docstring_start_space(physical_line, previous_logical): # it's important that we determine this is actually a docstring, # and not a doc block used somewhere after the first line of a # function def - if (previous_logical.startswith("def ") or - previous_logical.startswith("class ")): - pos = max([physical_line.find(i) for i in DOCSTRING_TRIPLE]) - if (pos != -1 and len(physical_line) > pos + 4): - if (physical_line[pos + 3] == ' '): + if in_docstring_position(previous_logical): + pos = max([physical_line.find(i) for i in START_DOCSTRING_TRIPLE]) + if pos != -1 and len(physical_line) > pos + 4: + if physical_line[pos + 3] == ' ': return (pos, "N401: docstring should not start with" " a space") @@ -330,33 +332,63 @@ def nova_docstring_one_line(physical_line): N402: '''This is not''' N402: '''Bad punctuation,''' """ + #TODO(jogo) make this apply to multi line docstrings as well line = physical_line.lstrip() if line.startswith('"') or line.startswith("'"): - pos = max([line.find(i) for i in DOCSTRING_TRIPLE]) # start - end = max([line[-4:-1] == i for i in DOCSTRING_TRIPLE]) # end + pos = max([line.find(i) for i in START_DOCSTRING_TRIPLE]) # start + end = max([line[-4:-1] == i for i in END_DOCSTRING_TRIPLE]) # end - if (pos != -1 and end and len(line) > pos + 4): - if (line[-5] not in ['.', '?', '!']): + if pos != -1 and end and len(line) > pos + 4: + if line[-5] not in ['.', '?', '!']: return pos, "N402: one line docstring needs punctuation." -def nova_docstring_multiline_end(physical_line): +def nova_docstring_multiline_end(physical_line, previous_logical): r"""Check multi line docstring end. nova HACKING guide recommendation for docstring: Docstring should end on a new line - Okay: '''\nfoo\nbar\n''' - # This test is not triggered, don't think it's right, removing - # the colon prevents it from running - N403 '''\nfoo\nbar\n ''' \n\n + Okay: '''foobar\nfoo\nbar\n''' + N403: def foo():\n'''foobar\nfoo\nbar\n d'''\n\n """ - # TODO(sdague) actually get these tests working - pos = max([physical_line.find(i) for i in DOCSTRING_TRIPLE]) # start - if (pos != -1 and len(physical_line) == pos): - if (physical_line[pos + 3] == ' '): - return (pos, "N403: multi line docstring end on new line") + if in_docstring_position(previous_logical): + pos = max(physical_line.find(i) for i in END_DOCSTRING_TRIPLE) + if pos != -1 and len(physical_line) == pos + 4: + if physical_line.strip() not in START_DOCSTRING_TRIPLE: + return (pos, "N403: multi line docstring end on new line") + + +def nova_docstring_multiline_start(physical_line, previous_logical, tokens): + r"""Check multi line docstring start with summary. + + nova HACKING guide recommendation for docstring: + Docstring should start with A multi line docstring has a one-line summary + + Okay: '''foobar\nfoo\nbar\n''' + N404: def foo():\n'''\nfoo\nbar\n''' \n\n + """ + if in_docstring_position(previous_logical): + pos = max([physical_line.find(i) for i in START_DOCSTRING_TRIPLE]) + # start of docstring when len(tokens)==0 + if len(tokens) == 0 and pos != -1 and len(physical_line) == pos + 4: + if physical_line.strip() in START_DOCSTRING_TRIPLE: + return (pos, "N404: multi line docstring " + "should start with a summary") + + +def nova_no_cr(physical_line): + r"""Check that we only use newlines not cariage returns. + + Okay: import os\nimport sys + # pep8 doesn't yet replace \r in strings, will work on an + # upstream fix + N901 import os\r\nimport sys + """ + pos = physical_line.find('\r') + if pos != -1 and pos == (len(physical_line) - 2): + return (pos, "N901: Windows style line endings not allowed in code") FORMAT_RE = re.compile("%(?:" diff --git a/tools/pip-requires b/tools/pip-requires index 1845ba7dd..231d5cfe5 100644 --- a/tools/pip-requires +++ b/tools/pip-requires @@ -25,3 +25,4 @@ python-quantumclient>=2.1 python-glanceclient>=0.5.0,<2 python-keystoneclient>=0.2.0 stevedore>=0.7 +websockify diff --git a/tools/test-requires b/tools/test-requires index 5f195d5c1..c691a6bca 100644 --- a/tools/test-requires +++ b/tools/test-requires @@ -12,4 +12,5 @@ pylint==0.25.2 python-subunit sphinx>=1.1.2 testrepository>=0.0.13 -testtools>=0.9.22 +# testtools 0.9.25 is broken, change this when upstream is fixed (bug 1102400) +testtools>=0.9.22,<=0.9.24 @@ -18,9 +18,9 @@ downloadcache = ~/cache/pip deps=pep8==1.3.3 commands = python tools/hacking.py --doctest - python tools/hacking.py --ignore=E12,E711,E721,E712 --repeat --show-source \ + python tools/hacking.py --ignore=E12,E711,E721,E712,N403,N404 --show-source \ --exclude=.venv,.git,.tox,dist,doc,*openstack/common*,*lib/python*,*egg . - python tools/hacking.py --ignore=E12,E711,E721,E712 --repeat --show-source \ + python tools/hacking.py --ignore=E12,E711,E721,E712,N403,N404 --show-source \ --filename=nova* bin [testenv:pylint] |