diff options
106 files changed, 3054 insertions, 319 deletions
diff --git a/.coveragerc b/.coveragerc index 82fe47792..902a94349 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1,6 +1,7 @@ [run] branch = True -omit = /usr*,setup.py,*egg*,.venv/*,.tox/*,nova/tests/* +source = nova +omit = nova/tests/*,DynamicallyCompiledCheetahTemplate.py [report] ignore-errors = True diff --git a/bin/nova-manage b/bin/nova-manage index c783c304b..4f3d889ea 100755 --- a/bin/nova-manage +++ b/bin/nova-manage @@ -1056,11 +1056,11 @@ class CellCommands(object): ctxt = context.get_admin_context() db.cell_create(ctxt, values) - @args('--cell_id', dest='cell_id', metavar='<cell_id>', - help='ID of the cell to delete') - def delete(self, cell_id): + @args('--cell_name', dest='cell_name', metavar='<cell_name>', + help='Name of the cell to delete') + def delete(self, cell_name): ctxt = context.get_admin_context() - db.cell_delete(ctxt, cell_id) + db.cell_delete(ctxt, cell_name) def list(self): ctxt = context.get_admin_context() 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/contrib/xen/vif-openstack b/contrib/xen/vif-openstack new file mode 100755 index 000000000..1df6ad6ac --- /dev/null +++ b/contrib/xen/vif-openstack @@ -0,0 +1,39 @@ +#!/bin/bash + +## vim: set syn=on ts=4 sw=4 sts=0 noet foldmethod=indent: +## copyright: B1 Systems GmbH <info@b1-systems.de>, 2012. +## author: Christian Berendt <berendt@b1-systems.de>, 2012. +## license: Apache License, Version 2.0 +## +## purpose: +## Creates a new vif device without attaching it to a +## bridge. Quantum Linux Bridge Agent will attach the +## created device to the belonging bridge. +## +## usage: +## place the script in ${XEN_SCRIPT_DIR}/vif-openstack and +## set (vif-script vif-openstack) in /etc/xen/xend-config.sxp. + +dir=$(dirname "$0") +. "$dir/vif-common.sh" + +case "$command" in + online) + setup_virtual_bridge_port "$dev" + ip link set $dev up + ;; + + offline) + ip link set $dev down + ;; + + add) + setup_virtual_bridge_port "$dev" + ip link set $dev up + ;; +esac + +if [ "$type_if" = vif -a "$command" = "online" ] +then + success +fi diff --git a/doc/api_samples/all_extensions/extensions-get-resp.json b/doc/api_samples/all_extensions/extensions-get-resp.json index 25d077f27..bd002c080 100644 --- a/doc/api_samples/all_extensions/extensions-get-resp.json +++ b/doc/api_samples/all_extensions/extensions-get-resp.json @@ -89,6 +89,14 @@ "updated": "2012-08-09T00:00:00+00:00" }, { + "alias": "os-cells", + "description": "Enables cells-related functionality such as adding neighbor cells,\n listing neighbor cells, and getting the capabilities of the local cell.\n ", + "links": [], + "name": "Cells", + "namespace": "http://docs.openstack.org/compute/ext/cells/api/v1.1", + "updated": "2011-09-21T00:00:00+00:00" + }, + { "alias": "os-certificates", "description": "Certificates support.", "links": [], diff --git a/doc/api_samples/all_extensions/extensions-get-resp.xml b/doc/api_samples/all_extensions/extensions-get-resp.xml index b66c3dbe7..ebb1c4302 100644 --- a/doc/api_samples/all_extensions/extensions-get-resp.xml +++ b/doc/api_samples/all_extensions/extensions-get-resp.xml @@ -37,6 +37,12 @@ <extension alias="os-availability-zone" updated="2012-08-09T00:00:00+00:00" namespace="http://docs.openstack.org/compute/ext/availabilityzone/api/v1.1" name="AvailabilityZone"> <description>Add availability_zone to the Create Server v1.1 API.</description> </extension> + <extension alias="os-cells" updated="2011-09-21T00:00:00+00:00" namespace="http://docs.openstack.org/compute/ext/cells/api/v1.1" name="Cells"> + <description>Enables cells-related functionality such as adding child cells, + listing child cells, getting the capabilities of the local cell, + and returning build plans to parent cells' schedulers + </description> + </extension> <extension alias="os-certificates" updated="2012-01-19T00:00:00+00:00" namespace="http://docs.openstack.org/compute/ext/certificates/api/v1.1" name="Certificates"> <description>Certificates support.</description> </extension> diff --git a/doc/api_samples/os-cells/cells-get-resp.json b/doc/api_samples/os-cells/cells-get-resp.json new file mode 100644 index 000000000..62eb8ec31 --- /dev/null +++ b/doc/api_samples/os-cells/cells-get-resp.json @@ -0,0 +1,9 @@ +{ + "cell": { + "name": "cell3", + "rpc_host": null, + "rpc_port": null, + "type": "child", + "username": "username3" + } +}
\ No newline at end of file diff --git a/doc/api_samples/os-cells/cells-get-resp.xml b/doc/api_samples/os-cells/cells-get-resp.xml new file mode 100644 index 000000000..12256a5bd --- /dev/null +++ b/doc/api_samples/os-cells/cells-get-resp.xml @@ -0,0 +1,2 @@ +<?xml version='1.0' encoding='UTF-8'?> +<cell xmlns="http://docs.rackspacecloud.com/servers/api/v1.0" username="username3" rpc_host="None" type="child" name="cell3" rpc_port="None"/>
\ No newline at end of file diff --git a/doc/api_samples/os-cells/cells-list-empty-resp.json b/doc/api_samples/os-cells/cells-list-empty-resp.json new file mode 100644 index 000000000..5325a4e85 --- /dev/null +++ b/doc/api_samples/os-cells/cells-list-empty-resp.json @@ -0,0 +1,3 @@ +{ + "cells": [] +}
\ No newline at end of file diff --git a/doc/api_samples/os-cells/cells-list-empty-resp.xml b/doc/api_samples/os-cells/cells-list-empty-resp.xml new file mode 100644 index 000000000..6ac77b4bd --- /dev/null +++ b/doc/api_samples/os-cells/cells-list-empty-resp.xml @@ -0,0 +1,2 @@ +<?xml version='1.0' encoding='UTF-8'?> +<cells xmlns="http://docs.rackspacecloud.com/servers/api/v1.0"/>
\ No newline at end of file diff --git a/doc/api_samples/os-cells/cells-list-resp.json b/doc/api_samples/os-cells/cells-list-resp.json new file mode 100644 index 000000000..97ea4c6dd --- /dev/null +++ b/doc/api_samples/os-cells/cells-list-resp.json @@ -0,0 +1,39 @@ +{ + "cells": [ + { + "name": "cell1", + "rpc_host": null, + "rpc_port": null, + "type": "child", + "username": "username1" + }, + { + "name": "cell3", + "rpc_host": null, + "rpc_port": null, + "type": "child", + "username": "username3" + }, + { + "name": "cell5", + "rpc_host": null, + "rpc_port": null, + "type": "child", + "username": "username5" + }, + { + "name": "cell2", + "rpc_host": null, + "rpc_port": null, + "type": "parent", + "username": "username2" + }, + { + "name": "cell4", + "rpc_host": null, + "rpc_port": null, + "type": "parent", + "username": "username4" + } + ] +}
\ No newline at end of file diff --git a/doc/api_samples/os-cells/cells-list-resp.xml b/doc/api_samples/os-cells/cells-list-resp.xml new file mode 100644 index 000000000..7d697bb91 --- /dev/null +++ b/doc/api_samples/os-cells/cells-list-resp.xml @@ -0,0 +1,8 @@ +<?xml version='1.0' encoding='UTF-8'?> +<cells xmlns="http://docs.rackspacecloud.com/servers/api/v1.0"> + <cell username="username1" rpc_host="None" type="child" name="cell1" rpc_port="None"/> + <cell username="username3" rpc_host="None" type="child" name="cell3" rpc_port="None"/> + <cell username="username5" rpc_host="None" type="child" name="cell5" rpc_port="None"/> + <cell username="username2" rpc_host="None" type="parent" name="cell2" rpc_port="None"/> + <cell username="username4" rpc_host="None" type="parent" name="cell4" rpc_port="None"/> +</cells>
\ No newline at end of file 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/api_samples/os-hosts/hosts-list-resp.json b/doc/api_samples/os-hosts/hosts-list-resp.json index 5a963c602..0c4126a7e 100644 --- a/doc/api_samples/os-hosts/hosts-list-resp.json +++ b/doc/api_samples/os-hosts/hosts-list-resp.json @@ -24,6 +24,11 @@ "host_name": "6e48bfe1a3304b7b86154326328750ae", "service": "conductor", "zone": "internal" + }, + { + "host_name": "39f55087a1024d1380755951c945ca69", + "service": "cells", + "zone": "internal" } ] } diff --git a/doc/api_samples/os-hosts/hosts-list-resp.xml b/doc/api_samples/os-hosts/hosts-list-resp.xml index 8266a5d49..9a99c577a 100644 --- a/doc/api_samples/os-hosts/hosts-list-resp.xml +++ b/doc/api_samples/os-hosts/hosts-list-resp.xml @@ -5,4 +5,5 @@ <host host_name="2d1bdd671b5d41fd89dec74be5770c63" service="network"/> <host host_name="7c2dd5ecb7494dd1bf4240b7f7f9bf3a" service="scheduler"/> <host host_name="f9c273d8e03141a2a01def0ad18e5be4" service="conductor"/> -</hosts>
\ No newline at end of file + <host host_name="2b893569cd824b979bd80a2c94570a1f" service="cells"/> +</hosts> 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/devref/development.environment.rst b/doc/source/devref/development.environment.rst index 4eb695963..a366c4893 100644 --- a/doc/source/devref/development.environment.rst +++ b/doc/source/devref/development.environment.rst @@ -70,7 +70,7 @@ On Ubuntu Precise (12.04) you may also need to add the following packages:: On Fedora-based distributions (e.g., Fedora/RHEL/CentOS/Scientific Linux):: - sudo yum install python-devel openssl-devel python-pip git + sudo yum install python-devel openssl-devel python-pip git gcc libxslt-devel mysql-devel Mac OS X Systems 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 77133d988..96118eb76 100644 --- a/etc/nova/nova.conf.sample +++ b/etc/nova/nova.conf.sample @@ -2518,4 +2518,32 @@ #attestation_auth_blob=<None> -# Total option count: 514 +[spice] + +# +# Options defined in nova.spice +# + +# location of spice html5 console proxy, in the form +# "http://127.0.0.1:6080/spice_auto.html" (string value) +#html5proxy_base_url=http://127.0.0.1:6080/spice_auto.html + +# IP address on which instance spice server should listen +# (string value) +#server_listen=127.0.0.1 + +# the address to which proxy clients (like nova- +# spicehtml5proxy) should connect (string value) +#server_proxyclient_address=127.0.0.1 + +# enable spice related features (boolean value) +#enabled=false + +# enable spice guest agent support (boolean value) +#agent_enabled=true + +# keymap for spice (string value) +#keymap=en-us + + +# Total option count: 519 diff --git a/etc/nova/policy.json b/etc/nova/policy.json index 04766371e..fd1f9c2e0 100644 --- a/etc/nova/policy.json +++ b/etc/nova/policy.json @@ -29,6 +29,7 @@ "compute_extension:admin_actions:migrate": "rule:admin_api", "compute_extension:aggregates": "rule:admin_api", "compute_extension:agents": "rule:admin_api", + "compute_extension:cells": "rule:admin_api", "compute_extension:certificates": "", "compute_extension:cloudpipe": "rule:admin_api", "compute_extension:cloudpipe_update": "rule:admin_api", diff --git a/nova/api/ec2/cloud.py b/nova/api/ec2/cloud.py index 353d08714..414b2e969 100644 --- a/nova/api/ec2/cloud.py +++ b/nova/api/ec2/cloud.py @@ -27,6 +27,7 @@ import time from nova.api.ec2 import ec2utils from nova.api.ec2 import inst_state +from nova.api.metadata import password from nova.api import validator from nova import availability_zones from nova import block_device @@ -758,6 +759,23 @@ class CloudController(object): return True + def get_password_data(self, context, instance_id, **kwargs): + # instance_id may be passed in as a list of instances + if isinstance(instance_id, list): + ec2_id = instance_id[0] + else: + ec2_id = instance_id + validate_ec2_id(ec2_id) + instance_uuid = ec2utils.ec2_inst_id_to_uuid(context, ec2_id) + instance = self.compute_api.get(context, instance_uuid) + output = password.extract_password(instance) + # NOTE(vish): this should be timestamp from the metadata fields + # but it isn't important enough to implement properly + now = timeutils.utcnow() + return {"InstanceId": ec2_id, + "Timestamp": now, + "passwordData": output} + def get_console_output(self, context, instance_id, **kwargs): LOG.audit(_("Get console output for instance %s"), instance_id, context=context) diff --git a/nova/api/openstack/compute/contrib/admin_actions.py b/nova/api/openstack/compute/contrib/admin_actions.py index f345d9617..fa7836b37 100644 --- a/nova/api/openstack/compute/contrib/admin_actions.py +++ b/nova/api/openstack/compute/contrib/admin_actions.py @@ -307,9 +307,7 @@ class AdminActionsController(wsgi.Controller): try: instance = self.compute_api.get(context, id) - self.compute_api.update(context, instance, - vm_state=state, - task_state=None) + self.compute_api.update_state(context, instance, state) except exception.InstanceNotFound: raise exc.HTTPNotFound(_("Server not found")) except Exception: diff --git a/nova/api/openstack/compute/contrib/cells.py b/nova/api/openstack/compute/contrib/cells.py new file mode 100644 index 000000000..03e2e4ca2 --- /dev/null +++ b/nova/api/openstack/compute/contrib/cells.py @@ -0,0 +1,303 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011-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. + +"""The cells extension.""" +from xml.dom import minidom +from xml.parsers import expat + +from webob import exc + +from nova.api.openstack import common +from nova.api.openstack import extensions +from nova.api.openstack import wsgi +from nova.api.openstack import xmlutil +from nova.cells import rpcapi as cells_rpcapi +from nova.compute import api as compute +from nova import db +from nova import exception +from nova.openstack.common import cfg +from nova.openstack.common import log as logging +from nova.openstack.common import timeutils + + +LOG = logging.getLogger(__name__) +CONF = cfg.CONF +CONF.import_opt('name', 'nova.cells.opts', group='cells') +CONF.import_opt('capabilities', 'nova.cells.opts', group='cells') + +authorize = extensions.extension_authorizer('compute', 'cells') + + +def make_cell(elem): + elem.set('name') + elem.set('username') + elem.set('type') + elem.set('rpc_host') + elem.set('rpc_port') + + caps = xmlutil.SubTemplateElement(elem, 'capabilities', + selector='capabilities') + cap = xmlutil.SubTemplateElement(caps, xmlutil.Selector(0), + selector=xmlutil.get_items) + cap.text = 1 + + +cell_nsmap = {None: wsgi.XMLNS_V10} + + +class CellTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('cell', selector='cell') + make_cell(root) + return xmlutil.MasterTemplate(root, 1, nsmap=cell_nsmap) + + +class CellsTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('cells') + elem = xmlutil.SubTemplateElement(root, 'cell', selector='cells') + make_cell(elem) + return xmlutil.MasterTemplate(root, 1, nsmap=cell_nsmap) + + +class CellDeserializer(wsgi.XMLDeserializer): + """Deserializer to handle xml-formatted cell create requests.""" + + def _extract_capabilities(self, cap_node): + caps = {} + for cap in cap_node.childNodes: + cap_name = cap.tagName + caps[cap_name] = self.extract_text(cap) + return caps + + def _extract_cell(self, node): + cell = {} + cell_node = self.find_first_child_named(node, 'cell') + + extract_fns = {'capabilities': self._extract_capabilities} + + for child in cell_node.childNodes: + name = child.tagName + extract_fn = extract_fns.get(name, self.extract_text) + cell[name] = extract_fn(child) + return cell + + def default(self, string): + """Deserialize an xml-formatted cell create request.""" + try: + node = minidom.parseString(string) + except expat.ExpatError: + msg = _("cannot understand XML") + raise exception.MalformedRequestBody(reason=msg) + + return {'body': {'cell': self._extract_cell(node)}} + + +def _filter_keys(item, keys): + """ + Filters all model attributes except for keys + item is a dict + + """ + return dict((k, v) for k, v in item.iteritems() if k in keys) + + +def _scrub_cell(cell, detail=False): + keys = ['name', 'username', 'rpc_host', 'rpc_port'] + if detail: + keys.append('capabilities') + + cell_info = _filter_keys(cell, keys) + cell_info['type'] = 'parent' if cell['is_parent'] else 'child' + return cell_info + + +class Controller(object): + """Controller for Cell resources.""" + + def __init__(self): + self.compute_api = compute.API() + self.cells_rpcapi = cells_rpcapi.CellsAPI() + + def _get_cells(self, ctxt, req, detail=False): + """Return all cells.""" + # Ask the CellsManager for the most recent data + items = self.cells_rpcapi.get_cell_info_for_neighbors(ctxt) + items = common.limited(items, req) + items = [_scrub_cell(item, detail=detail) for item in items] + return dict(cells=items) + + @wsgi.serializers(xml=CellsTemplate) + def index(self, req): + """Return all cells in brief.""" + ctxt = req.environ['nova.context'] + authorize(ctxt) + return self._get_cells(ctxt, req) + + @wsgi.serializers(xml=CellsTemplate) + def detail(self, req): + """Return all cells in detail.""" + ctxt = req.environ['nova.context'] + authorize(ctxt) + return self._get_cells(ctxt, req, detail=True) + + @wsgi.serializers(xml=CellTemplate) + def info(self, req): + """Return name and capabilities for this cell.""" + context = req.environ['nova.context'] + authorize(context) + cell_capabs = {} + my_caps = CONF.cells.capabilities + for cap in my_caps: + key, value = cap.split('=') + cell_capabs[key] = value + cell = {'name': CONF.cells.name, + 'type': 'self', + 'rpc_host': None, + 'rpc_port': 0, + 'username': None, + 'capabilities': cell_capabs} + return dict(cell=cell) + + @wsgi.serializers(xml=CellTemplate) + def show(self, req, id): + """Return data about the given cell name. 'id' is a cell name.""" + context = req.environ['nova.context'] + authorize(context) + try: + cell = db.cell_get(context, id) + except exception.CellNotFound: + raise exc.HTTPNotFound() + return dict(cell=_scrub_cell(cell)) + + def delete(self, req, id): + """Delete a child or parent cell entry. 'id' is a cell name.""" + context = req.environ['nova.context'] + authorize(context) + num_deleted = db.cell_delete(context, id) + if num_deleted == 0: + raise exc.HTTPNotFound() + return {} + + def _validate_cell_name(self, cell_name): + """Validate cell name is not empty and doesn't contain '!' or '.'.""" + if not cell_name: + msg = _("Cell name cannot be empty") + LOG.error(msg) + raise exc.HTTPBadRequest(explanation=msg) + if '!' in cell_name or '.' in cell_name: + msg = _("Cell name cannot contain '!' or '.'") + LOG.error(msg) + raise exc.HTTPBadRequest(explanation=msg) + + def _validate_cell_type(self, cell_type): + """Validate cell_type is 'parent' or 'child'.""" + if cell_type not in ['parent', 'child']: + msg = _("Cell type must be 'parent' or 'child'") + LOG.error(msg) + raise exc.HTTPBadRequest(explanation=msg) + + def _convert_cell_type(self, cell): + """Convert cell['type'] to is_parent boolean.""" + if 'type' in cell: + self._validate_cell_type(cell['type']) + cell['is_parent'] = cell['type'] == 'parent' + del cell['type'] + else: + cell['is_parent'] = False + + @wsgi.serializers(xml=CellTemplate) + @wsgi.deserializers(xml=CellDeserializer) + def create(self, req, body): + """Create a child cell entry.""" + context = req.environ['nova.context'] + authorize(context) + if 'cell' not in body: + msg = _("No cell information in request") + LOG.error(msg) + raise exc.HTTPBadRequest(explanation=msg) + cell = body['cell'] + if 'name' not in cell: + msg = _("No cell name in request") + LOG.error(msg) + raise exc.HTTPBadRequest(explanation=msg) + self._validate_cell_name(cell['name']) + self._convert_cell_type(cell) + cell = db.cell_create(context, cell) + return dict(cell=_scrub_cell(cell)) + + @wsgi.serializers(xml=CellTemplate) + @wsgi.deserializers(xml=CellDeserializer) + def update(self, req, id, body): + """Update a child cell entry. 'id' is the cell name to update.""" + context = req.environ['nova.context'] + authorize(context) + if 'cell' not in body: + msg = _("No cell information in request") + LOG.error(msg) + raise exc.HTTPBadRequest(explanation=msg) + cell = body['cell'] + cell.pop('id', None) + if 'name' in cell: + self._validate_cell_name(cell['name']) + self._convert_cell_type(cell) + try: + cell = db.cell_update(context, id, cell) + except exception.CellNotFound: + raise exc.HTTPNotFound() + return dict(cell=_scrub_cell(cell)) + + def sync_instances(self, req, body): + """Tell all cells to sync instance info.""" + context = req.environ['nova.context'] + authorize(context) + project_id = body.pop('project_id', None) + deleted = body.pop('deleted', False) + updated_since = body.pop('updated_since', None) + if body: + msg = _("Only 'updated_since' and 'project_id' are understood.") + raise exc.HTTPBadRequest(explanation=msg) + if updated_since: + try: + timeutils.parse_isotime(updated_since) + except ValueError: + msg = _('Invalid changes-since value') + raise exc.HTTPBadRequest(explanation=msg) + self.cells_rpcapi.sync_instances(context, project_id=project_id, + updated_since=updated_since, deleted=deleted) + + +class Cells(extensions.ExtensionDescriptor): + """Enables cells-related functionality such as adding neighbor cells, + listing neighbor cells, and getting the capabilities of the local cell. + """ + + name = "Cells" + alias = "os-cells" + namespace = "http://docs.openstack.org/compute/ext/cells/api/v1.1" + updated = "2011-09-21T00:00:00+00:00" + + def get_resources(self): + coll_actions = { + 'detail': 'GET', + 'info': 'GET', + 'sync_instances': 'POST', + } + + res = extensions.ResourceExtension('os-cells', + Controller(), collection_actions=coll_actions) + return [res] 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/views/servers.py b/nova/api/openstack/compute/views/servers.py index d281f6a61..939515468 100644 --- a/nova/api/openstack/compute/views/servers.py +++ b/nova/api/openstack/compute/views/servers.py @@ -211,9 +211,9 @@ class ViewBuilder(common.ViewBuilder): if fault.get('details', None): is_admin = False - context = getattr(request, 'context', None) + context = request.environ["nova.context"] if context: - is_admin = getattr(request.context, 'is_admin', False) + is_admin = getattr(context, 'is_admin', False) if is_admin or fault['code'] != 500: fault_dict['details'] = fault["details"] diff --git a/nova/api/sizelimit.py b/nova/api/sizelimit.py index 70ff73b2b..77ab4415c 100644 --- a/nova/api/sizelimit.py +++ b/nova/api/sizelimit.py @@ -38,7 +38,7 @@ LOG = logging.getLogger(__name__) class RequestBodySizeLimiter(wsgi.Middleware): - """Add a 'nova.context' to WSGI environ.""" + """Limit the size of incoming requests.""" def __init__(self, *args, **kwargs): super(RequestBodySizeLimiter, self).__init__(*args, **kwargs) diff --git a/nova/cells/manager.py b/nova/cells/manager.py index 0942bae28..133946794 100644 --- a/nova/cells/manager.py +++ b/nova/cells/manager.py @@ -65,7 +65,7 @@ class CellsManager(manager.Manager): Scheduling requests get passed to the scheduler class. """ - RPC_API_VERSION = '1.0' + RPC_API_VERSION = '1.1' def __init__(self, *args, **kwargs): # Mostly for tests. @@ -186,6 +186,10 @@ class CellsManager(manager.Manager): self.msg_runner.schedule_run_instance(ctxt, our_cell, host_sched_kwargs) + def get_cell_info_for_neighbors(self, _ctxt): + """Return cell information for our neighbor cells.""" + return self.state_manager.get_cell_info_for_neighbors() + def run_compute_api_method(self, ctxt, cell_name, method_info, call): """Call a compute API method in a specific cell.""" response = self.msg_runner.run_compute_api_method(ctxt, @@ -218,3 +222,10 @@ class CellsManager(manager.Manager): def bw_usage_update_at_top(self, ctxt, bw_update_info): """Update bandwidth usage at top level cell.""" self.msg_runner.bw_usage_update_at_top(ctxt, bw_update_info) + + def sync_instances(self, ctxt, project_id, updated_since, deleted): + """Force a sync of all instances, potentially by project_id, + and potentially since a certain date/time. + """ + self.msg_runner.sync_instances(ctxt, project_id, updated_since, + deleted) diff --git a/nova/cells/messaging.py b/nova/cells/messaging.py index 56d521892..34ca74855 100644 --- a/nova/cells/messaging.py +++ b/nova/cells/messaging.py @@ -27,6 +27,7 @@ import sys from eventlet import queue from nova.cells import state as cells_state +from nova.cells import utils as cells_utils from nova import compute from nova import context from nova.db import base @@ -37,6 +38,7 @@ from nova.openstack.common import importutils from nova.openstack.common import jsonutils from nova.openstack.common import log as logging from nova.openstack.common.rpc import common as rpc_common +from nova.openstack.common import timeutils from nova.openstack.common import uuidutils from nova import utils @@ -778,6 +780,26 @@ class _BroadcastMessageMethods(_BaseMessageMethods): return self.db.bw_usage_update(message.ctxt, **bw_update_info) + def _sync_instance(self, ctxt, instance): + if instance['deleted']: + self.msg_runner.instance_destroy_at_top(ctxt, instance) + else: + self.msg_runner.instance_update_at_top(ctxt, instance) + + def sync_instances(self, message, project_id, updated_since, deleted, + **kwargs): + projid_str = project_id is None and "<all>" or project_id + since_str = updated_since is None and "<all>" or updated_since + LOG.info(_("Forcing a sync of instances, project_id=" + "%(projid_str)s, updated_since=%(since_str)s"), locals()) + if updated_since is not None: + updated_since = timeutils.parse_isotime(updated_since) + instances = cells_utils.get_instances_to_sync(message.ctxt, + updated_since=updated_since, project_id=project_id, + deleted=deleted) + for instance in instances: + self._sync_instance(message.ctxt, instance) + _CELL_MESSAGE_TYPE_TO_MESSAGE_CLS = {'targeted': _TargetedMessage, 'broadcast': _BroadcastMessage, @@ -1004,6 +1026,18 @@ class MessageRunner(object): 'up', run_locally=False) message.process() + def sync_instances(self, ctxt, project_id, updated_since, deleted): + """Force a sync of all instances, potentially by project_id, + and potentially since a certain date/time. + """ + method_kwargs = dict(project_id=project_id, + updated_since=updated_since, + deleted=deleted) + message = _BroadcastMessage(self, ctxt, 'sync_instances', + method_kwargs, 'down', + run_locally=False) + message.process() + @staticmethod def get_message_types(): return _CELL_MESSAGE_TYPE_TO_MESSAGE_CLS.keys() diff --git a/nova/cells/rpcapi.py b/nova/cells/rpcapi.py index 8ce298829..0ab4fc352 100644 --- a/nova/cells/rpcapi.py +++ b/nova/cells/rpcapi.py @@ -40,6 +40,7 @@ class CellsAPI(rpc_proxy.RpcProxy): API version history: 1.0 - Initial version. + 1.1 - Adds get_cell_info_for_neighbors() and sync_instances() ''' BASE_RPC_API_VERSION = '1.0' @@ -136,3 +137,21 @@ class CellsAPI(rpc_proxy.RpcProxy): 'info_cache': iicache} self.cast(ctxt, self.make_msg('instance_update_at_top', instance=instance)) + + def get_cell_info_for_neighbors(self, ctxt): + """Get information about our neighbor cells from the manager.""" + if not CONF.cells.enable: + return [] + return self.call(ctxt, self.make_msg('get_cell_info_for_neighbors'), + version='1.1') + + def sync_instances(self, ctxt, project_id=None, updated_since=None, + deleted=False): + """Ask all cells to sync instance data.""" + if not CONF.cells.enable: + return + return self.cast(ctxt, self.make_msg('sync_instances', + project_id=project_id, + updated_since=updated_since, + deleted=deleted), + version='1.1') diff --git a/nova/cells/state.py b/nova/cells/state.py index 345c44ca9..e3886bedb 100644 --- a/nova/cells/state.py +++ b/nova/cells/state.py @@ -75,8 +75,8 @@ class CellState(object): def get_cell_info(self): """Return subset of cell information for OS API use.""" - db_fields_to_return = ['id', 'is_parent', 'weight_scale', - 'weight_offset', 'username', 'rpc_host', 'rpc_port'] + db_fields_to_return = ['is_parent', 'weight_scale', 'weight_offset', + 'username', 'rpc_host', 'rpc_port'] cell_info = dict(name=self.name, capabilities=self.capabilities) if self.db_info: for field in db_fields_to_return: @@ -267,6 +267,15 @@ class CellStateManager(base.Base): self._update_our_capacity(ctxt) @sync_from_db + def get_cell_info_for_neighbors(self): + """Return cell information for all neighbor cells.""" + cell_list = [cell.get_cell_info() + for cell in self.child_cells.itervalues()] + cell_list.extend([cell.get_cell_info() + for cell in self.parent_cells.itervalues()]) + return cell_list + + @sync_from_db def get_my_state(self): """Return information for my (this) cell.""" return self.my_cell_state diff --git a/nova/compute/api.py b/nova/compute/api.py index 7770bc9e6..c7ca0640d 100644 --- a/nova/compute/api.py +++ b/nova/compute/api.py @@ -878,6 +878,20 @@ class API(base.Base): for host_name in host_names: self.compute_rpcapi.refresh_provider_fw_rules(context, host_name) + def update_state(self, context, instance, new_state): + """Updates the state of a compute instance. + For example to 'active' or 'error'. + Also sets 'task_state' to None. + Used by admin_actions api + + :param context: The security context + :param instance: The instance to update + :param new_state: A member of vm_state, eg. 'active' + """ + self.update(context, instance, + vm_state=new_state, + task_state=None) + @wrap_check_policy def update(self, context, instance, **kwargs): """Updates the instance in the datastore. @@ -2017,6 +2031,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 d547c363a..d5427a04b 100644 --- a/nova/compute/cells_api.py +++ b/nova/compute/cells_api.py @@ -144,17 +144,45 @@ class ComputeCellsAPI(compute_api.API): """ return super(ComputeCellsAPI, self).create(*args, **kwargs) - @validate_cell - def update(self, context, instance, **kwargs): - """Update an instance.""" + def update_state(self, context, instance, new_state): + """Updates the state of a compute instance. + For example to 'active' or 'error'. + Also sets 'task_state' to None. + Used by admin_actions api + + :param context: The security context + :param instance: The instance to update + :param new_state: A member of vm_state to change + the instance's state to, + eg. 'active' + """ + self.update(context, instance, + pass_on_state_change=True, + vm_state=new_state, + task_state=None) + + def update(self, context, instance, pass_on_state_change=False, **kwargs): + """ + Update an instance. + :param pass_on_state_change: if true, the state change will be passed + on to child cells + """ + cell_name = instance['cell_name'] + if cell_name and self._cell_read_only(cell_name): + raise exception.InstanceInvalidState( + attr="vm_state", + instance_uuid=instance['uuid'], + state="temporary_readonly", + method='update') rv = super(ComputeCellsAPI, self).update(context, instance, **kwargs) - # We need to skip vm_state/task_state updates... those will - # happen when via a a _cast_to_cells for running a different - # compute api method kwargs_copy = kwargs.copy() - kwargs_copy.pop('vm_state', None) - kwargs_copy.pop('task_state', None) + if not pass_on_state_change: + # We need to skip vm_state/task_state updates... those will + # happen via a _cast_to_cells when running a different + # compute api method + kwargs_copy.pop('vm_state', None) + kwargs_copy.pop('task_state', None) if kwargs_copy: try: self._cast_to_cells(context, instance, 'update', @@ -411,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..fa1746b92 100644 --- a/nova/compute/manager.py +++ b/nova/compute/manager.py @@ -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.""" @@ -731,7 +731,7 @@ class ComputeManager(manager.SchedulerDependentManager): rescheduled = False compute_utils.add_instance_fault_from_exc(context, instance_uuid, - exc_info[0], exc_info=exc_info) + exc_info[1], exc_info=exc_info) try: self._deallocate_network(context, instance) @@ -2387,6 +2387,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 +2407,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""" diff --git a/nova/compute/resource_tracker.py b/nova/compute/resource_tracker.py index d2afcaa27..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 @@ -252,14 +251,15 @@ class ResourceTracker(object): self._report_hypervisor_resource_view(resources) # Grab all instances assigned to this node: - instances = db.instance_get_all_by_host_and_node(context, self.host, - self.nodename) + instances = self.conductor_api.instance_get_all_by_host_and_node( + context, self.host, self.nodename) # Now calculate usage based on instance utilization: self._update_usage_from_instances(resources, instances) # Grab all in-progress migrations: - migrations = db.migration_get_in_progress_by_host_and_node(context, + capi = self.conductor_api + migrations = capi.migration_get_in_progress_by_host_and_node(context, self.host, self.nodename) self._update_usage_from_migrations(resources, migrations) @@ -303,12 +303,13 @@ 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: - return db.service_get_by_compute_host(context, self.host) + return self.conductor_api.service_get_by_compute_host(context, + self.host) except exception.NotFound: LOG.warn(_("No service record for host %s"), self.host) @@ -347,15 +348,15 @@ 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.""" elevated = context.elevated() - db.migration_update(elevated, migration['id'], - {'status': status}) + self.conductor_api.migration_update(elevated, migration, status) self.update_available_resource(elevated) def revert_resize(self, context, migration, status='reverted'): 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/conductor/api.py b/nova/conductor/api.py index 63b64f830..31ee19601 100644 --- a/nova/conductor/api.py +++ b/nova/conductor/api.py @@ -97,6 +97,9 @@ class LocalAPI(object): def instance_get_all_by_host(self, context, host): return self._manager.instance_get_all_by_host(context, host) + def instance_get_all_by_host_and_node(self, context, host, node): + return self._manager.instance_get_all_by_host(context, host, node) + def instance_get_all_by_filters(self, context, filters, sort_key='created_at', sort_dir='desc'): @@ -134,6 +137,10 @@ class LocalAPI(object): return self._manager.migration_get_unconfirmed_by_dest_compute( context, confirm_window, dest_compute) + def migration_get_in_progress_by_host_and_node(self, context, host, node): + return self._manager.migration_get_in_progress_by_host_and_node( + context, host, node) + def migration_create(self, context, instance, values): return self._manager.migration_create(context, instance, values) @@ -271,6 +278,13 @@ 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) + class API(object): """Conductor API that does updates via RPC to the ConductorManager.""" @@ -331,6 +345,10 @@ class API(object): def instance_get_all_by_host(self, context, host): return self.conductor_rpcapi.instance_get_all_by_host(context, host) + def instance_get_all_by_host_and_node(self, context, host, node): + return self.conductor_rpcapi.instance_get_all_by_host(context, + host, node) + def instance_get_all_by_filters(self, context, filters, sort_key='created_at', sort_dir='desc'): @@ -370,6 +388,11 @@ class API(object): return crpcapi.migration_get_unconfirmed_by_dest_compute( context, confirm_window, dest_compute) + def migration_get_in_progress_by_host_and_node(self, context, host, node): + crpcapi = self.conductor_rpcapi + return crpcapi.migration_get_in_progress_by_host_and_node(context, + host, node) + def migration_create(self, context, instance, values): return self.conductor_rpcapi.migration_create(context, instance, values) @@ -518,3 +541,10 @@ 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) diff --git a/nova/conductor/manager.py b/nova/conductor/manager.py index b0d4011ad..9b18d1e00 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.30' + RPC_API_VERSION = '1.33' def __init__(self, *args, **kwargs): super(ConductorManager, self).__init__(service_name='conductor', @@ -83,9 +83,13 @@ class ConductorManager(manager.SchedulerDependentManager): def instance_get_all(self, context): return jsonutils.to_primitive(self.db.instance_get_all(context)) - def instance_get_all_by_host(self, context, host): - return jsonutils.to_primitive( - self.db.instance_get_all_by_host(context.elevated(), host)) + def instance_get_all_by_host(self, context, host, node=None): + if node is not None: + result = self.db.instance_get_all_by_host_and_node( + context.elevated(), host, node) + else: + result = self.db.instance_get_all_by_host(context.elevated(), host) + return jsonutils.to_primitive(result) @rpc_common.client_exceptions(exception.MigrationNotFound) def migration_get(self, context, migration_id): @@ -100,6 +104,12 @@ class ConductorManager(manager.SchedulerDependentManager): context, confirm_window, dest_compute) return jsonutils.to_primitive(migrations) + def migration_get_in_progress_by_host_and_node(self, context, + host, node): + migrations = self.db.migration_get_in_progress_by_host_and_node( + context, host, node) + return jsonutils.to_primitive(migrations) + def migration_create(self, context, instance, values): values.update({'instance_uuid': instance['uuid'], 'source_compute': instance['host'], @@ -291,3 +301,12 @@ 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) diff --git a/nova/conductor/rpcapi.py b/nova/conductor/rpcapi.py index b7f760cf5..95e332840 100644 --- a/nova/conductor/rpcapi.py +++ b/nova/conductor/rpcapi.py @@ -63,6 +63,9 @@ class ConductorAPI(nova.openstack.common.rpc.proxy.RpcProxy): 1.28 - Added binary arg to service_get_all_by 1.29 - Added service_destroy 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 """ BASE_RPC_API_VERSION = '1.0' @@ -106,6 +109,12 @@ class ConductorAPI(nova.openstack.common.rpc.proxy.RpcProxy): dest_compute=dest_compute) return self.call(context, msg, version='1.20') + def migration_get_in_progress_by_host_and_node(self, context, + host, node): + msg = self.make_msg('migration_get_in_progress_by_host_and_node', + host=host, node=node) + return self.call(context, msg, version='1.31') + def migration_create(self, context, instance, values): instance_p = jsonutils.to_primitive(instance) msg = self.make_msg('migration_create', instance=instance_p, @@ -271,9 +280,9 @@ class ConductorAPI(nova.openstack.common.rpc.proxy.RpcProxy): msg = self.make_msg('instance_get_all') return self.call(context, msg, version='1.23') - def instance_get_all_by_host(self, context, host): - msg = self.make_msg('instance_get_all_by_host', host=host) - return self.call(context, msg, version='1.23') + def instance_get_all_by_host(self, context, host, node=None): + msg = self.make_msg('instance_get_all_by_host', host=host, node=node) + return self.call(context, msg, version='1.32') def action_event_start(self, context, values): msg = self.make_msg('action_event_start', values=values) @@ -297,3 +306,13 @@ 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') 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 d7d9bd0d2..ecfcfab15 100644 --- a/nova/db/api.py +++ b/nova/db/api.py @@ -1360,19 +1360,19 @@ def cell_create(context, values): return IMPL.cell_create(context, values) -def cell_update(context, cell_id, values): +def cell_update(context, cell_name, values): """Update a child Cell entry.""" - return IMPL.cell_update(context, cell_id, values) + return IMPL.cell_update(context, cell_name, values) -def cell_delete(context, cell_id): +def cell_delete(context, cell_name): """Delete a child Cell.""" - return IMPL.cell_delete(context, cell_id) + return IMPL.cell_delete(context, cell_name) -def cell_get(context, cell_id): +def cell_get(context, cell_name): """Get a specific child Cell.""" - return IMPL.cell_get(context, cell_id) + return IMPL.cell_get(context, cell_name) def cell_get_all(context): diff --git a/nova/db/sqlalchemy/api.py b/nova/db/sqlalchemy/api.py index 3fdfd53c8..cb3d69f78 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 + ################### @@ -497,6 +503,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 +552,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 @@ -3719,34 +3727,30 @@ def cell_create(context, values): return cell -def _cell_get_by_id_query(context, cell_id, session=None): - return model_query(context, models.Cell, session=session).\ - filter_by(id=cell_id) +def _cell_get_by_name_query(context, cell_name, session=None): + return model_query(context, models.Cell, + session=session).filter_by(name=cell_name) @require_admin_context -def cell_update(context, cell_id, values): - cell = cell_get(context, cell_id) - cell.update(values) - cell.save() +def cell_update(context, cell_name, values): + session = get_session() + with session.begin(): + cell = _cell_get_by_name_query(context, cell_name, session=session) + cell.update(values) return cell @require_admin_context -def cell_delete(context, cell_id): - session = get_session() - with session.begin(): - return _cell_get_by_id_query(context, cell_id, session=session).\ - delete() +def cell_delete(context, cell_name): + return _cell_get_by_name_query(context, cell_name).soft_delete() @require_admin_context -def cell_get(context, cell_id): - result = _cell_get_by_id_query(context, cell_id).first() - +def cell_get(context, cell_name): + result = _cell_get_by_name_query(context, cell_name).first() if not result: - raise exception.CellNotFound(cell_id=cell_id) - + raise exception.CellNotFound(cell_name=cell_name) return result diff --git a/nova/db/sqlalchemy/migrate_repo/versions/149_inet_datatype_for_postgres.py b/nova/db/sqlalchemy/migrate_repo/versions/149_inet_datatype_for_postgres.py new file mode 100644 index 000000000..fe9889e35 --- /dev/null +++ b/nova/db/sqlalchemy/migrate_repo/versions/149_inet_datatype_for_postgres.py @@ -0,0 +1,70 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 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. + +from sqlalchemy import MetaData, String, Table +from sqlalchemy.dialects import postgresql + + +TABLE_COLUMNS = [ + # table name, column name + ('instances', 'access_ip_v4'), + ('instances', 'access_ip_v6'), + ('security_group_rules', 'cidr'), + ('provider_fw_rules', 'cidr'), + ('networks', 'cidr'), + ('networks', 'cidr_v6'), + ('networks', 'gateway'), + ('networks', 'gateway_v6'), + ('networks', 'netmask'), + ('networks', 'netmask_v6'), + ('networks', 'broadcast'), + ('networks', 'dns1'), + ('networks', 'dns2'), + ('networks', 'vpn_public_address'), + ('networks', 'vpn_private_address'), + ('networks', 'dhcp_start'), + ('fixed_ips', 'address'), + ('floating_ips', 'address'), + ('console_pools', 'address')] + + +def upgrade(migrate_engine): + """Convert String columns holding IP addresses to INET for postgresql.""" + meta = MetaData() + meta.bind = migrate_engine + dialect = migrate_engine.url.get_dialect() + if dialect is postgresql.dialect: + for table, column in TABLE_COLUMNS: + # can't use migrate's alter() because it does not support + # explicit casting + migrate_engine.execute( + "ALTER TABLE %(table)s " + "ALTER COLUMN %(column)s TYPE INET USING %(column)s::INET" + % locals()) + else: + for table, column in TABLE_COLUMNS: + t = Table(table, meta, autoload=True) + getattr(t.c, column).alter(type=String(39)) + + +def downgrade(migrate_engine): + """Convert columns back to the larger String(255).""" + meta = MetaData() + meta.bind = migrate_engine + for table, column in TABLE_COLUMNS: + t = Table(table, meta, autoload=True) + getattr(t.c, column).alter(type=String(255)) diff --git a/nova/db/sqlalchemy/models.py b/nova/db/sqlalchemy/models.py index 52985a3eb..56a4d944a 100644 --- a/nova/db/sqlalchemy/models.py +++ b/nova/db/sqlalchemy/models.py @@ -27,6 +27,7 @@ from sqlalchemy import ForeignKey, DateTime, Boolean, Text, Float from sqlalchemy.orm import relationship, backref, object_mapper from nova.db.sqlalchemy.session import get_session +from nova.db.sqlalchemy.types import IPAddress from nova.openstack.common import cfg from nova.openstack.common import timeutils @@ -290,8 +291,8 @@ class Instance(BASE, NovaBase): # User editable field meant to represent what ip should be used # to connect to the instance - access_ip_v4 = Column(String(255)) - access_ip_v6 = Column(String(255)) + access_ip_v4 = Column(IPAddress()) + access_ip_v6 = Column(IPAddress()) auto_disk_config = Column(Boolean()) progress = Column(Integer) @@ -592,7 +593,7 @@ class SecurityGroupIngressRule(BASE, NovaBase): protocol = Column(String(5)) # "tcp", "udp", or "icmp" from_port = Column(Integer) to_port = Column(Integer) - cidr = Column(String(255)) + cidr = Column(IPAddress()) # Note: This is not the parent SecurityGroup. It's SecurityGroup we're # granting access for. @@ -612,7 +613,7 @@ class ProviderFirewallRule(BASE, NovaBase): protocol = Column(String(5)) # "tcp", "udp", or "icmp" from_port = Column(Integer) to_port = Column(Integer) - cidr = Column(String(255)) + cidr = Column(IPAddress()) class KeyPair(BASE, NovaBase): @@ -662,25 +663,25 @@ class Network(BASE, NovaBase): label = Column(String(255)) injected = Column(Boolean, default=False) - cidr = Column(String(255), unique=True) - cidr_v6 = Column(String(255), unique=True) + cidr = Column(IPAddress(), unique=True) + cidr_v6 = Column(IPAddress(), unique=True) multi_host = Column(Boolean, default=False) - gateway_v6 = Column(String(255)) - netmask_v6 = Column(String(255)) - netmask = Column(String(255)) + gateway_v6 = Column(IPAddress()) + netmask_v6 = Column(IPAddress()) + netmask = Column(IPAddress()) bridge = Column(String(255)) bridge_interface = Column(String(255)) - gateway = Column(String(255)) - broadcast = Column(String(255)) - dns1 = Column(String(255)) - dns2 = Column(String(255)) + gateway = Column(IPAddress()) + broadcast = Column(IPAddress()) + dns1 = Column(IPAddress()) + dns2 = Column(IPAddress()) vlan = Column(Integer) - vpn_public_address = Column(String(255)) + vpn_public_address = Column(IPAddress()) vpn_public_port = Column(Integer) - vpn_private_address = Column(String(255)) - dhcp_start = Column(String(255)) + vpn_private_address = Column(IPAddress()) + dhcp_start = Column(IPAddress()) rxtx_base = Column(Integer) @@ -705,7 +706,7 @@ class FixedIp(BASE, NovaBase): """Represents a fixed ip for an instance.""" __tablename__ = 'fixed_ips' id = Column(Integer, primary_key=True) - address = Column(String(255)) + address = Column(IPAddress()) network_id = Column(Integer, nullable=True) virtual_interface_id = Column(Integer, nullable=True) instance_uuid = Column(String(36), nullable=True) @@ -722,7 +723,7 @@ class FloatingIp(BASE, NovaBase): """Represents a floating ip that dynamically forwards to a fixed ip.""" __tablename__ = 'floating_ips' id = Column(Integer, primary_key=True) - address = Column(String(255)) + address = Column(IPAddress()) fixed_ip_id = Column(Integer, nullable=True) project_id = Column(String(255)) host = Column(String(255)) # , ForeignKey('hosts.id')) @@ -744,7 +745,7 @@ class ConsolePool(BASE, NovaBase): """Represents pool of consoles on the same physical node.""" __tablename__ = 'console_pools' id = Column(Integer, primary_key=True) - address = Column(String(255)) + address = Column(IPAddress()) username = Column(String(255)) password = Column(String(255)) console_type = Column(String(255)) diff --git a/nova/db/sqlalchemy/types.py b/nova/db/sqlalchemy/types.py new file mode 100644 index 000000000..275e61a4c --- /dev/null +++ b/nova/db/sqlalchemy/types.py @@ -0,0 +1,26 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 OpenStack LLC. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +"""Custom SQLAlchemy types.""" + +from sqlalchemy.dialects import postgresql +from sqlalchemy import String + + +def IPAddress(): + """An SQLAlchemy type representing an IP-address.""" + return String(39).with_variant(postgresql.INET(), 'postgresql') diff --git a/nova/exception.py b/nova/exception.py index f96b1eaf3..1af92cd08 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): @@ -522,6 +526,10 @@ class PortNotFound(NotFound): message = _("Port %(port_id)s could not be found.") +class PortNotUsable(NovaException): + message = _("Port %(port_id)s not usable for instance %(instance)s.") + + class FixedIpNotFound(NotFound): message = _("No fixed IP associated with id %(id)s.") @@ -768,7 +776,7 @@ class FlavorAccessNotFound(NotFound): class CellNotFound(NotFound): - message = _("Cell %(cell_id)s could not be found.") + message = _("Cell %(cell_name)s doesn't exist.") class CellRoutingInconsistency(NovaException): diff --git a/nova/network/manager.py b/nova/network/manager.py index ccdac6f60..7b69c7a36 100644 --- a/nova/network/manager.py +++ b/nova/network/manager.py @@ -568,7 +568,7 @@ class FloatingIP(object): else: host = network['host'] - interface = CONF.public_interface or floating_ip['interface'] + interface = floating_ip.get('interface') if host == self.host: # i'm the correct host self._associate_floating_ip(context, floating_address, @@ -585,6 +585,7 @@ class FloatingIP(object): def _associate_floating_ip(self, context, floating_address, fixed_address, interface, instance_uuid): """Performs db and driver calls to associate floating ip & fixed ip.""" + interface = CONF.public_interface or interface @lockutils.synchronized(unicode(floating_address), 'nova-') def do_associate(): @@ -642,7 +643,7 @@ class FloatingIP(object): # send to correct host, unless i'm the correct host network = self._get_network_by_id(context, fixed_ip['network_id']) - interface = CONF.public_interface or floating_ip['interface'] + interface = floating_ip.get('interface') if network['multi_host']: instance = self.db.instance_get_by_uuid(context, fixed_ip['instance_uuid']) @@ -672,7 +673,7 @@ class FloatingIP(object): def _disassociate_floating_ip(self, context, address, interface, instance_uuid): """Performs db and driver calls to disassociate floating ip.""" - # disassociate floating ip + interface = CONF.public_interface or interface @lockutils.synchronized(unicode(address), 'nova-') def do_disassociate(): diff --git a/nova/network/quantumv2/api.py b/nova/network/quantumv2/api.py index 8347ee94d..0deb3a4bb 100644 --- a/nova/network/quantumv2/api.py +++ b/nova/network/quantumv2/api.py @@ -113,6 +113,7 @@ class API(base.Base): with requested_networks which is user supplied). NB: QuantumV2 does not yet honour mac address limits. """ + hypervisor_macs = kwargs.get('macs', None) quantum = quantumv2.get_client(context) LOG.debug(_('allocate_for_instance() for %s'), instance['display_name']) @@ -127,7 +128,11 @@ class API(base.Base): if requested_networks: for network_id, fixed_ip, port_id in requested_networks: if port_id: - port = quantum.show_port(port_id).get('port') + port = quantum.show_port(port_id)['port'] + if hypervisor_macs is not None: + if port['mac_address'] not in hypervisor_macs: + raise exception.PortNotUsable(port_id=port_id, + instance=instance['display_name']) network_id = port['network_id'] ports[network_id] = port elif fixed_ip: diff --git a/nova/quota.py b/nova/quota.py index 96e612503..1856c97c1 100644 --- a/nova/quota.py +++ b/nova/quota.py @@ -965,6 +965,7 @@ class QuotaEngine(object): # logged, however, because this is less than optimal. LOG.exception(_("Failed to commit reservations " "%(reservations)s") % locals()) + LOG.debug(_("Committed reservations %(reservations)s") % locals()) def rollback(self, context, reservations, project_id=None): """Roll back reservations. @@ -986,6 +987,7 @@ class QuotaEngine(object): # logged, however, because this is less than optimal. LOG.exception(_("Failed to roll back reservations " "%(reservations)s") % locals()) + LOG.debug(_("Rolled back reservations %(reservations)s") % locals()) def usage_reset(self, context, resources): """ diff --git a/nova/service.py b/nova/service.py index 39e414eb6..0fde14baa 100644 --- a/nova/service.py +++ b/nova/service.py @@ -32,7 +32,6 @@ import greenlet 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 eventlet_backdoor diff --git a/nova/spice/__init__.py b/nova/spice/__init__.py new file mode 100644 index 000000000..390957e27 --- /dev/null +++ b/nova/spice/__init__.py @@ -0,0 +1,47 @@ +#!/usr/bin/env python +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright (c) 2012 Red Hat, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Module for SPICE Proxying.""" + +from nova.openstack.common import cfg + + +spice_opts = [ + cfg.StrOpt('html5proxy_base_url', + default='http://127.0.0.1:6080/spice_auto.html', + help='location of spice html5 console proxy, in the form ' + '"http://127.0.0.1:6080/spice_auto.html"'), + cfg.StrOpt('server_listen', + default='127.0.0.1', + help='IP address on which instance spice server should listen'), + cfg.StrOpt('server_proxyclient_address', + default='127.0.0.1', + help='the address to which proxy clients ' + '(like nova-spicehtml5proxy) should connect'), + cfg.BoolOpt('enabled', + default=False, + help='enable spice related features'), + cfg.BoolOpt('agent_enabled', + default=True, + help='enable spice guest agent support'), + cfg.StrOpt('keymap', + default='en-us', + help='keymap for spice'), + ] + +CONF = cfg.CONF +CONF.register_opts(spice_opts, group='spice') diff --git a/nova/tests/api/ec2/test_cloud.py b/nova/tests/api/ec2/test_cloud.py index b30a3ddeb..562473121 100644 --- a/nova/tests/api/ec2/test_cloud.py +++ b/nova/tests/api/ec2/test_cloud.py @@ -30,6 +30,7 @@ import fixtures from nova.api.ec2 import cloud from nova.api.ec2 import ec2utils from nova.api.ec2 import inst_state +from nova.api.metadata import password from nova.compute import api as compute_api from nova.compute import power_state from nova.compute import utils as compute_utils @@ -1387,6 +1388,17 @@ class CloudTestCase(test.TestCase): instance_id = rv['instancesSet'][0]['instanceId'] return instance_id + def test_get_password_data(self): + instance_id = self._run_instance( + image_id='ami-1', + instance_type=CONF.default_instance_type, + max_count=1) + self.stubs.Set(password, 'extract_password', lambda i: 'fakepass') + output = self.cloud.get_password_data(context=self.context, + instance_id=[instance_id]) + self.assertEquals(output['passwordData'], 'fakepass') + rv = self.cloud.terminate_instances(self.context, [instance_id]) + def test_console_output(self): instance_id = self._run_instance( image_id='ami-1', diff --git a/nova/tests/api/openstack/compute/contrib/test_admin_actions_with_cells.py b/nova/tests/api/openstack/compute/contrib/test_admin_actions_with_cells.py new file mode 100644 index 000000000..b8f4e6398 --- /dev/null +++ b/nova/tests/api/openstack/compute/contrib/test_admin_actions_with_cells.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. +""" +Tests For Compute admin api w/ Cells +""" + +from nova.api.openstack.compute.contrib import admin_actions +from nova.compute import cells_api as compute_cells_api +from nova.compute import vm_states +from nova.openstack.common import log as logging +from nova.openstack.common import uuidutils +from nova import test +from nova.tests.api.openstack import fakes + +LOG = logging.getLogger('nova.tests.test_compute_cells') + +INSTANCE_IDS = {'inst_id': 1} + + +class CellsAdminAPITestCase(test.TestCase): + + def setUp(self): + super(CellsAdminAPITestCase, self).setUp() + + def _fake_cell_read_only(*args, **kwargs): + return False + + def _fake_validate_cell(*args, **kwargs): + return + + def _fake_compute_api_get(context, instance_id): + return {'id': 1, 'uuid': instance_id, 'vm_state': vm_states.ACTIVE, + 'task_state': None, 'cell_name': None} + + def _fake_instance_update_and_get_original(context, instance_uuid, + values): + inst = fakes.stub_instance(INSTANCE_IDS.get(instance_uuid), + name=values.get('display_name')) + return (inst, inst) + + def fake_cast_to_cells(context, instance, method, *args, **kwargs): + """ + Makes sure that the cells recieve the cast to update + the cell state + """ + self.cells_recieved_kwargs.update(kwargs) + + self.admin_api = admin_actions.AdminActionsController() + self.admin_api.compute_api = compute_cells_api.ComputeCellsAPI() + self.stubs.Set(self.admin_api.compute_api, '_cell_read_only', + _fake_cell_read_only) + self.stubs.Set(self.admin_api.compute_api, '_validate_cell', + _fake_validate_cell) + self.stubs.Set(self.admin_api.compute_api, 'get', + _fake_compute_api_get) + self.stubs.Set(self.admin_api.compute_api.db, + 'instance_update_and_get_original', + _fake_instance_update_and_get_original) + self.stubs.Set(self.admin_api.compute_api, '_cast_to_cells', + fake_cast_to_cells) + + self.uuid = uuidutils.generate_uuid() + url = '/fake/servers/%s/action' % self.uuid + self.request = fakes.HTTPRequest.blank(url) + self.cells_recieved_kwargs = {} + + def test_reset_active(self): + body = {"os-resetState": {"state": "error"}} + result = self.admin_api._reset_state(self.request, 'inst_id', body) + + self.assertEqual(result.status_int, 202) + # Make sure the cells recieved the update + self.assertEqual(self.cells_recieved_kwargs, + dict(vm_state=vm_states.ERROR, + task_state=None)) diff --git a/nova/tests/api/openstack/compute/contrib/test_cells.py b/nova/tests/api/openstack/compute/contrib/test_cells.py new file mode 100644 index 000000000..82d469524 --- /dev/null +++ b/nova/tests/api/openstack/compute/contrib/test_cells.py @@ -0,0 +1,396 @@ +# Copyright 2011-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. + +import copy + +from lxml import etree +from webob import exc + +from nova.api.openstack.compute.contrib import cells as cells_ext +from nova.api.openstack import xmlutil +from nova.cells import rpcapi as cells_rpcapi +from nova import context +from nova import db +from nova import exception +from nova.openstack.common import timeutils +from nova import test +from nova.tests.api.openstack import fakes + + +FAKE_CELLS = [ + dict(id=1, name='cell1', username='bob', is_parent=True, + weight_scale=1.0, weight_offset=0.0, + rpc_host='r1.example.org', password='xxxx'), + dict(id=2, name='cell2', username='alice', is_parent=False, + weight_scale=1.0, weight_offset=0.0, + rpc_host='r2.example.org', password='qwerty')] + + +FAKE_CAPABILITIES = [ + {'cap1': '0,1', 'cap2': '2,3'}, + {'cap3': '4,5', 'cap4': '5,6'}] + + +def fake_db_cell_get(context, cell_name): + for cell in FAKE_CELLS: + if cell_name == cell['name']: + return cell + else: + raise exception.CellNotFound(cell_name=cell_name) + + +def fake_db_cell_create(context, values): + cell = dict(id=1) + cell.update(values) + return cell + + +def fake_db_cell_update(context, cell_id, values): + cell = fake_db_cell_get(context, cell_id) + cell.update(values) + return cell + + +def fake_cells_api_get_all_cell_info(*args): + cells = copy.deepcopy(FAKE_CELLS) + del cells[0]['password'] + del cells[1]['password'] + for i, cell in enumerate(cells): + cell['capabilities'] = FAKE_CAPABILITIES[i] + return cells + + +def fake_db_cell_get_all(context): + return FAKE_CELLS + + +class CellsTest(test.TestCase): + def setUp(self): + super(CellsTest, self).setUp() + self.stubs.Set(db, 'cell_get', fake_db_cell_get) + self.stubs.Set(db, 'cell_get_all', fake_db_cell_get_all) + self.stubs.Set(db, 'cell_update', fake_db_cell_update) + self.stubs.Set(db, 'cell_create', fake_db_cell_create) + self.stubs.Set(cells_rpcapi.CellsAPI, 'get_cell_info_for_neighbors', + fake_cells_api_get_all_cell_info) + + self.controller = cells_ext.Controller() + self.context = context.get_admin_context() + + def _get_request(self, resource): + return fakes.HTTPRequest.blank('/v2/fake/' + resource) + + def test_index(self): + req = self._get_request("cells") + res_dict = self.controller.index(req) + + self.assertEqual(len(res_dict['cells']), 2) + for i, cell in enumerate(res_dict['cells']): + self.assertEqual(cell['name'], FAKE_CELLS[i]['name']) + self.assertNotIn('capabilitiles', cell) + self.assertNotIn('password', cell) + + def test_detail(self): + req = self._get_request("cells/detail") + res_dict = self.controller.detail(req) + + self.assertEqual(len(res_dict['cells']), 2) + for i, cell in enumerate(res_dict['cells']): + self.assertEqual(cell['name'], FAKE_CELLS[i]['name']) + self.assertEqual(cell['capabilities'], FAKE_CAPABILITIES[i]) + self.assertNotIn('password', cell) + + def test_show_bogus_cell_raises(self): + req = self._get_request("cells/bogus") + self.assertRaises(exc.HTTPNotFound, self.controller.show, req, 'bogus') + + def test_get_cell_by_name(self): + req = self._get_request("cells/cell1") + res_dict = self.controller.show(req, 'cell1') + cell = res_dict['cell'] + + self.assertEqual(cell['name'], 'cell1') + self.assertEqual(cell['rpc_host'], 'r1.example.org') + self.assertNotIn('password', cell) + + def test_cell_delete(self): + call_info = {'delete_called': 0} + + def fake_db_cell_delete(context, cell_name): + self.assertEqual(cell_name, 'cell999') + call_info['delete_called'] += 1 + + self.stubs.Set(db, 'cell_delete', fake_db_cell_delete) + + req = self._get_request("cells/cell999") + self.controller.delete(req, 'cell999') + self.assertEqual(call_info['delete_called'], 1) + + def test_delete_bogus_cell_raises(self): + req = self._get_request("cells/cell999") + req.environ['nova.context'] = self.context + self.assertRaises(exc.HTTPNotFound, self.controller.delete, req, + 'cell999') + + def test_cell_create_parent(self): + body = {'cell': {'name': 'meow', + 'username': 'fred', + 'password': 'fubar', + 'rpc_host': 'r3.example.org', + 'type': 'parent', + # Also test this is ignored/stripped + 'is_parent': False}} + + req = self._get_request("cells") + res_dict = self.controller.create(req, body) + cell = res_dict['cell'] + + self.assertEqual(cell['name'], 'meow') + self.assertEqual(cell['username'], 'fred') + self.assertEqual(cell['rpc_host'], 'r3.example.org') + self.assertEqual(cell['type'], 'parent') + self.assertNotIn('password', cell) + self.assertNotIn('is_parent', cell) + + def test_cell_create_child(self): + body = {'cell': {'name': 'meow', + 'username': 'fred', + 'password': 'fubar', + 'rpc_host': 'r3.example.org', + 'type': 'child'}} + + req = self._get_request("cells") + res_dict = self.controller.create(req, body) + cell = res_dict['cell'] + + self.assertEqual(cell['name'], 'meow') + self.assertEqual(cell['username'], 'fred') + self.assertEqual(cell['rpc_host'], 'r3.example.org') + self.assertEqual(cell['type'], 'child') + self.assertNotIn('password', cell) + self.assertNotIn('is_parent', cell) + + def test_cell_create_no_name_raises(self): + body = {'cell': {'username': 'moocow', + 'password': 'secret', + 'rpc_host': 'r3.example.org', + 'type': 'parent'}} + + req = self._get_request("cells") + self.assertRaises(exc.HTTPBadRequest, + self.controller.create, req, body) + + def test_cell_create_name_empty_string_raises(self): + body = {'cell': {'name': '', + 'username': 'fred', + 'password': 'secret', + 'rpc_host': 'r3.example.org', + 'type': 'parent'}} + + req = self._get_request("cells") + self.assertRaises(exc.HTTPBadRequest, + self.controller.create, req, body) + + def test_cell_create_name_with_bang_raises(self): + body = {'cell': {'name': 'moo!cow', + 'username': 'fred', + 'password': 'secret', + 'rpc_host': 'r3.example.org', + 'type': 'parent'}} + + req = self._get_request("cells") + self.assertRaises(exc.HTTPBadRequest, + self.controller.create, req, body) + + def test_cell_create_name_with_dot_raises(self): + body = {'cell': {'name': 'moo.cow', + 'username': 'fred', + 'password': 'secret', + 'rpc_host': 'r3.example.org', + 'type': 'parent'}} + + req = self._get_request("cells") + self.assertRaises(exc.HTTPBadRequest, + self.controller.create, req, body) + + def test_cell_create_name_with_invalid_type_raises(self): + body = {'cell': {'name': 'moocow', + 'username': 'fred', + 'password': 'secret', + 'rpc_host': 'r3.example.org', + 'type': 'invalid'}} + + req = self._get_request("cells") + self.assertRaises(exc.HTTPBadRequest, + self.controller.create, req, body) + + def test_cell_update(self): + body = {'cell': {'username': 'zeb', + 'password': 'sneaky'}} + + req = self._get_request("cells/cell1") + res_dict = self.controller.update(req, 'cell1', body) + cell = res_dict['cell'] + + self.assertEqual(cell['name'], 'cell1') + self.assertEqual(cell['rpc_host'], FAKE_CELLS[0]['rpc_host']) + self.assertEqual(cell['username'], 'zeb') + self.assertNotIn('password', cell) + + def test_cell_update_empty_name_raises(self): + body = {'cell': {'name': '', + 'username': 'zeb', + 'password': 'sneaky'}} + + req = self._get_request("cells/cell1") + self.assertRaises(exc.HTTPBadRequest, + self.controller.update, req, 'cell1', body) + + def test_cell_update_invalid_type_raises(self): + body = {'cell': {'username': 'zeb', + 'type': 'invalid', + 'password': 'sneaky'}} + + req = self._get_request("cells/cell1") + self.assertRaises(exc.HTTPBadRequest, + self.controller.update, req, 'cell1', body) + + def test_cell_info(self): + caps = ['cap1=a;b', 'cap2=c;d'] + self.flags(name='darksecret', capabilities=caps, group='cells') + + req = self._get_request("cells/info") + res_dict = self.controller.info(req) + cell = res_dict['cell'] + cell_caps = cell['capabilities'] + + self.assertEqual(cell['name'], 'darksecret') + self.assertEqual(cell_caps['cap1'], 'a;b') + self.assertEqual(cell_caps['cap2'], 'c;d') + + def test_sync_instances(self): + call_info = {} + + def sync_instances(self, context, **kwargs): + call_info['project_id'] = kwargs.get('project_id') + call_info['updated_since'] = kwargs.get('updated_since') + + self.stubs.Set(cells_rpcapi.CellsAPI, 'sync_instances', sync_instances) + + req = self._get_request("cells/sync_instances") + body = {} + self.controller.sync_instances(req, body=body) + self.assertEqual(call_info['project_id'], None) + self.assertEqual(call_info['updated_since'], None) + + body = {'project_id': 'test-project'} + self.controller.sync_instances(req, body=body) + self.assertEqual(call_info['project_id'], 'test-project') + self.assertEqual(call_info['updated_since'], None) + + expected = timeutils.utcnow().isoformat() + if not expected.endswith("+00:00"): + expected += "+00:00" + + body = {'updated_since': expected} + self.controller.sync_instances(req, body=body) + self.assertEqual(call_info['project_id'], None) + self.assertEqual(call_info['updated_since'], expected) + + body = {'updated_since': 'skjdfkjsdkf'} + self.assertRaises(exc.HTTPBadRequest, + self.controller.sync_instances, req, body=body) + + body = {'foo': 'meow'} + self.assertRaises(exc.HTTPBadRequest, + self.controller.sync_instances, req, body=body) + + +class TestCellsXMLSerializer(test.TestCase): + def test_multiple_cells(self): + fixture = {'cells': fake_cells_api_get_all_cell_info()} + + serializer = cells_ext.CellsTemplate() + output = serializer.serialize(fixture) + res_tree = etree.XML(output) + + self.assertEqual(res_tree.tag, '{%s}cells' % xmlutil.XMLNS_V10) + self.assertEqual(len(res_tree), 2) + self.assertEqual(res_tree[0].tag, '{%s}cell' % xmlutil.XMLNS_V10) + self.assertEqual(res_tree[1].tag, '{%s}cell' % xmlutil.XMLNS_V10) + + def test_single_cell_with_caps(self): + cell = {'id': 1, + 'name': 'darksecret', + 'username': 'meow', + 'capabilities': {'cap1': 'a;b', + 'cap2': 'c;d'}} + fixture = {'cell': cell} + + serializer = cells_ext.CellTemplate() + output = serializer.serialize(fixture) + res_tree = etree.XML(output) + + self.assertEqual(res_tree.tag, '{%s}cell' % xmlutil.XMLNS_V10) + self.assertEqual(res_tree.get('name'), 'darksecret') + self.assertEqual(res_tree.get('username'), 'meow') + self.assertEqual(res_tree.get('password'), None) + self.assertEqual(len(res_tree), 1) + + child = res_tree[0] + self.assertEqual(child.tag, + '{%s}capabilities' % xmlutil.XMLNS_V10) + for elem in child: + self.assertIn(elem.tag, ('{%s}cap1' % xmlutil.XMLNS_V10, + '{%s}cap2' % xmlutil.XMLNS_V10)) + if elem.tag == '{%s}cap1' % xmlutil.XMLNS_V10: + self.assertEqual(elem.text, 'a;b') + elif elem.tag == '{%s}cap2' % xmlutil.XMLNS_V10: + self.assertEqual(elem.text, 'c;d') + + def test_single_cell_without_caps(self): + cell = {'id': 1, + 'username': 'woof', + 'name': 'darksecret'} + fixture = {'cell': cell} + + serializer = cells_ext.CellTemplate() + output = serializer.serialize(fixture) + res_tree = etree.XML(output) + + self.assertEqual(res_tree.tag, '{%s}cell' % xmlutil.XMLNS_V10) + self.assertEqual(res_tree.get('name'), 'darksecret') + self.assertEqual(res_tree.get('username'), 'woof') + self.assertEqual(res_tree.get('password'), None) + self.assertEqual(len(res_tree), 0) + + +class TestCellsXMLDeserializer(test.TestCase): + def test_cell_deserializer(self): + caps_dict = {'cap1': 'a;b', + 'cap2': 'c;d'} + caps_xml = ("<capabilities><cap1>a;b</cap1>" + "<cap2>c;d</cap2></capabilities>") + expected = {'cell': {'name': 'testcell1', + 'type': 'child', + 'rpc_host': 'localhost', + 'capabilities': caps_dict}} + intext = ("<?xml version='1.0' encoding='UTF-8'?>\n" + "<cell><name>testcell1</name><type>child</type>" + "<rpc_host>localhost</rpc_host>" + "%s</cell>") % caps_xml + deserializer = cells_ext.CellDeserializer() + result = deserializer.deserialize(intext) + self.assertEqual(dict(body=expected), result) 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 2567558ab..b69268d2a 100644 --- a/nova/tests/api/openstack/compute/test_servers.py +++ b/nova/tests/api/openstack/compute/test_servers.py @@ -4008,7 +4008,7 @@ class ServersViewBuilderTest(test.TestCase): "message": "Error", 'details': 'Stock details for test'} - self.request.context = context.get_admin_context() + self.request.environ['nova.context'].is_admin = True output = self.view_builder.show(self.request, self.instance) self.assertThat(output['server']['fault'], matchers.DictMatches(expected_fault)) @@ -4027,7 +4027,7 @@ class ServersViewBuilderTest(test.TestCase): "created": "2010-10-10T12:00:00Z", "message": "Error"} - self.request.context = context.get_admin_context() + self.request.environ['nova.context'].is_admin = True output = self.view_builder.show(self.request, self.instance) self.assertThat(output['server']['fault'], matchers.DictMatches(expected_fault)) diff --git a/nova/tests/cells/test_cells_manager.py b/nova/tests/cells/test_cells_manager.py index 72ef3f1f0..ef165f4ed 100644 --- a/nova/tests/cells/test_cells_manager.py +++ b/nova/tests/cells/test_cells_manager.py @@ -38,6 +38,21 @@ class CellsManagerClassTestCase(test.TestCase): self.driver = self.cells_manager.driver self.ctxt = 'fake_context' + def _get_fake_responses(self): + responses = [] + expected_responses = [] + for x in xrange(1, 4): + responses.append(messaging.Response('cell%s' % x, x, False)) + expected_responses.append(('cell%s' % x, x)) + return expected_responses, responses + + def test_get_cell_info_for_neighbors(self): + self.mox.StubOutWithMock(self.cells_manager.state_manager, + 'get_cell_info_for_neighbors') + self.cells_manager.state_manager.get_cell_info_for_neighbors() + self.mox.ReplayAll() + self.cells_manager.get_cell_info_for_neighbors(self.ctxt) + def test_post_start_hook_child_cell(self): self.mox.StubOutWithMock(self.driver, 'start_consumers') self.mox.StubOutWithMock(context, 'get_admin_context') @@ -211,3 +226,14 @@ class CellsManagerClassTestCase(test.TestCase): # Now the last 1 and the first 1 self.assertEqual(call_info['sync_instances'], [instances[-1], instances[0]]) + + def test_sync_instances(self): + self.mox.StubOutWithMock(self.msg_runner, + 'sync_instances') + self.msg_runner.sync_instances(self.ctxt, 'fake-project', + 'fake-time', 'fake-deleted') + self.mox.ReplayAll() + self.cells_manager.sync_instances(self.ctxt, + project_id='fake-project', + updated_since='fake-time', + deleted='fake-deleted') diff --git a/nova/tests/cells/test_cells_messaging.py b/nova/tests/cells/test_cells_messaging.py index 9973716f6..da45721ed 100644 --- a/nova/tests/cells/test_cells_messaging.py +++ b/nova/tests/cells/test_cells_messaging.py @@ -14,11 +14,14 @@ """ Tests For Cells Messaging module """ +import mox from nova.cells import messaging +from nova.cells import utils as cells_utils from nova import context from nova import exception from nova.openstack.common import cfg +from nova.openstack.common import timeutils from nova import test from nova.tests.cells import fakes @@ -912,3 +915,46 @@ class CellsBroadcastMethodsTestCase(test.TestCase): self.src_msg_runner.bw_usage_update_at_top(self.ctxt, fake_bw_update_info) + + def test_sync_instances(self): + # Reset this, as this is a broadcast down. + self._setup_attrs(up=False) + project_id = 'fake_project_id' + updated_since_raw = 'fake_updated_since_raw' + updated_since_parsed = 'fake_updated_since_parsed' + deleted = 'fake_deleted' + + instance1 = dict(uuid='fake_uuid1', deleted=False) + instance2 = dict(uuid='fake_uuid2', deleted=True) + fake_instances = [instance1, instance2] + + self.mox.StubOutWithMock(self.tgt_msg_runner, + 'instance_update_at_top') + self.mox.StubOutWithMock(self.tgt_msg_runner, + 'instance_destroy_at_top') + + self.mox.StubOutWithMock(timeutils, 'parse_isotime') + self.mox.StubOutWithMock(cells_utils, 'get_instances_to_sync') + + # Middle cell. + timeutils.parse_isotime(updated_since_raw).AndReturn( + updated_since_parsed) + cells_utils.get_instances_to_sync(self.ctxt, + updated_since=updated_since_parsed, + project_id=project_id, + deleted=deleted).AndReturn([]) + + # Bottom/Target cell + timeutils.parse_isotime(updated_since_raw).AndReturn( + updated_since_parsed) + cells_utils.get_instances_to_sync(self.ctxt, + updated_since=updated_since_parsed, + project_id=project_id, + deleted=deleted).AndReturn(fake_instances) + self.tgt_msg_runner.instance_update_at_top(self.ctxt, instance1) + self.tgt_msg_runner.instance_destroy_at_top(self.ctxt, instance2) + + self.mox.ReplayAll() + + self.src_msg_runner.sync_instances(self.ctxt, + project_id, updated_since_raw, deleted) diff --git a/nova/tests/cells/test_cells_rpcapi.py b/nova/tests/cells/test_cells_rpcapi.py index b51bfa0c1..5e045aca9 100644 --- a/nova/tests/cells/test_cells_rpcapi.py +++ b/nova/tests/cells/test_cells_rpcapi.py @@ -204,3 +204,23 @@ class CellsAPITestCase(test.TestCase): expected_args = {'bw_update_info': bw_update_info} self._check_result(call_info, 'bw_usage_update_at_top', expected_args) + + def test_get_cell_info_for_neighbors(self): + call_info = self._stub_rpc_method('call', 'fake_response') + result = self.cells_rpcapi.get_cell_info_for_neighbors( + self.fake_context) + self._check_result(call_info, 'get_cell_info_for_neighbors', {}, + version='1.1') + self.assertEqual(result, 'fake_response') + + def test_sync_instances(self): + call_info = self._stub_rpc_method('cast', None) + self.cells_rpcapi.sync_instances(self.fake_context, + project_id='fake_project', updated_since='fake_time', + deleted=True) + + expected_args = {'project_id': 'fake_project', + 'updated_since': 'fake_time', + 'deleted': True} + self._check_result(call_info, 'sync_instances', expected_args, + version='1.1') diff --git a/nova/tests/compute/test_compute.py b/nova/tests/compute/test_compute.py index 0d9f67231..3740d598e 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, @@ -5512,6 +5565,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 diff --git a/nova/tests/compute/test_multiple_nodes.py b/nova/tests/compute/test_multiple_nodes.py index 78ed0cea7..27ee7aaba 100644 --- a/nova/tests/compute/test_multiple_nodes.py +++ b/nova/tests/compute/test_multiple_nodes.py @@ -80,6 +80,9 @@ class MultiNodeComputeTestCase(BaseTestCase): super(MultiNodeComputeTestCase, self).setUp() self.flags(compute_driver='nova.virt.fake.FakeDriver') self.compute = importutils.import_object(CONF.compute_manager) + self.flags(use_local=True, group='conductor') + self.conductor = self.start_service('conductor', + manager=CONF.conductor.manager) def test_update_available_resource_add_remove_node(self): ctx = context.get_admin_context() 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 cc3dbfcc0..b29db92e7 100644 --- a/nova/tests/conductor/test_conductor.py +++ b/nova/tests/conductor/test_conductor.py @@ -130,6 +130,16 @@ class _BaseTestCase(object): 'fake-window', 'fake-host') + def test_migration_get_in_progress_by_host_and_node(self): + self.mox.StubOutWithMock(db, + 'migration_get_in_progress_by_host_and_node') + db.migration_get_in_progress_by_host_and_node( + self.context, 'fake-host', 'fake-node').AndReturn('fake-result') + self.mox.ReplayAll() + result = self.conductor.migration_get_in_progress_by_host_and_node( + self.context, 'fake-host', 'fake-node') + self.assertEqual(result, 'fake-result') + def test_migration_create(self): inst = {'uuid': 'fake-uuid', 'host': 'fake-host', @@ -388,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.""" @@ -451,8 +480,23 @@ class ConductorTestCase(_BaseTestCase, test.TestCase): self.conductor.instance_get_all_by_filters(self.context, filters, 'fake-key', 'fake-sort') + def test_instance_get_all_by_host(self): + self.mox.StubOutWithMock(db, 'instance_get_all_by_host') + self.mox.StubOutWithMock(db, 'instance_get_all_by_host_and_node') + db.instance_get_all_by_host(self.context.elevated(), + 'host').AndReturn('result') + db.instance_get_all_by_host_and_node(self.context.elevated(), 'host', + 'node').AndReturn('result') + self.mox.ReplayAll() + result = self.conductor.instance_get_all_by_host(self.context, 'host') + self.assertEqual(result, 'result') + result = self.conductor.instance_get_all_by_host(self.context, 'host', + 'node') + self.assertEqual(result, 'result') + def _test_stubbed(self, name, dbargs, condargs, db_result_listified=False): + self.mox.StubOutWithMock(db, name) getattr(db, name)(self.context, *dbargs).AndReturn('fake-result') self.mox.ReplayAll() @@ -655,19 +699,22 @@ class ConductorAPITestCase(_BaseTestCase, test.TestCase): def test_instance_get_all(self): self.mox.StubOutWithMock(db, 'instance_get_all_by_filters') db.instance_get_all(self.context) - db.instance_get_all_by_host(self.context.elevated(), 'fake-host') db.instance_get_all_by_filters(self.context, {'name': 'fake-inst'}, 'updated_at', 'asc') self.mox.ReplayAll() self.conductor.instance_get_all(self.context) - self.conductor.instance_get_all_by_host(self.context, 'fake-host') self.conductor.instance_get_all_by_filters(self.context, {'name': 'fake-inst'}, 'updated_at', 'asc') def _test_stubbed(self, name, *args, **kwargs): + if args and isinstance(args[0], FakeContext): + ctxt = args[0] + args = args[1:] + else: + ctxt = self.context self.mox.StubOutWithMock(db, name) - getattr(db, name)(self.context, *args).AndReturn('fake-result') + getattr(db, name)(ctxt, *args).AndReturn('fake-result') if name == 'service_destroy': # TODO(russellb) This is a hack ... SetUp() starts the conductor() # service. There is a cleanup step that runs after this test which @@ -700,6 +747,14 @@ class ConductorAPITestCase(_BaseTestCase, test.TestCase): def test_service_destroy(self): self._test_stubbed('service_destroy', '', returns=False) + def test_instance_get_all_by_host(self): + self._test_stubbed('instance_get_all_by_host', + self.context.elevated(), 'host') + + def test_instance_get_all_by_host_and_node(self): + self._test_stubbed('instance_get_all_by_host_and_node', + self.context.elevated(), 'host', 'node') + def test_ping(self): timeouts = [] calls = dict(count=0) diff --git a/nova/tests/fake_policy.py b/nova/tests/fake_policy.py index 51f3a3f85..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": "", @@ -104,6 +105,7 @@ policy_data = """ "compute_extension:admin_actions:migrate": "", "compute_extension:aggregates": "", "compute_extension:agents": "", + "compute_extension:cells": "", "compute_extension:certificates": "", "compute_extension:cloudpipe": "", "compute_extension:cloudpipe_update": "", 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/all_extensions/extensions-get-resp.json.tpl b/nova/tests/integrated/api_samples/all_extensions/extensions-get-resp.json.tpl index 3d69fad45..fe0613646 100644 --- a/nova/tests/integrated/api_samples/all_extensions/extensions-get-resp.json.tpl +++ b/nova/tests/integrated/api_samples/all_extensions/extensions-get-resp.json.tpl @@ -89,6 +89,14 @@ "updated": "%(timestamp)s" }, { + "alias": "os-cells", + "description": "%(text)s", + "links": [], + "name": "Cells", + "namespace": "http://docs.openstack.org/compute/ext/cells/api/v1.1", + "updated": "%(timestamp)s" + }, + { "alias": "os-certificates", "description": "%(text)s", "links": [], diff --git a/nova/tests/integrated/api_samples/all_extensions/extensions-get-resp.xml.tpl b/nova/tests/integrated/api_samples/all_extensions/extensions-get-resp.xml.tpl index 5953ba704..2051d891a 100644 --- a/nova/tests/integrated/api_samples/all_extensions/extensions-get-resp.xml.tpl +++ b/nova/tests/integrated/api_samples/all_extensions/extensions-get-resp.xml.tpl @@ -33,6 +33,9 @@ <extension alias="os-agents" name="Agents" namespace="http://docs.openstack.org/compute/ext/agents/api/v2" updated="%(timestamp)s"> <description>%(text)s</description> </extension> + <extension alias="os-cells" updated="%(timestamp)s" namespace="http://docs.openstack.org/compute/ext/cells/api/v1.1" name="Cells"> + <description>%(text)s</description> + </extension> <extension alias="os-certificates" updated="%(timestamp)s" namespace="http://docs.openstack.org/compute/ext/certificates/api/v1.1" name="Certificates"> <description>%(text)s</description> </extension> diff --git a/nova/tests/integrated/api_samples/os-cells/cells-get-resp.json.tpl b/nova/tests/integrated/api_samples/os-cells/cells-get-resp.json.tpl new file mode 100644 index 000000000..2993b1df8 --- /dev/null +++ b/nova/tests/integrated/api_samples/os-cells/cells-get-resp.json.tpl @@ -0,0 +1,9 @@ +{ + "cell": { + "name": "cell3", + "username": "username3", + "rpc_host": null, + "rpc_port": null, + "type": "child" + } +} diff --git a/nova/tests/integrated/api_samples/os-cells/cells-get-resp.xml.tpl b/nova/tests/integrated/api_samples/os-cells/cells-get-resp.xml.tpl new file mode 100644 index 000000000..d31a674a2 --- /dev/null +++ b/nova/tests/integrated/api_samples/os-cells/cells-get-resp.xml.tpl @@ -0,0 +1,2 @@ +<?xml version='1.0' encoding='UTF-8'?> +<cell xmlns="http://docs.rackspacecloud.com/servers/api/v1.0" name="cell3" username="username3" rpc_port="None" rpc_host="None" type="child"/> diff --git a/nova/tests/integrated/api_samples/os-cells/cells-list-empty-resp.json.tpl b/nova/tests/integrated/api_samples/os-cells/cells-list-empty-resp.json.tpl new file mode 100644 index 000000000..b16e12cd6 --- /dev/null +++ b/nova/tests/integrated/api_samples/os-cells/cells-list-empty-resp.json.tpl @@ -0,0 +1,4 @@ +{ + "cells": [] +} + diff --git a/nova/tests/integrated/api_samples/os-cells/cells-list-empty-resp.xml.tpl b/nova/tests/integrated/api_samples/os-cells/cells-list-empty-resp.xml.tpl new file mode 100644 index 000000000..32fef4f04 --- /dev/null +++ b/nova/tests/integrated/api_samples/os-cells/cells-list-empty-resp.xml.tpl @@ -0,0 +1,2 @@ +<?xml version='1.0' encoding='UTF-8'?> +<cells xmlns="http://docs.rackspacecloud.com/servers/api/v1.0"/> diff --git a/nova/tests/integrated/api_samples/os-cells/cells-list-resp.json.tpl b/nova/tests/integrated/api_samples/os-cells/cells-list-resp.json.tpl new file mode 100644 index 000000000..3d7a6c207 --- /dev/null +++ b/nova/tests/integrated/api_samples/os-cells/cells-list-resp.json.tpl @@ -0,0 +1,39 @@ +{ + "cells": [ + { + "name": "cell1", + "username": "username1", + "rpc_host": null, + "rpc_port": null, + "type": "child" + }, + { + "name": "cell2", + "username": "username2", + "rpc_host": null, + "rpc_port": null, + "type": "parent" + }, + { + "name": "cell3", + "username": "username3", + "rpc_host": null, + "rpc_port": null, + "type": "child" + }, + { + "name": "cell4", + "username": "username4", + "rpc_host": null, + "rpc_port": null, + "type": "parent" + }, + { + "name": "cell5", + "username": "username5", + "rpc_host": null, + "rpc_port": null, + "type": "child" + } + ] +} diff --git a/nova/tests/integrated/api_samples/os-cells/cells-list-resp.xml.tpl b/nova/tests/integrated/api_samples/os-cells/cells-list-resp.xml.tpl new file mode 100644 index 000000000..58312201f --- /dev/null +++ b/nova/tests/integrated/api_samples/os-cells/cells-list-resp.xml.tpl @@ -0,0 +1,8 @@ +<?xml version='1.0' encoding='UTF-8'?> +<cells xmlns="http://docs.rackspacecloud.com/servers/api/v1.0"> + <cell name="cell1" username="username1" rpc_port="None" rpc_host="None" type="child"/> + <cell name="cell2" username="username2" rpc_port="None" rpc_host="None" type="parent"/> + <cell name="cell3" username="username3" rpc_port="None" rpc_host="None" type="child"/> + <cell name="cell4" username="username4" rpc_port="None" rpc_host="None" type="parent"/> + <cell name="cell5" username="username5" rpc_port="None" rpc_host="None" type="child"/> +</cells> 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/api_samples/os-hosts/hosts-list-resp.json.tpl b/nova/tests/integrated/api_samples/os-hosts/hosts-list-resp.json.tpl index eeb191597..504f66f59 100644 --- a/nova/tests/integrated/api_samples/os-hosts/hosts-list-resp.json.tpl +++ b/nova/tests/integrated/api_samples/os-hosts/hosts-list-resp.json.tpl @@ -21,9 +21,14 @@ "zone": "internal" }, { - "host_name": "%(host_name)s", - "service": "conductor", - "zone": "internal" + "host_name": "%(host_name)s", + "service": "conductor", + "zone": "internal" + }, + { + "host_name": "%(host_name)s", + "service": "cells", + "zone": "internal" } ] } diff --git a/nova/tests/integrated/api_samples/os-hosts/hosts-list-resp.xml.tpl b/nova/tests/integrated/api_samples/os-hosts/hosts-list-resp.xml.tpl index 25ef5a299..4e9d3195d 100644 --- a/nova/tests/integrated/api_samples/os-hosts/hosts-list-resp.xml.tpl +++ b/nova/tests/integrated/api_samples/os-hosts/hosts-list-resp.xml.tpl @@ -5,4 +5,5 @@ <host host_name="%(host_name)s" service="network"/> <host host_name="%(host_name)s" service="scheduler"/> <host host_name="%(host_name)s" service="conductor"/> + <host host_name="%(host_name)s" service="cells"/> </hosts> diff --git a/nova/tests/integrated/integrated_helpers.py b/nova/tests/integrated/integrated_helpers.py index e20d6881b..f17dc025f 100644 --- a/nova/tests/integrated/integrated_helpers.py +++ b/nova/tests/integrated/integrated_helpers.py @@ -27,7 +27,7 @@ import nova.image.glance from nova.openstack.common import cfg from nova.openstack.common.log import logging from nova import service -from nova import test # For the flags +from nova import test from nova.tests import fake_crypto import nova.tests.image.fake from nova.tests.integrated.api import client @@ -35,6 +35,8 @@ from nova.tests.integrated.api import client CONF = cfg.CONF LOG = logging.getLogger(__name__) +CONF = cfg.CONF +CONF.import_opt('manager', 'nova.cells.opts', group='cells') def generate_random_alphanumeric(length): @@ -81,6 +83,7 @@ class _IntegratedTestBase(test.TestCase): self.scheduler = self.start_service('cert') self.network = self.start_service('network') self.scheduler = self.start_service('scheduler') + self.cells = self.start_service('cells', manager=CONF.cells.manager) self._start_api_service() diff --git a/nova/tests/integrated/test_api_samples.py b/nova/tests/integrated/test_api_samples.py index 98ac6a230..aa41a8259 100644 --- a/nova/tests/integrated/test_api_samples.py +++ b/nova/tests/integrated/test_api_samples.py @@ -54,6 +54,8 @@ CONF.import_opt('osapi_compute_extension', CONF.import_opt('vpn_image_id', 'nova.cloudpipe.pipelib') CONF.import_opt('osapi_compute_link_prefix', 'nova.api.openstack.common') CONF.import_opt('osapi_glance_link_prefix', 'nova.api.openstack.common') +CONF.import_opt('enable', 'nova.cells.opts', group='cells') +CONF.import_opt('db_check_interval', 'nova.cells.state', group='cells') LOG = logging.getLogger(__name__) @@ -2114,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, @@ -2126,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' @@ -2501,3 +2520,63 @@ class QuotaClassesSampleJsonTests(ApiSampleTestBase): class QuotaClassesSampleXmlTests(QuotaClassesSampleJsonTests): ctype = "xml" + + +class CellsSampleJsonTest(ApiSampleTestBase): + extension_name = "nova.api.openstack.compute.contrib.cells.Cells" + + def setUp(self): + # db_check_interval < 0 makes cells manager always hit the DB + self.flags(enable=True, db_check_interval=-1, group='cells') + super(CellsSampleJsonTest, self).setUp() + self._stub_cells() + + def _stub_cells(self, num_cells=5): + self.cells = [] + self.cells_next_id = 1 + + def _fake_cell_get_all(context): + return self.cells + + def _fake_cell_get(context, cell_name): + for cell in self.cells: + if cell['name'] == cell_name: + return cell + raise exception.CellNotFound(cell_name=cell_name) + + for x in xrange(num_cells): + cell = models.Cell() + our_id = self.cells_next_id + self.cells_next_id += 1 + cell.update({'id': our_id, + 'name': 'cell%s' % our_id, + 'username': 'username%s' % our_id, + 'is_parent': our_id % 2 == 0}) + self.cells.append(cell) + + self.stubs.Set(db, 'cell_get_all', _fake_cell_get_all) + self.stubs.Set(db, 'cell_get', _fake_cell_get) + + def test_cells_empty_list(self): + # Override this + self._stub_cells(num_cells=0) + response = self._do_get('os-cells') + self.assertEqual(response.status, 200) + subs = self._get_regexes() + return self._verify_response('cells-list-empty-resp', subs, response) + + def test_cells_list(self): + response = self._do_get('os-cells') + self.assertEqual(response.status, 200) + subs = self._get_regexes() + return self._verify_response('cells-list-resp', subs, response) + + def test_cells_get(self): + response = self._do_get('os-cells/cell3') + self.assertEqual(response.status, 200) + subs = self._get_regexes() + return self._verify_response('cells-get-resp', subs, response) + + +class CellsSampleXmlTest(CellsSampleJsonTest): + ctype = 'xml' diff --git a/nova/tests/network/test_quantumv2.py b/nova/tests/network/test_quantumv2.py index f92dba443..876bce90d 100644 --- a/nova/tests/network/test_quantumv2.py +++ b/nova/tests/network/test_quantumv2.py @@ -342,15 +342,11 @@ class TestQuantumv2(test.TestCase): self.assertEquals('my_mac%s' % id_suffix, nw_inf[0]['address']) self.assertEquals(0, len(nw_inf[0]['network']['subnets'])) - def _allocate_for_instance(self, net_idx=1, **kwargs): + def _stub_allocate_for_instance(self, net_idx=1, **kwargs): api = quantumapi.API() self.mox.StubOutWithMock(api, 'get_instance_nw_info') # Net idx is 1-based for compatibility with existing unit tests nets = self.nets[net_idx - 1] - api.get_instance_nw_info(mox.IgnoreArg(), - self.instance, - networks=nets).AndReturn(None) - ports = {} fixed_ips = {} req_net_ids = [] @@ -359,7 +355,8 @@ class TestQuantumv2(test.TestCase): if port_id: self.moxed_client.show_port(port_id).AndReturn( {'port': {'id': 'my_portid1', - 'network_id': 'my_netid1'}}) + 'network_id': 'my_netid1', + 'mac_address': 'my_mac1'}}) ports['my_netid1'] = self.port_data1[0] id = 'my_netid1' else: @@ -368,6 +365,9 @@ class TestQuantumv2(test.TestCase): expected_network_order = req_net_ids else: expected_network_order = [n['id'] for n in nets] + if kwargs.get('_break_list_networks'): + self.mox.ReplayAll() + return api search_ids = [net['id'] for net in nets if net['id'] in req_net_ids] mox_list_network_params = dict(tenant_id=self.instance['project_id'], @@ -409,7 +409,15 @@ class TestQuantumv2(test.TestCase): res_port = {'port': {'id': 'fake'}} self.moxed_client.create_port( MyComparator(port_req_body)).AndReturn(res_port) + + api.get_instance_nw_info(mox.IgnoreArg(), + self.instance, + networks=nets).AndReturn(None) self.mox.ReplayAll() + return api + + def _allocate_for_instance(self, net_idx=1, **kwargs): + api = self._stub_allocate_for_instance(net_idx, **kwargs) api.allocate_for_instance(self.context, self.instance, **kwargs) def test_allocate_for_instance_1(self): @@ -428,6 +436,18 @@ class TestQuantumv2(test.TestCase): # The macs kwarg should be accepted, as a set. self._allocate_for_instance(1, macs=set(['ab:cd:ef:01:23:45'])) + 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) + self.assertRaises(exception.PortNotUsable, + api.allocate_for_instance, self.context, + self.instance, requested_networks=requested_networks, + macs=set(['unknown:mac'])) + def test_allocate_for_instance_with_requested_networks(self): # specify only first and last network requested_networks = [ @@ -443,7 +463,6 @@ class TestQuantumv2(test.TestCase): requested_networks=requested_networks) def test_allocate_for_instance_with_requested_networks_with_port(self): - # specify only first and last network requested_networks = [(None, None, 'myportid1')] self._allocate_for_instance(net_idx=1, requested_networks=requested_networks) 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 de0745654..0abf16801 100644 --- a/nova/tests/test_libvirt.py +++ b/nova/tests/test_libvirt.py @@ -769,6 +769,150 @@ class LibvirtConnTestCase(test.TestCase): vconfig.LibvirtConfigGuestDisk) self.assertEquals(cfg.devices[3].target_dev, 'vdd') + def test_get_guest_config_with_vnc(self): + self.flags(libvirt_type='kvm', + vnc_enabled=True, + use_usb_tablet=False) + self.flags(enabled=False, group='spice') + + conn = libvirt_driver.LibvirtDriver(fake.FakeVirtAPI(), True) + instance_ref = db.instance_create(self.context, self.test_instance) + + cfg = conn.get_guest_config(instance_ref, [], None, None) + self.assertEquals(len(cfg.devices), 5) + self.assertEquals(type(cfg.devices[0]), + vconfig.LibvirtConfigGuestDisk) + self.assertEquals(type(cfg.devices[1]), + vconfig.LibvirtConfigGuestDisk) + self.assertEquals(type(cfg.devices[2]), + vconfig.LibvirtConfigGuestSerial) + self.assertEquals(type(cfg.devices[3]), + vconfig.LibvirtConfigGuestSerial) + self.assertEquals(type(cfg.devices[4]), + vconfig.LibvirtConfigGuestGraphics) + + self.assertEquals(cfg.devices[4].type, "vnc") + + def test_get_guest_config_with_vnc_and_tablet(self): + self.flags(libvirt_type='kvm', + vnc_enabled=True, + use_usb_tablet=True) + self.flags(enabled=False, group='spice') + + conn = libvirt_driver.LibvirtDriver(fake.FakeVirtAPI(), True) + instance_ref = db.instance_create(self.context, self.test_instance) + + cfg = conn.get_guest_config(instance_ref, [], None, None) + self.assertEquals(len(cfg.devices), 6) + self.assertEquals(type(cfg.devices[0]), + vconfig.LibvirtConfigGuestDisk) + self.assertEquals(type(cfg.devices[1]), + vconfig.LibvirtConfigGuestDisk) + self.assertEquals(type(cfg.devices[2]), + vconfig.LibvirtConfigGuestSerial) + self.assertEquals(type(cfg.devices[3]), + vconfig.LibvirtConfigGuestSerial) + self.assertEquals(type(cfg.devices[4]), + vconfig.LibvirtConfigGuestInput) + self.assertEquals(type(cfg.devices[5]), + vconfig.LibvirtConfigGuestGraphics) + + self.assertEquals(cfg.devices[4].type, "tablet") + self.assertEquals(cfg.devices[5].type, "vnc") + + def test_get_guest_config_with_spice_and_tablet(self): + self.flags(libvirt_type='kvm', + vnc_enabled=False, + use_usb_tablet=True) + self.flags(enabled=True, + agent_enabled=False, + group='spice') + + conn = libvirt_driver.LibvirtDriver(fake.FakeVirtAPI(), True) + instance_ref = db.instance_create(self.context, self.test_instance) + + cfg = conn.get_guest_config(instance_ref, [], None, None) + self.assertEquals(len(cfg.devices), 6) + self.assertEquals(type(cfg.devices[0]), + vconfig.LibvirtConfigGuestDisk) + self.assertEquals(type(cfg.devices[1]), + vconfig.LibvirtConfigGuestDisk) + self.assertEquals(type(cfg.devices[2]), + vconfig.LibvirtConfigGuestSerial) + self.assertEquals(type(cfg.devices[3]), + vconfig.LibvirtConfigGuestSerial) + self.assertEquals(type(cfg.devices[4]), + vconfig.LibvirtConfigGuestInput) + self.assertEquals(type(cfg.devices[5]), + vconfig.LibvirtConfigGuestGraphics) + + self.assertEquals(cfg.devices[4].type, "tablet") + self.assertEquals(cfg.devices[5].type, "spice") + + def test_get_guest_config_with_spice_and_agent(self): + self.flags(libvirt_type='kvm', + vnc_enabled=False, + use_usb_tablet=True) + self.flags(enabled=True, + agent_enabled=True, + group='spice') + + conn = libvirt_driver.LibvirtDriver(fake.FakeVirtAPI(), True) + instance_ref = db.instance_create(self.context, self.test_instance) + + cfg = conn.get_guest_config(instance_ref, [], None, None) + self.assertEquals(len(cfg.devices), 6) + self.assertEquals(type(cfg.devices[0]), + vconfig.LibvirtConfigGuestDisk) + self.assertEquals(type(cfg.devices[1]), + vconfig.LibvirtConfigGuestDisk) + self.assertEquals(type(cfg.devices[2]), + vconfig.LibvirtConfigGuestSerial) + self.assertEquals(type(cfg.devices[3]), + vconfig.LibvirtConfigGuestSerial) + self.assertEquals(type(cfg.devices[4]), + vconfig.LibvirtConfigGuestChannel) + self.assertEquals(type(cfg.devices[5]), + vconfig.LibvirtConfigGuestGraphics) + + self.assertEquals(cfg.devices[4].target_name, "com.redhat.spice.0") + self.assertEquals(cfg.devices[5].type, "spice") + + def test_get_guest_config_with_vnc_and_spice(self): + self.flags(libvirt_type='kvm', + vnc_enabled=True, + use_usb_tablet=True) + self.flags(enabled=True, + agent_enabled=True, + group='spice') + + conn = libvirt_driver.LibvirtDriver(fake.FakeVirtAPI(), True) + instance_ref = db.instance_create(self.context, self.test_instance) + + cfg = conn.get_guest_config(instance_ref, [], None, None) + self.assertEquals(len(cfg.devices), 8) + self.assertEquals(type(cfg.devices[0]), + vconfig.LibvirtConfigGuestDisk) + self.assertEquals(type(cfg.devices[1]), + vconfig.LibvirtConfigGuestDisk) + self.assertEquals(type(cfg.devices[2]), + vconfig.LibvirtConfigGuestSerial) + self.assertEquals(type(cfg.devices[3]), + vconfig.LibvirtConfigGuestSerial) + self.assertEquals(type(cfg.devices[4]), + vconfig.LibvirtConfigGuestInput) + self.assertEquals(type(cfg.devices[5]), + vconfig.LibvirtConfigGuestChannel) + self.assertEquals(type(cfg.devices[6]), + vconfig.LibvirtConfigGuestGraphics) + self.assertEquals(type(cfg.devices[7]), + vconfig.LibvirtConfigGuestGraphics) + + self.assertEquals(cfg.devices[4].type, "tablet") + self.assertEquals(cfg.devices[5].target_name, "com.redhat.spice.0") + self.assertEquals(cfg.devices[6].type, "vnc") + self.assertEquals(cfg.devices[7].type, "spice") + def test_get_guest_cpu_config_none(self): self.flags(libvirt_cpu_mode="none") conn = libvirt_driver.LibvirtDriver(fake.FakeVirtAPI(), True) diff --git a/nova/tests/test_libvirt_config.py b/nova/tests/test_libvirt_config.py index 5eafba841..56719de11 100644 --- a/nova/tests/test_libvirt_config.py +++ b/nova/tests/test_libvirt_config.py @@ -539,6 +539,29 @@ class LibvirtConfigGuestConsoleTest(LibvirtConfigBaseTest): <console type="pty"/>""") +class LibvirtConfigGuestChannelTest(LibvirtConfigBaseTest): + def test_config_spice_minimal(self): + obj = config.LibvirtConfigGuestChannel() + obj.type = "spicevmc" + + xml = obj.to_xml() + self.assertXmlEqual(xml, """ + <channel type="spicevmc"> + <target type='virtio'/> + </channel>""") + + def test_config_spice_full(self): + obj = config.LibvirtConfigGuestChannel() + obj.type = "spicevmc" + obj.target_name = "com.redhat.spice.0" + + xml = obj.to_xml() + self.assertXmlEqual(xml, """ + <channel type="spicevmc"> + <target type='virtio' name='com.redhat.spice.0'/> + </channel>""") + + class LibvirtConfigGuestInterfaceTest(LibvirtConfigBaseTest): def test_config_ethernet(self): obj = config.LibvirtConfigGuestInterface() diff --git a/nova/tests/test_migrations.py b/nova/tests/test_migrations.py index 750326592..abd04a641 100644 --- a/nova/tests/test_migrations.py +++ b/nova/tests/test_migrations.py @@ -42,37 +42,48 @@ from nova import test LOG = logging.getLogger(__name__) -def _mysql_get_connect_string(user="openstack_citest", - passwd="openstack_citest", - database="openstack_citest"): +def _get_connect_string(backend, + user="openstack_citest", + passwd="openstack_citest", + database="openstack_citest"): """ Try to get a connection with a very specfic set of values, if we get - these then we'll run the mysql tests, otherwise they are skipped + these then we'll run the tests, otherwise they are skipped """ - return "mysql://%(user)s:%(passwd)s@localhost/%(database)s" % locals() + if backend == "postgres": + backend = "postgresql+psycopg2" + return ("%(backend)s://%(user)s:%(passwd)s@localhost/%(database)s" + % locals()) -def _is_mysql_avail(user="openstack_citest", - passwd="openstack_citest", - database="openstack_citest"): + +def _is_backend_avail(backend, + user="openstack_citest", + passwd="openstack_citest", + database="openstack_citest"): try: - connect_uri = _mysql_get_connect_string( - user=user, passwd=passwd, database=database) + if backend == "mysql": + connect_uri = _get_connect_string("mysql", + user=user, passwd=passwd, database=database) + elif backend == "postgres": + connect_uri = _get_connect_string("postgres", + user=user, passwd=passwd, database=database) engine = sqlalchemy.create_engine(connect_uri) connection = engine.connect() except Exception: # intentionally catch all to handle exceptions even if we don't - # have mysql code loaded at all. + # have any backend code loaded. return False else: connection.close() + engine.dispose() return True def _have_mysql(): present = os.environ.get('NOVA_TEST_MYSQL_PRESENT') if present is None: - return _is_mysql_avail() + return _is_backend_avail('mysql') return present.lower() in ('', 'true') @@ -121,7 +132,6 @@ class TestMigrations(test.TestCase): self._reset_databases() def tearDown(self): - # We destroy the test data store between each test case, # and recreate it, which ensures that we have no side-effects # from the tests @@ -142,6 +152,7 @@ class TestMigrations(test.TestCase): for key, engine in self.engines.items(): conn_string = self.test_databases[key] conn_pieces = urlparse.urlparse(conn_string) + engine.dispose() if conn_string.startswith('sqlite'): # We can just delete the SQLite database, which is # the easiest and cleanest solution @@ -172,6 +183,7 @@ class TestMigrations(test.TestCase): database = conn_pieces.path.strip('/') loc_pieces = conn_pieces.netloc.split('@') host = loc_pieces[1] + auth_pieces = loc_pieces[0].split(':') user = auth_pieces[0] password = "" @@ -207,16 +219,16 @@ class TestMigrations(test.TestCase): Test that we can trigger a mysql connection failure and we fail gracefully to ensure we don't break people without mysql """ - if _is_mysql_avail(user="openstack_cifail"): + if _is_backend_avail('mysql', user="openstack_cifail"): self.fail("Shouldn't have connected") def test_mysql_innodb(self): # Test that table creation on mysql only builds InnoDB tables - if not _have_mysql(): + if not _is_backend_avail('mysql'): self.skipTest("mysql not available") # add this to the global lists to make reset work with it, it's removed # automatically in tearDown so no need to clean it up here. - connect_string = _mysql_get_connect_string() + connect_string = _get_connect_string("mysql") engine = sqlalchemy.create_engine(connect_string) self.engines["mysqlcitest"] = engine self.test_databases["mysqlcitest"] = connect_string @@ -225,7 +237,7 @@ class TestMigrations(test.TestCase): self._reset_databases() self._walk_versions(engine, False, False) - uri = _mysql_get_connect_string(database="information_schema") + uri = _get_connect_string("mysql", database="information_schema") connection = sqlalchemy.create_engine(uri).connect() # sanity check @@ -242,6 +254,99 @@ class TestMigrations(test.TestCase): count = noninnodb.scalar() self.assertEqual(count, 0, "%d non InnoDB tables created" % count) + def test_migration_149_postgres(self): + """Test updating a table with IPAddress columns.""" + if not _is_backend_avail('postgres'): + self.skipTest("postgres not available") + + connect_string = _get_connect_string("postgres") + engine = sqlalchemy.create_engine(connect_string) + + self.engines["postgrescitest"] = engine + self.test_databases["postgrescitest"] = connect_string + + self._reset_databases() + migration_api.version_control(engine, TestMigrations.REPOSITORY, + migration.INIT_VERSION) + + connection = engine.connect() + + self._migrate_up(engine, 148) + IPS = ("127.0.0.1", "255.255.255.255", "2001:db8::1:2", "::1") + connection.execute("INSERT INTO provider_fw_rules " + " (protocol, from_port, to_port, cidr)" + "VALUES ('tcp', 1234, 1234, '%s'), " + " ('tcp', 1234, 1234, '%s'), " + " ('tcp', 1234, 1234, '%s'), " + " ('tcp', 1234, 1234, '%s')" % IPS) + self.assertEqual('character varying', + connection.execute( + "SELECT data_type FROM INFORMATION_SCHEMA.COLUMNS " + "WHERE table_name='provider_fw_rules' " + "AND table_catalog='openstack_citest' " + "AND column_name='cidr'").scalar()) + + self._migrate_up(engine, 149) + self.assertEqual(IPS, + tuple(tup[0] for tup in connection.execute( + "SELECT cidr from provider_fw_rules").fetchall())) + self.assertEqual('inet', + connection.execute( + "SELECT data_type FROM INFORMATION_SCHEMA.COLUMNS " + "WHERE table_name='provider_fw_rules' " + "AND table_catalog='openstack_citest' " + "AND column_name='cidr'").scalar()) + connection.close() + + def test_migration_149_mysql(self): + """Test updating a table with IPAddress columns.""" + if not _have_mysql(): + self.skipTest("mysql not available") + + connect_string = _get_connect_string("mysql") + engine = sqlalchemy.create_engine(connect_string) + self.engines["mysqlcitest"] = engine + self.test_databases["mysqlcitest"] = connect_string + + self._reset_databases() + migration_api.version_control(engine, TestMigrations.REPOSITORY, + migration.INIT_VERSION) + + uri = _get_connect_string("mysql", database="openstack_citest") + connection = sqlalchemy.create_engine(uri).connect() + + self._migrate_up(engine, 148) + + IPS = ("127.0.0.1", "255.255.255.255", "2001:db8::1:2", "::1") + connection.execute("INSERT INTO provider_fw_rules " + " (protocol, from_port, to_port, cidr)" + "VALUES ('tcp', 1234, 1234, '%s'), " + " ('tcp', 1234, 1234, '%s'), " + " ('tcp', 1234, 1234, '%s'), " + " ('tcp', 1234, 1234, '%s')" % IPS) + self.assertEqual('varchar(255)', + connection.execute( + "SELECT column_type FROM INFORMATION_SCHEMA.COLUMNS " + "WHERE table_name='provider_fw_rules' " + "AND table_schema='openstack_citest' " + "AND column_name='cidr'").scalar()) + + connection.close() + + self._migrate_up(engine, 149) + + connection = sqlalchemy.create_engine(uri).connect() + + self.assertEqual(IPS, + tuple(tup[0] for tup in connection.execute( + "SELECT cidr from provider_fw_rules").fetchall())) + self.assertEqual('varchar(39)', + connection.execute( + "SELECT column_type FROM INFORMATION_SCHEMA.COLUMNS " + "WHERE table_name='provider_fw_rules' " + "AND table_schema='openstack_citest' " + "AND column_name='cidr'").scalar()) + def _walk_versions(self, engine=None, snake_walk=False, downgrade=True): # Determine latest version script from the repo, then # upgrade from 1 through to the latest, with no data diff --git a/nova/tests/test_service.py b/nova/tests/test_service.py index 4873714f3..71beed51e 100644 --- a/nova/tests/test_service.py +++ b/nova/tests/test_service.py @@ -112,7 +112,6 @@ class ServiceTestCase(test.TestCase): self.host = 'foo' self.binary = 'nova-fake' self.topic = 'fake' - self.mox.StubOutWithMock(service, 'db') self.mox.StubOutWithMock(db, 'service_create') self.mox.StubOutWithMock(db, 'service_get_by_args') self.flags(use_local=True, group='conductor') 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/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/libvirt/config.py b/nova/virt/libvirt/config.py index 6785c8823..ed5b21c79 100644 --- a/nova/virt/libvirt/config.py +++ b/nova/virt/libvirt/config.py @@ -648,21 +648,34 @@ class LibvirtConfigGuestGraphics(LibvirtConfigGuestDevice): return dev -class LibvirtConfigGuestChar(LibvirtConfigGuestDevice): +class LibvirtConfigGuestCharBase(LibvirtConfigGuestDevice): def __init__(self, **kwargs): - super(LibvirtConfigGuestChar, self).__init__(**kwargs) + super(LibvirtConfigGuestCharBase, self).__init__(**kwargs) self.type = "pty" self.source_path = None - self.target_port = None def format_dom(self): - dev = super(LibvirtConfigGuestChar, self).format_dom() + dev = super(LibvirtConfigGuestCharBase, self).format_dom() dev.set("type", self.type) if self.type == "file": dev.append(etree.Element("source", path=self.source_path)) + + return dev + + +class LibvirtConfigGuestChar(LibvirtConfigGuestCharBase): + + def __init__(self, **kwargs): + super(LibvirtConfigGuestChar, self).__init__(**kwargs) + + self.target_port = None + + def format_dom(self): + dev = super(LibvirtConfigGuestChar, self).format_dom() + if self.target_port is not None: dev.append(etree.Element("target", port=str(self.target_port))) @@ -683,6 +696,26 @@ class LibvirtConfigGuestConsole(LibvirtConfigGuestChar): **kwargs) +class LibvirtConfigGuestChannel(LibvirtConfigGuestCharBase): + + def __init__(self, **kwargs): + super(LibvirtConfigGuestChannel, self).__init__(root_name="channel", + **kwargs) + + self.target_type = "virtio" + self.target_name = None + + def format_dom(self): + dev = super(LibvirtConfigGuestChannel, self).format_dom() + + target = etree.Element("target", type=self.target_type) + if self.target_name is not None: + target.set("name", self.target_name) + dev.append(target) + + return dev + + class LibvirtConfigGuest(LibvirtConfigObject): def __init__(self, **kwargs): diff --git a/nova/virt/libvirt/driver.py b/nova/virt/libvirt/driver.py index 4312086a8..a10dc6f2f 100644 --- a/nova/virt/libvirt/driver.py +++ b/nova/virt/libvirt/driver.py @@ -196,6 +196,7 @@ CONF.import_opt('default_ephemeral_format', 'nova.virt.driver') CONF.import_opt('use_cow_images', 'nova.virt.driver') CONF.import_opt('live_migration_retry_count', 'nova.compute.manager') CONF.import_opt('vncserver_proxyclient_address', 'nova.vnc') +CONF.import_opt('server_proxyclient_address', 'nova.spice', group='spice') DEFAULT_FIREWALL_DRIVER = "%s.%s" % ( libvirt_firewall.__name__, @@ -1145,6 +1146,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): @@ -1786,19 +1808,49 @@ class LibvirtDriver(driver.ComputeDriver): consolepty.type = "pty" guest.add_device(consolepty) - if CONF.vnc_enabled and CONF.libvirt_type not in ('lxc', 'uml'): - if CONF.use_usb_tablet and guest.os_type == vm_mode.HVM: - tablet = vconfig.LibvirtConfigGuestInput() - tablet.type = "tablet" - tablet.bus = "usb" - guest.add_device(tablet) + # We want a tablet if VNC is enabled, + # or SPICE is enabled and the SPICE agent is disabled + # NB: this implies that if both SPICE + VNC are enabled + # at the same time, we'll get the tablet whether the + # SPICE agent is used or not. + need_usb_tablet = False + if CONF.vnc_enabled: + need_usb_tablet = CONF.use_usb_tablet + elif CONF.spice.enabled and not CONF.spice.agent_enabled: + need_usb_tablet = CONF.use_usb_tablet + + if need_usb_tablet and guest.os_type == vm_mode.HVM: + tablet = vconfig.LibvirtConfigGuestInput() + tablet.type = "tablet" + tablet.bus = "usb" + guest.add_device(tablet) + + if CONF.spice.enabled and CONF.spice.agent_enabled and \ + CONF.libvirt_type not in ('lxc', 'uml', 'xen'): + channel = vconfig.LibvirtConfigGuestChannel() + channel.target_name = "com.redhat.spice.0" + guest.add_device(channel) + + # NB some versions of libvirt support both SPICE and VNC + # at the same time. We're not trying to second guess which + # those versions are. We'll just let libvirt report the + # errors appropriately if the user enables both. + if CONF.vnc_enabled and CONF.libvirt_type not in ('lxc', 'uml'): graphics = vconfig.LibvirtConfigGuestGraphics() graphics.type = "vnc" graphics.keymap = CONF.vnc_keymap graphics.listen = CONF.vncserver_listen guest.add_device(graphics) + if CONF.spice.enabled and \ + CONF.libvirt_type not in ('lxc', 'uml', 'xen'): + graphics = vconfig.LibvirtConfigGuestGraphics() + graphics.type = "spice" + graphics.keymap = CONF.spice.keymap + graphics.listen = CONF.spice.server_listen + guest.add_device(graphics) + return guest def to_xml(self, instance, network_info, image_meta=None, rescue=None, 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/volume/cinder.py b/nova/volume/cinder.py index 514295605..fccdedac8 100644 --- a/nova/volume/cinder.py +++ b/nova/volume/cinder.py @@ -42,6 +42,9 @@ cinder_opts = [ default=None, help='Override service catalog lookup with template for cinder ' 'endpoint e.g. http://localhost:8776/v1/%(project_id)s'), + cfg.StrOpt('os_region_name', + default=None, + help='region name of this node'), cfg.IntOpt('cinder_http_retries', default=3, help='Number of cinderclient retries on failed http calls'), @@ -66,7 +69,16 @@ def cinderclient(context): else: info = CONF.cinder_catalog_info service_type, service_name, endpoint_type = info.split(':') - url = sc.url_for(service_type=service_type, + # extract the region if set in configuration + if CONF.os_region_name: + attr = 'region' + filter_value = CONF.os_region_name + else: + attr = None + filter_value = None + url = sc.url_for(attr=attr, + filter_value=filter_value, + service_type=service_type, service_name=service_name, endpoint_type=endpoint_type) diff --git a/nova/wsgi.py b/nova/wsgi.py index c103526da..16851dba8 100644 --- a/nova/wsgi.py +++ b/nova/wsgi.py @@ -83,13 +83,21 @@ class Server(object): raise exception.InvalidInput( reason='The backlog must be more than 1') + bind_addr = (host, port) + # TODO(dims): eventlet's green dns/socket module does not actually + # support IPv6 in getaddrinfo(). We need to get around this in the + # future or monitor upstream for a fix try: - socket.inet_pton(socket.AF_INET6, host) - family = socket.AF_INET6 + info = socket.getaddrinfo(bind_addr[0], + bind_addr[1], + socket.AF_UNSPEC, + socket.SOCK_STREAM)[0] + family = info[0] + bind_addr = info[-1] except Exception: family = socket.AF_INET - self._socket = eventlet.listen((host, port), family, backlog=backlog) + self._socket = eventlet.listen(bind_addr, family, backlog=backlog) (self.host, self.port) = self._socket.getsockname()[0:2] LOG.info(_("%(name)s listening on %(host)s:%(port)s") % self.__dict__) diff --git a/run_tests.sh b/run_tests.sh index 1a54c1bef..3a579ca36 100755 --- a/run_tests.sh +++ b/run_tests.sh @@ -123,6 +123,12 @@ function run_pep8 { # Until all these issues get fixed, ignore. ignore='--ignore=E12,E711,E721,E712' + # First run the hacking selftest, to make sure it's right + echo "Running hacking.py self test" + ${wrapper} python tools/hacking.py --doctest + + # Then actually run it + echo "Running pep8" ${wrapper} python tools/hacking.py ${ignore} ${srcfiles} # NOTE(sdague): as of grizzly-2 these are passing however leaving the comment @@ -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 7322fd071..ed22956eb 100755 --- a/tools/hacking.py +++ b/tools/hacking.py @@ -110,66 +110,82 @@ def nova_todo_format(physical_line): nova HACKING guide recommendation for TODO: Include your name with TODOs as in "#TODO(termie)" - N101 + + Okay: #TODO(sdague) + N101: #TODO fail """ + # TODO(sdague): TODO check shouldn't fail inside of space pos = physical_line.find('TODO') pos1 = physical_line.find('TODO(') pos2 = physical_line.find('#') # make sure it's a comment - if (pos != pos1 and pos2 >= 0 and pos2 < pos): - return pos, "NOVA N101: Use TODO(NAME)" + # 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): + return pos, "N101: Use TODO(NAME)" def nova_except_format(logical_line): - """Check for 'except:'. + r"""Check for 'except:'. nova HACKING guide recommends not using except: Do not write "except:", use "except Exception:" at the very least - N201 + + Okay: except Exception: + N201: except: """ if logical_line.startswith("except:"): - yield 6, "NOVA N201: no 'except:' at least use 'except Exception:'" + yield 6, "N201: no 'except:' at least use 'except Exception:'" def nova_except_format_assert(logical_line): - """Check for 'assertRaises(Exception'. + r"""Check for 'assertRaises(Exception'. nova HACKING guide recommends not using assertRaises(Exception...): Do not use overly broad Exception type - N202 + + Okay: self.assertRaises(NovaException) + N202: self.assertRaises(Exception) """ if logical_line.startswith("self.assertRaises(Exception"): - yield 1, "NOVA N202: assertRaises Exception too broad" + yield 1, "N202: assertRaises Exception too broad" def nova_one_import_per_line(logical_line): - """Check for import format. + r"""Check for import format. nova HACKING guide recommends one import per line: Do not import more than one module per line Examples: - BAD: from nova.rpc.common import RemoteError, LOG - N301 + Okay: from nova.rpc.common import RemoteError + N301: from nova.rpc.common import RemoteError, LOG """ pos = logical_line.find(',') parts = logical_line.split() if (pos > -1 and (parts[0] == "import" or parts[0] == "from" and parts[2] == "import") and not is_import_exception(parts[1])): - yield pos, "NOVA N301: one import per line" + yield pos, "N301: one import per line" _missingImport = set([]) def nova_import_module_only(logical_line): - """Check for import module only. + r"""Check for import module only. nova HACKING guide recommends importing only modules: Do not import objects, only modules - N302 import only modules - N303 Invalid Import - N304 Relative Import + + Okay: from os import path + N302 from os.path import mkdir as mkdir2 + N303 import bubba + N304 import blueblue """ + # N302 import only modules + # N303 Invalid Import + # N304 Relative Import + + # TODO(sdague) actually get these tests working def importModuleCheck(mod, parent=None, added=False): """ If can't find module on first try, recursively check for relative @@ -193,10 +209,10 @@ def nova_import_module_only(logical_line): if added: sys.path.pop() added = False - return logical_line.find(mod), ("NOVA N304: No " + return logical_line.find(mod), ("N304: No " "relative imports. '%s' is a relative import" % logical_line) - return logical_line.find(mod), ("NOVA N302: import only " + return logical_line.find(mod), ("N302: import only " "modules. '%s' does not import a module" % logical_line) @@ -219,7 +235,7 @@ def nova_import_module_only(logical_line): except AttributeError: # Invalid import - return logical_line.find(mod), ("NOVA N303: Invalid import, " + return logical_line.find(mod), ("N303: Invalid import, " "AttributeError raised") # convert "from x import y" to " import x.y" @@ -240,41 +256,58 @@ def nova_import_module_only(logical_line): #TODO(jogo): import template: N305 -def nova_import_alphabetical(logical_line, line_number, lines): - """Check for imports in alphabetical order. +def nova_import_alphabetical(logical_line, blank_lines, previous_logical, + indent_level, previous_indent_level): + r""" + Check for imports in alphabetical order. nova HACKING guide recommendation for imports: imports in human alphabetical order - N306 + + Okay: import os\nimport sys\n\nimport nova\nfrom nova import test + N306: import sys\nimport os """ # handle import x # use .lower since capitalization shouldn't dictate order split_line = import_normalize(logical_line.strip()).lower().split() - split_previous = import_normalize(lines[line_number - 2] - ).strip().lower().split() - # with or without "as y" - length = [2, 4] - if (len(split_line) in length and len(split_previous) in length and - split_line[0] == "import" and split_previous[0] == "import"): - if split_line[1] < split_previous[1]: - yield (0, "NOVA N306: imports not in alphabetical order (%s, %s)" - % (split_previous[1], split_line[1])) + split_previous = import_normalize(previous_logical.strip()).lower().split() + + if blank_lines < 1 and indent_level == previous_indent_level: + length = [2, 4] + if (len(split_line) in length and len(split_previous) in length and + split_line[0] == "import" and split_previous[0] == "import"): + if split_line[1] < split_previous[1]: + yield (0, "N306: imports not in alphabetical order (%s, %s)" + % (split_previous[1], split_line[1])) def nova_import_no_db_in_virt(logical_line, filename): - if ("nova/virt" in filename and - not filename.endswith("fake.py") and - "nova import db" in logical_line): - yield (0, "NOVA N307: nova.db import not allowed in nova/virt/*") + """Check for db calls from nova/virt + + As of grizzly-2 all the database calls have been removed from + nova/virt, and we want to keep it that way. + + N307 + """ + if "nova/virt" in filename and not filename.endswith("fake.py"): + if logical_line.startswith("from nova import db"): + yield (0, "N307: nova.db import not allowed in nova/virt/*") def nova_docstring_start_space(physical_line, previous_logical): - """Check for docstring not start with space. + r"""Check for docstring not start with space. nova HACKING guide recommendation for docstring: Docstring should not start with space - N401 + + Okay: def foo():\n '''This is good.''' + N401: def foo():\n ''' This is not.''' """ + # short circuit so that we don't fail on our own fail test + # when running under external pep8 + if physical_line.find("N401: def foo()") != -1: + return + # 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 @@ -283,35 +316,47 @@ def nova_docstring_start_space(physical_line, previous_logical): 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] == ' '): - return (pos, "NOVA N401: docstring should not start with" + return (pos, "N401: docstring should not start with" " a space") def nova_docstring_one_line(physical_line): - """Check one line docstring end. + r"""Check one line docstring end. nova HACKING guide recommendation for one line docstring: - A one line docstring looks like this and ends in a period. - N402 + A one line docstring looks like this and ends in punctuation. + + Okay: '''This is good.''' + N402: '''This is not''' + N402: '''Bad punctuation,''' """ - pos = max([physical_line.find(i) for i in DOCSTRING_TRIPLE]) # start - end = max([physical_line[-4:-1] == i for i in DOCSTRING_TRIPLE]) # end - if (pos != -1 and end and len(physical_line) > pos + 4): - if (physical_line[-5] not in ['.', '?', '!']): - return pos, "NOVA N402: one line docstring needs a period" + 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 + + 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): - """Check multi line docstring end. + r"""Check multi line docstring end. nova HACKING guide recommendation for docstring: Docstring should end on a new line - N403 + 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 """ + # 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, "NOVA N403: multi line docstring end on new line") + return (pos, "N403: multi line docstring end on new line") FORMAT_RE = re.compile("%(?:" @@ -339,6 +384,7 @@ def check_i18n(): token_type, text, _, _, line = yield except GeneratorExit: return + if (token_type == tokenize.NAME and text == "_" and not line.startswith('def _(msg):')): @@ -361,22 +407,22 @@ def check_i18n(): if not format_string: raise LocalizationError(start, - "NOVA N701: Empty localization string") + "N701: Empty localization string") if token_type != tokenize.OP: raise LocalizationError(start, - "NOVA N701: Invalid localization call") + "N701: Invalid localization call") if text != ")": if text == "%": raise LocalizationError(start, - "NOVA N702: Formatting operation should be outside" + "N702: Formatting operation should be outside" " of localization method call") elif text == "+": raise LocalizationError(start, - "NOVA N702: Use bare string concatenation instead" + "N702: Use bare string concatenation instead" " of +") else: raise LocalizationError(start, - "NOVA N702: Argument to _ must be just a string") + "N702: Argument to _ must be just a string") format_specs = FORMAT_RE.findall(format_string) positional_specs = [(key, spec) for key, spec in format_specs @@ -384,17 +430,21 @@ def check_i18n(): # not spec means %%, key means %(smth)s if len(positional_specs) > 1: raise LocalizationError(start, - "NOVA N703: Multiple positional placeholders") + "N703: Multiple positional placeholders") def nova_localization_strings(logical_line, tokens): - """Check localization in line. - - N701: bad localization call - N702: complex expression instead of string as argument to _() - N703: multiple positional placeholders + r"""Check localization in line. + + Okay: _("This is fine") + Okay: _("This is also fine %s") + N701: _('') + N702: _("Bob" + " foo") + N702: _("Bob %s" % foo) + # N703 check is not quite right, disabled by removing colon + N703 _("%s %s" % (foo, bar)) """ - + # TODO(sdague) actually get these tests working gen = check_i18n() next(gen) try: @@ -466,18 +516,36 @@ def once_git_check_commit_title(): error = True return error +imports_on_separate_lines_N301_compliant = r""" + Imports should usually be on separate lines. + + Okay: import os\nimport sys + E401: import sys, os + + N301: from subprocess import Popen, PIPE + Okay: from myclas import MyClass + Okay: from foo.bar.yourclass import YourClass + Okay: import myclass + Okay: import foo.bar.yourclass + """ + if __name__ == "__main__": #include nova path sys.path.append(os.getcwd()) #Run once tests (not per line) once_error = once_git_check_commit_title() #NOVA error codes start with an N + pep8.SELFTEST_REGEX = re.compile(r'(Okay|[EWN]\d{3}):\s(.*)') pep8.ERRORCODE_REGEX = re.compile(r'[EWN]\d{3}') add_nova() pep8.current_file = current_file pep8.readlines = readlines pep8.StyleGuide.excluded = excluded pep8.StyleGuide.input_dir = input_dir + # we need to kill this doctring otherwise the self tests fail + pep8.imports_on_separate_lines.__doc__ = \ + imports_on_separate_lines_N301_compliant + try: pep8._main() sys.exit(once_error) 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 6ee42d31c..5f195d5c1 100644 --- a/tools/test-requires +++ b/tools/test-requires @@ -11,5 +11,5 @@ pep8==1.3.3 pylint==0.25.2 python-subunit sphinx>=1.1.2 -testrepository>=0.0.8 +testrepository>=0.0.13 testtools>=0.9.22 @@ -8,8 +8,7 @@ setenv = VIRTUAL_ENV={envdir} LC_ALL=C deps = -r{toxinidir}/tools/pip-requires -r{toxinidir}/tools/test-requires -commands = bash -c 'if [ ! -d ./.testrepository ] ; then testr init ; fi' - bash -c 'testr run --parallel {posargs} ; RET=$? ; echo "Slowest Tests" ; testr slowest && exit $RET' +commands = python setup.py testr --slowest --testr-args='{posargs}' [tox:jenkins] sitepackages = True @@ -18,6 +17,7 @@ downloadcache = ~/cache/pip [testenv:pep8] deps=pep8==1.3.3 commands = + python tools/hacking.py --doctest python tools/hacking.py --ignore=E12,E711,E721,E712 --repeat --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 \ @@ -34,13 +34,11 @@ deps = pyflakes commands = python tools/flakes.py nova [testenv:cover] -# Need to omit DynamicallyCompiledCheetahTemplate.py from coverage because -# it ceases to exist post test run. Also do not run test_coverage_ext tests -# while gathering coverage as those tests conflict with coverage. -setenv = OMIT=--omit=DynamicallyCompiledCheetahTemplate.py - PYTHON=coverage run --source nova --parallel-mode -commands = bash -c 'if [ ! -d ./.testrepository ] ; then testr init ; fi' - bash -c 'testr run --parallel \^\(\?\!\.\*test_coverage_ext\)\.\*\$ ; RET=$? ; coverage combine ; coverage html -d ./cover $OMIT && exit $RET' +# Also do not run test_coverage_ext tests while gathering coverage as those +# tests conflict with coverage. +commands = + python setup.py testr --coverage \ + --testr-args='^(?!.*test_coverage_ext).*$' [testenv:venv] commands = {posargs} |