summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--samples/ping-hot.yaml44
-rw-r--r--setup.py3
-rw-r--r--tools/README20
-rwxr-xr-xtools/ubuntu-server-cloudimg-modify.sh36
-rwxr-xr-xtools/yardstick-img-modify111
-rw-r--r--yardstick/benchmark/context/model.py97
-rw-r--r--yardstick/common/template_format.py63
-rwxr-xr-xyardstick/main.py50
-rw-r--r--yardstick/orchestrator/heat.py39
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"
+
diff --git a/setup.py b/setup.py
index 688185185..ff415aecd 100644
--- a/setup.py
+++ b/setup.py
@@ -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()