diff --git a/rally/verification/tempest/config.ini b/rally/verification/tempest/config.ini index 2c16250855..194596723a 100644 --- a/rally/verification/tempest/config.ini +++ b/rally/verification/tempest/config.ini @@ -43,6 +43,7 @@ ipv6 = True instance_type = [scenario] +img_file = [service_available] diff --git a/rally/verification/tempest/config.py b/rally/verification/tempest/config.py index 4c9bf20322..e31bdf0888 100644 --- a/rally/verification/tempest/config.py +++ b/rally/verification/tempest/config.py @@ -116,6 +116,41 @@ def _create_or_get_data_dir(): return data_dir +def _download_image(image_path, image=None): + if image: + LOG.debug("Downloading image '%s' " + "from Glance to %s" % (image.name, image_path)) + with open(image_path, "wb") as image_file: + for chunk in image.data(): + image_file.write(chunk) + else: + LOG.debug("Downloading image from %s " + "to %s" % (CONF.tempest.img_url, image_path)) + try: + response = requests.get(CONF.tempest.img_url, stream=True) + except requests.ConnectionError as err: + msg = _("Failed to download image. " + "Possibly there is no connection to Internet. " + "Error: %s.") % (str(err) or "unknown") + raise exceptions.TempestConfigCreationFailure(msg) + + if response.status_code == 200: + with open(image_path, "wb") as image_file: + for chunk in response.iter_content(chunk_size=1024): + if chunk: # filter out keep-alive new chunks + image_file.write(chunk) + image_file.flush() + else: + if response.status_code == 404: + msg = _("Failed to download image. Image was not found.") + else: + msg = _("Failed to download image. " + "HTTP error code %d.") % response.status_code + raise exceptions.TempestConfigCreationFailure(msg) + + LOG.debug("The image has been successfully downloaded!") + + def _write_config(conf_path, conf_data): with open(conf_path, "w+") as conf_file: conf_data.write(conf_file) @@ -137,39 +172,6 @@ class TempestConfig(utils.RandomNameGeneratorMixin): self.conf = configparser.ConfigParser() self.conf.read(os.path.join(os.path.dirname(__file__), "config.ini")) - self.image_name = parse.urlparse( - CONF.tempest.img_url).path.split("/")[-1] - self._download_image() - - def _download_image(self): - img_path = os.path.join(self.data_dir, self.image_name) - if os.path.isfile(img_path): - return - - try: - response = requests.get(CONF.tempest.img_url, stream=True) - except requests.ConnectionError as err: - msg = _("Failed to download image. " - "Possibly there is no connection to Internet. " - "Error: %s.") % (str(err) or "unknown") - raise exceptions.TempestConfigCreationFailure(msg) - - if response.status_code == 200: - with open(img_path + ".tmp", "wb") as img_file: - for chunk in response.iter_content(chunk_size=1024): - if chunk: # filter out keep-alive new chunks - img_file.write(chunk) - img_file.flush() - os.rename(img_path + ".tmp", img_path) - else: - if response.status_code == 404: - msg = _("Failed to download image. " - "Image was not found.") - else: - msg = _("Failed to download image. " - "HTTP error code %d.") % response.status_code - raise exceptions.TempestConfigCreationFailure(msg) - def _get_service_url(self, service_name): s_type = self._get_service_type_by_service_name(service_name) available_endpoints = self.keystone.service_catalog.get_endpoints() @@ -284,7 +286,6 @@ class TempestConfig(utils.RandomNameGeneratorMixin): def _configure_scenario(self, section_name="scenario"): self.conf.set(section_name, "img_dir", self.data_dir) - self.conf.set(section_name, "img_file", self.image_name) def _configure_service_available(self, section_name="service_available"): services = ["cinder", "glance", "heat", "ironic", "neutron", "nova", @@ -338,8 +339,8 @@ class TempestResourcesContext(utils.RandomNameGeneratorMixin): self.conf = configparser.ConfigParser() self.conf.read(conf_path) - self.image_name = parse.urlparse( - CONF.tempest.img_url).path.split("/")[-1] + self.data_dir = _create_or_get_data_dir() + self.image_name = "tempest-image" self._created_roles = [] self._created_images = [] @@ -348,16 +349,19 @@ class TempestResourcesContext(utils.RandomNameGeneratorMixin): def __enter__(self): self._create_tempest_roles() + + self._configure_option("scenario", "img_file", self.image_name, + helper_method=self._download_image) self._configure_option("compute", "image_ref", - self._discover_or_create_image) + helper_method=self._discover_or_create_image) self._configure_option("compute", "image_ref_alt", - self._discover_or_create_image) + helper_method=self._discover_or_create_image) self._configure_option("compute", "flavor_ref", - self._discover_or_create_flavor, - CONF.tempest.flavor_ref_ram) + helper_method=self._discover_or_create_flavor, + flv_ram=CONF.tempest.flavor_ref_ram) self._configure_option("compute", "flavor_ref_alt", - self._discover_or_create_flavor, - CONF.tempest.flavor_ref_alt_ram) + helper_method=self._discover_or_create_flavor, + flv_ram=CONF.tempest.flavor_ref_alt_ram) if "neutron" in self.available_services: neutronclient = self.clients.neutron() if neutronclient.list_networks(shared=True)["networks"]: @@ -370,12 +374,14 @@ class TempestResourcesContext(utils.RandomNameGeneratorMixin): # resources. LOG.debug("Shared networks found. " "'fixed_network_name' option should be configured") - self._configure_option("compute", "fixed_network_name", - self._create_network_resources) + self._configure_option( + "compute", "fixed_network_name", + helper_method=self._create_network_resources) if "heat" in self.available_services: - self._configure_option("orchestration", "instance_type", - self._discover_or_create_flavor, - CONF.tempest.heat_instance_type_ram) + self._configure_option( + "orchestration", "instance_type", + helper_method=self._discover_or_create_flavor, + flv_ram=CONF.tempest.heat_instance_type_ram) _write_config(self.conf_path, self.conf) @@ -406,14 +412,46 @@ class TempestResourcesContext(utils.RandomNameGeneratorMixin): LOG.debug("Creating role '%s'" % role) self._created_roles.append(keystoneclient.roles.create(role)) - def _configure_option(self, section, option, - create_method, *args, **kwargs): + def _discover_image(self): + LOG.debug("Trying to discover a public image with name matching " + "regular expression '%s'. Note that case insensitive " + "matching is performed." % CONF.tempest.img_name_regex) + glance_wrapper = glance.wrap(self.clients.glance, self) + images = glance_wrapper.list_images(status="active", + visibility="public") + for image in images: + if image.name and re.match(CONF.tempest.img_name_regex, + image.name, re.IGNORECASE): + LOG.debug("The following public " + "image discovered: '%s'" % image.name) + return image + + LOG.debug("There is no public image with name matching " + "regular expression '%s'" % CONF.tempest.img_name_regex) + + def _download_image(self): + image_path = os.path.join(self.data_dir, self.image_name) + if os.path.isfile(image_path): + LOG.debug("Image is already downloaded to %s" % image_path) + return + + if CONF.tempest.img_name_regex: + image = self._discover_image() + if image: + return _download_image(image_path, image) + + _download_image(image_path) + + def _configure_option(self, section, option, value=None, + helper_method=None, *args, **kwargs): option_value = self.conf.get(section, option) if not option_value: LOG.debug("Option '%s' from '%s' section " "is not configured" % (option, section)) - resource = create_method(*args, **kwargs) - value = resource["name"] if "network" in option else resource.id + if helper_method: + res = helper_method(*args, **kwargs) + if res: + value = res["name"] if "network" in option else res.id LOG.debug("Setting value '%s' for option '%s'" % (value, option)) self.conf.set(section, option, value) LOG.debug("Option '{opt}' is configured. " @@ -424,35 +462,25 @@ class TempestResourcesContext(utils.RandomNameGeneratorMixin): .format(opt=option, opt_val=option_value)) def _discover_or_create_image(self): - glance_wrapper = glance.wrap(self.clients.glance, self) - if CONF.tempest.img_name_regex: - LOG.debug("Trying to discover a public image with name matching " - "regular expression '%s'. Note that case insensitive " - "matching is performed" % CONF.tempest.img_name_regex) - images = glance_wrapper.list_images(status="active", - visibility="public") - for img in images: - if img.name and re.match(CONF.tempest.img_name_regex, - img.name, re.IGNORECASE): - LOG.debug( - "The following public image discovered: '{0}'. " - "Using image '{0}' for the tests".format(img.name)) - return img - - LOG.debug("There is no public image with name matching " - "regular expression '%s'" % CONF.tempest.img_name_regex) + image = self._discover_image() + if image: + LOG.debug("Using image '%s' (ID = %s) " + "for the tests" % (image.name, image.id)) + return image params = { "name": self.generate_random_name(), "disk_format": CONF.tempest.img_disk_format, "container_format": CONF.tempest.img_container_format, - "image_location": os.path.join(_create_or_get_data_dir(), - self.image_name), + "image_location": os.path.join(self.data_dir, self.image_name), "visibility": "public" } LOG.debug("Creating image '%s'" % params["name"]) + glance_wrapper = glance.wrap(self.clients.glance, self) image = glance_wrapper.create_image(**params) + LOG.debug("Image '%s' (ID = %s) has been " + "successfully created!" % (image.name, image.id)) self._created_images.append(image) return image @@ -463,10 +491,11 @@ class TempestResourcesContext(utils.RandomNameGeneratorMixin): LOG.debug("Trying to discover a flavor with the following " "properties: RAM = %dMB, VCPUs = 1, disk = 0GB" % flv_ram) for flavor in novaclient.flavors.list(): - if (flavor.ram == flv_ram - and flavor.vcpus == 1 and flavor.disk == 0): - LOG.debug("The following flavor discovered: '{0}'. Using " - "flavor '{0}' for the tests".format(flavor.name)) + if (flavor.ram == flv_ram and + flavor.vcpus == 1 and flavor.disk == 0): + LOG.debug("The following flavor discovered: '{0}'. " + "Using flavor '{0}' (ID = {1}) for the tests" + .format(flavor.name, flavor.id)) return flavor LOG.debug("There is no flavor with the mentioned properties") @@ -480,6 +509,8 @@ class TempestResourcesContext(utils.RandomNameGeneratorMixin): LOG.debug("Creating flavor '%s' with the following properties: RAM " "= %dMB, VCPUs = 1, disk = 0GB" % (params["name"], flv_ram)) flavor = novaclient.flavors.create(**params) + LOG.debug("Flavor '%s' (ID = %s) has been " + "successfully created!" % (flavor.name, flavor.id)) self._created_flavors.append(flavor) return flavor @@ -491,6 +522,7 @@ class TempestResourcesContext(utils.RandomNameGeneratorMixin): net = neutron_wrapper.create_network( tenant_id, subnets_num=1, add_router=True, network_create_args={"shared": True}) + LOG.debug("Network resources have been successfully created!") self._created_networks.append(net) return net @@ -500,6 +532,7 @@ class TempestResourcesContext(utils.RandomNameGeneratorMixin): for role in self._created_roles: LOG.debug("Deleting role '%s'" % role.name) keystoneclient.roles.delete(role.id) + LOG.debug("Role '%s' has been deleted" % role.name) def _cleanup_images(self): glance_wrapper = glance.wrap(self.clients.glance, self) @@ -513,6 +546,7 @@ class TempestResourcesContext(utils.RandomNameGeneratorMixin): timeout=CONF.benchmark.glance_image_delete_timeout, check_interval=CONF.benchmark. glance_image_delete_poll_interval) + LOG.debug("Image '%s' has been deleted" % image.name) self._remove_opt_value_from_config("compute", image.id) def _cleanup_flavors(self): @@ -520,6 +554,7 @@ class TempestResourcesContext(utils.RandomNameGeneratorMixin): for flavor in self._created_flavors: LOG.debug("Deleting flavor '%s'" % flavor.name) novaclient.flavors.delete(flavor.id) + LOG.debug("Flavor '%s' has been deleted" % flavor.name) self._remove_opt_value_from_config("compute", flavor.id) self._remove_opt_value_from_config("orchestration", flavor.id) @@ -529,6 +564,7 @@ class TempestResourcesContext(utils.RandomNameGeneratorMixin): LOG.debug("Deleting network resources: router, subnet, network") neutron_wrapper.delete_network(net) self._remove_opt_value_from_config("compute", net["name"]) + LOG.debug("Network resources have been deleted") def _remove_opt_value_from_config(self, section, opt_value): for option, value in self.conf.items(section): @@ -536,3 +572,4 @@ class TempestResourcesContext(utils.RandomNameGeneratorMixin): LOG.debug("Removing value '%s' for option '%s' " "from Tempest config file" % (opt_value, option)) self.conf.set(section, option, "") + LOG.debug("Value '%s' has been removed" % opt_value) diff --git a/tests/ci/rally_verify.py b/tests/ci/rally_verify.py index cd39125435..e9e4fb0c23 100755 --- a/tests/ci/rally_verify.py +++ b/tests/ci/rally_verify.py @@ -46,7 +46,7 @@ EXPECTED_FAILURES = { "This test fails because 'novnc' console type is unavailable." } -TEMPEST_PLUGIN = "https://github.com/MBonell/hello-world-tempest-plugin" +TEMPEST_PLUGIN = "https://git.openstack.org/openstack/ceilometer" # NOTE(andreykurilin): this variable is used to generate output file names # with prefix ${CALL_COUNT}_ . @@ -249,7 +249,7 @@ def main(): render_vars["reinstall"] = call_rally( "verify reinstall --version %s" % tempest_commit_id) - # Install a simple Tempest plugin + # Install a Tempest plugin render_vars["installplugin"] = call_rally( "verify installplugin --source %s" % TEMPEST_PLUGIN) diff --git a/tests/unit/verification/test_config.py b/tests/unit/verification/test_config.py index 5822914b30..ed03129e5c 100644 --- a/tests/unit/verification/test_config.py +++ b/tests/unit/verification/test_config.py @@ -20,7 +20,6 @@ import mock from oslo_config import cfg import requests import six -from six.moves.urllib import parse from rally import exceptions from rally.verification.tempest import config @@ -53,35 +52,9 @@ class TempestConfigTestCase(test.TestCase): mock.patch("rally.common.objects.deploy.db.deployment_get", return_value=CREDS).start() mock.patch("rally.osclients.Clients").start() - self.mock_isfile = mock.patch("os.path.isfile", - return_value=True).start() self.tempest_conf = config.TempestConfig("fake_deployment") - @mock.patch("os.rename") - @mock.patch("six.moves.builtins.open", side_effect=mock.mock_open()) - @mock.patch("requests.get", return_value=mock.MagicMock(status_code=200)) - def test__download_image_success(self, mock_get, - mock_open, mock_rename): - self.mock_isfile.return_value = False - self.tempest_conf._download_image() - mock_get.assert_called_once_with( - CONF.tempest.img_url, stream=True) - - @mock.patch("requests.get") - @ddt.data(404, 500) - def test__download_image_failure(self, status_code, mock_get): - self.mock_isfile.return_value = False - mock_get.return_value = mock.MagicMock(status_code=status_code) - self.assertRaises(exceptions.TempestConfigCreationFailure, - self.tempest_conf._download_image) - - @mock.patch("requests.get", side_effect=requests.ConnectionError()) - def test__download_image_connection_error(self, mock_requests_get): - self.mock_isfile.return_value = False - self.assertRaises(exceptions.TempestConfigCreationFailure, - self.tempest_conf._download_image) - @ddt.data({"publicURL": "test_url"}, {"interface": "public", "url": "test_url"}) def test__get_service_url(self, endpoint): @@ -223,10 +196,7 @@ class TempestConfigTestCase(test.TestCase): def test__configure_scenario(self): self.tempest_conf._configure_scenario() - image_name = parse.urlparse( - config.CONF.tempest.img_url).path.split("/")[-1] - expected = (("img_dir", self.tempest_conf.data_dir), - ("img_file", image_name)) + expected = (("img_dir", self.tempest_conf.data_dir),) result = self.tempest_conf.conf.items("scenario") for item in expected: self.assertIn(item, result) @@ -284,6 +254,7 @@ class TempestConfigTestCase(test.TestCase): conf_data.write.assert_called_once_with(mock_open.side_effect()) +@ddt.ddt class TempestResourcesContextTestCase(test.TestCase): def setUp(self): @@ -292,6 +263,8 @@ class TempestResourcesContextTestCase(test.TestCase): mock.patch("rally.common.objects.deploy.db.deployment_get", return_value=CREDS).start() mock.patch("rally.osclients.Clients").start() + self.mock_isfile = mock.patch("os.path.isfile", + return_value=True).start() fake_verification = {"uuid": "uuid"} self.context = config.TempestResourcesContext("fake_deployment", @@ -299,6 +272,54 @@ class TempestResourcesContextTestCase(test.TestCase): "/fake/path/to/config") self.context.conf.add_section("compute") self.context.conf.add_section("orchestration") + self.context.conf.add_section("scenario") + + @mock.patch("six.moves.builtins.open", side_effect=mock.mock_open(), + create=True) + def test__download_image_from_glance(self, mock_open): + self.mock_isfile.return_value = False + img_path = os.path.join(self.context.data_dir, "foo") + img = mock.MagicMock() + img.data.return_value = "data" + + config._download_image(img_path, img) + mock_open.assert_called_once_with(img_path, "wb") + mock_open().write.assert_has_calls([mock.call("d"), + mock.call("a"), + mock.call("t"), + mock.call("a")]) + + @mock.patch("six.moves.builtins.open", side_effect=mock.mock_open()) + @mock.patch("requests.get", return_value=mock.MagicMock(status_code=200)) + def test__download_image_from_url_success(self, mock_get, mock_open): + self.mock_isfile.return_value = False + img_path = os.path.join(self.context.data_dir, "foo") + mock_get.return_value.iter_content.return_value = "data" + + config._download_image(img_path) + mock_get.assert_called_once_with(CONF.tempest.img_url, stream=True) + mock_open.assert_called_once_with(img_path, "wb") + mock_open().write.assert_has_calls([mock.call("d"), + mock.call("a"), + mock.call("t"), + mock.call("a")]) + + @mock.patch("requests.get") + @ddt.data(404, 500) + def test__download_image_from_url_failure(self, status_code, mock_get): + self.mock_isfile.return_value = False + mock_get.return_value = mock.MagicMock(status_code=status_code) + self.assertRaises( + exceptions.TempestConfigCreationFailure, config._download_image, + os.path.join(self.context.data_dir, "foo")) + + @mock.patch("requests.get", side_effect=requests.ConnectionError()) + def test__download_image_from_url_connection_error( + self, mock_requests_get): + self.mock_isfile.return_value = False + self.assertRaises( + exceptions.TempestConfigCreationFailure, config._download_image, + os.path.join(self.context.data_dir, "foo")) @mock.patch("rally.plugins.openstack.wrappers." "network.NeutronWrapper.create_network") @@ -313,6 +334,7 @@ class TempestResourcesContextTestCase(test.TestCase): self.context.conf.set("compute", "flavor_ref_alt", "id4") self.context.conf.set("compute", "fixed_network_name", "name1") self.context.conf.set("orchestration", "instance_type", "id5") + self.context.conf.set("scenario", "img_file", "id6") self.context.__enter__() @@ -342,16 +364,45 @@ class TempestResourcesContextTestCase(test.TestCase): self.assertIn(role3, created_roles) self.assertIn(role4, created_roles) + @mock.patch("rally.plugins.openstack.wrappers.glance.wrap") + def test__discover_image(self, mock_wrap): + client = mock_wrap.return_value + client.list_images.return_value = [fakes.FakeImage(name="Foo"), + fakes.FakeImage(name="CirrOS")] + + image = self.context._discover_image() + self.assertEqual("CirrOS", image.name) + + @mock.patch("six.moves.builtins.open", side_effect=mock.mock_open(), + create=True) + @mock.patch("rally.plugins.openstack.wrappers.glance.wrap") + @mock.patch("os.path.isfile", return_value=False) + def test__download_image(self, mock_isfile, mock_wrap, mock_open): + img_1 = mock.MagicMock() + img_1.name = "Foo" + img_2 = mock.MagicMock() + img_2.name = "CirrOS" + img_2.data.return_value = "data" + mock_wrap.return_value.list_images.return_value = [img_1, img_2] + + self.context._download_image() + img_path = os.path.join(self.context.data_dir, self.context.image_name) + mock_open.assert_called_once_with(img_path, "wb") + mock_open().write.assert_has_calls([mock.call("d"), + mock.call("a"), + mock.call("t"), + mock.call("a")]) + # We can choose any option to test the '_configure_option' method. So let's # configure the 'flavor_ref' option. def test__configure_option(self): - create_method = mock.MagicMock() - create_method.side_effect = [fakes.FakeFlavor(id="id1")] + helper_method = mock.MagicMock() + helper_method.side_effect = [fakes.FakeFlavor(id="id1")] self.context.conf.set("compute", "flavor_ref", "") - self.context._configure_option("compute", - "flavor_ref", create_method, 64) - self.assertEqual(create_method.call_count, 1) + self.context._configure_option("compute", "flavor_ref", + helper_method=helper_method, flv_ram=64) + self.assertEqual(helper_method.call_count, 1) result = self.context.conf.get("compute", "flavor_ref") self.assertEqual("id1", result) @@ -374,8 +425,6 @@ class TempestResourcesContextTestCase(test.TestCase): self.assertEqual(image, client.create_image.return_value) self.assertEqual(self.context._created_images[0], client.create_image.return_value) - mock_wrap.assert_called_once_with(self.context.clients.glance, - self.context) client.create_image.assert_called_once_with( container_format=CONF.tempest.img_container_format, image_location=mock.ANY,