Add support for external HOT template 01/801/4
authorHans Feldt <hans.feldt@ericsson.com>
Wed, 3 Jun 2015 08:50:48 +0000 (10:50 +0200)
committerHans Feldt <hans.feldt@ericsson.com>
Mon, 15 Jun 2015 11:07:12 +0000 (11:07 +0000)
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>
samples/ping-hot.yaml [new file with mode: 0644]
yardstick/benchmark/context/model.py
yardstick/common/template_format.py [new file with mode: 0644]
yardstick/main.py
yardstick/orchestrator/heat.py

diff --git a/samples/ping-hot.yaml b/samples/ping-hot.yaml
new file mode 100644 (file)
index 0000000..b4b8f52
--- /dev/null
@@ -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"
+
index 87bb01d..0877859 100644 (file)
@@ -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 (file)
index 0000000..881b7e4
--- /dev/null
@@ -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
index 5669fde..942b46b 100755 (executable)
 
     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)
 
index ddab896..a3179a6 100644 (file)
@@ -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()