summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.coveragerc3
-rwxr-xr-xbin/nova-manage8
-rwxr-xr-xbin/nova-novncproxy96
-rwxr-xr-xbin/nova-spicehtml5proxy93
-rwxr-xr-xcontrib/xen/vif-openstack39
-rw-r--r--doc/api_samples/all_extensions/extensions-get-resp.json8
-rw-r--r--doc/api_samples/all_extensions/extensions-get-resp.xml6
-rw-r--r--doc/api_samples/os-cells/cells-get-resp.json9
-rw-r--r--doc/api_samples/os-cells/cells-get-resp.xml2
-rw-r--r--doc/api_samples/os-cells/cells-list-empty-resp.json3
-rw-r--r--doc/api_samples/os-cells/cells-list-empty-resp.xml2
-rw-r--r--doc/api_samples/os-cells/cells-list-resp.json39
-rw-r--r--doc/api_samples/os-cells/cells-list-resp.xml8
-rw-r--r--doc/api_samples/os-consoles/get-spice-console-post-req.json5
-rw-r--r--doc/api_samples/os-consoles/get-spice-console-post-req.xml2
-rw-r--r--doc/api_samples/os-consoles/get-spice-console-post-resp.json6
-rw-r--r--doc/api_samples/os-consoles/get-spice-console-post-resp.xml5
-rw-r--r--doc/api_samples/os-hosts/hosts-list-resp.json5
-rw-r--r--doc/api_samples/os-hosts/hosts-list-resp.xml3
-rw-r--r--doc/source/conf.py2
-rw-r--r--doc/source/devref/development.environment.rst2
-rw-r--r--doc/source/man/nova-spicehtml5proxy.rst48
-rw-r--r--etc/nova/nova.conf.sample30
-rw-r--r--etc/nova/policy.json1
-rw-r--r--nova/api/ec2/cloud.py18
-rw-r--r--nova/api/openstack/compute/contrib/admin_actions.py4
-rw-r--r--nova/api/openstack/compute/contrib/cells.py303
-rw-r--r--nova/api/openstack/compute/contrib/consoles.py25
-rw-r--r--nova/api/openstack/compute/views/servers.py4
-rw-r--r--nova/api/sizelimit.py2
-rw-r--r--nova/cells/manager.py13
-rw-r--r--nova/cells/messaging.py34
-rw-r--r--nova/cells/rpcapi.py19
-rw-r--r--nova/cells/state.py13
-rw-r--r--nova/compute/api.py37
-rw-r--r--nova/compute/cells_api.py59
-rw-r--r--nova/compute/manager.py34
-rw-r--r--nova/compute/resource_tracker.py25
-rw-r--r--nova/compute/rpcapi.py8
-rw-r--r--nova/conductor/api.py30
-rw-r--r--nova/conductor/manager.py27
-rw-r--r--nova/conductor/rpcapi.py25
-rw-r--r--nova/console/websocketproxy.py89
-rw-r--r--nova/crypto.py43
-rw-r--r--nova/db/api.py12
-rw-r--r--nova/db/sqlalchemy/api.py40
-rw-r--r--nova/db/sqlalchemy/migrate_repo/versions/149_inet_datatype_for_postgres.py70
-rw-r--r--nova/db/sqlalchemy/models.py39
-rw-r--r--nova/db/sqlalchemy/types.py26
-rw-r--r--nova/exception.py12
-rw-r--r--nova/network/manager.py7
-rw-r--r--nova/network/quantumv2/api.py7
-rw-r--r--nova/quota.py2
-rw-r--r--nova/service.py1
-rw-r--r--nova/spice/__init__.py47
-rw-r--r--nova/tests/api/ec2/test_cloud.py12
-rw-r--r--nova/tests/api/openstack/compute/contrib/test_admin_actions_with_cells.py89
-rw-r--r--nova/tests/api/openstack/compute/contrib/test_cells.py396
-rw-r--r--nova/tests/api/openstack/compute/contrib/test_consoles.py92
-rw-r--r--nova/tests/api/openstack/compute/test_servers.py4
-rw-r--r--nova/tests/cells/test_cells_manager.py26
-rw-r--r--nova/tests/cells/test_cells_messaging.py46
-rw-r--r--nova/tests/cells/test_cells_rpcapi.py20
-rw-r--r--nova/tests/compute/test_compute.py97
-rw-r--r--nova/tests/compute/test_multiple_nodes.py3
-rw-r--r--nova/tests/compute/test_rpcapi.py5
-rw-r--r--nova/tests/conductor/test_conductor.py61
-rw-r--r--nova/tests/fake_policy.py2
-rw-r--r--nova/tests/fakelibvirt.py1
-rw-r--r--nova/tests/integrated/api_samples/all_extensions/extensions-get-resp.json.tpl8
-rw-r--r--nova/tests/integrated/api_samples/all_extensions/extensions-get-resp.xml.tpl3
-rw-r--r--nova/tests/integrated/api_samples/os-cells/cells-get-resp.json.tpl9
-rw-r--r--nova/tests/integrated/api_samples/os-cells/cells-get-resp.xml.tpl2
-rw-r--r--nova/tests/integrated/api_samples/os-cells/cells-list-empty-resp.json.tpl4
-rw-r--r--nova/tests/integrated/api_samples/os-cells/cells-list-empty-resp.xml.tpl2
-rw-r--r--nova/tests/integrated/api_samples/os-cells/cells-list-resp.json.tpl39
-rw-r--r--nova/tests/integrated/api_samples/os-cells/cells-list-resp.xml.tpl8
-rw-r--r--nova/tests/integrated/api_samples/os-consoles/get-spice-console-post-req.json.tpl5
-rw-r--r--nova/tests/integrated/api_samples/os-consoles/get-spice-console-post-req.xml.tpl4
-rw-r--r--nova/tests/integrated/api_samples/os-consoles/get-spice-console-post-resp.json.tpl6
-rw-r--r--nova/tests/integrated/api_samples/os-consoles/get-spice-console-post-resp.xml.tpl5
-rw-r--r--nova/tests/integrated/api_samples/os-hosts/hosts-list-resp.json.tpl11
-rw-r--r--nova/tests/integrated/api_samples/os-hosts/hosts-list-resp.xml.tpl1
-rw-r--r--nova/tests/integrated/integrated_helpers.py5
-rw-r--r--nova/tests/integrated/test_api_samples.py79
-rw-r--r--nova/tests/network/test_quantumv2.py33
-rw-r--r--nova/tests/test_crypto.py63
-rw-r--r--nova/tests/test_fakelibvirt.py1
-rw-r--r--nova/tests/test_libvirt.py144
-rw-r--r--nova/tests/test_libvirt_config.py23
-rw-r--r--nova/tests/test_migrations.py139
-rw-r--r--nova/tests/test_service.py1
-rw-r--r--nova/tests/test_virt_drivers.py9
-rw-r--r--nova/virt/driver.py4
-rw-r--r--nova/virt/fake.py6
-rw-r--r--nova/virt/libvirt/config.py41
-rw-r--r--nova/virt/libvirt/driver.py64
-rw-r--r--nova/virt/libvirt/vif.py54
-rw-r--r--nova/volume/cinder.py14
-rw-r--r--nova/wsgi.py14
-rwxr-xr-xrun_tests.sh6
-rw-r--r--setup.py1
-rwxr-xr-xtools/hacking.py192
-rw-r--r--tools/pip-requires1
-rw-r--r--tools/test-requires2
-rw-r--r--tox.ini16
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
diff --git a/setup.py b/setup.py
index 12de5c4d6..78c9062c2 100644
--- a/setup.py
+++ b/setup.py
@@ -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
diff --git a/tox.ini b/tox.ini
index 1c43be4ed..e3322e044 100644
--- a/tox.ini
+++ b/tox.ini
@@ -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}