aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorHans Feldt <hans.feldt@ericsson.com>2015-06-03 10:50:48 +0200
committerHans Feldt <hans.feldt@ericsson.com>2015-06-15 11:07:12 +0000
commit585a2483e9fb021d853b0946f185d066335adea4 (patch)
tree828834e6a3dc8617eaaf9422087a6f847b3fe804
parent7492216de2f198e42bcd5e1539c8b21886a78d8c (diff)
Add support for external HOT template
An external HOT template is a separate yaml file in native Heat format HOT. The external template is referenced in the task file and used as template for a "context". Parameters required at template instantiation are also configured in the task file. See new sample file ping-hot.yaml Change-Id: Ie2b7ea96ea90b75ca4e08a29e2223ceeb1474724 JIRA: YARDSTICK-24 Signed-off-by: Hans Feldt <hans.feldt@ericsson.com>
-rw-r--r--samples/ping-hot.yaml44
-rw-r--r--yardstick/benchmark/context/model.py83
-rw-r--r--yardstick/common/template_format.py63
-rwxr-xr-xyardstick/main.py50
-rw-r--r--yardstick/orchestrator/heat.py39
5 files changed, 240 insertions, 39 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/yardstick/benchmark/context/model.py b/yardstick/benchmark/context/model.py
index 87bb01d3f..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"])
@@ -370,14 +379,16 @@ class Context(object):
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
- template = HeatTemplate(self.name)
- self._add_resources_to_template(template)
+ heat_template = HeatTemplate(self.name, self.template_file,
+ self.heat_parameters)
+
+ 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:
@@ -385,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'''
@@ -417,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()