summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTim Simpson <tim.simpson@rackspace.com>2012-11-16 11:50:34 -0600
committerTim Simpson <tim.simpson@rackspace.com>2012-11-19 12:33:40 -0600
commitc007356a7884b5690742683cac1e5b78f191456b (patch)
tree54332a3e3a2ce76177fd48fe51eaa2fea49c6d24
parenta07002375217fcef9bf5c0844d284daeefc6cc25 (diff)
downloadtrove-c007356a7884b5690742683cac1e5b78f191456b.tar.gz
Adding tests to Reddwarf.
The tests come from the Reddwarf Integration repository. wsgi_intercept is used to allow the test code to hit the server code directly. It also properly sets up the SqlLite database before each run. * Adds an "event simulator" which queues up functions that would normally be spawned with eventlet. The various sleep functions are then swapped with a method that runs these faux-events. * Adds many of the Reddwarf Integration tests. The idea is these could still run in a richer, real environment, but are running here enables us to quickly check feature breaks for each commit and lowers the learning curve necessary to test the API. The downside is some of these tests still have artifacts of their origins, such as (unused) classes to connect to MySQL databases. Some more work will be necessary to separate the "real mode" functionality of these tests further. Implements: blueprint tox-tests Change-Id: I9857f265c1cb46832906ef5e6a0c7bb4a092e637
-rw-r--r--.coveragerc26
-rw-r--r--.gitignore1
-rwxr-xr-xbin/reddwarf-manage4
-rwxr-xr-xbin/start_server.sh6
-rw-r--r--etc/reddwarf/reddwarf.conf.test4
-rw-r--r--etc/tests/core.test.conf38
-rw-r--r--etc/tests/localhost.test.conf95
-rw-r--r--reddwarf/taskmanager/api.py4
-rw-r--r--reddwarf/tests/__init__.py125
-rw-r--r--reddwarf/tests/api/__init__.py13
-rw-r--r--reddwarf/tests/api/databases.py199
-rw-r--r--reddwarf/tests/api/flavors.py165
-rw-r--r--reddwarf/tests/api/instances.py928
-rw-r--r--reddwarf/tests/api/instances_actions.py591
-rw-r--r--reddwarf/tests/api/instances_delete.py114
-rw-r--r--reddwarf/tests/api/instances_mysql_down.py119
-rw-r--r--reddwarf/tests/api/instances_pagination.py227
-rw-r--r--reddwarf/tests/api/mgmt/__init__.py13
-rw-r--r--reddwarf/tests/api/mgmt/accounts.py219
-rw-r--r--reddwarf/tests/api/mgmt/admin_required.py93
-rw-r--r--reddwarf/tests/api/mgmt/instances.py225
-rw-r--r--reddwarf/tests/api/mgmt/storage.py108
-rw-r--r--reddwarf/tests/api/root.py180
-rw-r--r--reddwarf/tests/api/users.py274
-rw-r--r--reddwarf/tests/api/versions.py89
-rw-r--r--reddwarf/tests/config.py173
-rw-r--r--reddwarf/tests/fakes/common.py67
-rw-r--r--reddwarf/tests/fakes/guestagent.py5
-rw-r--r--reddwarf/tests/fakes/nova.py37
-rw-r--r--reddwarf/tests/util/__init__.py254
-rw-r--r--reddwarf/tests/util/check.py206
-rw-r--r--reddwarf/tests/util/client.py119
-rw-r--r--reddwarf/tests/util/users.py134
-rw-r--r--run_tests.py485
-rw-r--r--tools/pip-requires4
-rw-r--r--tools/test-requires3
-rw-r--r--tox.ini14
37 files changed, 4835 insertions, 526 deletions
diff --git a/.coveragerc b/.coveragerc
new file mode 100644
index 00000000..6f625763
--- /dev/null
+++ b/.coveragerc
@@ -0,0 +1,26 @@
+# .coveragerc to control coverage.py
+[run]
+branch = True
+
+timid=True
+include=*reddwarf*
+
+[report]
+# Regexes for lines to exclude from consideration
+exclude_lines =
+ # Have to re-enable the standard pragma
+ pragma: no cover
+
+ # Don't complain about missing debug-only code:
+ def __repr__
+ if self\.debug
+
+ # Don't complain if tests don't hit defensive assertion code:
+ raise AssertionError
+ raise NotImplementedError
+
+ # Don't complain if non-runnable code isn't run:
+ if 0:
+ if __name__ == .__main__.:
+
+ignore_errors = False
diff --git a/.gitignore b/.gitignore
index 93301144..91ab7361 100644
--- a/.gitignore
+++ b/.gitignore
@@ -14,3 +14,4 @@ covhtml/
host-syslog.log
tags
.tox
+rdtest.log
diff --git a/bin/reddwarf-manage b/bin/reddwarf-manage
index b7afffbf..319fe3a1 100755
--- a/bin/reddwarf-manage
+++ b/bin/reddwarf-manage
@@ -59,7 +59,7 @@ class Commands(object):
def __init__(self, conf):
self.conf = conf
- def db_sync(self, repo_path=None):
+ def db_sync(self):
db_api.db_sync(self.conf, repo_path=None)
def db_upgrade(self, version=None, repo_path=None):
@@ -89,7 +89,7 @@ class Commands(object):
from reddwarf.instance import models
from reddwarf.db.sqlalchemy import session
db_api.drop_db(self.conf)
- self.db_sync(repo_path)
+ self.db_sync()
# Sets up database engine, so the next line will work...
session.configure_db(self.conf)
models.ServiceImage.create(service_name=service_name,
diff --git a/bin/start_server.sh b/bin/start_server.sh
index 3569ec8e..bfba17e7 100755
--- a/bin/start_server.sh
+++ b/bin/start_server.sh
@@ -1,6 +1,10 @@
#!/usr/bin/env bash
# Arguments: Use --pid_file to specify a pid file location.
-tox -e py26
+
+
+if [ ! -d ".tox/py26" ]; then
+ tox -epy26
+fi
function run() {
.tox/py26/bin/python $@
diff --git a/etc/reddwarf/reddwarf.conf.test b/etc/reddwarf/reddwarf.conf.test
index df95527f..1c0ab51c 100644
--- a/etc/reddwarf/reddwarf.conf.test
+++ b/etc/reddwarf/reddwarf.conf.test
@@ -2,6 +2,8 @@
remote_implementation = fake
+log_file = rdtest.log
+
# Show more verbose log output (sets INFO log level output)
verbose = True
@@ -34,7 +36,7 @@ sql_connection = sqlite:///reddwarf_test.sqlite
sql_idle_timeout = 3600
#DB Api Implementation
-db_api_implementation = "reddwarf.db.sqlalchemy.api"
+db_api_implementation = reddwarf.db.sqlalchemy.api
# Path to the extensions
api_extensions_path = reddwarf/extensions
diff --git a/etc/tests/core.test.conf b/etc/tests/core.test.conf
new file mode 100644
index 00000000..479b2e80
--- /dev/null
+++ b/etc/tests/core.test.conf
@@ -0,0 +1,38 @@
+{
+ "report_directory":"rdli-test-report",
+ "start_services": false,
+
+
+ "test_mgmt":false,
+ "use_local_ovz":false,
+ "use_venv":false,
+ "glance_code_root":"/opt/stack/glance",
+ "glance_api_conf":"/vagrant/conf/glance-api.conf",
+ "glance_reg_conf":"/vagrant/conf/glance-reg.conf",
+ "glance_images_directory": "/glance_images",
+ "glance_image": "fakey_fakerson.tar.gz",
+ "instance_flavor_name":"m1.tiny",
+ "instance_bigger_flavor_name":"m1.rd-smaller",
+ "nova_code_root":"/opt/stack/nova",
+ "nova_conf":"/home/vagrant/nova.conf",
+ "keystone_code_root":"/opt/stack/keystone",
+ "keystone_conf":"/etc/keystone/keystone.conf",
+ "keystone_use_combined":true,
+ "reddwarf_code_root":"/opt/stack/reddwarf_lite",
+ "reddwarf_conf":"/tmp/reddwarf.conf",
+ "reddwarf_version":"v1.0",
+ "reddwarf_api_updated":"2012-08-01T00:00:00Z",
+ "reddwarf_must_have_volume":false,
+ "reddwarf_can_have_volume":true,
+ "reddwarf_main_instance_has_volume": true,
+ "reddwarf_max_accepted_volume_size": 1000,
+ "reddwarf_max_instances_per_user": 55,
+ "use_reaper":false,
+"root_removed_from_instance_api": true,
+ "root_timestamp_disabled": false,
+ "openvz_disabled": false,
+ "management_api_disabled": true,
+
+"dns_instance_entry_factory":"reddwarf.dns.rsdns.driver.RsDnsInstanceEntryFactory",
+ "sentinel": null
+}
diff --git a/etc/tests/localhost.test.conf b/etc/tests/localhost.test.conf
new file mode 100644
index 00000000..cae44a17
--- /dev/null
+++ b/etc/tests/localhost.test.conf
@@ -0,0 +1,95 @@
+{
+ "include-files":["core.test.conf"],
+
+ "fake_mode": true,
+ "dbaas_url":"http://localhost:8779/v1.0",
+ "version_url":"http://localhost:8779",
+ "nova_auth_url":"http://localhost:8779/v1.0/auth",
+ "reddwarf_auth_url":"http://localhost:8779/v1.0/auth",
+ "reddwarf_client_insecure":false,
+ "auth_strategy":"fake",
+
+ "reddwarf_version":"v1.0",
+ "reddwarf_api_updated":"2012-08-01T00:00:00Z",
+
+ "reddwarf_dns_support":false,
+ "reddwarf_ip_support":false,
+
+ "nova_client": null,
+
+
+ "users": [
+ {
+ "auth_user":"admin",
+ "auth_key":"password",
+ "tenant":"admin-1000",
+ "requirements": {
+ "is_admin":true,
+ "services": ["reddwarf"]
+ }
+ },
+ {
+ "auth_user":"jsmith",
+ "auth_key":"password",
+ "tenant":"2500",
+ "requirements": {
+ "is_admin":false,
+ "services": ["reddwarf"]
+ }
+ },
+ {
+ "auth_user":"hub_cap",
+ "auth_key":"password",
+ "tenant":"3000",
+ "requirements": {
+ "is_admin":false,
+ "services": ["reddwarf"]
+ }
+ }
+ ],
+
+ "flavors": [
+ {
+ "id": 1,
+ "name": "m1.tiny",
+ "ram": 512
+ },
+ {
+ "id": 2,
+ "name": "m1.small",
+ "ram": 2048
+ },
+ {
+ "id": 3,
+ "name": "m1.medium",
+ "ram": 4096
+ },
+ {
+ "id": 4,
+ "name": "m1.large",
+ "ram": 8192
+ },
+ {
+ "id": 5,
+ "name": "m1.xlarge",
+ "ram": 16384
+ },
+ {
+ "id": 6,
+ "name": "tinier",
+ "ram": 506
+ },
+ {
+ "id": 7,
+ "name": "m1.rd-tiny",
+ "ram": 512
+ },
+ {
+ "id": 8,
+ "name": "m1.rd-smaller",
+ "ram": 768
+ }
+
+ ],
+ "sentinel": null
+}
diff --git a/reddwarf/taskmanager/api.py b/reddwarf/taskmanager/api.py
index 9e994759..fac6726f 100644
--- a/reddwarf/taskmanager/api.py
+++ b/reddwarf/taskmanager/api.py
@@ -35,7 +35,7 @@ class API(ManagerAPI):
"""API for interacting with the task manager."""
def _fake_cast(self, method_name, **kwargs):
- import eventlet
+ from reddwarf.tests.fakes.common import get_event_spawer
from reddwarf.taskmanager.manager import TaskManager
instance = TaskManager()
method = getattr(instance, method_name)
@@ -49,7 +49,7 @@ class API(ManagerAPI):
logging.error((traceback.format_exception(type_, value, tb)))
raise type_, value, tb
- eventlet.spawn_after(0, func)
+ get_event_spawer()(0, func)
def _get_routing_key(self):
"""Create the routing key for the taskmanager"""
diff --git a/reddwarf/tests/__init__.py b/reddwarf/tests/__init__.py
index 82392451..50274c26 100644
--- a/reddwarf/tests/__init__.py
+++ b/reddwarf/tests/__init__.py
@@ -1,121 +1,4 @@
-# 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.
-
-# See http://code.google.com/p/python-nose/issues/detail?id=373
-# The code below enables nosetests to work with i18n _() blocks
-
-
-import __builtin__
-setattr(__builtin__, '_', lambda x: x)
-
-import os
-import unittest
-import urlparse
-
-import mox
-
-from reddwarf.db import db_api
-from reddwarf.common import config
-from reddwarf.common import utils
-
-
-def reddwarf_root_path():
- return os.path.join(os.path.dirname(__file__), "..", "..")
-
-
-def reddwarf_bin_path(filename="."):
- return os.path.join(reddwarf_root_path(), "bin", filename)
-
-
-def reddwarf_etc_path(filename="."):
- return os.path.join(reddwarf_root_path(), "etc", "reddwarf", filename)
-
-
-def test_config_file():
- return reddwarf_etc_path("reddwarf.conf.sample")
-
-
-class BaseTest(unittest.TestCase):
-
- def setUp(self):
- #maxDiff=None ensures diff output of assert methods are not truncated
- self.maxDiff = None
-
- self.mock = mox.Mox()
- conf, reddwarf_app = config.Config.load_paste_app(
- 'reddwarfapp',
- {"config_file": test_config_file()},
- None)
- db_api.configure_db(conf)
- db_api.clean_db()
- super(BaseTest, self).setUp()
-
- def tearDown(self):
- self.mock.UnsetStubs()
- self.mock.VerifyAll()
- super(BaseTest, self).tearDown()
-
- def assertRaisesExcMessage(self, exception, message,
- func, *args, **kwargs):
- """This is similar to assertRaisesRegexp in python 2.7"""
-
- try:
- func(*args, **kwargs)
- self.fail("Expected %r to raise %r" % (func, exception))
- except exception as error:
- self.assertIn(message, str(error))
-
- def assertIn(self, expected, actual):
- """This is similar to assertIn in python 2.7"""
- self.assertTrue(expected in actual,
- "%r does not contain %r" % (actual, expected))
-
- def assertNotIn(self, expected, actual):
- self.assertFalse(expected in actual,
- "%r does not contain %r" % (actual, expected))
-
- def assertIsNone(self, actual):
- """This is similar to assertIsNone in python 2.7"""
- self.assertEqual(actual, None)
-
- def assertIsNotNone(self, actual):
- """This is similar to assertIsNotNone in python 2.7"""
- self.assertNotEqual(actual, None)
-
- def assertItemsEqual(self, expected, actual):
- self.assertEqual(sorted(expected), sorted(actual))
-
- def assertModelsEqual(self, expected, actual):
- self.assertEqual(
- sorted(expected, key=lambda model: model.id),
- sorted(actual, key=lambda model: model.id))
-
- def assertUrlEqual(self, expected, actual):
- self.assertEqual(expected.partition("?")[0], actual.partition("?")[0])
-
- #params ordering might be different in the urls
- self.assertEqual(
- urlparse.parse_qs(expected.partition("?")[2]),
- urlparse.parse_qs(actual.partition("?")[2]))
-
- def assertErrorResponse(self, response, error_type, expected_error):
- self.assertEqual(response.status_int, error_type().code)
- self.assertIn(expected_error, response.body)
-
- def setup_uuid_with(self, fake_uuid):
- self.mock.StubOutWithMock(utils, "generate_uuid")
- utils.generate_uuid().MultipleTimes().AndReturn(fake_uuid)
+DBAAS_API = "dbaas.api"
+PRE_INSTANCES = "dbaas.api.pre_instances"
+INSTANCES = "dbaas.api.instances"
+POST_INSTANCES = "dbaas.api.post_instances"
diff --git a/reddwarf/tests/api/__init__.py b/reddwarf/tests/api/__init__.py
new file mode 100644
index 00000000..40d014dd
--- /dev/null
+++ b/reddwarf/tests/api/__init__.py
@@ -0,0 +1,13 @@
+# Copyright 2011 OpenStack LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
diff --git a/reddwarf/tests/api/databases.py b/reddwarf/tests/api/databases.py
new file mode 100644
index 00000000..d8dfa541
--- /dev/null
+++ b/reddwarf/tests/api/databases.py
@@ -0,0 +1,199 @@
+# Copyright 2011 OpenStack LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+import time
+
+from reddwarfclient import exceptions
+
+from proboscis import before_class
+from proboscis import test
+from proboscis.asserts import assert_equal
+from proboscis.asserts import assert_false
+from proboscis.asserts import assert_not_equal
+from proboscis.asserts import assert_raises
+from proboscis.asserts import assert_true
+from proboscis.asserts import fail
+from proboscis.decorators import expect_exception
+from proboscis.decorators import time_out
+
+from reddwarf import tests
+
+from reddwarf.tests import util
+from reddwarf.tests.api.instances import GROUP_START
+from reddwarf.tests.api.instances import instance_info
+from reddwarf.tests.util import test_config
+
+GROUP = "dbaas.api.databases"
+FAKE = test_config.values['fake_mode']
+
+
+@test(depends_on_groups=[GROUP_START],
+ groups=[tests.DBAAS_API, GROUP, tests.INSTANCES])
+class TestDatabases(object):
+ """
+ Test the creation and deletion of additional MySQL databases
+ """
+
+ dbname = "third #?@some_-"
+ dbname_urlencoded = "third%20%23%3F%40some_-"
+
+ dbname2 = "seconddb"
+ created_dbs = [dbname, dbname2]
+ system_dbs = ['information_schema', 'mysql', 'lost+found']
+
+ @before_class
+ def setUp(self):
+ self.dbaas = util.create_dbaas_client(instance_info.user)
+ self.dbaas_admin = util.create_dbaas_client(instance_info.admin_user)
+
+ @test
+ def test_cannot_create_taboo_database_names(self):
+ for name in self.system_dbs:
+ databases = [{"name": name, "charset": "latin2",
+ "collate": "latin2_general_ci"}]
+ assert_raises(exceptions.BadRequest, self.dbaas.databases.create,
+ instance_info.id, databases)
+ assert_equal(400, self.dbaas.last_http_code)
+
+ @test
+ def test_create_database(self):
+ databases = []
+ databases.append({"name": self.dbname, "charset": "latin2",
+ "collate": "latin2_general_ci"})
+ databases.append({"name": self.dbname2})
+
+ self.dbaas.databases.create(instance_info.id, databases)
+ assert_equal(202, self.dbaas.last_http_code)
+ if not FAKE:
+ time.sleep(5)
+
+ @test(depends_on=[test_create_database])
+ def test_create_database_list(self):
+ databases = self.dbaas.databases.list(instance_info.id)
+ assert_equal(200, self.dbaas.last_http_code)
+ found = False
+ for db in self.created_dbs:
+ for result in databases:
+ if result.name == db:
+ found = True
+ assert_true(found, "Database '%s' not found in result" % db)
+ found = False
+
+ @test(depends_on=[test_create_database])
+ def test_fails_when_creating_a_db_twice(self):
+ databases = []
+ databases.append({"name": self.dbname, "charset": "latin2",
+ "collate": "latin2_general_ci"})
+ databases.append({"name": self.dbname2})
+
+ assert_raises(exceptions.BadRequest, self.dbaas.databases.create,
+ instance_info.id, databases)
+ assert_equal(400, self.dbaas.last_http_code)
+
+ @test
+ def test_create_database_list_system(self):
+ #Databases that should not be returned in the list
+ databases = self.dbaas.databases.list(instance_info.id)
+ assert_equal(200, self.dbaas.last_http_code)
+ found = False
+ for db in self.system_dbs:
+ found = any(result.name == db for result in databases)
+ msg = "Database '%s' SHOULD NOT be found in result" % db
+ assert_false(found, msg)
+ found = False
+
+ @test
+ def test_create_database_on_missing_instance(self):
+ databases = [{"name": "invalid_db", "charset": "latin2",
+ "collate": "latin2_general_ci"}]
+ assert_raises(exceptions.NotFound, self.dbaas.databases.create,
+ -1, databases)
+ assert_equal(404, self.dbaas.last_http_code)
+
+ @test(runs_after=[test_create_database])
+ def test_delete_database(self):
+ self.dbaas.databases.delete(instance_info.id, self.dbname_urlencoded)
+ assert_equal(202, self.dbaas.last_http_code)
+ if not FAKE:
+ time.sleep(5)
+ dbs = self.dbaas.databases.list(instance_info.id)
+ assert_equal(200, self.dbaas.last_http_code)
+ found = any(result.name == self.dbname_urlencoded for result in dbs)
+ assert_false(found, "Database '%s' SHOULD NOT be found in result" %
+ self.dbname_urlencoded)
+
+ @test(runs_after=[test_delete_database])
+ def test_cannot_delete_taboo_database_names(self):
+ for name in self.system_dbs:
+ assert_raises(exceptions.BadRequest, self.dbaas.databases.delete,
+ instance_info.id, name)
+ assert_equal(400, self.dbaas.last_http_code)
+
+ @test(runs_after=[test_delete_database])
+ def test_delete_database_on_missing_instance(self):
+ assert_raises(exceptions.NotFound, self.dbaas.databases.delete,
+ -1, self.dbname_urlencoded)
+ assert_equal(404, self.dbaas.last_http_code)
+
+ @test
+ def test_database_name_too_long(self):
+ databases = []
+ name = ("aasdlkhaglkjhakjdkjgfakjgadgfkajsg"
+ "34523dfkljgasldkjfglkjadsgflkjagsdd")
+ databases.append({"name": name})
+ assert_raises(exceptions.BadRequest, self.dbaas.databases.create,
+ instance_info.id, databases)
+ assert_equal(400, self.dbaas.last_http_code)
+
+ @test
+ def test_invalid_database_name(self):
+ databases = []
+ databases.append({"name": "sdfsd,"})
+ assert_raises(exceptions.BadRequest, self.dbaas.databases.create,
+ instance_info.id, databases)
+ assert_equal(400, self.dbaas.last_http_code)
+
+ @test
+ def test_pagination(self):
+ databases = []
+ databases.append({"name": "Sprockets", "charset": "latin2",
+ "collate": "latin2_general_ci"})
+ databases.append({"name": "Cogs"})
+ databases.append({"name": "Widgets"})
+
+ self.dbaas.databases.create(instance_info.id, databases)
+ assert_equal(202, self.dbaas.last_http_code)
+ if not FAKE:
+ time.sleep(5)
+ limit = 2
+ databases = self.dbaas.databases.list(instance_info.id, limit=limit)
+ assert_equal(200, self.dbaas.last_http_code)
+ marker = databases.next
+
+ # Better get only as many as we asked for
+ assert_true(len(databases) <= limit)
+ assert_true(databases.next is not None)
+ assert_equal(marker, databases[-1].name)
+ marker = databases.next
+
+ # I better get new databases if I use the marker I was handed.
+ databases = self.dbaas.databases.list(instance_info.id, limit=limit,
+ marker=marker)
+ assert_equal(200, self.dbaas.last_http_code)
+ assert_true(marker not in [database.name for database in databases])
+
+ # Now fetch again with a larger limit.
+ databases = self.dbaas.databases.list(instance_info.id)
+ assert_equal(200, self.dbaas.last_http_code)
+ assert_true(databases.next is None)
diff --git a/reddwarf/tests/api/flavors.py b/reddwarf/tests/api/flavors.py
new file mode 100644
index 00000000..b560d062
--- /dev/null
+++ b/reddwarf/tests/api/flavors.py
@@ -0,0 +1,165 @@
+# Copyright (c) 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.
+
+import os
+
+from nose.tools import assert_equal
+from nose.tools import assert_false
+from nose.tools import assert_true
+from reddwarfclient import exceptions
+from reddwarfclient.flavors import Flavor
+from reddwarfclient.flavors import Flavors
+
+from proboscis import before_class
+from proboscis import test
+from proboscis.asserts import assert_raises
+from proboscis import SkipTest
+
+from reddwarf import tests
+from reddwarf.tests.util import create_dbaas_client
+from reddwarf.tests.util import create_nova_client
+from reddwarf.tests.util import test_config
+from reddwarf.tests.util.users import Requirements
+from reddwarf.tests.util.check import AttrCheck
+
+GROUP = "dbaas.api.flavors"
+
+
+servers_flavors = None
+dbaas_flavors = None
+user = None
+
+
+def assert_attributes_equal(name, os_flavor, dbaas_flavor):
+ """Given an attribute name and two objects,
+ ensures the attribute is equal."""
+ assert_true(hasattr(os_flavor, name),
+ "open stack flavor did not have attribute %s" % name)
+ assert_true(hasattr(dbaas_flavor, name),
+ "dbaas flavor did not have attribute %s" % name)
+ expected = getattr(os_flavor, name)
+ actual = getattr(dbaas_flavor, name)
+ assert_equal(expected, actual,
+ 'DBaas flavor differs from Open Stack on attribute ' + name)
+
+
+def assert_flavors_roughly_equivalent(os_flavor, dbaas_flavor):
+ assert_attributes_equal('name', os_flavor, dbaas_flavor)
+ assert_attributes_equal('ram', os_flavor, dbaas_flavor)
+ assert_false(hasattr(dbaas_flavor, 'disk'),
+ "The attribute 'disk' s/b absent from the dbaas API.")
+
+
+def assert_link_list_is_equal(flavor):
+ assert_true(hasattr(flavor, 'links'))
+ assert_true(flavor.links)
+
+ for link in flavor.links:
+ href = link['href']
+ if "self" in link['rel']:
+ expected_href = os.path.join(test_config.dbaas_url, "flavors",
+ str(flavor.id))
+ url = test_config.dbaas_url.replace('http:', 'https:', 1)
+ msg = ("REL HREF %s doesn't start with %s" %
+ (href, test_config.dbaas_url))
+ assert_true(href.startswith(url), msg)
+ url = os.path.join("flavors", str(flavor.id))
+ msg = "REL HREF %s doesn't end in 'flavors/id'" % href
+ assert_true(href.endswith(url), msg)
+ elif "bookmark" in link['rel']:
+ base_url = test_config.version_url.replace('http:', 'https:', 1)
+ expected_href = os.path.join(base_url, "flavors", str(flavor.id))
+ msg = 'bookmark "href" must be %s, not %s' % (expected_href, href)
+ assert_equal(href, expected_href, msg)
+ else:
+ assert_false(True, "Unexpected rel - %s" % link['rel'])
+
+
+@test(groups=[tests.DBAAS_API, GROUP, tests.PRE_INSTANCES],
+ depends_on_groups=["services.initialize"])
+class Flavors(object):
+
+ @before_class
+ def setUp(self):
+ rd_user = test_config.users.find_user(
+ Requirements(is_admin=False, services=["reddwarf"]))
+ self.rd_client = create_dbaas_client(rd_user)
+
+ if test_config.nova_client is not None:
+ nova_user = test_config.users.find_user(
+ Requirements(is_admin=False, services=["nova"]))
+ self.nova_client = create_nova_client(nova_user)
+
+ def get_expected_flavors(self):
+ # If we have access to the client, great! Let's use that as the flavors
+ # returned by Reddwarf should be identical.
+ if test_config.nova_client is not None:
+ return self.nova_client.flavors.list()
+ # If we don't have access to the client the flavors need to be spelled
+ # out in the config file.
+ flavors = [Flavor(Flavors, flavor_dict, loaded=True)
+ for flavor_dict in test_config.flavors]
+ return flavors
+
+ @test
+ def confirm_flavors_lists_nearly_identical(self):
+ os_flavors = self.get_expected_flavors()
+ dbaas_flavors = self.rd_client.flavors.list()
+
+ print("Open Stack Flavors:")
+ print(os_flavors)
+ print("DBaaS Flavors:")
+ print(dbaas_flavors)
+ #Length of both flavors list should be identical.
+ assert_equal(len(os_flavors), len(dbaas_flavors))
+ for os_flavor in os_flavors:
+ found_index = None
+ for index, dbaas_flavor in enumerate(dbaas_flavors):
+ if os_flavor.name == dbaas_flavor.name:
+ msg = ("Flavor ID '%s' appears in elements #%s and #%d." %
+ (dbaas_flavor.id, str(found_index), index))
+ assert_true(found_index is None, msg)
+ assert_flavors_roughly_equivalent(os_flavor, dbaas_flavor)
+ found_index = index
+ msg = "Some flavors from OS list were missing in DBAAS list."
+ assert_false(found_index is None, msg)
+ for flavor in dbaas_flavors:
+ assert_link_list_is_equal(flavor)
+
+ @test
+ def test_flavor_list_attrs(self):
+ expected_attrs = ['id', 'name', 'ram', 'links']
+ flavors = self.rd_client.flavors.list()
+ attrcheck = AttrCheck()
+ for flavor in flavors:
+ flavor_dict = flavor._info
+ attrcheck.attrs_exist(flavor_dict, expected_attrs,
+ msg="Flavors list")
+ attrcheck.links(flavor_dict['links'])
+
+ @test
+ def test_flavor_get_attrs(self):
+ expected_attrs = ['id', 'name', 'ram', 'links']
+ flavor = self.rd_client.flavors.get(1)
+ attrcheck = AttrCheck()
+ flavor_dict = flavor._info
+ attrcheck.attrs_exist(flavor_dict, expected_attrs,
+ msg="Flavor Get 1")
+ attrcheck.links(flavor_dict['links'])
+
+ @test
+ def test_flavor_not_found(self):
+ assert_raises(exceptions.NotFound,
+ self.rd_client.flavors.get, "detail")
diff --git a/reddwarf/tests/api/instances.py b/reddwarf/tests/api/instances.py
new file mode 100644
index 00000000..0e84a37f
--- /dev/null
+++ b/reddwarf/tests/api/instances.py
@@ -0,0 +1,928 @@
+# 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.
+import hashlib
+
+import os
+import re
+import string
+import time
+import unittest
+from reddwarf.tests import util
+import urlparse
+
+
+GROUP = "dbaas.guest"
+GROUP_START = "dbaas.guest.initialize"
+GROUP_TEST = "dbaas.guest.test"
+GROUP_STOP = "dbaas.guest.shutdown"
+GROUP_USERS = "dbaas.api.users"
+GROUP_ROOT = "dbaas.api.root"
+GROUP_DATABASES = "dbaas.api.databases"
+
+from datetime import datetime
+from nose.plugins.skip import SkipTest
+from nose.tools import assert_true
+
+from reddwarfclient import exceptions
+
+from proboscis.decorators import time_out
+from proboscis import before_class
+from proboscis import test
+from proboscis.asserts import assert_equal
+from proboscis.asserts import assert_false
+from proboscis.asserts import assert_not_equal
+from proboscis.asserts import assert_raises
+from proboscis.asserts import assert_is
+from proboscis.asserts import assert_is_none
+from proboscis.asserts import assert_is_not
+from proboscis.asserts import assert_true
+from proboscis.asserts import Check
+from proboscis.asserts import fail
+
+from reddwarf import tests
+from reddwarf.tests.config import CONFIG
+from reddwarf.tests.util import create_client
+from reddwarf.tests.util import create_dbaas_client
+from reddwarf.tests.util import create_nova_client
+from reddwarf.tests.util import process
+from reddwarf.tests.util.users import Requirements
+from reddwarf.tests.util import string_in_list
+from reddwarf.tests.util import poll_until
+from reddwarf.tests.util.check import AttrCheck
+
+
+class InstanceTestInfo(object):
+ """Stores new instance information used by dependent tests."""
+
+ def __init__(self):
+ self.dbaas = None # The rich client instance used by these tests.
+ self.dbaas_admin = None # The rich client with admin access.
+ self.dbaas_flavor = None # The flavor object of the instance.
+ self.dbaas_flavor_href = None # The flavor of the instance.
+ self.dbaas_image = None # The image used to create the instance.
+ self.dbaas_image_href = None # The link of the image.
+ self.id = None # The ID of the instance in the database.
+ self.local_id = None
+ self.address = None
+ self.initial_result = None # The initial result from the create call.
+ self.user_ip = None # The IP address of the instance, given to user.
+ self.infra_ip = None # The infrastructure network IP address.
+ self.result = None # The instance info returned by the API
+ self.nova_client = None # The instance of novaclient.
+ self.volume_client = None # The instance of the volume client.
+ self.name = None # Test name, generated each test run.
+ self.pid = None # The process ID of the instance.
+ self.user = None # The user instance who owns the instance.
+ self.admin_user = None # The admin user for the management interfaces.
+ self.volume = None # The volume the instance will have.
+ self.volume_id = None # Id for the attached vo186lume
+ self.storage = None # The storage device info for the volumes.
+ self.databases = None # The databases created on the instance.
+ self.host_info = None # Host Info before creating instances
+ self.user_context = None # A regular user context
+ self.users = None # The users created on the instance.
+
+ def get_address(self):
+ result = self.dbaas_admin.mgmt.instances.show(self.id)
+ addresses = result.server['addresses']
+ if "infranet" in addresses:
+ address = addresses['infranet'][0]
+ else:
+ address = addresses['private'][0]
+ return address['addr']
+
+ def get_local_id(self):
+ mgmt_instance = self.dbaas_admin.management.show(self.id)
+ return mgmt_instance.server["local_id"]
+
+
+# The two variables are used below by tests which depend on an instance
+# existing.
+instance_info = InstanceTestInfo()
+dbaas = None # Rich client used throughout this test.
+dbaas_admin = None # Same as above, with admin privs.
+
+
+# This is like a cheat code which allows the tests to skip creating a new
+# instance and use an old one.
+def existing_instance():
+ return os.environ.get("TESTS_USE_INSTANCE_ID", None)
+
+
+def do_not_delete_instance():
+ return os.environ.get("TESTS_DO_NOT_DELETE_INSTANCE", None) is not None
+
+
+def create_new_instance():
+ return existing_instance() is None
+
+
+@test(groups=[GROUP, GROUP_START, 'dbaas.setup'],
+ depends_on_groups=["services.initialize"])
+class InstanceSetup(object):
+ """Makes sure the client can hit the ReST service.
+
+ This test also uses the API to find the image and flavor to use.
+
+ """
+
+ @before_class
+ def setUp(self):
+ """Sets up the client."""
+
+ reqs = Requirements(is_admin=True)
+ instance_info.admin_user = CONFIG.users.find_user(reqs)
+ instance_info.dbaas_admin = create_dbaas_client(
+ instance_info.admin_user)
+ global dbaas_admin
+ dbaas_admin = instance_info.dbaas_admin
+
+ # Make sure we create the client as the correct user if we're using
+ # a pre-built instance.
+ if existing_instance():
+ mgmt_inst = dbaas_admin.mgmt.instances.show(existing_instance())
+ t_id = mgmt_inst.tenant_id
+ instance_info.user = CONFIG.users.find_user_by_tenant_id(t_id)
+ else:
+ reqs = Requirements(is_admin=False)
+ instance_info.user = CONFIG.users.find_user(reqs)
+
+ instance_info.dbaas = create_dbaas_client(instance_info.user)
+ if CONFIG.white_box:
+ instance_info.nova_client = create_nova_client(instance_info.user)
+ instance_info.volume_client = create_nova_client(
+ instance_info.user,
+ service_type=CONFIG.nova_client['volume_service_type'])
+ global dbaas
+ dbaas = instance_info.dbaas
+
+ if CONFIG.white_box:
+ user = instance_info.user.auth_user
+ tenant = instance_info.user.tenant
+ instance_info.user_context = context.RequestContext(user, tenant)
+
+ @test(enabled=CONFIG.white_box)
+ def find_image(self):
+ result = dbaas_admin.find_image_and_self_href(CONFIG.dbaas_image)
+ instance_info.dbaas_image, instance_info.dbaas_image_href = result
+
+ @test
+ def test_find_flavor(self):
+ flavor_name = CONFIG.values.get('instance_flavor_name', 'm1.tiny')
+ flavors = dbaas.find_flavors_by_name(flavor_name)
+ assert_equal(len(flavors), 1, "Number of flavors with name '%s' "
+ "found was '%d'." % (flavor_name, len(flavors)))
+ flavor = flavors[0]
+ assert_true(flavor is not None, "Flavor '%s' not found!" % flavor_name)
+ flavor_href = dbaas.find_flavor_self_href(flavor)
+ assert_true(flavor_href is not None,
+ "Flavor href '%s' not found!" % flavor_name)
+ instance_info.dbaas_flavor = flavor
+ instance_info.dbaas_flavor_href = flavor_href
+
+ @test(enabled=CONFIG.white_box)
+ def test_add_imageref_config(self):
+ #TODO(tim.simpson): I'm not sure why this is here. The default image
+ # setup should be in initialization test code that lives somewhere
+ # else, probably with the code that uploads the image.
+ key = "reddwarf_imageref"
+ value = 1
+ description = "Default Image for Reddwarf"
+ config = {'key': key, 'value': value, 'description': description}
+ try:
+ dbaas_admin.configs.create([config])
+ except exceptions.ClientException as e:
+ # configs.create will throw an exception if the config already
+ # exists we will check the value after to make sure it is correct
+ # and set
+ pass
+ result = dbaas_admin.configs.get(key)
+ assert_equal(result.value, str(value))
+
+ @test
+ def create_instance_name(self):
+ id = existing_instance()
+ if id is None:
+ instance_info.name = "TEST_" + str(datetime.now())
+ else:
+ instance_info.name = dbaas.instances.get(id).name
+
+
+@test(depends_on_classes=[InstanceSetup], groups=[GROUP])
+def test_delete_instance_not_found():
+ """Deletes an instance that does not exist."""
+ # Looks for a random UUID that (most probably) does not exist.
+ assert_raises(exceptions.NotFound, dbaas.instances.delete,
+ "7016efb6-c02c-403e-9628-f6f57d0920d0")
+
+
+@test(depends_on_classes=[InstanceSetup],
+ groups=[GROUP, GROUP_START, tests.INSTANCES],
+ runs_after_groups=[tests.PRE_INSTANCES])
+class CreateInstance(unittest.TestCase):
+ """Test to create a Database Instance
+
+ If the call returns without raising an exception this test passes.
+
+ """
+
+ def test_instance_size_too_big(self):
+ if 'reddwarf_max_accepted_volume_size' in CONFIG.values:
+ too_big = CONFIG.values['reddwarf_max_accepted_volume_size']
+ assert_raises(exceptions.OverLimit, dbaas.instances.create,
+ "way_too_large", instance_info.dbaas_flavor_href,
+ {'size': too_big + 1}, [])
+ assert_equal(413, dbaas.last_http_code)
+ #else:
+ # raise SkipTest("N/A: No max accepted volume size defined.")
+
+ def test_create(self):
+ databases = []
+ databases.append({"name": "firstdb", "character_set": "latin2",
+ "collate": "latin2_general_ci"})
+ databases.append({"name": "db2"})
+ instance_info.databases = databases
+ users = []
+ users.append({"name": "lite", "password": "litepass",
+ "databases": [{"name": "firstdb"}]})
+ instance_info.users = users
+ if CONFIG.values['reddwarf_main_instance_has_volume']:
+ instance_info.volume = {'size': 1}
+ else:
+ instance_info.volume = None
+
+ if create_new_instance():
+ instance_info.initial_result = dbaas.instances.create(
+ instance_info.name,
+ instance_info.dbaas_flavor_href,
+ instance_info.volume,
+ databases,
+ users)
+ assert_equal(200, dbaas.last_http_code)
+ else:
+ id = existing_instance()
+ instance_info.initial_result = dbaas.instances.get(id)
+
+ result = instance_info.initial_result
+ instance_info.id = result.id
+ if CONFIG.white_box:
+ instance_info.local_id = dbapi.localid_from_uuid(result.id)
+
+ report = CONFIG.get_report()
+ report.log("Instance UUID = %s" % instance_info.id)
+ if create_new_instance():
+ if CONFIG.white_box:
+ building = dbaas_mapping[power_state.BUILDING]
+ assert_equal(result.status, building)
+ assert_equal("BUILD", instance_info.initial_result.status)
+
+ else:
+ report.log("Test was invoked with TESTS_USE_INSTANCE_ID=%s, so no "
+ "instance was actually created." % id)
+ report.log("Local id = %d" % instance_info.get_local_id())
+
+ # Check these attrs only are returned in create response
+ expected_attrs = ['created', 'flavor', 'addresses', 'id', 'links',
+ 'name', 'status', 'updated']
+ if CONFIG.values['reddwarf_can_have_volume']:
+ expected_attrs.append('volume')
+ if CONFIG.values['reddwarf_dns_support']:
+ expected_attrs.append('hostname')
+
+ with CheckInstance(result._info) as check:
+ if create_new_instance():
+ check.attrs_exist(result._info, expected_attrs,
+ msg="Create response")
+ # Don't CheckInstance if the instance already exists.
+ check.flavor()
+ check.links(result._info['links'])
+ if CONFIG.values['reddwarf_can_have_volume']:
+ check.volume()
+
+ def test_create_failure_with_empty_volume(self):
+ if CONFIG.values['reddwarf_must_have_volume']:
+ instance_name = "instance-failure-with-no-volume-size"
+ databases = []
+ volume = {}
+ assert_raises(exceptions.BadRequest, dbaas.instances.create,
+ instance_name, instance_info.dbaas_flavor_href,
+ volume, databases)
+ assert_equal(400, dbaas.last_http_code)
+
+ def test_create_failure_with_no_volume_size(self):
+ if CONFIG.values['reddwarf_must_have_volume']:
+ instance_name = "instance-failure-with-no-volume-size"
+ databases = []
+ volume = {'size': None}
+ assert_raises(exceptions.BadRequest, dbaas.instances.create,
+ instance_name, instance_info.dbaas_flavor_href,
+ volume, databases)
+ assert_equal(400, dbaas.last_http_code)
+
+ def test_mgmt_get_instance_on_create(self):
+ if CONFIG.test_mgmt:
+ result = dbaas_admin.management.show(instance_info.id)
+ expected_attrs = ['account_id', 'addresses', 'created',
+ 'databases', 'flavor', 'guest_status', 'host',
+ 'hostname', 'id', 'name',
+ 'server_state_description', 'status', 'updated',
+ 'users', 'volume', 'root_enabled_at',
+ 'root_enabled_by']
+ with CheckInstance(result._info) as check:
+ check.attrs_exist(result._info, expected_attrs,
+ msg="Mgmt get instance")
+ check.flavor()
+ check.guest_status()
+
+
+def assert_unprocessable(func, *args):
+ try:
+ func(*args)
+ # If the exception didn't get raised, but the instance is still in
+ # the BUILDING state, that's a bug.
+ result = dbaas.instances.get(instance_info.id)
+ if result.status == "BUILD":
+ fail("When an instance is being built, this function should "
+ "always raise UnprocessableEntity.")
+ except exceptions.UnprocessableEntity:
+ assert_equal(422, dbaas.last_http_code)
+ pass # Good
+
+
+@test(depends_on_classes=[CreateInstance],
+ groups=[GROUP, GROUP_START, 'dbaas.mgmt.hosts_post_install'],
+ enabled=create_new_instance())
+class AfterInstanceCreation(unittest.TestCase):
+
+ # instance calls
+ def test_instance_delete_right_after_create(self):
+ assert_unprocessable(dbaas.instances.delete, instance_info.id)
+
+ # root calls
+ def test_root_create_root_user_after_create(self):
+ assert_unprocessable(dbaas.root.create, instance_info.id)
+
+ def test_root_is_root_enabled_after_create(self):
+ assert_unprocessable(dbaas.root.is_root_enabled, instance_info.id)
+
+ # database calls
+ def test_database_index_after_create(self):
+ assert_unprocessable(dbaas.databases.list, instance_info.id)
+
+ def test_database_delete_after_create(self):
+ assert_unprocessable(dbaas.databases.delete, instance_info.id,
+ "testdb")
+
+ def test_database_create_after_create(self):
+ assert_unprocessable(dbaas.databases.create, instance_info.id,
+ instance_info.databases)
+
+ # user calls
+ def test_users_index_after_create(self):
+ assert_unprocessable(dbaas.users.list, instance_info.id)
+
+ def test_users_delete_after_create(self):
+ assert_unprocessable(dbaas.users.delete, instance_info.id,
+ "testuser")
+
+ def test_users_create_after_create(self):
+ users = list()
+ users.append({"name": "testuser", "password": "password",
+ "database": "testdb"})
+ assert_unprocessable(dbaas.users.create, instance_info.id, users)
+
+ def test_resize_instance_after_create(self):
+ assert_unprocessable(dbaas.instances.resize_instance,
+ instance_info.id, 8)
+
+ def test_resize_volume_after_create(self):
+ assert_unprocessable(dbaas.instances.resize_volume,
+ instance_info.id, 2)
+
+
+@test(depends_on_classes=[CreateInstance],
+ runs_after=[AfterInstanceCreation],
+ groups=[GROUP, GROUP_START],
+ enabled=create_new_instance())
+class WaitForGuestInstallationToFinish(object):
+ """
+ Wait until the Guest is finished installing. It takes quite a while...
+ """
+
+ @test(groups=['lemon'])
+ @time_out(60 * 16)
+ def test_instance_created(self):
+ # This version just checks the REST API status.
+ def result_is_active():
+ instance = dbaas.instances.get(instance_info.id)
+ if instance.status == "ACTIVE":
+ return True
+ else:
+ # If its not ACTIVE, anything but BUILD must be
+ # an error.
+ assert_equal("BUILD", instance.status)
+ assert_equal(instance.volume.get('used', None), None)
+ return False
+
+ poll_until(result_is_active)
+ result = dbaas.instances.get(instance_info.id)
+
+ report = CONFIG.get_report()
+ report.log("Created an instance, ID = %s." % instance_info.id)
+ report.log("TIP:")
+ report.log("Rerun the tests with TESTS_USE_INSTANCE_ID=%s "
+ "to skip ahead to this point." % instance_info.id)
+ report.log("Add TESTS_DO_NOT_DELETE_INSTANCE=True to avoid deleting "
+ "the instance at the end of the tests.")
+
+
+@test(depends_on_classes=[WaitForGuestInstallationToFinish],
+ groups=[GROUP, GROUP_START],
+ enabled=CONFIG.white_box and create_new_instance())
+class VerifyGuestStarted(unittest.TestCase):
+ """
+ Test to verify the guest instance is started and we can get the init
+ process pid.
+ """
+
+ def test_instance_created(self):
+ def check_status_of_instance():
+ status, err = process("sudo vzctl status %s | awk '{print $5}'"
+ % str(instance_info.local_id))
+ if string_in_list(status, ["running"]):
+ self.assertEqual("running", status.strip())
+ return True
+ else:
+ return False
+ poll_until(check_status_of_instance, sleep_time=5, time_out=(60 * 8))
+
+ def test_get_init_pid(self):
+ def get_the_pid():
+ out, err = process("pgrep init | vzpid - | awk '/%s/{print $1}'"
+ % str(instance_info.local_id))
+ instance_info.pid = out.strip()
+ return len(instance_info.pid) > 0
+ poll_until(get_the_pid, sleep_time=10, time_out=(60 * 10))
+
+
+@test(depends_on_classes=[WaitForGuestInstallationToFinish],
+ groups=[GROUP, GROUP_START], enabled=create_new_instance())
+class TestGuestProcess(object):
+ """
+ Test that the guest process is started with all the right parameters
+ """
+
+ @test(enabled=CONFIG.values['use_local_ovz'])
+ @time_out(60 * 10)
+ def check_process_alive_via_local_ovz(self):
+ init_re = ("[\w\W\|\-\s\d,]*nova-guest "
+ "--flagfile=/etc/nova/nova.conf nova[\W\w\s]*")
+ init_proc = re.compile(init_re)
+ guest_re = ("[\w\W\|\-\s]*/usr/bin/nova-guest "
+ "--flagfile=/etc/nova/nova.conf[\W\w\s]*")
+ guest_proc = re.compile(guest_re)
+ apt = re.compile("[\w\W\|\-\s]*apt-get[\w\W\|\-\s]*")
+ while True:
+ guest_process, err = process("pstree -ap %s | grep nova-guest"
+ % instance_info.pid)
+ if not string_in_list(guest_process, ["nova-guest"]):
+ time.sleep(10)
+ else:
+ if apt.match(guest_process):
+ time.sleep(10)
+ else:
+ init = init_proc.match(guest_process)
+ guest = guest_proc.match(guest_process)
+ if init and guest:
+ assert_true(True, init.group())
+ else:
+ assert_false(False, guest_process)
+ break
+
+ @test
+ def check_hwinfo_before_tests(self):
+ hwinfo = dbaas_admin.hwinfo.get(instance_info.id)
+ print("hwinfo : %r" % hwinfo._info)
+ expected_attrs = ['hwinfo']
+ CheckInstance(None).attrs_exist(hwinfo._info, expected_attrs,
+ msg="Hardware information")
+ # TODO(pdmars): instead of just checking that these are int's, get the
+ # instance flavor and verify that the values are correct for the flavor
+ assert_true(isinstance(hwinfo.hwinfo['mem_total'], int))
+ assert_true(isinstance(hwinfo.hwinfo['num_cpus'], int))
+
+ @test
+ def grab_diagnostics_before_tests(self):
+ diagnostics = dbaas_admin.diagnostics.get(instance_info.id)
+ diagnostic_tests_helper(diagnostics)
+
+
+@test(depends_on_classes=[CreateInstance],
+ groups=[GROUP, GROUP_START, GROUP_TEST, "nova.volumes.instance"],
+ enabled=CONFIG.white_box)
+class TestVolume(unittest.TestCase):
+ """Make sure the volume is attached to instance correctly."""
+
+ def test_db_should_have_instance_to_volume_association(self):
+ """The compute manager should associate a volume to the instance."""
+ volumes = db.volume_get_all_by_instance(context.get_admin_context(),
+ instance_info.local_id)
+ self.assertEqual(1, len(volumes))
+
+
+@test(depends_on_classes=[WaitForGuestInstallationToFinish],
+ groups=[GROUP, GROUP_TEST, "dbaas.guest.start.test"])
+class TestAfterInstanceCreatedGuestData(object):
+ """
+ Test the optional parameters (databases and users) passed in to create
+ instance call were created.
+ """
+
+ @test
+ def test_databases(self):
+ databases = dbaas.databases.list(instance_info.id)
+ dbs = [database.name for database in databases]
+ for db in instance_info.databases:
+ assert_true(db["name"] in dbs)
+
+ @test
+ def test_users(self):
+ users = dbaas.users.list(instance_info.id)
+ usernames = [user.name for user in users]
+ for user in instance_info.users:
+ assert_true(user["name"] in usernames)
+
+
+@test(depends_on_classes=[WaitForGuestInstallationToFinish],
+ groups=[GROUP, GROUP_START, "dbaas.listing"])
+class TestInstanceListing(object):
+ """ Test the listing of the instance information """
+
+ @before_class
+ def setUp(self):
+ reqs = Requirements(is_admin=False)
+ self.other_user = CONFIG.users.find_user(
+ reqs,
+ black_list=[instance_info.user.auth_user])
+ self.other_client = create_dbaas_client(self.other_user)
+
+ @test
+ def test_index_list(self):
+ expected_attrs = ['id', 'links', 'name', 'status', 'flavor', 'volume']
+ instances = dbaas.instances.list()
+ assert_equal(200, dbaas.last_http_code)
+ for instance in instances:
+ instance_dict = instance._info
+ with CheckInstance(instance_dict) as check:
+ print("testing instance_dict=%s" % instance_dict)
+ check.attrs_exist(instance_dict, expected_attrs,
+ msg="Instance Index")
+ check.links(instance_dict['links'])
+ check.flavor()
+ check.volume()
+
+ @test
+ def test_get_instance(self):
+ expected_attrs = ['created', 'databases', 'flavor', 'hostname', 'id',
+ 'links', 'name', 'status', 'updated', 'volume', 'ip']
+ instance = dbaas.instances.get(instance_info.id)
+ assert_equal(200, dbaas.last_http_code)
+ instance_dict = instance._info
+ print("instance_dict=%s" % instance_dict)
+ with CheckInstance(instance_dict) as check:
+ check.attrs_exist(instance_dict, expected_attrs,
+ msg="Get Instance")
+ check.flavor()
+ check.links(instance_dict['links'])
+ check.used_volume()
+
+ @test
+ def test_get_instance_status(self):
+ result = dbaas.instances.get(instance_info.id)
+ assert_equal(200, dbaas.last_http_code)
+ assert_equal("ACTIVE", result.status)
+
+ @test
+ def test_get_legacy_status(self):
+ result = dbaas.instances.get(instance_info.id)
+ assert_equal(200, dbaas.last_http_code)
+ assert_true(result is not None)
+
+ @test
+ def test_get_legacy_status_notfound(self):
+ assert_raises(exceptions.NotFound, dbaas.instances.get, -2)
+
+ @test(enabled=CONFIG.values["reddwarf_main_instance_has_volume"])
+ def test_volume_found(self):
+ instance = dbaas.instances.get(instance_info.id)
+ if create_new_instance():
+ assert_equal(instance_info.volume['size'], instance.volume['size'])
+ else:
+ assert_true(isinstance(instance_info.volume['size'], int))
+ if create_new_instance():
+ assert_true(0.12 < instance.volume['used'] < 0.25)
+
+ @test
+ def test_instance_not_shown_to_other_user(self):
+ daffy_ids = [instance.id for instance in
+ self.other_client.instances.list()]
+ assert_equal(200, self.other_client.last_http_code)
+ admin_ids = [instance.id for instance in dbaas.instances.list()]
+ assert_equal(200, dbaas.last_http_code)
+ assert_equal(len(daffy_ids), 0)
+ assert_not_equal(sorted(admin_ids), sorted(daffy_ids))
+ assert_raises(exceptions.NotFound,
+ self.other_client.instances.get, instance_info.id)
+ for id in admin_ids:
+ assert_equal(daffy_ids.count(id), 0)
+
+ @test
+ def test_instance_not_deleted_by_other_user(self):
+ assert_raises(exceptions.NotFound,
+ self.other_client.instances.get, instance_info.id)
+ assert_raises(exceptions.NotFound,
+ self.other_client.instances.delete, instance_info.id)
+
+ @test(enabled=CONFIG.values['test_mgmt'])
+ def test_mgmt_get_instance_after_started(self):
+ result = dbaas_admin.management.show(instance_info.id)
+ expected_attrs = ['account_id', 'addresses', 'created', 'databases',
+ 'flavor', 'guest_status', 'host', 'hostname', 'id',
+ 'name', 'root_enabled_at', 'root_enabled_by',
+ 'server_state_description', 'status',
+ 'updated', 'users', 'volume']
+ with CheckInstance(result._info) as check:
+ check.attrs_exist(result._info, expected_attrs,
+ msg="Mgmt get instance")
+ check.flavor()
+ check.guest_status()
+ check.addresses()
+ check.volume_mgmt()
+
+
+@test(depends_on_groups=['dbaas.api.instances.actions'],
+ groups=[GROUP, tests.INSTANCES, "dbaas.diagnostics"])
+class CheckDiagnosticsAfterTests(object):
+ """ Check the diagnostics after running api commands on an instance. """
+ @test
+ def test_check_diagnostics_on_instance_after_tests(self):
+ diagnostics = dbaas_admin.diagnostics.get(instance_info.id)
+ assert_equal(200, dbaas.last_http_code)
+ diagnostic_tests_helper(diagnostics)
+ msg = "Fat Pete has emerged. size (%s > 30MB)" % diagnostics.vmPeak
+ assert_true(diagnostics.vmPeak < (30 * 1024), msg)
+
+
+@test(depends_on=[WaitForGuestInstallationToFinish],
+ depends_on_groups=[GROUP_USERS, GROUP_DATABASES, GROUP_ROOT],
+ groups=[GROUP, GROUP_STOP])
+class DeleteInstance(object):
+ """ Delete the created instance """
+
+ @time_out(3)
+ @test(runs_after_groups=[GROUP_START, GROUP_TEST, tests.INSTANCES])
+ def test_delete(self):
+ if do_not_delete_instance():
+ CONFIG.get_report().log("TESTS_DO_NOT_DELETE_INSTANCE=True was "
+ "specified, skipping delete...")
+ raise SkipTest("TESTS_DO_NOT_DELETE_INSTANCE was specified.")
+ global dbaas
+ if not hasattr(instance_info, "initial_result"):
+ raise SkipTest("Instance was never created, skipping test...")
+ if CONFIG.white_box:
+ # Change this code to get the volume using the API.
+ # That way we can keep it while keeping it black box.
+ admin_context = context.get_admin_context()
+ volumes = db.volume_get_all_by_instance(admin_context(),
+ instance_info.local_id)
+ instance_info.volume_id = volumes[0].id
+ # Update the report so the logs inside the instance will be saved.
+ CONFIG.get_report().update()
+ dbaas.instances.delete(instance_info.id)
+
+ attempts = 0
+ try:
+ time.sleep(1)
+ result = True
+ while result is not None:
+ attempts += 1
+ time.sleep(1)
+ result = dbaas.instances.get(instance_info.id)
+ assert_equal(200, dbaas.last_http_code)
+ assert_equal("SHUTDOWN", result.status)
+ except exceptions.NotFound:
+ pass
+ except Exception as ex:
+ fail("A failure occured when trying to GET instance %s for the %d "
+ "time: %s" % (str(instance_info.id), attempts, str(ex)))
+
+ @time_out(30)
+ @test(enabled=CONFIG.values["reddwarf_can_have_volume"],
+ depends_on=[test_delete])
+ def test_volume_is_deleted(self):
+ raise SkipTest("Cannot test volume is deleted from db.")
+ try:
+ while True:
+ db.volume_get(instance_info.user_context,
+ instance_info.volume_id)
+ time.sleep(1)
+ except backend_exception.VolumeNotFound:
+ pass
+
+ #TODO: make sure that the actual instance, volume, guest status, and DNS
+ # entries are deleted.
+
+
+@test(depends_on_classes=[CreateInstance, VerifyGuestStarted,
+ WaitForGuestInstallationToFinish], groups=[GROUP, GROUP_START],
+ enabled=CONFIG.values['test_mgmt'])
+class VerifyInstanceMgmtInfo(object):
+
+ @before_class
+ def set_up(self):
+ self.mgmt_details = dbaas_admin.management.show(instance_info.id)
+
+ def _assert_key(self, k, expected):
+ v = getattr(self.mgmt_details, k)
+ err = "Key %r does not match expected value of %r (was %r)." \
+ % (k, expected, v)
+ assert_equal(str(v), str(expected), err)
+
+ @test
+ def test_id_matches(self):
+ self._assert_key('id', instance_info.id)
+
+ @test
+ def test_bogus_instance_mgmt_data(self):
+ # Make sure that a management call to a bogus API 500s.
+ # The client reshapes the exception into just an OpenStackException.
+ assert_raises(exceptions.NotFound,
+ dbaas_admin.management.show, -1)
+
+ @test
+ def test_mgmt_ips_associated(self):
+ # Test that the management index properly associates an instances with
+ # ONLY its IPs.
+ mgmt_index = dbaas_admin.management.index()
+ # Every instances has exactly one address.
+ for instance in mgmt_index:
+ assert_equal(1, len(instance.ips))
+
+ @test
+ def test_mgmt_data(self):
+ # Test that the management API returns all the values we expect it to.
+ info = instance_info
+ ir = info.initial_result
+ cid = ir.id
+ instance_id = instance_info.local_id
+ expected = {
+ 'id': ir.id,
+ 'name': ir.name,
+ 'account_id': info.user.auth_user,
+ # TODO(hub-cap): fix this since its a flavor object now
+ #'flavorRef': info.dbaas_flavor_href,
+ 'databases': [
+ {
+ 'name': 'db2',
+ 'character_set': 'utf8',
+ 'collate': 'utf8_general_ci',
+ },
+ {
+ 'name': 'firstdb',
+ 'character_set': 'latin2',
+ 'collate': 'latin2_general_ci',
+ }
+ ],
+ }
+
+ if CONFIG.white_box:
+ admin_context = context.get_admin_context()
+ volumes = db.volume_get_all_by_instance(admin_context(),
+ instance_id)
+ assert_equal(len(volumes), 1)
+ volume = volumes[0]
+ expected['volume'] = {
+ 'id': volume.id,
+ 'name': volume.display_name,
+ 'size': volume.size,
+ 'description': volume.display_description,
+ }
+
+ expected_entry = info.expected_dns_entry()
+ if expected_entry:
+ expected['hostname'] = expected_entry.name
+
+ assert_true(self.mgmt_details is not None)
+ for (k, v) in expected.items():
+ msg = "Attr %r is missing." % k
+ assert_true(hasattr(self.mgmt_details, k), msg)
+ msg = ("Attr %r expected to be %r but was %r." %
+ (k, v, getattr(self.mgmt_details, k)))
+ assert_equal(getattr(self.mgmt_details, k), v, msg)
+ print(self.mgmt_details.users)
+ for user in self.mgmt_details.users:
+ assert_true('name' in user, "'name' not in users element.")
+
+
+class CheckInstance(AttrCheck):
+ """Class to check various attributes of Instance details"""
+
+ def __init__(self, instance):
+ super(CheckInstance, self).__init__()
+ self.instance = instance
+
+ def flavor(self):
+ if 'flavor' not in self.instance:
+ self.fail("'flavor' not found in instance.")
+ else:
+ expected_attrs = ['id', 'links']
+ self.attrs_exist(self.instance['flavor'], expected_attrs,
+ msg="Flavor")
+ self.links(self.instance['flavor']['links'])
+
+ def volume_key_exists(self):
+ if CONFIG.values['reddwarf_main_instance_has_volume']:
+ if 'volume' not in self.instance:
+ self.fail("'volume' not found in instance.")
+ return False
+ return True
+
+ def volume(self):
+ if not CONFIG.values["reddwarf_can_have_volume"]:
+ return
+ if self.volume_key_exists():
+ expected_attrs = ['size']
+ if not create_new_instance():
+ expected_attrs.append('used')
+ self.attrs_exist(self.instance['volume'], expected_attrs,
+ msg="Volumes")
+
+ def used_volume(self):
+ if not CONFIG.values["reddwarf_can_have_volume"]:
+ return
+ if self.volume_key_exists():
+ expected_attrs = ['size', 'used']
+ print self.instance
+ self.attrs_exist(self.instance['volume'], expected_attrs,
+ msg="Volumes")
+
+ def volume_mgmt(self):
+ if self.volume_key_exists():
+ expected_attrs = ['description', 'id', 'name', 'size']
+ self.attrs_exist(self.instance['volume'], expected_attrs,
+ msg="Volumes")
+
+ def addresses(self):
+ expected_attrs = ['addr', 'version']
+ print self.instance
+ networks = ['usernet']
+ for network in networks:
+ for address in self.instance['addresses'][network]:
+ self.attrs_exist(address, expected_attrs,
+ msg="Address")
+
+ def guest_status(self):
+ expected_attrs = ['created_at', 'deleted', 'deleted_at', 'instance_id',
+ 'state', 'state_description', 'updated_at']
+ self.attrs_exist(self.instance['guest_status'], expected_attrs,
+ msg="Guest status")
+
+ def mgmt_volume(self):
+ expected_attrs = ['description', 'id', 'name', 'size']
+ self.attrs_exist(self.instance['volume'], expected_attrs,
+ msg="Volume")
+
+
+def diagnostic_tests_helper(diagnostics):
+ print("diagnostics : %r" % diagnostics._info)
+ expected_attrs = ['version', 'fdSize', 'vmSize', 'vmHwm', 'vmRss',
+ 'vmPeak', 'threads']
+ CheckInstance(None).attrs_exist(diagnostics._info, expected_attrs,
+ msg="Diagnostics")
+ assert_true(isinstance(diagnostics.fdSize, int))
+ assert_true(isinstance(diagnostics.threads, int))
+ assert_true(isinstance(diagnostics.vmHwm, int))
+ assert_true(isinstance(diagnostics.vmPeak, int))
+ assert_true(isinstance(diagnostics.vmRss, int))
+ assert_true(isinstance(diagnostics.vmSize, int))
+ actual_version = diagnostics.version
+ update_test_conf = CONFIG.values.get("guest-update-test", None)
+ if update_test_conf is not None:
+ if actual_version == update_test_conf['next-version']:
+ return # This is acceptable but may not match the regex.
+ version_pattern = re.compile(r'[a-f0-9]+')
+ msg = "Version %s does not match pattern %s." % (actual_version,
+ version_pattern)
+ assert_true(version_pattern.match(actual_version), msg)
diff --git a/reddwarf/tests/api/instances_actions.py b/reddwarf/tests/api/instances_actions.py
new file mode 100644
index 00000000..ac63d127
--- /dev/null
+++ b/reddwarf/tests/api/instances_actions.py
@@ -0,0 +1,591 @@
+# 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.
+
+import time
+
+from proboscis import after_class
+from proboscis import before_class
+from proboscis import test
+from proboscis.asserts import assert_equal
+from proboscis.asserts import assert_false
+from proboscis.asserts import assert_not_equal
+from proboscis.asserts import assert_raises
+from proboscis.asserts import assert_true
+from proboscis.asserts import fail
+from proboscis.decorators import time_out
+from proboscis import SkipTest
+
+from reddwarf import tests
+from reddwarf.tests.util.check import Checker
+from reddwarfclient.exceptions import BadRequest
+from reddwarfclient.exceptions import UnprocessableEntity
+from reddwarf.tests.api.instances import GROUP as INSTANCE_GROUP
+from reddwarf.tests.api.instances import GROUP_START
+from reddwarf.tests.api.instances import instance_info
+from reddwarf.tests.api.instances import assert_unprocessable
+from reddwarf.tests import util
+from reddwarf.tests.util import poll_until
+from reddwarf.tests.config import CONFIG
+from reddwarf.tests.util import LocalSqlClient
+from sqlalchemy import create_engine
+from sqlalchemy import exc as sqlalchemy_exc
+from reddwarf.tests.util.check import TypeCheck
+from sqlalchemy.sql.expression import text
+
+GROUP = "dbaas.api.instances.actions"
+GROUP_REBOOT = "dbaas.api.instances.actions.reboot"
+GROUP_RESTART = "dbaas.api.instances.actions.restart"
+GROUP_STOP_MYSQL = "dbaas.api.instances.actions.stop"
+MYSQL_USERNAME = "test_user"
+MYSQL_PASSWORD = "abcde"
+FAKE_MODE = CONFIG.values['fake_mode']
+# If true, then we will actually log into the database.
+USE_IP = not CONFIG.values['fake_mode']
+# If true, then we will actually search for the process
+USE_LOCAL_OVZ = CONFIG.values['use_local_ovz']
+
+
+class MySqlConnection(object):
+
+ def __init__(self, host):
+ self.host = host
+
+ def connect(self):
+ """Connect to MySQL database."""
+ print("Connecting to MySQL, mysql --host %s -u %s -p%s"
+ % (self.host, MYSQL_USERNAME, MYSQL_PASSWORD))
+ self.client = LocalSqlClient(util.init_engine(
+ MYSQL_USERNAME, MYSQL_PASSWORD, self.host), use_flush=False)
+
+ def is_connected(self):
+ try:
+ with self.client:
+ self.client.execute(text("""SELECT "Hello.";"""))
+ return True
+ except (sqlalchemy_exc.OperationalError,
+ sqlalchemy_exc.DisconnectionError,
+ sqlalchemy_exc.TimeoutError):
+ return False
+ except Exception as ex:
+ print("EX WAS:")
+ print(type(ex))
+ print(ex)
+ raise ex
+
+
+TIME_OUT_TIME = 10 * 60
+USER_WAS_DELETED = False
+
+
+class ActionTestBase(object):
+ """Has some helpful functions for testing actions.
+
+ The test user must be created for some of these functions to work.
+
+ """
+
+ def set_up(self):
+ """If you're using this as a base class, call this method first."""
+ if USE_IP:
+ address = instance_info.get_address()
+ self.connection = MySqlConnection(address)
+ self.dbaas = instance_info.dbaas
+
+ @property
+ def instance(self):
+ return self.dbaas.instances.get(self.instance_id)
+
+ @property
+ def instance_local_id(self):
+ return instance_info.get_local_id()
+
+ @property
+ def instance_id(self):
+ return instance_info.id
+
+ def create_user(self):
+ """Create a MySQL user we can use for this test."""
+
+ users = [{"name": MYSQL_USERNAME, "password": MYSQL_PASSWORD,
+ "database": MYSQL_USERNAME}]
+ self.dbaas.users.create(instance_info.id, users)
+
+ def has_user():
+ users = self.dbaas.users.list(instance_info.id)
+ return any([user.name == MYSQL_USERNAME for user in users])
+
+ poll_until(has_user, time_out=30)
+ if not FAKE_MODE:
+ time.sleep(5)
+
+ def ensure_mysql_is_running(self):
+ """Make sure MySQL is accessible before restarting."""
+ with Checker() as check:
+ if USE_IP:
+ self.connection.connect()
+ check.true(self.connection.is_connected(),
+ "Able to connect to MySQL.")
+ if USE_LOCAL_OVZ:
+ self.proc_id = self.find_mysql_proc_on_instance()
+ check.true(self.proc_id is not None,
+ "MySQL process can be found.")
+ instance = self.instance
+ check.false(instance is None)
+ check.equal(instance.status, "ACTIVE")
+
+ def find_mysql_proc_on_instance(self):
+ return util.find_mysql_procid_on_instance(self.instance_local_id)
+
+ def log_current_users(self):
+ users = self.dbaas.users.list(self.instance_id)
+ CONFIG.get_report().log("Current user count = %d" % len(users))
+ for user in users:
+ CONFIG.get_report().log("\t" + str(user))
+
+
+@test(depends_on_groups=[GROUP_START])
+def create_user():
+ """Create a test user so that subsequent tests can log in."""
+ helper = ActionTestBase()
+ helper.set_up()
+ if USE_IP:
+ try:
+ helper.create_user()
+ except BadRequest:
+ pass # Ignore this if the user already exists.
+ helper.connection.connect()
+ assert_true(helper.connection.is_connected(),
+ "Test user must be able to connect to MySQL.")
+
+
+class RebootTestBase(ActionTestBase):
+ """Tests restarting MySQL."""
+
+ def call_reboot(self):
+ raise NotImplementedError()
+
+ def wait_for_broken_connection(self):
+ """Wait until our connection breaks."""
+ if not USE_IP:
+ return
+ poll_until(self.connection.is_connected,
+ lambda connected: not connected,
+ time_out=TIME_OUT_TIME)
+
+ def wait_for_successful_restart(self):
+ """Wait until status becomes running."""
+ def is_finished_rebooting():
+ instance = self.instance
+ instance.get()
+ print(instance.status)
+ if instance.status == "REBOOT":
+ return False
+ assert_equal("ACTIVE", instance.status)
+ return True
+
+ poll_until(is_finished_rebooting, time_out=TIME_OUT_TIME)
+
+ def assert_mysql_proc_is_different(self):
+ if not USE_LOCAL_OVZ:
+ return
+ new_proc_id = self.find_mysql_proc_on_instance()
+ assert_not_equal(new_proc_id, self.proc_id,
+ "MySQL process ID should be different!")
+
+ def successful_restart(self):
+ """Restart MySQL via the REST API successfully."""
+ self.fix_mysql()
+ self.call_reboot()
+ self.wait_for_broken_connection()
+ self.wait_for_successful_restart()
+ self.assert_mysql_proc_is_different()
+
+ def mess_up_mysql(self):
+ """Ruin MySQL's ability to restart."""
+ self.fix_mysql() # kill files
+ cmd = """ssh %s 'sudo cp /dev/null /var/lib/mysql/ib_logfile%d'"""
+ for index in range(2):
+ full_cmd = cmd % (self.instance_local_id, index)
+ print("RUNNING COMMAND: %s" % full_cmd)
+ util.process(full_cmd)
+
+ def fix_mysql(self):
+ """Fix MySQL's ability to restart."""
+ if not FAKE_MODE:
+ cmd = "ssh %s 'sudo rm /var/lib/mysql/ib_logfile%d'"
+ for index in range(2):
+ util.process(cmd % (self.instance_local_id, index))
+
+ def wait_for_failure_status(self):
+ """Wait until status becomes running."""
+ def is_finished_rebooting():
+ instance = self.instance
+ if instance.status == "REBOOT":
+ return False
+ assert_equal("SHUTDOWN", instance.status)
+ return True
+
+ poll_until(is_finished_rebooting, time_out=TIME_OUT_TIME)
+
+ def unsuccessful_restart(self):
+ """Restart MySQL via the REST when it should fail, assert it does."""
+ assert not FAKE_MODE
+ self.mess_up_mysql()
+ self.call_reboot()
+ self.wait_for_broken_connection()
+ self.wait_for_failure_status()
+
+ def restart_normally(self):
+ """Fix iblogs and reboot normally."""
+ self.fix_mysql()
+ self.test_successful_restart()
+
+
+@test(groups=[tests.INSTANCES, INSTANCE_GROUP, GROUP, GROUP_RESTART],
+ depends_on_groups=[GROUP_START], depends_on=[create_user])
+class RestartTests(RebootTestBase):
+ """Tests restarting MySQL."""
+
+ def call_reboot(self):
+ self.instance.restart()
+ assert_equal(202, self.dbaas.last_http_code)
+
+ @before_class
+ def test_set_up(self):
+ self.set_up()
+
+ @test
+ def test_ensure_mysql_is_running(self):
+ """Make sure MySQL is accessible before restarting."""
+ self.ensure_mysql_is_running()
+
+ @test(depends_on=[test_ensure_mysql_is_running], enabled=not FAKE_MODE)
+ def test_unsuccessful_restart(self):
+ """Restart MySQL via the REST when it should fail, assert it does."""
+ self.unsuccessful_restart()
+
+ @test(depends_on=[test_set_up],
+ runs_after=[test_ensure_mysql_is_running, test_unsuccessful_restart])
+ def test_successful_restart(self):
+ """Restart MySQL via the REST API successfully."""
+ self.successful_restart()
+
+
+@test(groups=[tests.INSTANCES, INSTANCE_GROUP, GROUP, GROUP_STOP_MYSQL],
+ depends_on_groups=[GROUP_START], depends_on=[create_user])
+class StopTests(RebootTestBase):
+ """Tests which involve stopping MySQL."""
+
+ def call_reboot(self):
+ self.instance.restart()
+
+ @before_class
+ def test_set_up(self):
+ self.set_up()
+
+ @test
+ def test_ensure_mysql_is_running(self):
+ """Make sure MySQL is accessible before restarting."""
+ self.ensure_mysql_is_running()
+
+ @test(depends_on=[test_ensure_mysql_is_running])
+ def test_stop_mysql(self):
+ """Stops MySQL."""
+ instance_info.dbaas_admin.management.stop(self.instance_id)
+ self.wait_for_broken_connection()
+ self.wait_for_failure_status()
+
+ @test(depends_on=[test_stop_mysql])
+ def test_instance_get_shows_volume_info_while_mysql_is_down(self):
+ """
+ Confirms the get call behaves appropriately while an instance is
+ down.
+ """
+ instance = self.dbaas.instances.get(self.instance_id)
+ with TypeCheck("instance", instance) as check:
+ check.has_field("volume", dict)
+ check.true('size' in instance.volume)
+ check.true('used' in instance.volume)
+ check.true(isinstance(instance.volume.get('size', None), int))
+ check.true(isinstance(instance.volume.get('used', None), float))
+
+ @test(depends_on=[test_set_up],
+ runs_after=[test_instance_get_shows_volume_info_while_mysql_is_down],
+ groups=['donut'])
+ def test_successful_restart_when_in_shutdown_state(self):
+ """Restart MySQL via the REST API successfully when MySQL is down."""
+ self.successful_restart()
+
+
+@test(groups=[tests.INSTANCES, INSTANCE_GROUP, GROUP, GROUP_REBOOT],
+ depends_on_groups=[GROUP_START], depends_on=[RestartTests, create_user])
+class RebootTests(RebootTestBase):
+ """Tests restarting instance."""
+
+ def call_reboot(self):
+ instance_info.dbaas_admin.management.reboot(self.instance_id)
+
+ @before_class
+ def test_set_up(self):
+ self.set_up()
+
+ @test
+ def test_ensure_mysql_is_running(self):
+ """Make sure MySQL is accessible before restarting."""
+ self.ensure_mysql_is_running()
+
+ @test(depends_on=[test_ensure_mysql_is_running], enabled=not FAKE_MODE)
+ def test_unsuccessful_restart(self):
+ """Restart MySQL via the REST when it should fail, assert it does."""
+ self.unsuccessful_restart()
+
+ @after_class(always_run=True)
+ def test_successful_restart(self):
+ """Restart MySQL via the REST API successfully."""
+ self.successful_restart()
+
+
+@test(groups=[tests.INSTANCES, INSTANCE_GROUP, GROUP,
+ GROUP + ".resize.instance"],
+ depends_on_groups=[GROUP_START], depends_on=[create_user],
+ runs_after=[RebootTests])
+class ResizeInstanceTest(ActionTestBase):
+
+ """
+ Integration Test cases for resize instance
+ """
+ @property
+ def flavor_id(self):
+ return instance_info.dbaas_flavor_href
+
+ def get_flavor_href(self, flavor_id=2):
+ res = instance_info.dbaas.find_flavor_and_self_href(flavor_id)
+ dbaas_flavor, dbaas_flavor_href = res
+ return dbaas_flavor_href
+
+ def wait_for_resize(self):
+ def is_finished_resizing():
+ instance = self.instance
+ if instance.status == "RESIZE":
+ return False
+ assert_equal("ACTIVE", instance.status)
+ return True
+ poll_until(is_finished_resizing, time_out=TIME_OUT_TIME)
+
+ @before_class
+ def setup(self):
+ self.set_up()
+ if USE_IP:
+ self.connection.connect()
+ assert_true(self.connection.is_connected(),
+ "Should be able to connect before resize.")
+ self.user_was_deleted = False
+
+ @test
+ def test_instance_resize_same_size_should_fail(self):
+ assert_raises(BadRequest, self.dbaas.instances.resize_instance,
+ self.instance_id, self.flavor_id)
+
+ def obtain_flavor_ids(self):
+ old_id = self.instance.flavor['id']
+ self.expected_old_flavor_id = old_id
+ res = instance_info.dbaas.find_flavor_and_self_href(old_id)
+ self.expected_dbaas_flavor, _dontcare_ = res
+
+ flavor_name = CONFIG.values.get('instance_bigger_flavor_name',
+ 'm1.small')
+ flavors = self.dbaas.find_flavors_by_name(flavor_name)
+ assert_equal(len(flavors), 1, "Number of flavors with name '%s' "
+ "found was '%d'." % (flavor_name, len(flavors)))
+ flavor = flavors[0]
+ assert_true(flavor is not None, "Flavor '%s' not found!" % flavor_name)
+ flavor_href = self.dbaas.find_flavor_self_href(flavor)
+ assert_true(flavor_href is not None,
+ "Flavor href '%s' not found!" % flavor_name)
+ self.expected_new_flavor_id = flavor.id
+
+ @test(depends_on=[test_instance_resize_same_size_should_fail])
+ def test_status_changed_to_resize(self):
+ self.log_current_users()
+ self.obtain_flavor_ids()
+ self.dbaas.instances.resize_instance(
+ self.instance_id,
+ self.get_flavor_href(flavor_id=self.expected_new_flavor_id))
+ assert_equal(202, self.dbaas.last_http_code)
+
+ #(WARNING) IF THE RESIZE IS WAY TOO FAST THIS WILL FAIL
+ assert_unprocessable(
+ self.dbaas.instances.resize_instance,
+ self.instance_id,
+ self.get_flavor_href(flavor_id=self.expected_new_flavor_id))
+
+ @test(depends_on=[test_status_changed_to_resize])
+ @time_out(TIME_OUT_TIME)
+ def test_instance_returns_to_active_after_resize(self):
+ self.wait_for_resize()
+
+ @test(depends_on=[test_instance_returns_to_active_after_resize])
+ def resize_should_not_delete_users(self):
+ """Resize should not delete users."""
+ # Resize has an incredibly weird bug where users are deleted after
+ # a resize. The code below is an attempt to catch this while proceeding
+ # with the rest of the test (note the use of runs_after).
+ if USE_IP:
+ self.connection.connect()
+ if not self.connection.is_connected():
+ # Ok, this is def. a failure, but before we toss up an error
+ # lets recreate to see how far we can get.
+ CONFIG.get_report().log(
+ "Having to recreate the test_user! Resizing killed it!")
+ self.log_current_users()
+ self.create_user()
+ fail("Somehow, the resize made the test user disappear.")
+
+ @test(depends_on=[test_instance_returns_to_active_after_resize],
+ runs_after=[resize_should_not_delete_users])
+ def test_make_sure_mysql_is_running_after_resize(self):
+ self.ensure_mysql_is_running()
+ actual = self.get_flavor_href(self.instance.flavor['id'])
+ expected = self.get_flavor_href(flavor_id=self.expected_new_flavor_id)
+ assert_equal(actual, expected)
+
+ @test(depends_on=[test_make_sure_mysql_is_running_after_resize])
+ @time_out(TIME_OUT_TIME)
+ def test_resize_down(self):
+ expected_dbaas_flavor = self.expected_dbaas_flavor
+ self.dbaas.instances.resize_instance(
+ self.instance_id,
+ self.get_flavor_href(flavor_id=self.expected_old_flavor_id))
+ assert_equal(202, self.dbaas.last_http_code)
+ self.old_dbaas_flavor = instance_info.dbaas_flavor
+ instance_info.dbaas_flavor = expected_dbaas_flavor
+ self.wait_for_resize()
+ assert_equal(str(self.instance.flavor['id']),
+ str(self.expected_old_flavor_id))
+
+
+@test(groups=[tests.INSTANCES, INSTANCE_GROUP, GROUP,
+ GROUP + ".resize.instance"],
+ depends_on_groups=[GROUP_START], depends_on=[create_user],
+ runs_after=[RebootTests, ResizeInstanceTest])
+def resize_should_not_delete_users():
+ if USER_WAS_DELETED:
+ fail("Somehow, the resize made the test user disappear.")
+
+
+@test(depends_on_classes=[ResizeInstanceTest], depends_on=[create_user],
+ groups=[GROUP, tests.INSTANCES])
+class ResizeInstanceVolume(object):
+ """ Resize the volume of the instance """
+
+ @before_class
+ def setUp(self):
+ self.old_volume_size = int(instance_info.volume['size'])
+ self.new_volume_size = self.old_volume_size + 1
+
+ # Create some databases to check they still exist after the resize
+ self.expected_dbs = ['salmon', 'halibut']
+ databases = []
+ for name in self.expected_dbs:
+ databases.append({"name": name})
+ instance_info.dbaas.databases.create(instance_info.id, databases)
+
+ @test
+ @time_out(60)
+ def test_volume_resize(self):
+ instance_info.dbaas.instances.resize_volume(instance_info.id,
+ self.new_volume_size)
+
+ @test
+ @time_out(300)
+ def test_volume_resize_success(self):
+
+ def check_resize_status():
+ instance = instance_info.dbaas.instances.get(instance_info.id)
+ if instance.status == "ACTIVE":
+ return True
+ elif instance.status == "RESIZE":
+ return False
+ else:
+ fail("Status should not be %s" % instance.status)
+
+ poll_until(check_resize_status, sleep_time=2, time_out=300)
+ instance = instance_info.dbaas.instances.get(instance_info.id)
+ assert_equal(instance.volume['size'], self.new_volume_size)
+
+ @test
+ @time_out(300)
+ def test_volume_resize_success_databases(self):
+ databases = instance_info.dbaas.databases.list(instance_info.id)
+ db_list = []
+ for database in databases:
+ db_list.append(database.name)
+ for name in self.expected_dbs:
+ if not name in db_list:
+ fail("Database %s was not found after the volume resize. "
+ "Returned list: %s" % (name, databases))
+
+
+# This tests the ability of the guest to upgrade itself.
+# It is necessarily tricky because we need to be able to upload a new copy of
+# the guest into an apt-repo in the middle of the test.
+# "guest-update-test" is where the knowledge of how to do this is set in the
+# test conf. If it is not specified this test never runs.
+UPDATE_GUEST_CONF = CONFIG.values.get("guest-update-test", None)
+
+
+@test(groups=[tests.INSTANCES, INSTANCE_GROUP, GROUP, GROUP + ".update_guest"],
+ depends_on=[create_user],
+ depends_on_groups=[GROUP_START])
+class UpdateGuest(object):
+
+ def get_version(self):
+ info = instance_info.dbaas_admin.diagnostics.get(instance_info.id)
+ return info.version
+
+ @before_class(enabled=UPDATE_GUEST_CONF is not None)
+ def check_version_is_old(self):
+ """Make sure we have the old version before proceeding."""
+ self.old_version = self.get_version()
+ self.next_version = UPDATE_GUEST_CONF["next-version"]
+ assert_not_equal(self.old_version, self.next_version)
+
+ @test(enabled=UPDATE_GUEST_CONF is not None)
+ def upload_update_to_repo(self):
+ cmds = UPDATE_GUEST_CONF["install-repo-cmd"]
+ utils.execute(*cmds, run_as_root=True)
+
+ @test(enabled=UPDATE_GUEST_CONF is not None,
+ depends_on=[upload_update_to_repo])
+ def update_and_wait_to_finish(self):
+ instance_info.dbaas_admin.management.update(instance_info.id)
+
+ def finished():
+ current_version = self.get_version()
+ if current_version == self.next_version:
+ return True
+ # The only valid thing for it to be aside from next_version is
+ # old version.
+ assert_equal(current_version, self.old_version)
+ poll_until(finished, sleep_time=1, time_out=3 * 60)
+
+ @test(enabled=UPDATE_GUEST_CONF is not None,
+ depends_on=[upload_update_to_repo])
+ @time_out(30)
+ def update_again(self):
+ """Test the wait time of a pointless update."""
+ instance_info.dbaas_admin.management.update(instance_info.id)
+ # Make sure this isn't taking too long.
+ instance_info.dbaas_admin.diagnostics.get(instance_info.id)
diff --git a/reddwarf/tests/api/instances_delete.py b/reddwarf/tests/api/instances_delete.py
new file mode 100644
index 00000000..9bef213d
--- /dev/null
+++ b/reddwarf/tests/api/instances_delete.py
@@ -0,0 +1,114 @@
+import time
+
+from proboscis import after_class
+from proboscis import before_class
+from proboscis import test
+from proboscis.asserts import *
+from proboscis.decorators import time_out
+
+from reddwarfclient import exceptions
+from reddwarf.tests.util import create_dbaas_client
+from reddwarf.tests.util import poll_until
+from reddwarf.tests.util import test_config
+from reddwarf.tests.util.users import Requirements
+
+
+class TestBase(object):
+
+ def set_up(self):
+ reqs = Requirements(is_admin=True)
+ self.user = test_config.users.find_user(reqs)
+ self.dbaas = create_dbaas_client(self.user)
+
+ def create_instance(self, name, size=1):
+ result = self.dbaas.instances.create(name, 1, {'size': size}, [], [])
+ return result.id
+
+ def wait_for_instance_status(self, instance_id, status="ACTIVE"):
+ poll_until(lambda: self.dbaas.instances.get(instance_id),
+ lambda instance: instance.status == status,
+ time_out=10)
+
+ def wait_for_instance_task_status(self, instance_id, description):
+ poll_until(lambda: self.dbaas.management.show(instance_id),
+ lambda instance: instance.task_description == description,
+ time_out=10)
+
+ def is_instance_deleted(self, instance_id):
+ while True:
+ try:
+ instance = self.dbaas.instances.get(instance_id)
+ except exceptions.NotFound:
+ return True
+ time.sleep(.5)
+
+ def get_task_info(self, instance_id):
+ instance = self.dbaas.management.show(instance_id)
+ return instance.status, instance.task_description
+
+ def delete_instance(self, instance_id, assert_deleted=True):
+ instance = self.dbaas.instances.get(instance_id)
+ instance.delete()
+ if assert_deleted:
+ assert_true(self.is_instance_deleted(instance_id))
+
+ def delete_errored_instance(self, instance_id):
+ self.wait_for_instance_status(instance_id, 'ERROR')
+ status, desc = self.get_task_info(instance_id)
+ assert_equal(status, "ERROR")
+ self.delete_instance(instance_id)
+
+
+@test(runs_after_groups=["services.initialize"],
+ groups=['dbaas.api.instances.delete'])
+class ErroredInstanceDelete(TestBase):
+ """
+ Test that an instance in an ERROR state is actually deleted when delete
+ is called.
+ """
+
+ @before_class
+ def set_up(self):
+ """Create some flawed instances."""
+ super(ErroredInstanceDelete, self).set_up()
+ # Create an instance that fails during server prov.
+ self.server_error = self.create_instance('test_SERVER_ERROR')
+ # Create an instance that fails during volume prov.
+ self.volume_error = self.create_instance('test_VOLUME_ERROR', size=9)
+ # Create an instance that fails during DNS prov.
+ #self.dns_error = self.create_instance('test_DNS_ERROR')
+ # Create an instance that fails while it's been deleted the first time.
+ self.delete_error = self.create_instance('test_ERROR_ON_DELETE')
+
+ @test
+ @time_out(20)
+ def delete_server_error(self):
+ self.delete_errored_instance(self.server_error)
+
+ @test
+ @time_out(20)
+ def delete_volume_error(self):
+ self.delete_errored_instance(self.volume_error)
+
+ @test(enabled=False)
+ @time_out(20)
+ def delete_dns_error(self):
+ self.delete_errored_instance(self.dns_error)
+
+ @test
+ @time_out(20)
+ def delete_error_on_delete_instance(self):
+ id = self.delete_error
+ self.wait_for_instance_status(id, 'ACTIVE')
+ self.wait_for_instance_task_status(id, 'No tasks for the instance.')
+ instance = self.dbaas.management.show(id)
+ assert_equal(instance.status, "ACTIVE")
+ assert_equal(instance.task_description, 'No tasks for the instance.')
+ # Try to delete the instance. This fails the first time due to how
+ # the test fake is setup.
+ self.delete_instance(id, assert_deleted=False)
+ instance = self.dbaas.management.show(id)
+ assert_equal(instance.status, "SHUTDOWN")
+ assert_equal(instance.task_description, "Deleting the instance.")
+ # Try a second time. This will succeed.
+ self.delete_instance(id)
diff --git a/reddwarf/tests/api/instances_mysql_down.py b/reddwarf/tests/api/instances_mysql_down.py
new file mode 100644
index 00000000..bbca1307
--- /dev/null
+++ b/reddwarf/tests/api/instances_mysql_down.py
@@ -0,0 +1,119 @@
+# 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.
+"""
+Extra tests to create an instance, shut down MySQL, and delete it.
+"""
+
+from proboscis.decorators import time_out
+from proboscis import after_class
+from proboscis import before_class
+from proboscis import test
+from proboscis.asserts import assert_equal
+from proboscis.asserts import assert_false
+from proboscis.asserts import assert_is
+from proboscis.asserts import assert_is_not
+from proboscis.asserts import assert_is_none
+from proboscis.asserts import assert_not_equal
+from proboscis.asserts import assert_raises
+from proboscis.asserts import assert_true
+from proboscis.asserts import Check
+from proboscis.asserts import fail
+import time
+
+from datetime import datetime
+from reddwarfclient import exceptions
+from reddwarf.tests import util
+from reddwarf.tests.util import create_client
+from reddwarf.tests.util import poll_until
+from reddwarf.tests.util import test_config
+
+
+@test(groups=["dbaas.api.instances.down"])
+class TestBase(object):
+
+ @before_class
+ def set_up(self):
+ self.client = create_client(is_admin=False)
+ self.mgmt_client = create_client(is_admin=True)
+ flavor_name = test_config.values.get('instance_flavor_name', 'm1.tiny')
+ flavors = self.client.find_flavors_by_name(flavor_name)
+ self.flavor_id = flavors[0].id
+ self.name = "TEST_" + str(datetime.now())
+ # Get the resize to flavor.
+ flavor2_name = test_config.values.get('instance_bigger_flavor_name',
+ 'm1.small')
+ flavors2 = self.client.find_flavors_by_name(flavor2_name)
+ self.new_flavor_id = flavors2[0].id
+ assert_not_equal(self.flavor_id, self.new_flavor_id)
+
+ def _wait_for_active(self):
+ poll_until(lambda: self.client.instances.get(self.id),
+ lambda instance: instance.status == "ACTIVE",
+ time_out=(60 * 8))
+
+ def _wait_for_new_volume_size(self, new_size):
+ poll_until(lambda: self.client.instances.get(self.id),
+ lambda instance: instance.volume['size'] == new_size,
+ time_out=(60 * 8))
+
+ @test
+ def create_instance(self):
+ initial = self.client.instances.create(self.name, self.flavor_id,
+ {'size': 1}, [], [])
+ self.id = initial.id
+ self._wait_for_active()
+
+ @test(depends_on=[create_instance])
+ def put_into_shutdown_state(self):
+ instance = self.client.instances.get(self.id)
+ self.mgmt_client.management.stop(self.id)
+
+ @test(depends_on=[put_into_shutdown_state])
+ @time_out(60 * 5)
+ def resize_instance_in_shutdown_state(self):
+ self.client.instances.resize_instance(self.id, self.new_flavor_id)
+ self._wait_for_active()
+
+ @test(depends_on=[create_instance],
+ runs_after=[resize_instance_in_shutdown_state])
+ def put_into_shutdown_state_2(self):
+ instance = self.client.instances.get(self.id)
+ self.mgmt_client.management.stop(self.id)
+
+ @test(depends_on=[put_into_shutdown_state_2])
+ @time_out(60 * 5)
+ def resize_volume_in_shutdown_state(self):
+ self.client.instances.resize_volume(self.id, 2)
+ self._wait_for_new_volume_size(2)
+
+ @test(depends_on=[create_instance],
+ runs_after=[resize_volume_in_shutdown_state])
+ def put_into_shutdown_state_3(self):
+ instance = self.client.instances.get(self.id)
+ self.mgmt_client.management.stop(self.id)
+
+ @test(depends_on=[create_instance],
+ runs_after=[put_into_shutdown_state_3])
+ @time_out(2 * 60)
+ def delete_instances(self):
+ instance = self.client.instances.get(self.id)
+ instance.delete()
+ while True:
+ try:
+ instance = self.client.instances.get(self.id)
+ assert_equal("SHUTDOWN", instance.status)
+ except exceptions.NotFound:
+ break
+ time.sleep(0.25)
diff --git a/reddwarf/tests/api/instances_pagination.py b/reddwarf/tests/api/instances_pagination.py
new file mode 100644
index 00000000..56c0753d
--- /dev/null
+++ b/reddwarf/tests/api/instances_pagination.py
@@ -0,0 +1,227 @@
+from proboscis.decorators import time_out
+from proboscis import after_class
+from proboscis import before_class
+from proboscis import test
+from proboscis.asserts import assert_equal
+from proboscis.asserts import assert_false
+from proboscis.asserts import assert_is
+from proboscis.asserts import assert_is_not
+from proboscis.asserts import assert_is_none
+from proboscis.asserts import assert_not_equal
+from proboscis.asserts import assert_raises
+from proboscis.asserts import assert_true
+from proboscis.asserts import Check
+from proboscis.asserts import fail
+import time
+
+from reddwarfclient import exceptions
+from reddwarf.tests import util
+from reddwarf.tests.util import create_dbaas_client
+from reddwarf.tests.util import test_config
+from reddwarf.tests.util.users import Requirements
+
+
+class TestBase(object):
+
+ def set_up(self):
+ """Create a ton of instances."""
+ reqs = Requirements(is_admin=False)
+ self.user = test_config.users.find_user(reqs)
+ self.dbaas = create_dbaas_client(self.user)
+
+ def delete_instances(self):
+ chunk = 0
+ while True:
+ chunk += 1
+ attempts = 0
+ instances = self.dbaas.instances.list()
+ if len(instances) == 0:
+ break
+ # Sit around and try to delete this chunk.
+ while True:
+ instance_results = []
+ attempts += 1
+ deleted_count = 0
+ for instance in instances:
+ try:
+ instance.delete()
+ result = "[w]"
+ except exceptions.UnprocessableEntity:
+ result = "[W]"
+ except exceptions.NotFound:
+ result = "[O]"
+ deleted_count += 1
+ except Exception:
+ result = "[X]"
+ instance_results.append(result)
+ print("Chunk %d, attempt %d : %s"
+ % (chunk, attempts, ",".join(instance_results)))
+ if deleted_count == len(instances):
+ break
+ time.sleep(0.2)
+
+ def create_instances(self):
+ self.ids = []
+ for index in range(self.max):
+ name = "multi-%03d" % index
+ result = self.dbaas.instances.create(name, 1,
+ {'size': 1}, [], [])
+ self.ids.append(result.id)
+ # Sort the list of IDs in order, so we can confirm the lists pagination
+ # returns is also sorted correctly.
+ self.ids.sort()
+
+ @staticmethod
+ def assert_instances_sorted_by_ids(instances):
+ # Assert that the strings are always increasing.
+ last_id = ""
+ for instance in instances:
+ assert_true(last_id < instance.id)
+
+ def print_list(self, instances):
+ print("Length = %d" % len(instances))
+ print(",".join([instance.id for instance in instances]))
+
+ def test_pagination(self, requested_limit, requested_marker,
+ expected_length, expected_marker, expected_last_item):
+ instances = self.dbaas.instances.list(limit=requested_limit,
+ marker=requested_marker)
+ marker = instances.next
+
+ self.print_list(instances)
+
+ # Better get as many as we asked for.
+ assert_equal(len(instances), expected_length)
+ # The last one should be roughly this one in the list.
+ assert_equal(instances[-1].id, expected_last_item)
+ # Because limit < count, the marker must be something.
+ if expected_marker:
+ assert_is_not(marker, None)
+ assert_equal(marker, expected_marker)
+ else:
+ assert_is_none(marker)
+ self.assert_instances_sorted_by_ids(instances)
+
+
+@test(runs_after_groups=["dbaas.guest.shutdown"],
+ groups=['dbaas.api.instances.pagination'])
+class SimpleCreateAndDestroy(TestBase):
+ """
+ It turns out a big part of guaranteeing pagination works is to make sure
+ we can create a big batch of instances and delete them without problems.
+ Even in fake mode though its worth it to check this is the case.
+ """
+
+ max = 5
+
+ @before_class
+ def set_up(self):
+ """Create a ton of instances."""
+ super(SimpleCreateAndDestroy, self).set_up()
+ self.delete_instances()
+
+ @test
+ def spin_up(self):
+ self.create_instances()
+
+ @after_class(always_run=True)
+ def tear_down(self):
+ self.delete_instances()
+
+
+@test(runs_after_groups=["dbaas.guest.shutdown"],
+ groups=['dbaas.api.instances.pagination'])
+class InstancePagination50(TestBase):
+
+ max = 50
+
+ @before_class
+ def set_up(self):
+ """Create a ton of instances."""
+ super(InstancePagination50, self).set_up()
+ self.delete_instances()
+ self.create_instances()
+
+ @after_class(always_run=True)
+ def tear_down(self):
+ """Tear down all instances."""
+ self.delete_instances()
+
+ @test
+ def pagination_short(self):
+ self.test_pagination(requested_limit=10, requested_marker=None,
+ expected_length=10, expected_marker=self.ids[9],
+ expected_last_item=self.ids[9])
+
+ @test
+ def pagination_default(self):
+ self.test_pagination(requested_limit=None, requested_marker=None,
+ expected_length=20, expected_marker=self.ids[19],
+ expected_last_item=self.ids[19])
+
+ @test
+ def pagination_full(self):
+ self.test_pagination(requested_limit=50, requested_marker=None,
+ expected_length=20, expected_marker=self.ids[19],
+ expected_last_item=self.ids[19])
+
+
+@test(runs_after_groups=["dbaas.guest.shutdown"],
+ groups=['dbaas.api.instances.pagination'])
+class InstancePagination20(TestBase):
+
+ max = 20
+
+ @before_class
+ def set_up(self):
+ """Create a ton of instances."""
+ super(InstancePagination20, self).set_up()
+ self.delete_instances()
+ self.create_instances()
+
+ @after_class(always_run=True)
+ def tear_down(self):
+ """Tear down all instances."""
+ self.delete_instances()
+
+ @test
+ def pagination_short(self):
+ self.test_pagination(requested_limit=10, requested_marker=None,
+ expected_length=10, expected_marker=self.ids[9],
+ expected_last_item=self.ids[9])
+
+ @test
+ def pagination_default(self):
+ self.test_pagination(requested_limit=None, requested_marker=None,
+ expected_length=20, expected_marker=None,
+ expected_last_item=self.ids[19])
+
+ @test
+ def pagination_full(self):
+ self.test_pagination(requested_limit=20, requested_marker=None,
+ expected_length=20, expected_marker=None,
+ expected_last_item=self.ids[19])
+
+ @test
+ def pagination_overkill(self):
+ self.test_pagination(requested_limit=30, requested_marker=None,
+ expected_length=20, expected_marker=None,
+ expected_last_item=self.ids[19])
+
+ @test
+ def pagination_last_half(self):
+ self.test_pagination(requested_limit=10, requested_marker=self.ids[9],
+ expected_length=10, expected_marker=None,
+ expected_last_item=self.ids[19])
+
+ @test
+ def pagination_third_quarter(self):
+ self.test_pagination(requested_limit=5, requested_marker=self.ids[9],
+ expected_length=5, expected_marker=self.ids[14],
+ expected_last_item=self.ids[14])
+
+ @test
+ def pagination_fourth_quarter(self):
+ self.test_pagination(requested_limit=20, requested_marker=self.ids[14],
+ expected_length=5, expected_marker=None,
+ expected_last_item=self.ids[19])
diff --git a/reddwarf/tests/api/mgmt/__init__.py b/reddwarf/tests/api/mgmt/__init__.py
new file mode 100644
index 00000000..40d014dd
--- /dev/null
+++ b/reddwarf/tests/api/mgmt/__init__.py
@@ -0,0 +1,13 @@
+# Copyright 2011 OpenStack LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
diff --git a/reddwarf/tests/api/mgmt/accounts.py b/reddwarf/tests/api/mgmt/accounts.py
new file mode 100644
index 00000000..26e46c06
--- /dev/null
+++ b/reddwarf/tests/api/mgmt/accounts.py
@@ -0,0 +1,219 @@
+# Copyright 2011 OpenStack LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+from reddwarfclient import exceptions
+
+from nose.plugins.skip import SkipTest
+
+from proboscis import after_class
+from proboscis import before_class
+from proboscis import test
+from proboscis.asserts import *
+from proboscis.decorators import time_out
+
+from reddwarf import tests
+from reddwarf.tests.api.instances import instance_info
+from reddwarf.tests.util import test_config
+from reddwarf.tests.util import create_dbaas_client
+from reddwarf.tests.util import poll_until
+from reddwarf.tests.util.users import Requirements
+from reddwarf.tests.api.instances import existing_instance
+
+GROUP = "dbaas.api.mgmt.accounts"
+
+
+@test(groups=[tests.DBAAS_API, GROUP, tests.PRE_INSTANCES],
+ depends_on_groups=["services.initialize"])
+class AccountsBeforeInstanceCreation(object):
+
+ @before_class
+ def setUp(self):
+ self.user = test_config.users.find_user(Requirements(is_admin=True))
+ self.client = create_dbaas_client(self.user)
+
+ @test
+ def test_invalid_account(self):
+ raise SkipTest("Don't have a good way to know if accounts are valid.")
+ assert_raises(exceptions.NotFound, self.client.accounts.show,
+ "asd#4#@fasdf")
+
+ @test
+ def test_invalid_account_fails(self):
+ account_info = self.client.accounts.show("badaccount")
+ assert_not_equal(self.user.tenant_id, account_info.id)
+
+ @test
+ def test_account_zero_instances(self):
+ account_info = self.client.accounts.show(self.user.tenant_id)
+ expected_instances = 0 if not existing_instance() else 1
+ assert_equal(expected_instances, len(account_info.instances))
+ expected = self.user.tenant_id
+ if expected is None:
+ expected = "None"
+ assert_equal(expected, account_info.id)
+
+ @test
+ def test_list_empty_accounts(self):
+ accounts_info = self.client.accounts.index()
+ expected_accounts = 0 if not existing_instance() else 1
+ assert_equal(expected_accounts, len(accounts_info.accounts))
+
+
+@test(groups=[tests.INSTANCES, GROUP], depends_on_groups=["dbaas.listing"])
+class AccountsAfterInstanceCreation(object):
+
+ @before_class
+ def setUp(self):
+ self.user = test_config.users.find_user(Requirements(is_admin=True))
+ self.client = create_dbaas_client(self.user)
+
+ @test
+ def test_account_details_available(self):
+ if test_config.auth_strategy == "fake":
+ raise SkipTest("Skipping this as auth is faked anyway.")
+ account_info = self.client.accounts.show(instance_info.user.tenant_id)
+ # Now check the results.
+ expected = instance_info.user.tenant_id
+ if expected is None:
+ expected = "None"
+ print("account_id.id = '%s'" % account_info.id)
+ print("expected = '%s'" % expected)
+ assert_equal(account_info.id, expected)
+ # Instances: Here we know we've only created one instance.
+ assert_equal(1, len(account_info.instances))
+ assert_is_not_none(account_info.instances[0]['host'])
+ # We know the there's only 1 instance
+ instance = account_info.instances[0]
+ print("instances in account: %s" % instance)
+ assert_equal(instance['id'], instance_info.id)
+ assert_equal(instance['name'], instance_info.name)
+ assert_equal(instance['status'], "ACTIVE")
+ assert_is_not_none(instance['host'])
+
+ @test
+ def test_list_accounts(self):
+ if test_config.auth_strategy == "fake":
+ raise SkipTest("Skipping this as auth is faked anyway.")
+ accounts_info = self.client.accounts.index()
+ assert_equal(1, len(accounts_info.accounts))
+ account = accounts_info.accounts[0]
+ assert_equal(1, account['num_instances'])
+ assert_equal(instance_info.user.tenant_id, account['id'])
+
+
+@test(groups=[tests.POST_INSTANCES, GROUP],
+ depends_on_groups=["dbaas.guest.shutdown"])
+class AccountsAfterInstanceDeletion(object):
+
+ @before_class
+ def setUp(self):
+ self.user = test_config.users.find_user(Requirements(is_admin=True))
+ self.client = create_dbaas_client(self.user)
+
+ @test
+ def test_no_details_empty_account(self):
+ account_info = self.client.accounts.show(instance_info.user.tenant_id)
+ assert_equal(0, len(account_info.instances))
+
+
+@test(groups=["fake.dbaas.api.mgmt.allaccounts"],
+ depends_on_groups=["services.initialize"])
+class AllAccounts(object):
+ max = 5
+
+ def _delete_instances_for_users(self):
+ for user in self.users:
+ user_client = create_dbaas_client(user)
+ while True:
+ deleted_count = 0
+ user_instances = user_client.instances.list()
+ for instance in user_instances:
+ try:
+ instance.delete()
+ except exceptions.NotFound:
+ deleted_count += 1
+ except Exception:
+ print "Failed to delete instance"
+ if deleted_count == len(user_instances):
+ break
+
+ def _create_instances_for_users(self):
+ for user in self.users:
+ user_client = create_dbaas_client(user)
+ for index in range(self.max):
+ name = "instance-%s-%03d" % (user.auth_user, index)
+ user_client.instances.create(name, 1, {'size': 1}, [], [])
+
+ @before_class
+ def setUp(self):
+ admin_req = Requirements(is_admin=True)
+ self.admin_user = test_config.users.find_user(admin_req)
+ self.admin_client = create_dbaas_client(self.admin_user)
+ user_req = Requirements(is_admin=False)
+ self.users = test_config.users.find_all_users_who_satisfy(user_req)
+ self.user_tenant_ids = [user.tenant_id for user in self.users]
+ self._create_instances_for_users()
+
+ @test
+ def test_list_accounts_with_multiple_users(self):
+ accounts_info = self.admin_client.accounts.index()
+ for account in accounts_info.accounts:
+ assert_true(account['id'] in self.user_tenant_ids)
+ assert_equal(self.max, account['num_instances'])
+
+ @after_class(always_run=True)
+ @time_out(60)
+ def tear_down(self):
+ self._delete_instances_for_users()
+
+
+@test(groups=["fake.%s.broken" % GROUP],
+ depends_on_groups=["services.initialize"])
+class AccountWithBrokenInstance(object):
+
+ @before_class
+ def setUp(self):
+ self.user = test_config.users.find_user(Requirements(is_admin=True))
+ self.client = create_dbaas_client(self.user)
+ self.name = 'test_SERVER_ERROR'
+ # Create an instance with a broken compute instance.
+ self.response = self.client.instances.create(self.name, 1,
+ {'size': 1}, [])
+ poll_until(lambda: self.client.instances.get(self.response.id),
+ lambda instance: instance.status == 'ERROR',
+ time_out=10)
+ self.instance = self.client.instances.get(self.response.id)
+ print "Status: %s" % self.instance.status
+ msg = "Instance did not drop to error after server prov failure."
+ assert_equal(self.instance.status, "ERROR", msg)
+
+ @test
+ def no_compute_instance_no_problem(self):
+ '''Get account by ID shows even instances lacking computes'''
+ if test_config.auth_strategy == "fake":
+ raise SkipTest("Skipping this as auth is faked anyway.")
+ account_info = self.client.accounts.show(self.user.tenant_id)
+ # All we care about is that accounts.show doesn't 500 on us
+ # for having a broken instance in the roster.
+ assert_equal(len(account_info.instances), 1)
+ instance = account_info.instances[0]
+ assert_true(isinstance(instance['id'], basestring))
+ assert_equal(len(instance['id']), 36)
+ assert_equal(instance['name'], self.name)
+ assert_equal(instance['status'], "ERROR")
+ assert_is_none(instance['host'])
+
+ @after_class
+ def tear_down(self):
+ self.client.instances.delete(self.response.id)
diff --git a/reddwarf/tests/api/mgmt/admin_required.py b/reddwarf/tests/api/mgmt/admin_required.py
new file mode 100644
index 00000000..919c4403
--- /dev/null
+++ b/reddwarf/tests/api/mgmt/admin_required.py
@@ -0,0 +1,93 @@
+# Copyright 2011 OpenStack LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+from reddwarfclient.exceptions import Unauthorized
+
+from proboscis import before_class
+from proboscis import test
+from proboscis.asserts import assert_raises
+
+from reddwarf import tests
+from reddwarf.tests.util import create_dbaas_client
+from reddwarf.tests.util import test_config
+from reddwarf.tests.util.users import Requirements
+
+GROUP = "dbaas.api.mgmt.admin"
+
+
+@test(groups=[tests.DBAAS_API, GROUP, tests.PRE_INSTANCES],
+ depends_on_groups=["services.initialize"])
+class TestAdminRequired(object):
+ """
+ These tests verify that admin privileges are checked
+ when calling management level functions.
+ """
+
+ @before_class
+ def setUp(self):
+ """ Create the user and client for use in the subsequent tests."""
+ self.user = test_config.users.find_user(Requirements(is_admin=False))
+ self.dbaas = create_dbaas_client(self.user)
+
+ @test
+ def test_accounts_show(self):
+ """ A regular user may not view the details of any account. """
+ assert_raises(Unauthorized, self.dbaas.accounts.show, 0)
+
+ @test
+ def test_hosts_index(self):
+ """ A regular user may not view the list of hosts. """
+ assert_raises(Unauthorized, self.dbaas.hosts.index)
+
+ @test
+ def test_hosts_get(self):
+ """ A regular user may not view the details of any host. """
+ assert_raises(Unauthorized, self.dbaas.hosts.get, 0)
+
+ @test
+ def test_mgmt_show(self):
+ """
+ A regular user may not view the management details
+ of any instance.
+ """
+ assert_raises(Unauthorized, self.dbaas.management.show, 0)
+
+ @test
+ def test_mgmt_root_history(self):
+ """
+ A regular user may not view the root access history of
+ any instance.
+ """
+ assert_raises(Unauthorized,
+ self.dbaas.management.root_enabled_history, 0)
+
+ @test
+ def test_mgmt_instance_reboot(self):
+ """ A regular user may not perform an instance reboot. """
+ assert_raises(Unauthorized, self.dbaas.management.reboot, 0)
+
+ @test
+ def test_storage_index(self):
+ """ A regular user may not view the list of storage available. """
+ assert_raises(Unauthorized, self.dbaas.storage.index)
+
+ @test
+ def test_diagnostics_get(self):
+ """ A regular user may not view the diagnostics. """
+ assert_raises(Unauthorized, self.dbaas.diagnostics.get, 0)
+
+ @test
+ def test_hwinfo_get(self):
+ """ A regular user may not view the hardware info. """
+ assert_raises(Unauthorized, self.dbaas.hwinfo.get, 0)
diff --git a/reddwarf/tests/api/mgmt/instances.py b/reddwarf/tests/api/mgmt/instances.py
new file mode 100644
index 00000000..9daf3615
--- /dev/null
+++ b/reddwarf/tests/api/mgmt/instances.py
@@ -0,0 +1,225 @@
+# Copyright 2011 OpenStack LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+from reddwarfclient import exceptions
+
+from proboscis import before_class
+from proboscis import test
+from proboscis.asserts import assert_equal
+from proboscis.asserts import assert_raises
+from proboscis.asserts import assert_true
+from proboscis.check import Check
+
+from reddwarf import tests
+from reddwarf.tests.config import CONFIG
+from reddwarf.tests.util import create_client
+from reddwarf.tests.util import create_dbaas_client
+from reddwarf.tests.util.users import Requirements
+from reddwarf.tests.util.check import CollectionCheck
+from reddwarf.tests.util.check import TypeCheck
+
+from reddwarf.tests.api.instances import CreateInstance
+from reddwarf.tests.api.instances import instance_info
+from reddwarf.tests.api.instances import GROUP_START
+from reddwarf.tests.api.instances import GROUP_TEST
+from reddwarf.tests.util import poll_until
+
+GROUP = "dbaas.api.mgmt.instances"
+
+
+@test(groups=[GROUP])
+def mgmt_index_requires_admin_account():
+ """ Verify that an admin context is required to call this function. """
+ client = create_client(is_admin=False)
+ assert_raises(exceptions.Unauthorized, client.management.index)
+
+
+# These functions check some dictionaries in the returned response.
+def flavor_check(flavor):
+ with CollectionCheck("flavor", flavor) as check:
+ check.has_element("id", basestring)
+ check.has_element("links", list)
+
+
+def guest_status_check(guest_status):
+ with CollectionCheck("guest_status", guest_status) as check:
+ check.has_element("state_description", basestring)
+
+
+def volume_check(volume):
+ with CollectionCheck("volume", volume) as check:
+ check.has_element("id", basestring)
+ check.has_element("size", int)
+
+
+@test(depends_on_groups=[GROUP_START], groups=[GROUP, GROUP_TEST])
+def mgmt_instance_get():
+ """ Tests the mgmt instances index method. """
+ reqs = Requirements(is_admin=True)
+ user = CONFIG.users.find_user(reqs)
+ client = create_dbaas_client(user)
+ mgmt = client.management
+ # Grab the info.id created by the main instance test which is stored in
+ # a global.
+ id = instance_info.id
+ api_instance = mgmt.show(id)
+
+ # Print out all fields for extra info if the test fails.
+ for name in dir(api_instance):
+ print(str(name) + "=" + str(getattr(api_instance, name)))
+ with TypeCheck("instance", api_instance) as instance:
+ instance.has_field('created', basestring)
+ instance.has_field('deleted', bool)
+ # If the instance hasn't been deleted, this should be false... but
+ # lets avoid creating more ordering work.
+ instance.has_field('deleted_at', (basestring, None))
+ instance.has_field('flavor', dict, flavor_check)
+ instance.has_field('guest_status', dict, guest_status_check)
+ instance.has_field('id', basestring)
+ instance.has_field('links', list)
+ instance.has_field('name', basestring)
+ #instance.has_field('server_status', basestring)
+ instance.has_field('status', basestring)
+ instance.has_field('tenant_id', basestring)
+ instance.has_field('updated', basestring)
+ # Can be None if no volume is given on this instance.
+ if CONFIG.values['reddwarf_main_instance_has_volume']:
+ instance.has_field('volume', dict, volume_check)
+ else:
+ instance.has_field('volume', None)
+ #TODO: Validate additional fields, assert no extra fields exist.
+ with CollectionCheck("server", api_instance.server) as server:
+ server.has_element("addresses", dict)
+ server.has_element("deleted", bool)
+ server.has_element("deleted_at", (basestring, None))
+ server.has_element("host", basestring)
+ server.has_element("id", basestring)
+ server.has_element("local_id", int)
+ server.has_element("name", basestring)
+ server.has_element("status", basestring)
+ server.has_element("tenant_id", basestring)
+ with CollectionCheck("volume", api_instance.volume) as volume:
+ volume.has_element("attachments", list)
+ volume.has_element("availability_zone", basestring)
+ volume.has_element("created_at", (basestring, None))
+ volume.has_element("id", basestring)
+ volume.has_element("size", int)
+ volume.has_element("status", basestring)
+
+
+@test(groups=["fake." + GROUP])
+class WhenMgmtInstanceGetIsCalledButServerIsNotReady(object):
+
+ @before_class
+ def set_up(self):
+ """Create client for mgmt instance test (2)."""
+ if not CONFIG.fake_mode:
+ raise SkipTest("This test only works in fake mode.")
+ self.client = create_client(is_admin=True)
+ self.mgmt = self.client.management
+ # Fake nova will fail a server ending with 'test_SERVER_ERROR'."
+ response = self.client.instances.create('test_SERVER_ERROR', 1,
+ {'size': 1}, [])
+ poll_until(lambda: self.client.instances.get(response.id),
+ lambda instance: instance.status == 'ERROR',
+ time_out=10)
+ self.id = response.id
+
+ @test
+ def mgmt_instance_get(self):
+ """Tests the mgmt get call works when the Nova server isn't ready."""
+ api_instance = self.mgmt.show(self.id)
+ # Print out all fields for extra info if the test fails.
+ for name in dir(api_instance):
+ print(str(name) + "=" + str(getattr(api_instance, name)))
+ # Print out all fields for extra info if the test fails.
+ for name in dir(api_instance):
+ print(str(name) + "=" + str(getattr(api_instance, name)))
+ with TypeCheck("instance", api_instance) as instance:
+ instance.has_field('created', basestring)
+ instance.has_field('deleted', bool)
+ # If the instance hasn't been deleted, this should be false... but
+ # lets avoid creating more ordering work.
+ instance.has_field('deleted_at', (basestring, None))
+ instance.has_field('flavor', dict, flavor_check)
+ instance.has_field('guest_status', dict, guest_status_check)
+ instance.has_field('id', basestring)
+ instance.has_field('links', list)
+ instance.has_field('name', basestring)
+ #instance.has_field('server_status', basestring)
+ instance.has_field('status', basestring)
+ instance.has_field('tenant_id', basestring)
+ instance.has_field('updated', basestring)
+ # Can be None if no volume is given on this instance.
+ instance.has_field('server', None)
+ instance.has_field('volume', None)
+ #TODO: Validate additional fields, assert no extra fields exist.
+
+
+@test(depends_on_classes=[CreateInstance], groups=[GROUP])
+class MgmtInstancesIndex(object):
+ """ Tests the mgmt instances index method. """
+
+ @before_class
+ def setUp(self):
+ """Create client for mgmt instance test."""
+ reqs = Requirements(is_admin=True)
+ self.user = CONFIG.users.find_user(reqs)
+ self.client = create_dbaas_client(self.user)
+
+ @test
+ def test_mgmt_instance_index_fields_present(self):
+ """
+ Verify that all the expected fields are returned by the index method.
+ """
+ expected_fields = [
+ 'created',
+ 'deleted',
+ 'deleted_at',
+ 'flavor',
+ 'id',
+ 'links',
+ 'name',
+ 'server',
+ 'status',
+ 'task_description',
+ 'tenant_id',
+ 'updated',
+ 'volume',
+ ]
+ index = self.client.management.index()
+ for instance in index:
+ with Check() as check:
+ for field in expected_fields:
+ check.true(hasattr(instance, field),
+ "Index lacks field %s" % field)
+
+ @test
+ def test_mgmt_instance_index_check_filter(self):
+ """
+ Make sure that the deleted= filter works as expected, and no instances
+ are excluded.
+ """
+ instance_counts = []
+ for deleted_filter in (True, False):
+ filtered_index = self.client.management.index(
+ deleted=deleted_filter)
+ instance_counts.append(len(filtered_index))
+ for instance in filtered_index:
+ # Every instance listed here should have the proper value
+ # for 'deleted'.
+ assert_equal(deleted_filter, instance.deleted)
+ full_index = self.client.management.index()
+ # There should be no instances that are neither deleted or not-deleted.
+ assert_equal(len(full_index), sum(instance_counts))
diff --git a/reddwarf/tests/api/mgmt/storage.py b/reddwarf/tests/api/mgmt/storage.py
new file mode 100644
index 00000000..454408ea
--- /dev/null
+++ b/reddwarf/tests/api/mgmt/storage.py
@@ -0,0 +1,108 @@
+# Copyright 2011 OpenStack LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+from reddwarfclient import exceptions
+
+from nose.plugins.skip import SkipTest
+
+from proboscis import before_class
+from proboscis import test
+from proboscis.asserts import *
+
+from reddwarf import tests
+from reddwarf.tests.api.instances import CheckInstance
+from reddwarf.tests.api.instances import instance_info
+from reddwarf.tests.util import test_config
+from reddwarf.tests.util import create_dbaas_client
+from reddwarf.tests.util.users import Requirements
+
+FAKE_MODE = test_config.values['fake_mode']
+GROUP = "dbaas.api.mgmt.storage"
+
+
+@test(groups=[tests.DBAAS_API, GROUP, tests.PRE_INSTANCES],
+ depends_on_groups=["services.initialize"])
+class StorageBeforeInstanceCreation(object):
+
+ @before_class
+ def setUp(self):
+ self.user = test_config.users.find_user(Requirements(is_admin=True))
+ self.client = create_dbaas_client(self.user)
+
+ @test
+ def test_storage_on_host(self):
+ if not FAKE_MODE:
+ raise SkipTest("Volume driver currently not working.")
+ storage = self.client.storage.index()
+ print("storage : %r" % storage)
+ for device in storage:
+ assert_true(hasattr(device, 'name'),
+ "device.name: %r" % device.name)
+ assert_true(hasattr(device, 'type'),
+ "device.type: %r" % device.name)
+ assert_true(hasattr(device, 'used'),
+ "device.used: %r" % device.used)
+
+ assert_true(hasattr(device, 'provision'),
+ "device.provision: %r" % device.provision)
+ provision = device.provision
+ assert_true('available' in provision,
+ "provision.available: %r" % provision['available'])
+ assert_true('percent' in provision,
+ "provision.percent: %r" % provision['percent'])
+ assert_true('total' in provision,
+ "provision.total: %r" % provision['total'])
+
+ assert_true(hasattr(device, 'capacity'),
+ "device.capacity: %r" % device.capacity)
+ capacity = device.capacity
+ assert_true('available' in capacity,
+ "capacity.available: %r" % capacity['available'])
+ assert_true('total' in capacity,
+ "capacity.total: %r" % capacity['total'])
+ instance_info.storage = storage
+
+
+@test(groups=[tests.INSTANCES, GROUP],
+ depends_on_groups=["dbaas.listing"])
+class StorageAfterInstanceCreation(object):
+
+ @before_class
+ def setUp(self):
+ self.user = test_config.users.find_user(Requirements(is_admin=True))
+ self.client = create_dbaas_client(self.user)
+
+ @test
+ def test_storage_on_host(self):
+ if not FAKE_MODE:
+ raise SkipTest("Volume driver currently not working.")
+ storage = self.client.storage.index()
+ print("storage : %r" % storage)
+ print("instance_info.storage : %r" % instance_info.storage)
+ expected_attrs = ['name', 'type', 'used', 'provision', 'capacity']
+ for index, device in enumerate(storage):
+ CheckInstance(None).attrs_exist(device._info, expected_attrs,
+ msg="Storage")
+ assert_equal(device.name, instance_info.storage[index].name)
+ assert_equal(device.used, instance_info.storage[index].used)
+ assert_equal(device.type, instance_info.storage[index].type)
+
+ provision = instance_info.storage[index].provision
+ assert_equal(device.provision['available'], provision['available'])
+ assert_equal(device.provision['percent'], provision['percent'])
+ assert_equal(device.provision['total'], provision['total'])
+
+ capacity = instance_info.storage[index].capacity
+ assert_equal(device.capacity['available'], capacity['available'])
+ assert_equal(device.capacity['total'], capacity['total'])
diff --git a/reddwarf/tests/api/root.py b/reddwarf/tests/api/root.py
new file mode 100644
index 00000000..7cdb95bb
--- /dev/null
+++ b/reddwarf/tests/api/root.py
@@ -0,0 +1,180 @@
+# Copyright 2011 OpenStack LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+import time
+
+from reddwarfclient import exceptions
+
+from nose.plugins.skip import SkipTest
+from proboscis import before_class
+from proboscis import test
+from proboscis.asserts import assert_equal
+from proboscis.asserts import assert_false
+from proboscis.asserts import assert_not_equal
+from proboscis.asserts import assert_raises
+from proboscis.asserts import assert_true
+from proboscis.asserts import fail
+from proboscis.decorators import expect_exception
+from proboscis.decorators import time_out
+
+from reddwarf import tests
+from reddwarf.tests.api.users import TestUsers
+from reddwarf.tests.api.instances import GROUP_START
+from reddwarf.tests.api.instances import instance_info
+from reddwarf.tests import util
+from reddwarf.tests.util import test_config
+
+
+GROUP = "dbaas.api.root"
+
+
+def log_in_as_root(root_password):
+ con = create_mysql_connection(instance_info.get_address(), 'root',
+ root_password)
+ return con
+
+
+@test(depends_on_groups=[GROUP_START],
+ runs_after=[TestUsers],
+ groups=[tests.DBAAS_API, GROUP, tests.INSTANCES])
+class TestRoot(object):
+ """
+ Test the root operations
+ """
+
+ root_enabled_timestamp = 'Never'
+ system_users = ['root', 'debian_sys_maint']
+
+ @before_class
+ def setUp(self):
+ self.dbaas = util.create_dbaas_client(instance_info.user)
+ self.dbaas_admin = util.create_dbaas_client(instance_info.admin_user)
+
+ def _verify_root_timestamp(self, id):
+ reh = self.dbaas_admin.management.root_enabled_history(id)
+ timestamp = reh.enabled
+ assert_equal(self.root_enabled_timestamp, timestamp)
+ assert_equal(id, reh.id)
+
+ def _root(self):
+ global root_password
+ host = "%"
+ user, password = self.dbaas.root.create(instance_info.id)
+ assert_equal(200, self.dbaas.last_http_code)
+ reh = self.dbaas_admin.management.root_enabled_history
+ self.root_enabled_timestamp = reh(instance_info.id).enabled
+
+ @test
+ def test_root_initially_disabled(self):
+ """Test that root is disabled"""
+ enabled = self.dbaas.root.is_root_enabled(instance_info.id)
+ assert_equal(200, self.dbaas.last_http_code)
+ assert_false(enabled, "Root SHOULD NOT be enabled.")
+
+ @test
+ def test_create_user_os_admin_failure(self):
+ users = []
+ users.append({"name": "os_admin", "password": "12345"})
+ assert_raises(exceptions.BadRequest, self.dbaas.users.create,
+ instance_info.id, users)
+
+ @test
+ def test_delete_user_os_admin_failure(self):
+ assert_raises(exceptions.BadRequest, self.dbaas.users.delete,
+ instance_info.id, "os_admin")
+
+ @test(depends_on=[test_root_initially_disabled],
+ enabled=not test_config.values['root_removed_from_instance_api'])
+ def test_root_initially_disabled_details(self):
+ """Use instance details to test that root is disabled."""
+ instance = self.dbaas.instances.get(instance_info.id)
+ assert_true(hasattr(instance, 'rootEnabled'),
+ "Instance has no rootEnabled property.")
+ assert_false(instance.rootEnabled, "Root SHOULD NOT be enabled.")
+ assert_equal(self.root_enabled_timestamp, 'Never')
+
+ @test(depends_on=[test_root_initially_disabled_details])
+ def test_root_disabled_in_mgmt_api(self):
+ """Verifies in the management api that the timestamp exists"""
+ self._verify_root_timestamp(instance_info.id)
+
+ @test(depends_on=[test_root_initially_disabled_details])
+ def test_enable_root(self):
+ self._root()
+
+ @test(depends_on=[test_enable_root])
+ def test_enabled_timestamp(self):
+ assert_not_equal(self.root_enabled_timestamp, 'Never')
+
+ @test(depends_on=[test_enable_root])
+ def test_root_not_in_users_list(self):
+ """
+ Tests that despite having enabled root, user root doesn't appear
+ in the users list for the instance.
+ """
+ users = self.dbaas.users.list(instance_info.id)
+ usernames = [user.name for user in users]
+ assert_true('root' not in usernames)
+
+ @test(depends_on=[test_enable_root])
+ def test_root_now_enabled(self):
+ """Test that root is now enabled."""
+ enabled = self.dbaas.root.is_root_enabled(instance_info.id)
+ assert_equal(200, self.dbaas.last_http_code)
+ assert_true(enabled, "Root SHOULD be enabled.")
+
+ @test(depends_on=[test_root_now_enabled],
+ enabled=not test_config.values['root_removed_from_instance_api'])
+ def test_root_now_enabled_details(self):
+ """Use instance details to test that root is now enabled."""
+ instance = self.dbaas.instances.get(instance_info.id)
+ assert_true(hasattr(instance, 'rootEnabled'),
+ "Instance has no rootEnabled property.")
+ assert_true(instance.rootEnabled, "Root SHOULD be enabled.")
+ assert_not_equal(self.root_enabled_timestamp, 'Never')
+ self._verify_root_timestamp(instance_info.id)
+
+ @test(depends_on=[test_root_now_enabled_details])
+ def test_reset_root(self):
+ if test_config.values['root_timestamp_disabled']:
+ raise SkipTest("Enabled timestamp not enabled yet")
+ old_ts = self.root_enabled_timestamp
+ self._root()
+ assert_not_equal(self.root_enabled_timestamp, 'Never')
+ assert_equal(self.root_enabled_timestamp, old_ts)
+
+ @test(depends_on=[test_reset_root])
+ def test_root_still_enabled(self):
+ """Test that after root was reset it's still enabled."""
+ enabled = self.dbaas.root.is_root_enabled(instance_info.id)
+ assert_equal(200, self.dbaas.last_http_code)
+ assert_true(enabled, "Root SHOULD still be enabled.")
+
+ @test(depends_on=[test_root_still_enabled],
+ enabled=not test_config.values['root_removed_from_instance_api'])
+ def test_root_still_enabled_details(self):
+ """Use instance details to test that after root was reset,
+ it's still enabled."""
+ instance = self.dbaas.instances.get(instance_info.id)
+ assert_true(hasattr(instance, 'rootEnabled'),
+ "Instance has no rootEnabled property.")
+ assert_true(instance.rootEnabled, "Root SHOULD still be enabled.")
+ assert_not_equal(self.root_enabled_timestamp, 'Never')
+ self._verify_root_timestamp(instance_info.id)
+
+ @test(depends_on=[test_enable_root])
+ def test_root_cannot_be_deleted(self):
+ """Even if root was enabled, the user root cannot be deleted."""
+ assert_raises(exceptions.BadRequest, self.dbaas.users.delete,
+ instance_info.id, "root")
diff --git a/reddwarf/tests/api/users.py b/reddwarf/tests/api/users.py
new file mode 100644
index 00000000..5e1180fb
--- /dev/null
+++ b/reddwarf/tests/api/users.py
@@ -0,0 +1,274 @@
+# Copyright 2011 OpenStack LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+import time
+import re
+
+from reddwarfclient import exceptions
+
+from proboscis import after_class
+from proboscis import before_class
+from proboscis import test
+from proboscis.asserts import assert_equal
+from proboscis.asserts import assert_false
+from proboscis.asserts import assert_not_equal
+from proboscis.asserts import assert_raises
+from proboscis.asserts import assert_true
+from proboscis.asserts import fail
+from proboscis.decorators import expect_exception
+from proboscis.decorators import time_out
+
+from reddwarf import tests
+from reddwarf.tests.api.databases import TestDatabases
+from reddwarf.tests.api.instances import GROUP_START
+from reddwarf.tests.api.instances import instance_info
+from reddwarf.tests import util
+from reddwarf.tests.util import test_config
+
+
+GROUP = "dbaas.api.users"
+FAKE = test_config.values['fake_mode']
+
+
+@test(depends_on_groups=[GROUP_START],
+ groups=[tests.DBAAS_API, GROUP, tests.INSTANCES],
+ runs_after=[TestDatabases])
+class TestUsers(object):
+ """
+ Test the creation and deletion of users
+ """
+
+ username = "tes!@#tuser"
+ username_urlencoded = "tes%21%40%23tuser"
+ password = "testpa$^%ssword"
+ username1 = "anous*&^er"
+ username1_urlendcoded = "anous%2A%26%5Eer"
+ password1 = "anopas*?.sword"
+ db1 = "firstdb"
+ db2 = "seconddb"
+
+ created_users = [username, username1]
+ system_users = ['root', 'debian_sys_maint']
+
+ @before_class
+ def setUp(self):
+ self.dbaas = util.create_dbaas_client(instance_info.user)
+ self.dbaas_admin = util.create_dbaas_client(instance_info.admin_user)
+ databases = [{"name": self.db1, "charset": "latin2",
+ "collate": "latin2_general_ci"},
+ {"name": self.db2}]
+ try:
+ self.dbaas.databases.create(instance_info.id, databases)
+ except exceptions.BadRequest:
+ pass # If the db already exists that's OK.
+ if not FAKE:
+ time.sleep(5)
+
+ @after_class
+ def tearDown(self):
+ self.dbaas.databases.delete(instance_info.id, self.db1)
+ self.dbaas.databases.delete(instance_info.id, self.db2)
+
+ @test()
+ def test_create_users(self):
+ users = []
+ users.append({"name": self.username, "password": self.password,
+ "databases": [{"name": self.db1}]})
+ users.append({"name": self.username1, "password": self.password1,
+ "databases": [{"name": self.db1}, {"name": self.db2}]})
+ self.dbaas.users.create(instance_info.id, users)
+ assert_equal(202, self.dbaas.last_http_code)
+ # Do we need this?
+ if not FAKE:
+ time.sleep(5)
+
+ self.check_database_for_user(self.username, self.password,
+ [self.db1])
+ self.check_database_for_user(self.username1, self.password1,
+ [self.db1, self.db2])
+
+ @test(depends_on=[test_create_users])
+ def test_create_users_list(self):
+ #tests for users that should be listed
+ users = self.dbaas.users.list(instance_info.id)
+ assert_equal(200, self.dbaas.last_http_code)
+ found = False
+ for user in self.created_users:
+ for result in users:
+ if user == result.name:
+ found = True
+ assert_true(found, "User '%s' not found in result" % user)
+ found = False
+
+ @test(depends_on=[test_create_users])
+ def test_fails_when_creating_user_twice(self):
+ users = []
+ users.append({"name": self.username, "password": self.password,
+ "databases": [{"name": self.db1}]})
+ users.append({"name": self.username1, "password": self.password1,
+ "databases": [{"name": self.db1}, {"name": self.db2}]})
+ assert_raises(exceptions.BadRequest, self.dbaas.users.create,
+ instance_info.id, users)
+ assert_equal(400, self.dbaas.last_http_code)
+
+ @test(depends_on=[test_create_users_list])
+ def test_cannot_create_root_user(self):
+ # Tests that the user root (in Config:ignore_users) cannot be created.
+ users = [{"name": "root", "password": "12345",
+ "databases": [{"name": self.db1}]}]
+ assert_raises(exceptions.BadRequest, self.dbaas.users.create,
+ instance_info.id, users)
+
+ @test(depends_on=[test_create_users_list])
+ def test_create_users_list_system(self):
+ #tests for users that should not be listed
+ users = self.dbaas.users.list(instance_info.id)
+ assert_equal(200, self.dbaas.last_http_code)
+ found = False
+ for user in self.system_users:
+ found = any(result.name == user for result in users)
+ msg = "User '%s' SHOULD NOT BE found in result" % user
+ assert_false(found, msg)
+ found = False
+
+ @test(depends_on=[test_create_users_list],
+ runs_after=[test_fails_when_creating_user_twice])
+ def test_delete_users(self):
+ self.dbaas.users.delete(instance_info.id, self.username_urlencoded)
+ assert_equal(202, self.dbaas.last_http_code)
+ self.dbaas.users.delete(instance_info.id, self.username1_urlendcoded)
+ assert_equal(202, self.dbaas.last_http_code)
+ if not FAKE:
+ time.sleep(5)
+
+ self._check_connection(self.username, self.password)
+ self._check_connection(self.username1, self.password1)
+
+ def show_databases(self, user, password):
+ print("Going to connect to %s, %s, %s"
+ % (instance_info.get_address(), user, password))
+ with create_mysql_connection(instance_info.get_address(),
+ user, password) as db:
+ print(db)
+ dbs = db.execute("show databases")
+ return [row['Database'] for row in dbs]
+
+ def check_database_for_user(self, user, password, dbs):
+ if not FAKE:
+ # Make the real call to the database to check things.
+ actual_list = self.show_databases(user, password)
+ for db in dbs:
+ assert_true(
+ db in actual_list,
+ "No match for db %s in dblist. %s :(" % (db, actual_list))
+ # Confirm via API.
+ result = self.dbaas.users.list(instance_info.id)
+ assert_equal(200, self.dbaas.last_http_code)
+ for item in result:
+ if item.name == user:
+ break
+ else:
+ fail("User %s not added to collection." % user)
+
+ @test
+ def test_username_too_long(self):
+ users = []
+ users.append({"name": "1233asdwer345tyg56", "password": self.password,
+ "database": self.db1})
+ assert_raises(exceptions.BadRequest, self.dbaas.users.create,
+ instance_info.id, users)
+ assert_equal(400, self.dbaas.last_http_code)
+
+ @test
+ def test_invalid_username(self):
+ users = []
+ users.append({"name": "user,", "password": self.password,
+ "database": self.db1})
+ assert_raises(exceptions.BadRequest, self.dbaas.users.create,
+ instance_info.id, users)
+ assert_equal(400, self.dbaas.last_http_code)
+
+ @test(enabled=False)
+ #TODO(hub_cap): Make this test work once python-routes is updated, if ever.
+ def test_delete_user_with_period_in_name(self):
+ """Attempt to create/destroy a user with a period in its name"""
+ users = []
+ username_with_period = "user.name"
+ users.append({"name": username_with_period, "password": self.password,
+ "databases": [{"name": self.db1}]})
+ self.dbaas.users.create(instance_info.id, users)
+ assert_equal(202, self.dbaas.last_http_code)
+ if not FAKE:
+ time.sleep(5)
+
+ self.check_database_for_user(username_with_period, self.password,
+ [self.db1])
+ self.dbaas.users.delete(instance_info.id, username_with_period)
+ assert_equal(202, self.dbaas.last_http_code)
+
+ @test
+ def test_invalid_password(self):
+ users = []
+ users.append({"name": "anouser", "password": "sdf,;",
+ "database": self.db1})
+ assert_raises(exceptions.BadRequest, self.dbaas.users.create,
+ instance_info.id, users)
+ assert_equal(400, self.dbaas.last_http_code)
+
+ @test
+ def test_pagination(self):
+ users = []
+ users.append({"name": "Jetson", "password": "george",
+ "databases": [{"name": "Sprockets"}]})
+ users.append({"name": "Spacely", "password": "cosmo",
+ "databases": [{"name": "Sprockets"}]})
+ users.append({"name": "Uniblab", "password": "fired",
+ "databases": [{"name": "Sprockets"}]})
+
+ self.dbaas.users.create(instance_info.id, users)
+ assert_equal(202, self.dbaas.last_http_code)
+ if not FAKE:
+ time.sleep(5)
+ limit = 2
+ users = self.dbaas.users.list(instance_info.id, limit=limit)
+ assert_equal(200, self.dbaas.last_http_code)
+ marker = users.next
+
+ # Better get only as many as we asked for
+ assert_true(len(users) <= limit)
+ assert_true(users.next is not None)
+ assert_equal(marker, users[-1].name)
+ marker = users.next
+
+ # I better get new users if I use the marker I was handed.
+ users = self.dbaas.users.list(instance_info.id, limit=limit,
+ marker=marker)
+ assert_equal(200, self.dbaas.last_http_code)
+ assert_true(marker not in [user.name for user in users])
+
+ # Now fetch again with a larger limit.
+ users = self.dbaas.users.list(instance_info.id)
+ assert_equal(200, self.dbaas.last_http_code)
+ assert_true(users.next is None)
+
+ def _check_connection(self, username, password):
+ if not FAKE:
+ util.assert_mysql_connection_fails(username, password,
+ instance_info.get_address())
+ # Also determine the db is gone via API.
+ result = self.dbaas.users.list(instance_info.id)
+ assert_equal(200, self.dbaas.last_http_code)
+ for item in result:
+ if item.name == username:
+ fail("User %s was not deleted." % user)
diff --git a/reddwarf/tests/api/versions.py b/reddwarf/tests/api/versions.py
new file mode 100644
index 00000000..3cf3e5fa
--- /dev/null
+++ b/reddwarf/tests/api/versions.py
@@ -0,0 +1,89 @@
+# Copyright 2011 OpenStack LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+from proboscis import before_class
+from proboscis import test
+from proboscis.asserts import assert_equal
+from proboscis import SkipTest
+
+from reddwarfclient.exceptions import ClientException
+from reddwarf import tests
+from reddwarf.tests.util import test_config
+from reddwarf.tests.util import create_dbaas_client
+from reddwarf.tests.util.users import Requirements
+
+GROUP = "dbaas.api.versions"
+
+
+@test(groups=[tests.DBAAS_API, GROUP, tests.PRE_INSTANCES, 'DBAAS_VERSIONS'],
+ depends_on_groups=["services.initialize"])
+class Versions(object):
+ """Test listing all versions and verify the current version"""
+
+ @before_class
+ def setUp(self):
+ """Sets up the client."""
+ user = test_config.users.find_user(Requirements(is_admin=False))
+ self.client = create_dbaas_client(user)
+
+ @test
+ def test_list_versions_index(self):
+ versions = self.client.versions.index(test_config.version_url)
+ assert_equal(1, len(versions))
+ assert_equal("CURRENT", versions[0].status,
+ message="Version status: %s" % versions[0].status)
+ expected_version = test_config.values['reddwarf_version']
+ assert_equal(expected_version, versions[0].id,
+ message="Version ID: %s" % versions[0].id)
+ expected_api_updated = test_config.values['reddwarf_api_updated']
+ assert_equal(expected_api_updated, versions[0].updated,
+ message="Version updated: %s" % versions[0].updated)
+
+ def _request(self, url, method='GET', response='200'):
+ resp, body = None, None
+ full_url = test_config.version_url + url
+ try:
+ resp, body = self.client.client.request(full_url, method)
+ assert_equal(resp.get('status', ''), response)
+ except ClientException as ce:
+ assert_equal(str(ce.http_status), response)
+ return body
+
+ @test
+ def test_no_slash_no_version(self):
+ body = self._request('')
+
+ @test
+ def test_no_slash_with_version(self):
+ if test_config.auth_strategy == "fake":
+ raise SkipTest("Skipping this test since auth is faked.")
+ body = self._request('/v1.0', response='401')
+
+ @test
+ def test_with_slash_no_version(self):
+ body = self._request('/')
+
+ @test
+ def test_with_slash_with_version(self):
+ if test_config.auth_strategy == "fake":
+ raise SkipTest("Skipping this test since auth is faked.")
+ body = self._request('/v1.0/', response='401')
+
+ @test
+ def test_request_no_version(self):
+ body = self._request('/dbaas/instances', response='404')
+
+ @test
+ def test_request_bogus_version(self):
+ body = self._request('/0.0/', response='404')
diff --git a/reddwarf/tests/config.py b/reddwarf/tests/config.py
new file mode 100644
index 00000000..53e469bc
--- /dev/null
+++ b/reddwarf/tests/config.py
@@ -0,0 +1,173 @@
+# Copyright (c) 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.
+
+"""Handles configuration options for the tests.
+
+The tests are capable of running in other contexts, such as in a VM or against
+a real deployment. Using this configuration ensures we can run them in other
+environments if we choose to.
+
+"""
+
+import json
+import os
+from collections import Mapping
+from reddwarf.tests.fakes.common import event_simulator_sleep
+
+
+#TODO(tim.simpson): I feel like this class already exists somewhere in core
+# Python.
+class FrozenDict(Mapping):
+
+ def __init__(self, original):
+ self.original = original
+
+ def __len__(self):
+ return self.original.__len__()
+
+ def __iter__(self, *args, **kwargs):
+ return self.original.__iter__(self, *args, **kwargs)
+
+ def __getitem__(self, *args, **kwargs):
+ return self.original.__getitem__(*args, **kwargs)
+
+ def __str__(self):
+ return self.original.__str__()
+
+
+class TestConfig(object):
+ """
+ Holds test configuration values which can be accessed as attributes
+ or using the values dictionary.
+ """
+
+ def __init__(self):
+ """
+ Create TestConfig, and set default values. These will be overwritten by
+ the "load_from" methods below.
+ """
+ self._loaded_files = []
+ self._values = {
+ 'clean_slate': os.environ.get("CLEAN_SLATE", "False") == "True",
+ 'fake_mode': os.environ.get("FAKE_MODE", "False") == "True",
+ 'nova_auth_url': "http://localhost:5000/v2.0",
+ 'reddwarf_auth_url': "http://localhost:5000/v2.0/tokens",
+ 'dbaas_url': "http://localhost:8775/v1.0/dbaas",
+ 'version_url': "http://localhost:8775/",
+ 'nova_url': "http://localhost:8774/v1.1",
+ 'instance_create_time': 16 * 60,
+ 'dbaas_image': None,
+ 'mysql_connection_method': {"type": "direct"},
+ 'typical_nova_image_name': None,
+ 'white_box': os.environ.get("WHITE_BOX", "False") == "True",
+ 'test_mgmt': False,
+ 'use_local_ovz': False,
+ "known_bugs": {},
+ "in_proc_server": True,
+ "report_directory": os.environ.get("REPORT_DIRECTORY", None),
+ "sleep_mode": "simulated",
+ }
+ self._frozen_values = FrozenDict(self._values)
+ self._users = None
+ self._dawdler = None
+
+ @property
+ def dawdler(self):
+ """Equivalent (in theory) to time.sleep.
+
+ Calling this in place of sleep allows the tests to run faster in
+ fake mode.
+ """
+ if not self._dawdler:
+ if self.sleep_mode == "simulated":
+ self._dawdler = event_simulator_sleep
+ else:
+ self._dawdler = greenthread.sleep
+ return self._dawdler
+
+ def get(self, name, default_value):
+ return self.values.get(name, default_value)
+
+ def get_report(self):
+ return PrintReporter()
+
+ def load_from_line(self, line):
+ index = line.find("=")
+ if index >= 0:
+ key = line[:index]
+ value = line[index + 1:]
+ self._values[key] = value
+
+ def load_include_files(self, original_file, files):
+ directory = os.path.dirname(original_file)
+ for file_sub_path in files:
+ file_full_path = os.path.join(directory, file_sub_path)
+ self.load_from_file(file_full_path)
+
+ def load_from_file(self, file_path):
+ if file_path in self._loaded_files:
+ return
+ file_contents = open(file_path, "r").read()
+ try:
+ contents = json.loads(file_contents)
+ except Exception as exception:
+ raise RuntimeError("Error loading conf file \"%s\"." % file_path,
+ exception)
+ finally:
+ self._loaded_files.append(file_path)
+
+ if "include-files" in contents:
+ self.load_include_files(file_path, contents['include-files'])
+ del contents['include-files']
+ self._values.update(contents)
+
+ def __getattr__(self, name):
+ if name not in self._values:
+ raise AttributeError('Configuration value "%s" not found.' % name)
+ else:
+ return self._values[name]
+
+ def python_cmd_list(self):
+ """The start of a command list to use when running Python scripts."""
+ commands = []
+ if self.use_venv:
+ commands.append("%s/tools/with_venv.sh" % self.nova_code_root)
+ return list
+ commands.append("python")
+ return commands
+
+ @property
+ def users(self):
+ if self._users is None:
+ from reddwarf.tests.util.users import Users
+ self._users = Users(self.values['users'])
+ return self._users
+
+ @property
+ def values(self):
+ return self._frozen_values
+
+
+class PrintReporter(object):
+
+ def log(self, msg):
+ print("[REPORT] %s" % msg)
+
+ def update(self):
+ pass # Ignore. This is used in other reporters.
+
+
+CONFIG = TestConfig()
+del TestConfig.__init__
diff --git a/reddwarf/tests/fakes/common.py b/reddwarf/tests/fakes/common.py
index cccef089..d242bfb2 100644
--- a/reddwarf/tests/fakes/common.py
+++ b/reddwarf/tests/fakes/common.py
@@ -18,9 +18,12 @@
"""Common code to help in faking the models."""
import time
-import stubout
from novaclient import exceptions as nova_exceptions
+from reddwarf.common import config
+
+
+CONFIG = config.Config
def authorize(context):
@@ -28,19 +31,55 @@ def authorize(context):
raise nova_exceptions.Forbidden(403, "Forbidden")
-class EventSimulator(object):
- """Simulates a resource that changes over time.
+def get_event_spawer():
+ if CONFIG.get('fake_mode_events') == "simulated":
+ return event_simulator
+ else:
+ return eventlet_spawner
+
+
+pending_events = []
+sleep_entrance_count = 0
+
+
+def eventlet_spawner(time_from_now_in_seconds, func):
+ """Uses eventlet to spawn events."""
+ if time_from_now_in_seconds <= 0:
+ func()
+ else:
+ import eventlet
+ eventlet.spawn_after(time_from_now_in_seconds, func)
+
+
+def event_simulator(time_from_now_in_seconds, func):
+ """Fakes events without doing any actual waiting."""
+ pending_events.append({"time": time_from_now_in_seconds, "func": func})
- Has a list of events which execute in real time to change state.
- The implementation is very dumb; if you give it two events at once the
- last one wins.
- """
+def event_simulator_sleep(time_to_sleep):
+ """Simulates waiting for an event."""
+ global sleep_entrance_count
+ sleep_entrance_count += 1
+ time_to_sleep = float(time_to_sleep)
+ global pending_events
+ while time_to_sleep > 0:
+ itr_sleep = 0.5
+ for i in range(len(pending_events)):
+ event = pending_events[i]
+ event["time"] = event["time"] - itr_sleep
+ if event["func"] is not None and event["time"] < 0:
+ # Call event, but first delete it so this function can be
+ # reentrant.
+ func = event["func"]
+ event["func"] = None
+ try:
+ func()
+ except Exception:
+ pass # Ignore exceptions, which can potentially occur.
- @staticmethod
- def add_event(time_from_now_in_seconds, func):
- if time_from_now_in_seconds <= 0:
- func()
- else:
- import eventlet
- eventlet.spawn_after(time_from_now_in_seconds, func)
+ time_to_sleep -= itr_sleep
+ sleep_entrance_count -= 1
+ if sleep_entrance_count < 1:
+ # Clear out old events
+ pending_events = [event for event in pending_events
+ if event["func"] is not None]
diff --git a/reddwarf/tests/fakes/guestagent.py b/reddwarf/tests/fakes/guestagent.py
index 3241a7fd..a243fabf 100644
--- a/reddwarf/tests/fakes/guestagent.py
+++ b/reddwarf/tests/fakes/guestagent.py
@@ -18,7 +18,7 @@
import logging
import time
-from reddwarf.tests.fakes.common import EventSimulator
+from reddwarf.tests.fakes.common import get_event_spawer
DB = {}
LOG = logging.getLogger(__name__)
@@ -32,6 +32,7 @@ class FakeGuest(object):
self.dbs = {}
self.root_was_enabled = False
self.version = 1
+ self.event_spawn = get_event_spawer()
def get_hwinfo(self):
return {'mem_total': 524288, 'num_cpus': 1}
@@ -122,7 +123,7 @@ class FakeGuest(object):
status.status = ServiceStatuses.RUNNING
status.save()
AgentHeartBeat.create(instance_id=self.id)
- EventSimulator.add_event(1.0, update_db)
+ self.event_spawn(1.0, update_db)
def restart(self):
from reddwarf.instance.models import InstanceServiceStatus
diff --git a/reddwarf/tests/fakes/nova.py b/reddwarf/tests/fakes/nova.py
index 9edd47ff..6b60061a 100644
--- a/reddwarf/tests/fakes/nova.py
+++ b/reddwarf/tests/fakes/nova.py
@@ -15,13 +15,13 @@
# License for the specific language governing permissions and limitations
# under the License.
+import eventlet
import logging
from novaclient.v1_1.client import Client
from novaclient import exceptions as nova_exceptions
-import eventlet
import uuid
from reddwarf.tests.fakes.common import authorize
-from reddwarf.tests.fakes.common import EventSimulator
+from reddwarf.tests.fakes.common import get_event_spawer
from reddwarf.common.utils import poll_until
from reddwarf.common.exception import PollTimeOut
@@ -100,7 +100,7 @@ class FakeServer(object):
self.name = name
self.image_id = image_id
self.flavor_ref = flavor_ref
- self.events = EventSimulator()
+ self.event_spawn = get_event_spawer()
self.schedule_status("BUILD", 0.0)
self.volumes = volumes
# This is used by "RdServers". Its easier to compute the
@@ -127,9 +127,11 @@ class FakeServer(object):
def reboot(self):
LOG.debug("Rebooting server %s" % (self.id))
self._current_status = "REBOOT"
- eventlet.sleep(1)
- self._current_status = "ACTIVE"
- self.parent.schedule_simulate_running_server(self.id, 1.5)
+
+ def set_to_active():
+ self._current_status = "ACTIVE"
+ self.parent.schedule_simulate_running_server(self.id, 1.5)
+ self.event_spawn(1, set_to_active)
def delete(self):
self.schedule_status = []
@@ -169,15 +171,15 @@ class FakeServer(object):
else:
flavor = self.parent.flavors.get(new_flavor_id)
self.flavor_ref = flavor.links[0]['href']
- self.events.add_event(1, set_to_confirm_mode)
+ self.event_spawn(1, set_to_confirm_mode)
- self.events.add_event(1, set_flavor)
+ self.event_spawn(1, set_flavor)
def schedule_status(self, new_status, time_from_now):
"""Makes a new status take effect at the given time."""
def set_status():
self._current_status = new_status
- self.events.add_event(time_from_now, set_status)
+ self.event_spawn(time_from_now, set_status)
@property
def status(self):
@@ -211,7 +213,7 @@ class FakeServers(object):
self.context = context
self.db = FAKE_SERVERS_DB
self.flavors = flavors
- self.events = EventSimulator()
+ self.event_spawn = get_event_spawer()
def can_see(self, id):
"""Can this FakeServers, with its context, see some resource?"""
@@ -286,7 +288,7 @@ class FakeServers(object):
def delete_server():
LOG.info("Simulated event ended, deleting server %s." % id)
del self.db[id]
- self.events.add_event(time_from_now, delete_server)
+ self.event_spawn(time_from_now, delete_server)
def schedule_simulate_running_server(self, id, time_from_now):
def set_server_running():
@@ -298,7 +300,7 @@ class FakeServers(object):
status = InstanceServiceStatus.find_by(instance_id=instance.id)
status.status = ServiceStatuses.RUNNING
status.save()
- self.events.add_event(time_from_now, set_server_running)
+ self.event_spawn(time_from_now, set_server_running)
class FakeRdServer(object):
@@ -356,7 +358,7 @@ class FakeVolume(object):
self.size = size
self.display_name = display_name
self.display_description = display_description
- self.events = EventSimulator()
+ self.event_spawn = get_event_spawer()
self._current_status = "BUILD"
# For some reason we grab this thing from device then call it mount
# point.
@@ -384,7 +386,7 @@ class FakeVolume(object):
"""Makes a new status take effect at the given time."""
def set_status():
self._current_status = new_status
- self.events.add_event(time_from_now, set_status)
+ self.event_spawn(time_from_now, set_status)
def set_attachment(self, server_id):
"""Fake method we've added to set attachments. Idempotent."""
@@ -417,7 +419,7 @@ class FakeVolumes(object):
def __init__(self, context):
self.context = context
self.db = FAKE_VOLUMES_DB
- self.events = EventSimulator()
+ self.event_spawn = get_event_spawer()
def can_see(self, id):
"""Can this FakeVolumes, with its context, see some resource?"""
@@ -458,7 +460,7 @@ class FakeVolumes(object):
def finish_resize():
volume._current_status = "in-use"
volume.size = new_size
- self.events.add_event(1.0, finish_resize)
+ self.event_spawn(1.0, finish_resize)
class FakeAccount(object):
@@ -486,7 +488,7 @@ class FakeAccounts(object):
self.context = context
self.db = FAKE_SERVERS_DB
self.servers = servers
- self.events = EventSimulator()
+ self.event_spawn = get_event_spawer()
def _belongs_to_tenant(self, tenant, id):
server = self.db[id]
@@ -494,7 +496,6 @@ class FakeAccounts(object):
def get_instances(self, id):
authorize(self.context)
-
servers = [v for (k, v) in self.db.items()
if self._belongs_to_tenant(id, v.id)]
return FakeAccount(id, servers)
diff --git a/reddwarf/tests/util/__init__.py b/reddwarf/tests/util/__init__.py
new file mode 100644
index 00000000..90b7f3c7
--- /dev/null
+++ b/reddwarf/tests/util/__init__.py
@@ -0,0 +1,254 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright (c) 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.
+
+"""
+:mod:`tests` -- Utility methods for tests.
+===================================
+
+.. automodule:: utils
+ :platform: Unix
+ :synopsis: Tests for Nova.
+.. moduleauthor:: Nirmal Ranganathan <nirmal.ranganathan@rackspace.com>
+.. moduleauthor:: Tim Simpson <tim.simpson@rackspace.com>
+"""
+
+# This emulates the old way we did things, which was to load the config
+# as a module.
+# TODO(tim.simpson): Change all references from "test_config" to CONFIG.
+from reddwarf.tests.config import CONFIG as test_config
+
+import re
+import subprocess
+import sys
+import time
+
+try:
+ from eventlet import event
+ from eventlet import greenthread
+ EVENT_AVAILABLE = True
+except ImportError:
+ EVENT_AVAILABLE = False
+
+from sqlalchemy import create_engine
+
+from reddwarfclient import exceptions
+
+from proboscis import test
+from proboscis.asserts import assert_false
+from proboscis.asserts import assert_raises
+from proboscis.asserts import assert_true
+from proboscis.asserts import Check
+from proboscis.asserts import fail
+from proboscis.asserts import ASSERTION_ERROR
+from proboscis import SkipTest
+from reddwarfclient import Dbaas
+from reddwarfclient.client import ReddwarfHTTPClient
+from reddwarf.tests.util import test_config
+from reddwarf.tests.util.client import TestClient as TestClient
+from reddwarf.tests.util.users import Requirements
+
+
+WHITE_BOX = test_config.white_box
+
+
+def assert_http_code(expected_http_code, func, *args, **kwargs):
+ try:
+ rtn_value = func(*args, **kwargs)
+ assert_equal(
+ expected_http_code,
+ 200,
+ "Expected the function to return http code %s but instead got "
+ "no error (code 200?)." % expected_http_code)
+ return rtn_value
+ except exceptions.ClientException as ce:
+ assert_equal(
+ expected_http_code,
+ ce.code,
+ "Expected the function to return http code %s but instead got "
+ "code %s." % (expected_http_code, ce.code))
+
+
+def create_client(*args, **kwargs):
+ """
+ Using the User Requirements as arguments, finds a user and grabs a new
+ DBAAS client.
+ """
+ reqs = Requirements(*args, **kwargs)
+ user = test_config.users.find_user(reqs)
+ return create_dbaas_client(user)
+
+
+def create_dbaas_client(user):
+ """Creates a rich client for the RedDwarf API using the test config."""
+ auth_strategy = None
+
+ kwargs = {
+ 'service_type': 'reddwarf',
+ 'insecure': test_config.values['reddwarf_client_insecure'],
+ }
+
+ def set_optional(kwargs_name, test_conf_name):
+ value = test_config.values.get(test_conf_name, None)
+ if value is not None:
+ kwargs[kwargs_name] = value
+ force_url = 'override_reddwarf_api_url' in test_config.values
+
+ service_url = test_config.get('override_reddwarf_api_url', None)
+ if user.requirements.is_admin:
+ service_url = test_config.get('override_admin_reddwarf_api_url',
+ service_url)
+ if service_url:
+ kwargs['service_url'] = service_url
+
+ auth_strategy = None
+ if user.requirements.is_admin:
+ auth_strategy = test_config.get('admin_auth_strategy',
+ test_config.auth_strategy)
+ else:
+ auth_strategy = test_config.auth_strategy
+ set_optional('region_name', 'reddwarf_client_region_name')
+ if test_config.values.get('override_reddwarf_api_url_append_tenant',
+ False):
+ kwargs['service_url'] += "/" + user.tenant
+
+ if auth_strategy == 'fake':
+ from reddwarfclient import auth
+
+ class FakeAuth(auth.Authenticator):
+
+ def authenticate(self):
+ class FakeCatalog(object):
+ def __init__(self, auth):
+ self.auth = auth
+
+ def get_public_url(self):
+ return "%s/%s" % (test_config.dbaas_url,
+ self.auth.tenant)
+
+ def get_token(self):
+ return self.auth.tenant
+
+ return FakeCatalog(self)
+
+ auth_strategy = FakeAuth
+
+ if auth_strategy:
+ kwargs['auth_strategy'] = auth_strategy
+
+ if not user.requirements.is_admin:
+ auth_url = test_config.reddwarf_auth_url
+ else:
+ auth_url = test_config.values.get('reddwarf_admin_auth_url',
+ test_config.reddwarf_auth_url)
+
+ dbaas = Dbaas(user.auth_user, user.auth_key, tenant=user.tenant,
+ auth_url=auth_url, **kwargs)
+ dbaas.authenticate()
+ with Check() as check:
+ check.is_not_none(dbaas.client.auth_token, "Auth token not set!")
+ if not force_url and user.requirements.is_admin:
+ expected_prefix = test_config.dbaas_url
+ actual = dbaas.client.service_url
+ msg = "Dbaas management url was expected to start with %s, but " \
+ "was %s." % (expected_prefix, actual)
+ check.true(actual.startswith(expected_prefix), msg)
+ return TestClient(dbaas)
+
+
+def create_nova_client(user, service_type=None):
+ """Creates a rich client for the Nova API using the test config."""
+ if test_config.nova_client is None:
+ raise SkipTest("No nova_client info specified in the Test Config "
+ "so this test will be skipped.")
+ from novaclient.v1_1.client import Client
+ if not service_type:
+ service_type = test_config.nova_client['nova_service_type']
+ openstack = Client(user.auth_user, user.auth_key,
+ user.tenant, test_config.nova_client['auth_url'],
+ service_type=service_type)
+ openstack.authenticate()
+ return TestClient(openstack)
+
+
+def process(cmd):
+ process = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE)
+ result = process.communicate()
+ return result
+
+
+def string_in_list(str, substr_list):
+ """Returns True if the string appears in the list."""
+ return any([str.find(x) >= 0 for x in substr_list])
+
+
+class PollTimeOut(RuntimeError):
+ message = _("Polling request timed out.")
+
+
+# Without event let, this just calls time.sleep.
+def poll_until(retriever, condition=lambda value: value,
+ sleep_time=1, time_out=None):
+ """Retrieves object until it passes condition, then returns it.
+
+ If time_out_limit is passed in, PollTimeOut will be raised once that
+ amount of time is eclipsed.
+
+ """
+ start_time = time.time()
+
+ def check_timeout():
+ if time_out is not None and time.time() > start_time + time_out:
+ raise PollTimeOut
+
+ while True:
+ obj = retriever()
+ if condition(obj):
+ return
+ check_timeout()
+ time.sleep(sleep_time)
+
+
+class LocalSqlClient(object):
+ """A sqlalchemy wrapper to manage transactions"""
+
+ def __init__(self, engine, use_flush=True):
+ self.engine = engine
+ self.use_flush = use_flush
+
+ def __enter__(self):
+ self.conn = self.engine.connect()
+ self.trans = self.conn.begin()
+ return self.conn
+
+ def __exit__(self, type, value, traceback):
+ if self.trans:
+ if type is not None: # An error occurred
+ self.trans.rollback()
+ else:
+ if self.use_flush:
+ self.conn.execute(FLUSH)
+ self.trans.commit()
+ self.conn.close()
+
+ def execute(self, t, **kwargs):
+ try:
+ return self.conn.execute(t, kwargs)
+ except:
+ self.trans.rollback()
+ self.trans = None
+ raise
diff --git a/reddwarf/tests/util/check.py b/reddwarf/tests/util/check.py
new file mode 100644
index 00000000..d55c8e42
--- /dev/null
+++ b/reddwarf/tests/util/check.py
@@ -0,0 +1,206 @@
+# Copyright (c) 2012 OpenStack
+# 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.
+
+"""Like asserts, but does not raise an exception until the end of a block."""
+
+import traceback
+from proboscis.asserts import ASSERTION_ERROR
+from proboscis.asserts import assert_equal
+from proboscis.asserts import assert_false
+from proboscis.asserts import assert_not_equal
+from proboscis.asserts import assert_true
+from proboscis.asserts import Check
+
+
+def get_stack_trace_of_caller(level_up):
+ """Gets the stack trace at the point of the caller."""
+ level_up += 1
+ st = traceback.extract_stack()
+ caller_index = len(st) - level_up
+ if caller_index < 0:
+ caller_index = 0
+ new_st = st[0:caller_index]
+ return new_st
+
+
+def raise_blame_caller(level_up, ex):
+ """Raises an exception, changing the stack trace to point to the caller."""
+ new_st = get_stack_trace_of_caller(level_up + 2)
+ raise type(ex), ex, new_st
+
+
+class Checker(object):
+
+ def __init__(self):
+ self.messages = []
+ self.odd = True
+ self.protected = False
+
+ def _add_exception(self, _type, value, tb):
+ """Takes an exception, and adds it as a string."""
+ if self.odd:
+ prefix = "* "
+ else:
+ prefix = "- "
+ start = "Check failure! Traceback:"
+ middle = prefix.join(traceback.format_list(tb))
+ end = '\n'.join(traceback.format_exception_only(_type, value))
+ msg = '\n'.join([start, middle, end])
+ self.messages.append(msg)
+ self.odd = not self.odd
+
+ def equal(self, *args, **kwargs):
+ self._run_assertion(assert_equal, *args, **kwargs)
+
+ def false(self, *args, **kwargs):
+ self._run_assertion(assert_false, *args, **kwargs)
+
+ def not_equal(self, *args, **kwargs):
+ _run_assertion(assert_not_equal, *args, **kwargs)
+
+ def _run_assertion(self, assert_func, *args, **kwargs):
+ """
+ Runs an assertion method, but catches any failure and adds it as a
+ string to the messages list.
+ """
+ if self.protected:
+ try:
+ assert_func(*args, **kwargs)
+ except ASSERTION_ERROR as ae:
+ st = get_stack_trace_of_caller(2)
+ self._add_exception(ASSERTION_ERROR, ae, st)
+ else:
+ assert_func(*args, **kwargs)
+
+ def __enter__(self):
+ self.protected = True
+ return self
+
+ def __exit__(self, _type, value, tb):
+ self.protected = False
+ if len(self.messages) == 0:
+ final_message = None
+ else:
+ final_message = '\n'.join(self.messages)
+ if _type is not None: # An error occurred
+ if len(self.messages) == 0:
+ raise _type, value, tb
+ self._add_exception(_type, value, tb)
+ if len(self.messages) != 0:
+ final_message = '\n'.join(self.messages)
+ raise ASSERTION_ERROR(final_message)
+
+ def true(self, *args, **kwargs):
+ self._run_assertion(assert_true, *args, **kwargs)
+
+
+class AttrCheck(Check):
+ """Class for attr checks, links and other common items."""
+
+ def __init__(self):
+ super(AttrCheck, self).__init__()
+
+ def fail(self, msg):
+ self.true(False, msg)
+
+ def attrs_exist(self, list, expected_attrs, msg=None):
+ # Check these attrs only are returned in create response
+ for attr in list:
+ if attr not in expected_attrs:
+ self.fail("%s should not contain '%s'" % (msg, attr))
+
+ def links(self, links):
+ expected_attrs = ['href', 'rel']
+ for link in links:
+ self.attrs_exist(link, expected_attrs, msg="Links")
+
+
+class CollectionCheck(Check):
+ """Checks for elements in a dictionary."""
+
+ def __init__(self, name, collection):
+ self.name = name
+ self.collection = collection
+ super(CollectionCheck, self).__init__()
+
+ def element_equals(self, key, expected_value):
+ if key not in self.collection:
+ message = 'Element "%s.%s" does not exist.' % (self.name, key)
+ self.fail(message)
+ else:
+ value = self.collection[key]
+ self.equal(value, expected_value)
+
+ def has_element(self, key, element_type):
+ if key not in self.collection:
+ message = 'Element "%s.%s" does not exist.' % (self.name, key)
+ self.fail(message)
+ else:
+ value = self.collection[key]
+ match = False
+ if not isinstance(element_type, tuple):
+ type_list = [element_type]
+ else:
+ type_list = element_type
+ for possible_type in type_list:
+ if possible_type is None:
+ if value is None:
+ match = True
+ else:
+ if isinstance(value, possible_type):
+ match = True
+ if not match:
+ self.fail('Element "%s.%s" does not match any of these '
+ 'expected types: %s' % (self.name, key, type_list))
+
+
+class TypeCheck(Check):
+ """Checks for attributes in an object."""
+
+ def __init__(self, name, instance):
+ self.name = name
+ self.instance = instance
+ super(TypeCheck, self).__init__()
+
+ def _check_type(value, attribute_type):
+ if not isinstance(value, attribute_type):
+ self.fail("%s attribute %s is of type %s (expected %s)."
+ % (self.name, attribute_name, type(value),
+ attribute_type))
+
+ def has_field(self, attribute_name, attribute_type,
+ additional_checks=None):
+ if not hasattr(self.instance, attribute_name):
+ self.fail("%s missing attribute %s." % (self.name, attribute_name))
+ else:
+ value = getattr(self.instance, attribute_name)
+ match = False
+ if isinstance(attribute_type, tuple):
+ type_list = attribute_type
+ else:
+ type_list = [attribute_type]
+ for possible_type in type_list:
+ if possible_type is None:
+ if value is None:
+ match = True
+ else:
+ if isinstance(value, possible_type):
+ match = True
+ if not match:
+ self.fail("%s attribute %s is of type %s (expected one of "
+ "the following: %s)." % (self.name, attribute_name,
+ type(value), attribute_type))
+ if match and additional_checks:
+ additional_checks(value)
diff --git a/reddwarf/tests/util/client.py b/reddwarf/tests/util/client.py
new file mode 100644
index 00000000..a234fad1
--- /dev/null
+++ b/reddwarf/tests/util/client.py
@@ -0,0 +1,119 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright (c) 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.
+
+"""
+:mod:`tests` -- Utility methods for tests.
+===================================
+
+.. automodule:: utils
+ :platform: Unix
+ :synopsis: Tests for Nova.
+.. moduleauthor:: Nirmal Ranganathan <nirmal.ranganathan@rackspace.com>
+.. moduleauthor:: Tim Simpson <tim.simpson@rackspace.com>
+"""
+
+
+from nose.tools import assert_false
+from nose.tools import assert_true
+from reddwarf.tests.config import CONFIG
+
+
+def add_report_event_to(home, name):
+ """Takes a module, class, etc, and an attribute name to decorate."""
+ func = getattr(home, name)
+
+ def __cb(*args, **kwargs):
+ # While %s turns a var into a string but in some rare cases explicit
+ # str() is less likely to raise an exception.
+ arg_strs = [repr(arg) for arg in args]
+ arg_strs += ['%s=%s' % (repr(key), repr(value))
+ for (key, value) in kwargs.items()]
+ CONFIG.get_reporter().log("[RDC] Calling : %s(%s)..."
+ % (name, ','.join(arg_strs)))
+ value = func(*args, **kwargs)
+ CONFIG.get_reporter.log("[RDC] returned %s." % str(value))
+ return value
+ setattr(home, name, __cb)
+
+
+class TestClient(object):
+ """Decorates the rich clients with some extra methods.
+
+ These methods are filled with test asserts, meaning if you use this you
+ get the tests for free.
+
+ """
+
+ def __init__(self, real_client):
+ """Accepts a normal client."""
+ self.real_client = real_client
+
+ def assert_http_code(self, expected_http_code):
+ resp, body = self.real_client.client.last_response
+ assert_equal(resp.status, expected_http_code)
+
+ @property
+ def last_http_code(self):
+ resp, body = self.real_client.client.last_response
+ return resp.status
+
+ @staticmethod
+ def find_flavor_self_href(flavor):
+ self_links = [link for link in flavor.links if link['rel'] == 'self']
+ assert_true(len(self_links) > 0, "Flavor had no self href!")
+ flavor_href = self_links[0]['href']
+ assert_false(flavor_href is None, "Flavor link self href missing.")
+ return flavor_href
+
+ def find_flavors_by(self, condition, flavor_manager=None):
+ flavor_manager = flavor_manager or self.flavors
+ flavors = flavor_manager.list()
+ return [flavor for flavor in flavors if condition(flavor)]
+
+ def find_flavors_by_name(self, name, flavor_manager=None):
+ return self.find_flavors_by(lambda flavor: flavor.name == name,
+ flavor_manager)
+
+ def find_flavors_by_ram(self, ram, flavor_manager=None):
+ return self.find_flavors_by(lambda flavor: flavor.ram == ram,
+ flavor_manager)
+
+ def find_flavor_and_self_href(self, flavor_id, flavor_manager=None):
+ """Given an ID, returns flavor and its self href."""
+ flavor_manager = flavor_manager or self.flavors
+ assert_false(flavor_id is None)
+ flavor = flavor_manager.get(flavor_id)
+ assert_false(flavor is None)
+ flavor_href = self.find_flavor_self_href(flavor)
+ return flavor, flavor_href
+
+ def find_image_and_self_href(self, image_id):
+ """Given an ID, returns tuple with image and its self href."""
+ assert_false(image_id is None)
+ image = self.images.get(image_id)
+ assert_true(image is not None)
+ self_links = [link['href'] for link in image.links
+ if link['rel'] == 'self']
+ assert_true(len(self_links) > 0,
+ "Found image with ID %s but it had no self link!" %
+ str(image_id))
+ image_href = self_links[0]
+ assert_false(image_href is None, "Image link self href missing.")
+ return image, image_href
+
+ def __getattr__(self, item):
+ return getattr(self.real_client, item)
diff --git a/reddwarf/tests/util/users.py b/reddwarf/tests/util/users.py
new file mode 100644
index 00000000..bffd8eee
--- /dev/null
+++ b/reddwarf/tests/util/users.py
@@ -0,0 +1,134 @@
+# Copyright (c) 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.
+
+"""Information on users / identities we can hit the services on behalf of.
+
+This code allows tests to grab from a set of users based on the features they
+possess instead of specifying exact identities in the test code.
+
+"""
+
+
+class Requirements(object):
+ """Defines requirements a test has of a user."""
+
+ def __init__(self, is_admin, services=None):
+ self.is_admin = is_admin
+ self.services = services or ["reddwarf"]
+ # Make sure they're all the same kind of string.
+ self.services = [str(service) for service in self.services]
+
+ def satisfies(self, reqs):
+ """True if these requirements conform to the given requirements."""
+ if reqs.is_admin != self.is_admin:
+ return False
+ for service in reqs.services:
+ if service not in self.services:
+ return False
+ return True
+
+ def __str__(self):
+ return "is_admin=%s, services=%s" % (self.is_admin, self.services)
+
+
+class ServiceUser(object):
+ """Represents a user who uses a service.
+
+ Importantly, this represents general information, such that a test can be
+ written to state the general information about a user it needs (for
+ example, if the user is an admin or not) rather than explicitly list
+ users.
+
+ """
+
+ def __init__(self, auth_user=None, auth_key=None, services=None,
+ tenant=None, tenant_id=None, requirements=None):
+ """Creates info on a user."""
+ self.auth_user = auth_user
+ self.auth_key = auth_key
+ self.tenant = tenant
+ self.tenant_id = tenant_id
+ self.requirements = requirements
+ self.test_count = 0
+
+ def __str__(self):
+ return "{ user_name=%s, tenant_id=%s, reqs=%s, tests=%d }" % (
+ self.auth_user, self.tenant_id, self.requirements, self.test_count)
+ self.auth_key = auth_key
+ self.tenant = tenant
+ self.tenant_id = tenant_id
+ self.requirements = requirements
+ self.test_count = 0
+
+
+class Users(object):
+ """Collection of users with methods to find them via requirements."""
+
+ def __init__(self, user_list):
+ self.users = []
+ for user_dict in user_list:
+ reqs = Requirements(**user_dict["requirements"])
+ user = ServiceUser(auth_user=user_dict["auth_user"],
+ auth_key=user_dict["auth_key"],
+ tenant=user_dict["tenant"],
+ tenant_id=user_dict.get("tenant_id", None),
+ requirements=reqs)
+ self.users.append(user)
+
+ def find_all_users_who_satisfy(self, requirements, black_list=None):
+ """Returns a list of all users who satisfy the given requirements."""
+ black_list = black_list or []
+ print("Searching for a user who meets requirements %s in our list..."
+ % requirements)
+ print("Users:")
+ for user in self.users:
+ print("\t" + str(user))
+ print("Black list")
+ for item in black_list:
+ print("\t" + str(item))
+ black_list = black_list or []
+ return (user for user in self.users
+ if user.auth_user not in black_list and
+ user.requirements.satisfies(requirements))
+
+ def find_user(self, requirements, black_list=None):
+ """Finds a user who meets the requirements and has been used least."""
+ users = self.find_all_users_who_satisfy(requirements, black_list)
+ try:
+ user = min(users, key=lambda user: user.test_count)
+ except ValueError: # Raised when "users" is empty.
+ raise RuntimeError("The test configuration data lacks a user "
+ "who meets these requirements: %s"
+ % requirements)
+ user.test_count += 1
+ return user
+
+ def _find_user_by_condition(self, condition):
+ users = (user for user in self.users if condition(user))
+ try:
+ user = min(users, key=lambda user: user.test_count)
+ except ValueError:
+ raise RuntimeError('Did not find a user with name "%s".' % name)
+ user.test_count += 1
+ return user
+
+ def find_user_by_name(self, name):
+ """Finds a user who meets the requirements and has been used least."""
+ condition = lambda user: user.auth_user == name
+ return self._find_user_by_condition(condition)
+
+ def find_user_by_tenant_id(self, tenant_id):
+ condition = lambda user: user.tenant_id == tenant_id
+ return self._find_user_by_condition(condition)
diff --git a/run_tests.py b/run_tests.py
index 775adf40..b438092d 100644
--- a/run_tests.py
+++ b/run_tests.py
@@ -1,367 +1,138 @@
-#!/usr/bin/env python
-# vim: tabstop=4 shiftwidth=4 softtabstop=4
-
-# Copyright 2010 United States Government as represented by the
-# Administrator of the National Aeronautics and Space Administration.
-# 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.
-
-# Colorizer Code is borrowed from Twisted:
-# Copyright (c) 2001-2010 Twisted Matrix Laboratories.
-#
-# Permission is hereby granted, free of charge, to any person obtaining
-# a copy of this software and associated documentation files (the
-# "Software"), to deal in the Software without restriction, including
-# without limitation the rights to use, copy, modify, merge, publish,
-# distribute, sublicense, and/or sell copies of the Software, and to
-# permit persons to whom the Software is furnished to do so, subject to
-# the following conditions:
-#
-# The above copyright notice and this permission notice shall be
-# included in all copies or substantial portions of the Software.
-#
-# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
-# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
-# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
-# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
-# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
-# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
-# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
-"""Unittest runner for Nova.
-
-To run all tests
- python run_tests.py
-
-To run a single test:
- python run_tests.py test_compute:ComputeTestCase.test_run_terminate
-
-To run a single test module:
- python run_tests.py test_compute
-
- or
-
- python run_tests.py api.test_wsgi
-
-"""
-
import gettext
-import heapq
-import logging
import os
-import unittest
+import urllib
import sys
-import time
-
-gettext.install('reddwarf', unicode=1)
-from nose import config
-from nose import core
-from nose import result
+from reddwarf.tests.config import CONFIG
+from wsgi_intercept.httplib2_intercept import install as wsgi_install
+import proboscis
+from eventlet import greenthread
+import wsgi_intercept
-class _AnsiColorizer(object):
- """
- A colorizer is an object that loosely wraps around a stream, allowing
- callers to write text to the stream in a particular color.
-
- Colorizer classes must implement C{supported()} and C{write(text, color)}.
- """
- _colors = dict(black=30, red=31, green=32, yellow=33,
- blue=34, magenta=35, cyan=36, white=37)
- def __init__(self, stream):
- self.stream = stream
+def add_support_for_localization():
+ """Adds support for localization in the logging.
- def supported(cls, stream=sys.stdout):
- """
- A class method that returns True if the current platform supports
- coloring terminal output using this method. Returns False otherwise.
- """
- if not stream.isatty():
- return False # auto color only on TTYs
- try:
- import curses
- except ImportError:
- return False
- else:
- try:
- try:
- return curses.tigetnum("colors") > 2
- except curses.error:
- curses.setupterm()
- return curses.tigetnum("colors") > 2
- except:
- raise
- # guess false in case of error
- return False
- supported = classmethod(supported)
+ If ../nova/__init__.py exists, add ../ to Python search path, so that
+ it will override what happens to be installed in
+ /usr/(local/)lib/python...
- def write(self, text, color):
- """
- Write the given text to the stream in the given color.
-
- @param text: Text to be written to the stream.
-
- @param color: A string label for a color. e.g. 'red', 'white'.
- """
- color = self._colors[color]
- self.stream.write('\x1b[%s;1m%s\x1b[0m' % (color, text))
-
-
-class _Win32Colorizer(object):
- """
- See _AnsiColorizer docstring.
- """
- def __init__(self, stream):
- from win32console import GetStdHandle, STD_OUT_HANDLE,\
- FOREGROUND_RED, FOREGROUND_BLUE, FOREGROUND_GREEN,\
- FOREGROUND_INTENSITY
- red, green, blue, bold = (FOREGROUND_RED, FOREGROUND_GREEN,
- FOREGROUND_BLUE, FOREGROUND_INTENSITY)
- self.stream = stream
- self.screenBuffer = GetStdHandle(STD_OUT_HANDLE)
- self._colors = {
- 'normal': red | green | blue,
- 'red': red | bold,
- 'green': green | bold,
- 'blue': blue | bold,
- 'yellow': red | green | bold,
- 'magenta': red | blue | bold,
- 'cyan': green | blue | bold,
- 'white': red | green | blue | bold
- }
-
- def supported(cls, stream=sys.stdout):
- try:
- import win32console
- screenBuffer = win32console.GetStdHandle(
- win32console.STD_OUT_HANDLE)
- except ImportError:
- return False
- import pywintypes
- try:
- screenBuffer.SetConsoleTextAttribute(
- win32console.FOREGROUND_RED |
- win32console.FOREGROUND_GREEN |
- win32console.FOREGROUND_BLUE)
- except pywintypes.error:
- return False
- else:
- return True
- supported = classmethod(supported)
-
- def write(self, text, color):
- color = self._colors[color]
- self.screenBuffer.SetConsoleTextAttribute(color)
- self.stream.write(text)
- self.screenBuffer.SetConsoleTextAttribute(self._colors['normal'])
-
-
-class _NullColorizer(object):
- """
- See _AnsiColorizer docstring.
"""
- def __init__(self, stream):
- self.stream = stream
-
- def supported(cls, stream=sys.stdout):
- return True
- supported = classmethod(supported)
-
- def write(self, text, color):
- self.stream.write(text)
-
-
-def get_elapsed_time_color(elapsed_time):
- if elapsed_time > 1.0:
- return 'red'
- elif elapsed_time > 0.25:
- return 'yellow'
- else:
- return 'green'
-
-
-class ReddwarfTestResult(result.TextTestResult):
- def __init__(self, *args, **kw):
- self.show_elapsed = kw.pop('show_elapsed')
- result.TextTestResult.__init__(self, *args, **kw)
- self.num_slow_tests = 5
- self.slow_tests = [] # this is a fixed-sized heap
- self._last_case = None
- self.colorizer = None
- # NOTE(vish): reset stdout for the terminal check
- stdout = sys.stdout
- sys.stdout = sys.__stdout__
- for colorizer in [_Win32Colorizer, _AnsiColorizer, _NullColorizer]:
- if colorizer.supported():
- self.colorizer = colorizer(self.stream)
- break
- sys.stdout = stdout
-
- # NOTE(lorinh): Initialize start_time in case a sqlalchemy-migrate
- # error results in it failing to be initialized later. Otherwise,
- # _handleElapsedTime will fail, causing the wrong error message to
- # be outputted.
- self.start_time = time.time()
-
- def getDescription(self, test):
- return str(test)
-
- def _handleElapsedTime(self, test):
- self.elapsed_time = time.time() - self.start_time
- item = (self.elapsed_time, test)
- # Record only the n-slowest tests using heap
- if len(self.slow_tests) >= self.num_slow_tests:
- heapq.heappushpop(self.slow_tests, item)
- else:
- heapq.heappush(self.slow_tests, item)
-
- def _writeElapsedTime(self, test):
- color = get_elapsed_time_color(self.elapsed_time)
- self.colorizer.write(" %.2f" % self.elapsed_time, color)
-
- def _writeResult(self, test, long_result, color, short_result, success):
- if self.showAll:
- self.colorizer.write(long_result, color)
- if self.show_elapsed and success:
- self._writeElapsedTime(test)
- self.stream.writeln()
- elif self.dots:
- self.stream.write(short_result)
- self.stream.flush()
-
- # NOTE(vish): copied from unittest with edit to add color
- def addSuccess(self, test):
- unittest.TestResult.addSuccess(self, test)
- self._handleElapsedTime(test)
- self._writeResult(test, 'OK', 'green', '.', True)
-
- # NOTE(vish): copied from unittest with edit to add color
- def addFailure(self, test, err):
- unittest.TestResult.addFailure(self, test, err)
- self._handleElapsedTime(test)
- self._writeResult(test, 'FAIL', 'red', 'F', False)
-
- # NOTE(vish): copied from nose with edit to add color
- def addError(self, test, err):
- """Overrides normal addError to add support for
- errorClasses. If the exception is a registered class, the
- error will be added to the list for that class, not errors.
- """
- self._handleElapsedTime(test)
- stream = getattr(self, 'stream', None)
- ec, ev, tb = err
- try:
- exc_info = self._exc_info_to_string(err, test)
- except TypeError:
- # 2.3 compat
- exc_info = self._exc_info_to_string(err)
- for cls, (storage, label, isfail) in self.errorClasses.items():
- if result.isclass(ec) and issubclass(ec, cls):
- if isfail:
- test.passed = False
- storage.append((test, exc_info))
- # Might get patched into a streamless result
- if stream is not None:
- if self.showAll:
- message = [label]
- detail = result._exception_detail(err[1])
- if detail:
- message.append(detail)
- stream.writeln(": ".join(message))
- elif self.dots:
- stream.write(label[:1])
- return
- self.errors.append((test, exc_info))
- test.passed = False
- if stream is not None:
- self._writeResult(test, 'ERROR', 'red', 'E', False)
-
- def startTest(self, test):
- unittest.TestResult.startTest(self, test)
- self.start_time = time.time()
- current_case = test.test.__class__.__name__
-
- if self.showAll:
- if current_case != self._last_case:
- self.stream.writeln(current_case)
- self._last_case = current_case
-
- self.stream.write(
- ' %s' % str(test.test._testMethodName).ljust(60))
- self.stream.flush()
-
-
-class ReddwarfTestRunner(core.TextTestRunner):
- def __init__(self, *args, **kwargs):
- self.show_elapsed = kwargs.pop('show_elapsed')
- core.TextTestRunner.__init__(self, *args, **kwargs)
-
- def _makeResult(self):
- return ReddwarfTestResult(self.stream,
- self.descriptions,
- self.verbosity,
- self.config,
- show_elapsed=self.show_elapsed)
-
- def _writeSlowTests(self, result_):
- # Pare out 'fast' tests
- slow_tests = [item for item in result_.slow_tests
- if get_elapsed_time_color(item[0]) != 'green']
- if slow_tests:
- slow_total_time = sum(item[0] for item in slow_tests)
- self.stream.writeln("Slowest %i tests took %.2f secs:"
- % (len(slow_tests), slow_total_time))
- for elapsed_time, test in sorted(slow_tests, reverse=True):
- time_str = "%.2f" % elapsed_time
- self.stream.writeln(" %s %s" % (time_str.ljust(10), test))
-
- def run(self, test):
- result_ = core.TextTestRunner.run(self, test)
- if self.show_elapsed:
- self._writeSlowTests(result_)
- return result_
-
-
-if __name__ == '__main__':
- logger = logging.getLogger()
- hdlr = logging.StreamHandler()
- formatter = logging.Formatter('%(asctime)s %(levelname)s %(message)s')
- hdlr.setFormatter(formatter)
- logger.addHandler(hdlr)
- logger.setLevel(logging.DEBUG)
- # If any argument looks like a test name but doesn't have "reddwarf.tests"
- # in front of it, automatically add that so we don't have to type as much
- show_elapsed = True
- argv = []
- for x in sys.argv:
- if x.startswith('test_'):
- argv.append('reddwarf.tests.%s' % x)
- elif x.startswith('--hide-elapsed'):
- show_elapsed = False
- else:
- argv.append(x)
-
- testdir = os.path.abspath(os.path.join("reddwarf", "tests"))
- c = config.Config(stream=sys.stdout,
- env=os.environ,
- verbosity=3,
- workingDir=testdir,
- plugins=core.DefaultPluginManager())
-
- runner = ReddwarfTestRunner(stream=c.stream,
- verbosity=c.verbosity,
- config=c,
- show_elapsed=show_elapsed)
- sys.exit(not core.run(config=c, testRunner=runner, argv=argv))
+ path = os.path.join(os.path.abspath(sys.argv[0]), os.pardir, os.pardir)
+ possible_topdir = os.path.normpath(path)
+ if os.path.exists(os.path.join(possible_topdir, 'nova', '__init__.py')):
+ sys.path.insert(0, possible_topdir)
+
+ gettext.install('nova', unicode=1)
+
+
+def initialize_reddwarf(config_file):
+ # The test version of poll_until doesn't utilize LoopingCall.
+ import optparse
+ from reddwarf.db import db_api
+ from reddwarf.common import config as rd_config
+ from reddwarf.common import wsgi
+ from reddwarf import version
+
+
+ def create_options(parser):
+ parser.add_option('-p', '--port', dest="port", metavar="PORT",
+ type=int, default=9898,
+ help="Port the Reddwarf API host listens on. "
+ "Default: %default")
+ rd_config.add_common_options(parser)
+ rd_config.add_log_options(parser)
+
+ def usage():
+ usage = ""
+
+ oparser = optparse.OptionParser(version="%%prog %s"
+ % version.version_string(),
+ usage=usage())
+ create_options(oparser)
+ (options, args) = rd_config.parse_options(oparser, cli_args=[config_file])
+ rd_config.Config.load_paste_config('reddwarf', options, args)
+ # Modify these values by hand
+ rd_config.Config.instance['fake_mode_events'] = 'simulated'
+ rd_config.Config.instance['log_file'] = 'rdtest.log'
+ conf, app = rd_config.Config.load_paste_app('reddwarf', options, args)
+ rd_config.setup_logging(options, conf)
+ return conf, app
+
+
+def initialize_database(rd_conf):
+ from reddwarf.db import db_api
+ from reddwarf.instance import models
+ from reddwarf.db.sqlalchemy import session
+ db_api.drop_db(rd_conf) # Destroys the database, if it exists.
+ db_api.db_sync(rd_conf)
+ session.configure_db(rd_conf)
+ # Adds the image for mysql (needed to make most calls work).
+ models.ServiceImage.create(service_name="mysql", image_id="fake")
+ db_api.configure_db(rd_conf)
+
+
+def initialize_fakes(app):
+ # Set up WSGI interceptor. This sets up a fake host that responds each
+ # time httplib tries to communicate to localhost, port 8779.
+ def wsgi_interceptor(*args, **kwargs):
+
+ def call_back(env, start_response):
+ path_info = env.get('PATH_INFO')
+ if path_info:
+ env['PATH_INFO'] = urllib.unquote(path_info)
+ #print("%s %s" % (args, kwargs))
+ return app.__call__(env, start_response)
+
+ return call_back
+
+ wsgi_intercept.add_wsgi_intercept('localhost', 8779, wsgi_interceptor)
+
+ # Finally, engage in some truly evil monkey business. We want
+ # to change anything which spawns threads with eventlet to instead simply
+ # put those functions on a queue in memory. Then, we swap out any functions
+ # which might try to take a nap to instead call functions that go through
+ # this queue and call the functions that would normally run in seperate
+ # threads.
+ import eventlet
+ from reddwarf.tests.fakes.common import event_simulator_sleep
+ eventlet.sleep = event_simulator_sleep
+ greenthread.sleep = event_simulator_sleep
+ import time
+ time.sleep = event_simulator_sleep
+
+
+def replace_poll_until():
+ from reddwarf.common import utils as rd_utils
+ from reddwarf.tests import util as test_utils
+ rd_utils.poll_until = test_utils.poll_until
+
+if __name__=="__main__":
+ wsgi_install()
+ add_support_for_localization()
+ replace_poll_until()
+ # Load Reddwarf config file.
+ conf, app = initialize_reddwarf("etc/reddwarf/reddwarf.conf.test")
+ # Initialize sqlite database.
+ initialize_database(conf)
+ # Swap out WSGI, httplib, and several sleep functions with test doubles.
+ initialize_fakes(app)
+ # Initialize the test configuration.
+ CONFIG.load_from_file("etc/tests/localhost.test.conf")
+
+ from reddwarf.tests.api import flavors
+ from reddwarf.tests.api import versions
+ from reddwarf.tests.api import instances
+ from reddwarf.tests.api import instances_actions
+ from reddwarf.tests.api import instances_delete
+ from reddwarf.tests.api import instances_mysql_down
+ from reddwarf.tests.api import databases
+ from reddwarf.tests.api import root
+ from reddwarf.tests.api import users
+ from reddwarf.tests.api.mgmt import accounts
+ from reddwarf.tests.api.mgmt import admin_required
+ from reddwarf.tests.api.mgmt import instances
+ from reddwarf.tests.api.mgmt import storage
+
+ proboscis.TestProgram().run_and_exit()
diff --git a/tools/pip-requires b/tools/pip-requires
index 2ce121a1..59b5ba1c 100644
--- a/tools/pip-requires
+++ b/tools/pip-requires
@@ -1,11 +1,11 @@
-SQLAlchemy
+SQLAlchemy>=0.7.8,<=0.7.9
eventlet
kombu==1.5.1
routes
WebOb
PasteDeploy
paste
-sqlalchemy-migrate
+sqlalchemy-migrate>=0.7.2
netaddr
factory_boy
httplib2
diff --git a/tools/test-requires b/tools/test-requires
index c1a784fc..da4941ec 100644
--- a/tools/test-requires
+++ b/tools/test-requires
@@ -9,3 +9,6 @@ openstack.nose_plugin
pep8==1.3.3
pylint
webtest
+wsgi_intercept
+proboscis
+python-reddwarfclient
diff --git a/tox.ini b/tox.ini
index de585b82..fec75f37 100644
--- a/tox.ini
+++ b/tox.ini
@@ -3,15 +3,10 @@ envlist = py26,py27,pep8
[testenv]
setenv = VIRTUAL_ENV={envdir}
- NOSE_WITH_OPENSTACK=1
- NOSE_OPENSTACK_COLOR=1
- NOSE_OPENSTACK_RED=0.05
- NOSE_OPENSTACK_YELLOW=0.025
- NOSE_OPENSTACK_SHOW_ELAPSED=1
- NOSE_OPENSTACK_STDOUT=1
deps = -r{toxinidir}/tools/pip-requires
-r{toxinidir}/tools/test-requires
setuptools_git>=0.4
+commands = {envpython} run_tests.py {posargs}
[tox:jenkins]
sitepackages = True
@@ -24,6 +19,13 @@ commands = pep8 --repeat --show-source --ignore=E125 --exclude=.venv,.tox,dist,d
[testenv:cover]
setenv = NOSE_WITH_COVERAGE=1
+# TODO(tim.simpson): For some reason, this must run twice before the coverage
+# report is accurate.
+commands =
+ {toxinidir}/.tox/cover/bin/coverage erase
+ {toxinidir}/.tox/cover/bin/coverage run --timid -p run_tests.py {posargs}
+ {toxinidir}/.tox/cover/bin/coverage html -d covhtml -i --omit=*tests*,*openstack*,*reddwarfclient*
+
[testenv:venv]
commands = {posargs}