Add the context custom_image

Add a new context with the name `custom_image'. This context is
hidden and acts as the base for the contexts of the virtual machine
boot from a custom prepared images. This is used for e.g. preparing
an image with benchmark application preinstalled.

If there is `admin' user then image is created using the first available
user and then makes it public using the `admin' user.
If no `admin' user is available then image is created for each user.

Co-Authored-By: Tzanetos Balitsaris <tzabal@freebsd.org>
Implements: blueprint benchmark-vms
Change-Id: I63a6a0f0f8f1270014ef8322b8e1998b162eb760
This commit is contained in:
Pavel Boldin 2015-01-24 01:42:20 +02:00
parent 8995027941
commit 415931a627
7 changed files with 522 additions and 17 deletions

View File

View File

@ -0,0 +1,245 @@
# Copyright 2015: Mirantis Inc.
# 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 abc
import six
from rally.benchmark.context import base
from rally.benchmark.scenarios.nova import utils as nova_utils
from rally.benchmark.scenarios.vm import vmtasks
from rally.benchmark import types
from rally.common import broker
from rally.common.i18n import _
from rally.common import log as logging
from rally.common import utils
from rally import consts
from rally import osclients
LOG = logging.getLogger(__name__)
@six.add_metaclass(abc.ABCMeta)
@base.context(name="custom_image", order=500, hidden=True)
class BaseCustomImageGenerator(base.Context):
"""Base class for the contexts providing customized image with.
Every context class for the specific cusomization must implement the method
`_customize_image` that is able to connect to the server using SSH
and e.g. install applications inside it.
This is used e.g. to install the benchmark application using SSH
access.
This base context class provides a way to prepare an image with
custom preinstalled applications. Basically, this code boots a VM, calls
the `_customize_image` and then snapshots the VM disk, removing the VM
afterwards. The image UUID is stored in the user["custom_image"]["id"]
and can be used afterwards by scenario.
"""
CONFIG_SCHEMA = {
"type": "object",
"$schema": consts.JSON_SCHEMA,
"properties": {
"image": {
"type": "object",
"properties": {
"name": {
"type": "string"
}
}
},
"flavor": {
"type": "object",
"properties": {
"name": {
"type": "string"
}
}
},
"username": {
"type": "string"
},
"password": {
"type": "string"
},
"floating_network": {
"type": "string"
},
"internal_network": {
"type": "string"
},
"port": {
"type": "integer",
"minimum": 1,
"maximum": 65535
},
"userdata": {
"type": "string"
},
"workers": {
"type": "integer",
"minimum": 1,
}
},
"required": ["image", "flavor"],
"additionalProperties": False
}
DEFAULT_CONFIG = {
"username": "root",
"port": 22,
"workers": 1
}
@utils.log_task_wrapper(LOG.info, _("Enter context: `custom_image`"))
def setup(self):
"""Creates custom image(s) with preinstalled applications.
When admin is present creates one public image that is usable
from all the tenants and users. Otherwise create one image
per user and tenant.
"""
if "admin" in self.context:
# NOTE(pboldin): Create by first user and make it public by
# the admin
user = self.context["users"][0]
tenant = self.context["tenants"][user["tenant_id"]]
nics = None
if "networks" in tenant:
nics = [{"net-id": tenant["networks"][0]["id"]}]
custom_image = self.create_one_image(user, nics=nics)
self.make_image_public(custom_image)
for tenant in self.context["tenants"].values():
tenant["custom_image"] = custom_image
else:
def publish(queue):
users = self.context.get("users", [])
for user, tenant_id in utils.iterate_per_tenants(users):
queue.append((user, tenant_id))
def consume(cache, args):
user, tenant_id = args
tenant = self.context["tenants"][tenant_id]
tenant["custom_image"] = self.create_one_image(user)
broker.run(publish, consume, self.config["workers"])
def create_one_image(self, user, **kwargs):
"""Create one image for the user."""
clients = osclients.Clients(user["endpoint"])
image_id = types.ImageResourceType.transform(
clients=clients, resource_config=self.config["image"])
flavor_id = types.FlavorResourceType.transform(
clients=clients, resource_config=self.config["flavor"])
vm_scenario = vmtasks.VMTasks(self.context, clients=clients)
server, fip = vm_scenario._boot_server_with_fip(
name=vm_scenario._generate_random_name("rally_ctx_custom_image_"),
image=image_id, flavor=flavor_id,
floating_network=self.config.get("floating_network"),
userdata=self.config.get("userdata"),
key_name=user["keypair"]["name"],
security_groups=[user["secgroup"]["name"]],
**kwargs)
LOG.debug("Installing benchmark on %r %s", server, fip["ip"])
self.customize_image(server, fip, user)
LOG.debug("Stopping server %r", server)
vm_scenario._stop_server(server)
LOG.debug("Creating snapshot for %r", server)
custom_image = vm_scenario._create_image(server).to_dict()
vm_scenario._delete_server_with_fip(server, fip)
return custom_image
def make_image_public(self, custom_image):
"""Make the image available publicly."""
admin_clients = osclients.Clients(self.context["admin"]["endpoint"])
LOG.debug("Making image %r public", custom_image["id"])
admin_clients.glance().images.get(
custom_image["id"]).update(is_public=True)
@utils.log_task_wrapper(LOG.info, _("Exit context: `custom_image`"))
def cleanup(self):
"""Delete created custom image(s)."""
if "admin" in self.context:
user = self.context["users"][0]
tenant = self.context["tenants"][user["tenant_id"]]
if "custom_image" in tenant:
self.delete_one_image(user, tenant["custom_image"])
tenant.pop("custom_image")
else:
def publish(queue):
users = self.context.get("users", [])
for user, tenant_id in utils.iterate_per_tenants(users):
queue.append((user, tenant_id))
def consume(cache, args):
user, tenant_id = args
tenant = self.context["tenants"][tenant_id]
if "custom_image" in tenant:
self.delete_one_image(user, tenant["custom_image"])
tenant.pop("custom_image")
broker.run(publish, consume, self.config["workers"])
def delete_one_image(self, user, custom_image):
"""Delete the image created for the user and tenant."""
clients = osclients.Clients(user["endpoint"])
nova_scenario = nova_utils.NovaScenario(
context=self.context, clients=clients)
with logging.ExceptionLogger(
LOG, _("Unable to delete image %s") % custom_image["id"]):
custom_image = nova_scenario.clients("nova").images.get(
custom_image["id"])
nova_scenario._delete_image(custom_image)
@utils.log_task_wrapper(LOG.info,
_("Custom image context: customizing"))
def customize_image(self, server, fip, user):
return self._customize_image(server, fip, user)
@abc.abstractmethod
def _customize_image(self, server, fip, user):
"""Override this method with one that customizes image.
Basically, code can simply call `VMScenario._run_command` function
specifying an installation script and interpreter. This script will
be then executed using SSH.
:param server: nova.Server instance
:param fip: dict with Floating IP details
:param user: user who started a VM instance. Used to extract keypair
"""
pass

