From 585a2483e9fb021d853b0946f185d066335adea4 Mon Sep 17 00:00:00 2001 From: Hans Feldt Date: Wed, 3 Jun 2015 10:50:48 +0200 Subject: 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 --- samples/ping-hot.yaml | 44 +++++++++++++++++++ yardstick/benchmark/context/model.py | 83 ++++++++++++++++++++++++++++-------- yardstick/common/template_format.py | 63 +++++++++++++++++++++++++++ yardstick/main.py | 50 ++++++++++++++++------ yardstick/orchestrator/heat.py | 39 ++++++++++++----- 5 files changed, 240 insertions(+), 39 deletions(-) create mode 100644 samples/ping-hot.yaml create mode 100644 yardstick/common/template_format.py 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() -- cgit 1.2.3-korg