diff options
-rw-r--r-- | samples/ping-hot.yaml | 44 | ||||
-rw-r--r-- | setup.py | 3 | ||||
-rw-r--r-- | tools/README | 20 | ||||
-rwxr-xr-x | tools/ubuntu-server-cloudimg-modify.sh | 36 | ||||
-rwxr-xr-x | tools/yardstick-img-modify | 111 | ||||
-rw-r--r-- | yardstick/benchmark/context/model.py | 97 | ||||
-rw-r--r-- | yardstick/common/template_format.py | 63 | ||||
-rwxr-xr-x | yardstick/main.py | 50 | ||||
-rw-r--r-- | yardstick/orchestrator/heat.py | 39 |
9 files changed, 422 insertions, 41 deletions
diff --git a/samples/ping-hot.yaml b/samples/ping-hot.yaml new file mode 100644 index 000000000..b4b8f5228 --- /dev/null +++ b/samples/ping-hot.yaml @@ -0,0 +1,44 @@ +--- +# Sample benchmark task config file to measure network latency using ping +# An external HOT template (file) is configured in the context section using +# the heat_template attribute. Parameters for the template is specified with the +# heat_parameters attribute. + +schema: "yardstick:task:0.1" + +scenarios: +- + type: Ping + options: + packetsize: 200 + host: + name: "server1.demo" + public_ip_attr: "server1_public_ip" + target: + name: "server2.demo" + private_ip_attr: "server2_private_ip" + + runner: + type: Duration + duration: 60 + interval: 1 + + sla: + max_rtt: 10 + action: monitor + +context: + name: demo + user: cirros + heat_template: /tmp/heat-templates/hot/servers_in_new_neutron_net.yaml + heat_parameters: + image: cirros-0.3.3 + flavor: m1.tiny + key_name: yardstick + public_net: "660fc7c3-7a56-4faf-91e5-3c9ebdda0104" + private_net_name: "test" + private_net_cidr: "10.0.1.0/24" + private_net_gateway: "10.0.1.1" + private_net_pool_start: "10.0.1.2" + private_net_pool_end: "10.0.1.200" + @@ -26,5 +26,6 @@ setup( 'console_scripts': [ 'yardstick=yardstick.main:main', ], - } + }, + scripts =['tools/yardstick-img-modify'] ) diff --git a/tools/README b/tools/README new file mode 100644 index 000000000..9477c8988 --- /dev/null +++ b/tools/README @@ -0,0 +1,20 @@ +############################################################################## +# Copyright (c) 2015 Ericsson AB and others. +# +# All rights reserved. This program and the accompanying materials +# are made available under the terms of the Apache License, Version 2.0 +# which accompanies this distribution, and is available at +# http://www.apache.org/licenses/LICENSE-2.0 +############################################################################## + +This directory contains various utilities needed in the yardstick environment. + +yardstick-img-modify is a generic script (but ubuntu cloud image specific) that +takes a another script as an argument. This second script does the actual +modifications of the image. sudo is required since the base image is mounted +using qemu's network block device support. + +Usage example: + +$ sudo yardstick-img-modify $HOME/yardstick/tools/ubuntu-server-cloudimg-modify.sh + diff --git a/tools/ubuntu-server-cloudimg-modify.sh b/tools/ubuntu-server-cloudimg-modify.sh new file mode 100755 index 000000000..c2977896f --- /dev/null +++ b/tools/ubuntu-server-cloudimg-modify.sh @@ -0,0 +1,36 @@ +############################################################################## +# Copyright (c) 2015 Ericsson AB and others. +# +# All rights reserved. This program and the accompanying materials +# are made available under the terms of the Apache License, Version 2.0 +# which accompanies this distribution, and is available at +# http://www.apache.org/licenses/LICENSE-2.0 +############################################################################## + +#!/bin/bash + +# installs required packages +# must be run from inside the image (either chrooted or running) + +set -ex + +if [ $# -eq 1 ]; then + nameserver_ip=$1 + + # /etc/resolv.conf is a symbolic link to /run, restore at end + rm /etc/resolv.conf + echo "nameserver $nameserver_ip" > /etc/resolv.conf +fi + +# iperf3 only available for trusty in backports +grep trusty /etc/apt/sources.list && \ + echo "deb http://archive.ubuntu.com/ubuntu/ trusty-backports main restricted universe multiverse" >> /etc/apt/sources.list +apt-get update +apt-get install -y \ + iperf3 \ + lmbench \ + stress + +# restore symlink +ln -sf /run/resolvconf/resolv.conf /etc/resolv.conf + diff --git a/tools/yardstick-img-modify b/tools/yardstick-img-modify new file mode 100755 index 000000000..eba85473c --- /dev/null +++ b/tools/yardstick-img-modify @@ -0,0 +1,111 @@ +#!/bin/bash + +############################################################################## +# Copyright (c) 2015 Ericsson AB and others. +# +# All rights reserved. This program and the accompanying materials +# are made available under the terms of the Apache License, Version 2.0 +# which accompanies this distribution, and is available at +# http://www.apache.org/licenses/LICENSE-2.0 +############################################################################## + +# yardstick-img-modify - download and modify a Ubuntu cloud image +# +# The actual customization is done by a script passed with an absolute path as +# the only single argument. The command needs to be invoked as sudo +# +# Example invocation: +# yardstick-img-modify /home/yardstick/tools/ubuntu-server-cloudimg-modify.sh +# +# Warning: the script will create files by default in: +# /tmp/workspace/yardstick +# the files will be owned by root! +# +# TODO: image resize is needed if the base image is too small +# + +set -e + +die() { + echo "error: $1" >&2 + exit 1 +} + +test $# -eq 1 || die "no image specific script as argument" +test $(id -u) -eq 0 || die "should invoke using sudo" + +cmd=$1 +test -x $cmd +mountdir="/mnt/yardstick" + +workspace=${WORKSPACE:-"/tmp/workspace/yardstick"} +host=${HOST:-"cloud-images.ubuntu.com"} +release=${RELEASE:-"trusty"} +image_path="${release}/current/${release}-server-cloudimg-amd64-disk1.img" +image_url=${IMAGE_URL:-"https://${host}/${image_path}"} +md5sums_path="${release}/current/MD5SUMS" +md5sums_url=${MD5SUMS_URL:-"https://${host}/${md5sums_path}"} + +imgfile="${workspace}/yardstick-${release}-server.img" +filename=$(basename $image_url) + +# download and checksum base image, conditionally if local copy is outdated +download() { + test -d $workspace || mkdir -p $workspace + cd $workspace + rm -f MD5SUMS # always download the checksum file to a detect stale image + wget $md5sums_url + test -e $filename || wget -nc $image_url + grep $filename MD5SUMS | md5sum -c || + if [ $? -ne 0 ]; then + rm $filename + wget -nc $image_url + grep $filename MD5SUMS | md5sum -c + fi + cp $filename $imgfile + cd - +} + +# mount image using qemu-nbd +setup() { + modprobe nbd max_part=16 + qemu-nbd -c /dev/nbd0 $imgfile + partprobe /dev/nbd0 + + mkdir -p $mountdir + mount /dev/nbd0p1 $mountdir + + cp $cmd $mountdir/$(basename $cmd) +} + +# modify image running a script using in a chrooted environment +modify() { + # resolv.conf does not exist in base image, pass nameserver value from host + nameserver_ip=$(grep -m 1 '^nameserver' \ + /etc/resolv.conf | awk '{ print $2 '}) + chroot $mountdir /$(basename $cmd) $nameserver_ip +} + +# cleanup (umount) the image +cleanup() { + # designed to be idempotent + mount | grep $mountdir && umount $mountdir + test -b /dev/nbd0 && partprobe /dev/nbd0 + pgrep qemu-nbd && qemu-nbd -d /dev/nbd0 + rm -rf $mountdir + killall qemu-nbd 2> /dev/null || true + lsmod | grep nbd && rmmod nbd || true +} + +main() { + cleanup + download + setup + modify + cleanup + + echo "the modified image is found here: $imgfile" +} + +main + diff --git a/yardstick/benchmark/context/model.py b/yardstick/benchmark/context/model.py index afb2d56dd..08778598d 100644 --- a/yardstick/benchmark/context/model.py +++ b/yardstick/benchmark/context/model.py @@ -12,7 +12,6 @@ """ import sys -import os from yardstick.orchestrator.heat import HeatTemplate @@ -124,6 +123,8 @@ class Server(Object): self.keypair_name = context.keypair_name self.secgroup_name = context.secgroup_name self.context = context + self.public_ip = None + self.private_ip = None if attrs is None: attrs = {} @@ -256,11 +257,22 @@ class Context(object): self._image = None self._flavor = None self._user = None + self.template_file = None + self.heat_parameters = None Context.list.append(self) def init(self, attrs): '''initializes itself from the supplied arguments''' self.name = attrs["name"] + + if "user" in attrs: + self._user = attrs["user"] + + if "heat_template" in attrs: + self.template_file = attrs["heat_template"] + self.heat_parameters = attrs.get("heat_parameters", None) + return + self.keypair_name = self.name + "-key" self.secgroup_name = self.name + "-secgroup" @@ -270,9 +282,6 @@ class Context(object): if "flavor" in attrs: self._flavor = attrs["flavor"] - if "user" in attrs: - self._user = attrs["user"] - if "placement_groups" in attrs: for name, pgattrs in attrs["placement_groups"].items(): pg = PlacementGroup(name, self, pgattrs["policy"]) @@ -324,6 +333,11 @@ class Context(object): list_of_servers = sorted(self.servers, key=lambda s: len(s.placement_groups)) + # + # add servers with scheduler hints derived from placement groups + # + + # create list of servers with availability policy availability_servers = [] for server in list_of_servers: for pg in server.placement_groups: @@ -331,7 +345,7 @@ class Context(object): availability_servers.append(server) break - # add servers with scheduler hints derived from placement groups + # add servers with availability policy added_servers = [] for server in availability_servers: scheduler_hints = {} @@ -340,6 +354,7 @@ class Context(object): server.add_to_template(template, self.networks, scheduler_hints) added_servers.append(server.stack_name) + # create list of servers with affinity policy affinity_servers = [] for server in list_of_servers: for pg in server.placement_groups: @@ -347,6 +362,7 @@ class Context(object): affinity_servers.append(server) break + # add servers with affinity policy for server in affinity_servers: if server.stack_name in added_servers: continue @@ -356,16 +372,23 @@ class Context(object): server.add_to_template(template, self.networks, scheduler_hints) added_servers.append(server.stack_name) + # add remaining servers with no placement group configured + for server in list_of_servers: + if len(server.placement_groups) == 0: + server.add_to_template(template, self.networks, {}) + def deploy(self): '''deploys template into a stack using cloud''' - print "Deploying context as stack '%s' using auth_url %s" % ( - self.name, os.environ.get('OS_AUTH_URL')) + print "Deploying context '%s'" % self.name + + heat_template = HeatTemplate(self.name, self.template_file, + self.heat_parameters) - template = HeatTemplate(self.name) - self._add_resources_to_template(template) + if self.template_file is None: + self._add_resources_to_template(heat_template) try: - self.stack = template.create() + self.stack = heat_template.create() except KeyboardInterrupt: sys.exit("\nStack create interrupted") except RuntimeError as err: @@ -373,27 +396,29 @@ class Context(object): except Exception as err: sys.exit("error: failed to deploy stack: '%s'" % err) - # Iterate the servers in this context and copy out needed info + # copy some vital stack output into server objects for server in self.servers: - for port in server.ports.itervalues(): - port["ipaddr"] = self.stack.outputs[port["stack_name"]] + if len(server.ports) > 0: + # TODO(hafe) can only handle one internal network for now + port = server.ports.values()[0] + server.private_ip = self.stack.outputs[port["stack_name"]] if server.floating_ip: - server.floating_ip["ipaddr"] = \ + server.public_ip = \ self.stack.outputs[server.floating_ip["stack_name"]] - print "Context deployed" + print "Context '%s' deployed" % self.name def undeploy(self): '''undeploys stack from cloud''' if self.stack: - print "Undeploying context (stack) '%s'" % self.name + print "Undeploying context '%s'" % self.name self.stack.delete() self.stack = None - print "Context undeployed" + print "Context '%s' undeployed" % self.name @staticmethod - def get_server(dn): + def get_server_by_name(dn): '''lookup server object by DN dn is a distinguished name including the context name''' @@ -405,3 +430,39 @@ class Context(object): return context._server_map[dn] return None + + @staticmethod + def get_context_by_name(name): + for context in Context.list: + if name == context.name: + return context + return None + + @staticmethod + def get_server(attr_name): + '''lookup server object by name from context + attr_name: either a name for a server created by yardstick or a dict + with attribute name mapping when using external heat templates + ''' + if type(attr_name) is dict: + cname = attr_name["name"].split(".")[1] + context = Context.get_context_by_name(cname) + if context is None: + raise ValueError("context not found for server '%s'" % + attr_name["name"]) + + public_ip = None + private_ip = None + if "public_ip_attr" in attr_name: + public_ip = context.stack.outputs[attr_name["public_ip_attr"]] + if "private_ip_attr" in attr_name: + private_ip = context.stack.outputs[ + attr_name["private_ip_attr"]] + + # Create a dummy server instance for holding the *_ip attributes + server = Server(attr_name["name"].split(".")[0], context, {}) + server.public_ip = public_ip + server.private_ip = private_ip + return server + else: + return Context.get_server_by_name(attr_name) diff --git a/yardstick/common/template_format.py b/yardstick/common/template_format.py new file mode 100644 index 000000000..881b7e45b --- /dev/null +++ b/yardstick/common/template_format.py @@ -0,0 +1,63 @@ +# 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. + +# yardstick: this file is copied from python-heatclient and slightly modified + +import json +import yaml + +if hasattr(yaml, 'CSafeLoader'): + yaml_loader = yaml.CSafeLoader +else: + yaml_loader = yaml.SafeLoader + +if hasattr(yaml, 'CSafeDumper'): + yaml_dumper = yaml.CSafeDumper +else: + yaml_dumper = yaml.SafeDumper + + +def _construct_yaml_str(self, node): + # Override the default string handling function + # to always return unicode objects + return self.construct_scalar(node) +yaml_loader.add_constructor(u'tag:yaml.org,2002:str', _construct_yaml_str) +# Unquoted dates like 2013-05-23 in yaml files get loaded as objects of type +# datetime.data which causes problems in API layer when being processed by +# openstack.common.jsonutils. Therefore, make unicode string out of timestamps +# until jsonutils can handle dates. +yaml_loader.add_constructor(u'tag:yaml.org,2002:timestamp', + _construct_yaml_str) + + +def parse(tmpl_str): + '''Takes a string and returns a dict containing the parsed structure. + + This includes determination of whether the string is using the + JSON or YAML format. + ''' + if tmpl_str.startswith('{'): + tpl = json.loads(tmpl_str) + else: + try: + tpl = yaml.load(tmpl_str, Loader=yaml_loader) + except yaml.YAMLError as yea: + raise ValueError(yea) + else: + if tpl is None: + tpl = {} + # Looking for supported version keys in the loaded template + if not ('HeatTemplateFormatVersion' in tpl + or 'heat_template_version' in tpl + or 'AWSTemplateFormatVersion' in tpl): + raise ValueError("Template format version not found.") + return tpl diff --git a/yardstick/main.py b/yardstick/main.py index 5669fde10..942b46be9 100755 --- a/yardstick/main.py +++ b/yardstick/main.py @@ -13,6 +13,30 @@ Example invocation: $ yardstick samples/ping-task.yaml + + Servers are the same as VMs (Nova call them servers in the API) + + Many tests use a client/server architecture. A test client is configured + to use a specific test server e.g. using an IP address. This is true for + example iperf. In some cases the test server is included in the kernel + (ping, pktgen) and no additional software is needed on the server. In other + cases (iperf) a server process needs to be installed and started + + One server is required to host the test client program (such as ping or + iperf). In the task file this server is called host. + + A server can be the _target_ of a test client (think ping destination + argument). A target server is optional but needed in most test scenarios. + In the task file this server is called target. This is probably the same + as DUT in existing terminology. + + Existing terminology: + https://www.ietf.org/rfc/rfc1242.txt (throughput/latency) + https://www.ietf.org/rfc/rfc2285.txt (DUT/SUT) + + New terminology: + NFV TST + """ import sys @@ -77,23 +101,25 @@ def run_one_scenario(scenario_cfg, output_file): host = Context.get_server(scenario_cfg["host"]) runner_cfg = scenario_cfg["runner"] - runner_cfg['host'] = host.floating_ip["ipaddr"] + runner_cfg['host'] = host.public_ip runner_cfg['user'] = host.context.user runner_cfg['key_filename'] = key_filename runner_cfg['output_filename'] = output_file - # TODO target should be optional to support single VM scenarios - target = Context.get_server(scenario_cfg["target"]) - if target.floating_ip: - runner_cfg['target'] = target.floating_ip["ipaddr"] + if "target" in scenario_cfg: + target = Context.get_server(scenario_cfg["target"]) - # TODO scenario_cfg["ipaddr"] is bad, "dest_ip" is better - if host.context != target.context: - # target is in another context, get its public IP - scenario_cfg["ipaddr"] = target.floating_ip["ipaddr"] - else: - # TODO hardcoded name below, a server can be attached to several nets - scenario_cfg["ipaddr"] = target.ports["test"]["ipaddr"] + # get public IP for target server, some scenarios require it + if target.public_ip: + runner_cfg['target'] = target.public_ip + + # TODO scenario_cfg["ipaddr"] is bad naming + if host.context != target.context: + # target is in another context, get its public IP + scenario_cfg["ipaddr"] = target.public_ip + else: + # target is in the same context, get its private IP + scenario_cfg["ipaddr"] = target.private_ip runner = base_runner.Runner.get(runner_cfg) diff --git a/yardstick/orchestrator/heat.py b/yardstick/orchestrator/heat.py index ddab89640..a3179a6f1 100644 --- a/yardstick/orchestrator/heat.py +++ b/yardstick/orchestrator/heat.py @@ -21,6 +21,9 @@ import json import heatclient.client import keystoneclient +from yardstick.common import template_format + + log = logging.getLogger(__name__) @@ -145,14 +148,7 @@ class HeatStack(HeatObject): class HeatTemplate(HeatObject): '''Describes a Heat template and a method to deploy template to a stack''' - def __init__(self, name): - super(HeatTemplate, self).__init__() - self.name = name - self.state = "NOT_CREATED" - self.keystone_client = None - self.heat_client = None - - # Heat template + def _init_template(self): self._template = {} self._template['heat_template_version'] = '2013-05-23' @@ -161,13 +157,35 @@ class HeatTemplate(HeatObject): '''Stack built by the yardstick framework for %s on host %s %s. All referred generated resources are prefixed with the template name (i.e. %s).''' % (getpass.getuser(), socket.gethostname(), - timestamp, name) + timestamp, self.name) # short hand for resources part of template self.resources = self._template['resources'] = {} self._template['outputs'] = {} + def __init__(self, name, template_file=None, heat_parameters=None): + super(HeatTemplate, self).__init__() + self.name = name + self.state = "NOT_CREATED" + self.keystone_client = None + self.heat_client = None + self.heat_parameters = {} + + # heat_parameters is passed to heat in stack create, empty dict when + # yardstick creates the template (no get_param in resources part) + if heat_parameters: + self.heat_parameters = heat_parameters + + if template_file: + with open(template_file) as stream: + print "Parsing external template:", template_file + template_str = stream.read() + self._template = template_format.parse(template_str) + self._parameters = heat_parameters + else: + self._init_template() + # holds results of requested output after deployment self.outputs = {} @@ -404,7 +422,8 @@ class HeatTemplate(HeatObject): json_template = json.dumps(self._template) start_time = time.time() stack.uuid = self.uuid = heat.stacks.create( - stack_name=self.name, template=json_template)['stack']['id'] + stack_name=self.name, template=json_template, + parameters=self.heat_parameters)['stack']['id'] status = self.status() |