View File

@ -104,8 +104,8 @@ class NovaScenario(base.Scenario):
kwargs["security_groups"].append(secgroup["name"]) kwargs["security_groups"].append(secgroup["name"])
if auto_assign_nic and not kwargs.get("nics", False): if auto_assign_nic and not kwargs.get("nics", False):
nets = [net["id"] nets = [net["id"] for net in
for net in self.context["tenant"].get("networks", [])] self.context.get("tenant", {}).get("networks", [])]
if nets: if nets:
# NOTE(amaretskiy): Balance servers among networks: # NOTE(amaretskiy): Balance servers among networks:
# divmod(iteration % tenants_num, nets_num)[1] # divmod(iteration % tenants_num, nets_num)[1]

View File

@ -17,12 +17,15 @@ import subprocess
import sys import sys
import netaddr import netaddr
import six
from rally.benchmark.scenarios import base from rally.benchmark.scenarios import base
from rally.benchmark import utils as bench_utils from rally.benchmark import utils as bench_utils
from rally.benchmark.wrappers import network as network_wrapper from rally.benchmark.wrappers import network as network_wrapper
from rally.common.i18n import _
from rally.common import log as logging from rally.common import log as logging
from rally.common import sshutils from rally.common import sshutils
from rally import exceptions
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
@ -33,15 +36,29 @@ class VMScenario(base.Scenario):
VM scenarios are scenarios executed inside some launched VM instance. VM scenarios are scenarios executed inside some launched VM instance.
""" """
@base.atomic_action_timer("vm.run_command") @base.atomic_action_timer("vm.run_command_over_ssh")
def _run_action(self, ssh, interpreter, script): def _run_command_over_ssh(self, ssh, interpreter, script):
"""Run command inside an instance. """Run command inside an instance.
This is a separate function so that only script execution is timed. This is a separate function so that only script execution is timed.
:param ssh: A SSHClient instance.
:param interpreter: The interpreter that will be used to execute
the script.
:param script: Path to the script file or its content in a StringIO.
:returns: tuple (exit_status, stdout, stderr) :returns: tuple (exit_status, stdout, stderr)
""" """
return ssh.execute(interpreter, stdin=open(script, "rb")) if isinstance(script, six.string_types):
stdin = open(script, "rb")
elif isinstance(script, six.moves.StringIO):
stdin = script
else:
raise exceptions.ScriptError(
"Either file path or StringIO expected, given %s" %
type(script).__name__)
return ssh.execute(interpreter, stdin=stdin)
def _get_netwrap(self): def _get_netwrap(self):
if not hasattr(self, "_netwrap"): if not hasattr(self, "_netwrap"):
@ -106,20 +123,19 @@ class VMScenario(base.Scenario):
) )
def _run_command(self, server_ip, port, username, password, def _run_command(self, server_ip, port, username, password,
interpreter, script): interpreter, script, pkey=None):
"""Run command via SSH on server. """Run command via SSH on server.
Create SSH connection for server, wait for server to become Create SSH connection for server, wait for server to become
available (there is a delay between server being set to ACTIVE available (there is a delay between server being set to ACTIVE
and sshd being available). Then call run_action to actually and sshd being available). Then call run_command_over_ssh to actually
execute the command. execute the command.
""" """
pkey = pkey if pkey else self.context["user"]["keypair"]["private"]
ssh = sshutils.SSH(username, server_ip, port=port, ssh = sshutils.SSH(username, server_ip, port=port,
pkey=self.context["user"]["keypair"]["private"], pkey=pkey, password=password)
password=password)
self._wait_for_ssh(ssh) self._wait_for_ssh(ssh)
return self._run_action(ssh, interpreter, script) return self._run_command_over_ssh(ssh, interpreter, script)
@staticmethod @staticmethod
def _ping_ip_address(host, should_succeed=True): def _ping_ip_address(host, should_succeed=True):

View File

@ -0,0 +1,227 @@
# Copyright 2015: Mirantis Inc.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
"""Tests for the Benchmark VM image context."""
import mock
from rally.benchmark.context.vm import custom_image
from tests.unit import test
BASE = "rally.benchmark.context.vm.custom_image"
class TestImageGenerator(custom_image.BaseCustomImageGenerator):
def _customize_image(self, *args):
pass
class BaseCustomImageContextVMTestCase(test.TestCase):
def setUp(self):
super(BaseCustomImageContextVMTestCase, self).setUp()
self.context = {
"task": mock.MagicMock(),
"config": {
"custom_image": {
"image": {"name": "image"},
"flavor": {"name": "flavor"},
"username": "fedora",
"floating_network": "floating",
"port": 1022,
}
},
"admin": {
"endpoint": "endpoint",
},
"users": [
{"tenant_id": "tenant_id0"},
{"tenant_id": "tenant_id1"},
{"tenant_id": "tenant_id2"}
],
"tenants": {
"tenant_id0": {},
"tenant_id1": {},
"tenant_id2": {}
}
}
@mock.patch("%s.vmtasks.VMTasks" % BASE)
@mock.patch("%s.osclients.Clients" % BASE)
@mock.patch("%s.types.ImageResourceType.transform" % BASE,
return_value="image")
@mock.patch("%s.types.FlavorResourceType.transform" % BASE,
return_value="flavor")
def test_create_one_image(self, mock_flavor_transform,
mock_image_transform, mock_osclients,
mock_vmtasks):
fip = {"ip": "foo_ip"}
fake_server = mock.Mock()
fake_image = mock.MagicMock(
to_dict=mock.MagicMock(return_value={"id": "image"}))
mock_vm_scenario = mock_vmtasks.return_value = mock.MagicMock(
_create_image=mock.MagicMock(return_value=fake_image),
_boot_server_with_fip=mock.MagicMock(
return_value=(fake_server, fip)),
_generate_random_name=mock.MagicMock(return_value="foo_name"),
)
generator_ctx = TestImageGenerator(self.context)
generator_ctx._customize_image = mock.MagicMock()
user = {
"endpoint": "endpoint",
"keypair": {"name": "keypair_name"},
"secgroup": {"name": "secgroup_name"}
}
custom_image = generator_ctx.create_one_image(user,
foo_arg="foo_value")
mock_flavor_transform.assert_called_once_with(
clients=mock_osclients.return_value,
resource_config={"name": "flavor"})
mock_image_transform.assert_called_once_with(
clients=mock_osclients.return_value,
resource_config={"name": "image"})
mock_vmtasks.assert_called_once_with(
self.context, clients=mock_osclients.return_value)
mock_vm_scenario._boot_server_with_fip.assert_called_once_with(
name="foo_name", image="image", flavor="flavor",
floating_network="floating", key_name="keypair_name",
security_groups=["secgroup_name"],
userdata=None, foo_arg="foo_value")
mock_vm_scenario._stop_server.assert_called_once_with(fake_server)
generator_ctx._customize_image.assert_called_once_with(
fake_server, fip, user)
mock_vm_scenario._create_image.assert_called_once_with(fake_server)
mock_vm_scenario._delete_server_with_fip.assert_called_once_with(
fake_server, fip)
self.assertEqual({"id": "image"}, custom_image)
@mock.patch("%s.osclients.Clients" % BASE)
def test_make_image_public(self, mock_osclients):
fc = mock.MagicMock()
mock_osclients.return_value = fc
generator_ctx = TestImageGenerator(self.context)
custom_image = {"id": "image"}
generator_ctx.make_image_public(custom_image=custom_image)
mock_osclients.assert_called_once_with(
self.context["admin"]["endpoint"])
fc.glance.assert_called_once_with()
fc.glance.return_value.images.get.assert_called_once_with("image")
(fc.glance.return_value.images.get.
return_value.update.assert_called_once_with(is_public=True))
@mock.patch("%s.nova_utils.NovaScenario" % BASE)
@mock.patch("%s.osclients.Clients" % BASE)
def test_delete_one_image(self, mock_osclients, mock_nova_scenario):
nova_scenario = mock_nova_scenario.return_value = mock.MagicMock()
nova_client = nova_scenario.clients.return_value
nova_client.images.get.return_value = "image_obj"
generator_ctx = TestImageGenerator(self.context)
user = {"endpoint": "endpoint", "keypair": {"name": "keypair_name"}}
custom_image = {"id": "image"}
generator_ctx.delete_one_image(user, custom_image)
mock_nova_scenario.assert_called_once_with(
context=self.context, clients=mock_osclients.return_value)
nova_scenario.clients.assert_called_once_with("nova")
nova_client.images.get.assert_called_once_with("image")
nova_scenario._delete_image.assert_called_once_with("image_obj")
def test_setup_admin(self):
self.context["tenants"]["tenant_id0"]["networks"] = [
{"id": "network_id"}]
generator_ctx = TestImageGenerator(self.context)
generator_ctx.create_one_image = mock.Mock(
return_value="custom_image")
generator_ctx.make_image_public = mock.Mock()
generator_ctx.setup()
generator_ctx.create_one_image.assert_called_once_with(
self.context["users"][0], nics=[{"net-id": "network_id"}])
generator_ctx.make_image_public.assert_called_once_with(
"custom_image")
def test_cleanup_admin(self):
tenant = self.context["tenants"]["tenant_id0"]
custom_image = tenant["custom_image"] = {"id": "image"}
generator_ctx = TestImageGenerator(self.context)
generator_ctx.delete_one_image = mock.Mock()
generator_ctx.cleanup()
generator_ctx.delete_one_image.assert_called_once_with(
self.context["users"][0], custom_image)
def test_setup(self):
self.context.pop("admin")
generator_ctx = TestImageGenerator(self.context)
generator_ctx.create_one_image = mock.Mock(
side_effect=["custom_image0", "custom_image1", "custom_image2"])
generator_ctx.setup()
self.assertEqual(
[mock.call(user) for user in self.context["users"]],
generator_ctx.create_one_image.mock_calls)
for i in range(3):
self.assertEqual(
"custom_image%d" % i,
self.context["tenants"]["tenant_id%d" % i]["custom_image"]
)
def test_cleanup(self):
self.context.pop("admin")
for i in range(3):
self.context["tenants"]["tenant_id%d" % i]["custom_image"] = {
"id": "custom_image%d" % i}
generator_ctx = TestImageGenerator(self.context)
generator_ctx.delete_one_image = mock.Mock()
generator_ctx.cleanup()
self.assertEqual(
[mock.call(self.context["users"][i],
{"id": "custom_image%d" % i}) for i in range(3)],
generator_ctx.delete_one_image.mock_calls)

View File

@ -16,10 +16,13 @@
import subprocess import subprocess
import mock import mock
from oslotest import mockpatch from oslotest import mockpatch
import six
from rally.benchmark.scenarios.vm import utils from rally.benchmark.scenarios.vm import utils
from rally import exceptions
from tests.unit import test from tests.unit import test
@ -36,13 +39,27 @@ class VMScenarioTestCase(test.TestCase):
@mock.patch("%s.open" % VMTASKS_UTILS, @mock.patch("%s.open" % VMTASKS_UTILS,
side_effect=mock.mock_open(), create=True) side_effect=mock.mock_open(), create=True)
def test__run_action(self, mock_open): def test__run_command_over_ssh(self, mock_open):
mock_ssh = mock.MagicMock() mock_ssh = mock.MagicMock()
vm_scenario = utils.VMScenario() vm_scenario = utils.VMScenario()
vm_scenario._run_action(mock_ssh, "interpreter", "script") vm_scenario._run_command_over_ssh(mock_ssh, "interpreter", "script")
mock_ssh.execute.assert_called_once_with("interpreter", mock_ssh.execute.assert_called_once_with("interpreter",
stdin=mock_open.side_effect()) stdin=mock_open.side_effect())
def test__run_command_over_ssh_stringio(self):
mock_ssh = mock.MagicMock()
vm_scenario = utils.VMScenario()
script = six.moves.StringIO("script")
vm_scenario._run_command_over_ssh(mock_ssh, "interpreter", script)
mock_ssh.execute.assert_called_once_with("interpreter",
stdin=script)
def test__run_command_over_ssh_fails(self):
vm_scenario = utils.VMScenario()
self.assertRaises(exceptions.ScriptError,
vm_scenario._run_command_over_ssh,
None, "interpreter", 10)
def test__wait_for_ssh(self): def test__wait_for_ssh(self):
ssh = mock.MagicMock() ssh = mock.MagicMock()
vm_scenario = utils.VMScenario() vm_scenario = utils.VMScenario()
@ -58,9 +75,9 @@ class VMScenarioTestCase(test.TestCase):
is_ready=mock__ping, is_ready=mock__ping,
timeout=120) timeout=120)
@mock.patch(VMTASKS_UTILS + ".VMScenario._run_action") @mock.patch(VMTASKS_UTILS + ".VMScenario._run_command_over_ssh")
@mock.patch("rally.common.sshutils.SSH") @mock.patch("rally.common.sshutils.SSH")
def test__run_command(self, mock_ssh_class, mock_run_action): def test__run_command(self, mock_ssh_class, mock_run_command_over_ssh):
mock_ssh_instance = mock.MagicMock() mock_ssh_instance = mock.MagicMock()
mock_ssh_class.return_value = mock_ssh_instance mock_ssh_class.return_value = mock_ssh_instance
@ -73,8 +90,8 @@ class VMScenarioTestCase(test.TestCase):
pkey="ssh", pkey="ssh",
password="password") password="password")
mock_ssh_instance.wait.assert_called_once_with() mock_ssh_instance.wait.assert_called_once_with()
mock_run_action.assert_called_once_with(mock_ssh_instance, mock_run_command_over_ssh.assert_called_once_with(
"int", "script") mock_ssh_instance, "int", "script")
@mock.patch(VMTASKS_UTILS + ".sys") @mock.patch(VMTASKS_UTILS + ".sys")
@mock.patch("subprocess.Popen") @mock.patch("subprocess.Popen")