Merge "Create test suite k8-nosdn-lb-noha run with k8s context"
[yardstick.git] / yardstick / orchestrator / heat.py
1 #############################################################################
2 # Copyright (c) 2015-2017 Ericsson AB and others.
3 #
4 # All rights reserved. This program and the accompanying materials
5 # are made available under the terms of the Apache License, Version 2.0
6 # which accompanies this distribution, and is available at
7 # http://www.apache.org/licenses/LICENSE-2.0
8 ##############################################################################
9
10 """Heat template and stack management"""
11
12 from __future__ import absolute_import
13 from __future__ import print_function
14 from six.moves import range
15
16 import collections
17 import datetime
18 import getpass
19 import logging
20
21 import socket
22 import time
23
24 import heatclient.client
25 import pkg_resources
26
27 from oslo_utils import encodeutils
28
29 import yardstick.common.openstack_utils as op_utils
30 from yardstick.common import template_format
31
32 log = logging.getLogger(__name__)
33
34
35 HEAT_KEY_UUID_LENGTH = 8
36
37 PROVIDER_SRIOV = "sriov"
38
39
40 def get_short_key_uuid(uuid):
41     return str(uuid)[:HEAT_KEY_UUID_LENGTH]
42
43
44 class HeatObject(object):
45     """base class for template and stack"""
46
47     def __init__(self):
48         self._heat_client = None
49         self.uuid = None
50
51     @property
52     def heat_client(self):
53         """returns a heat client instance"""
54
55         if self._heat_client is None:
56             sess = op_utils.get_session()
57             heat_endpoint = op_utils.get_endpoint(service_type='orchestration')
58             self._heat_client = heatclient.client.Client(
59                 op_utils.get_heat_api_version(),
60                 endpoint=heat_endpoint, session=sess)
61
62         return self._heat_client
63
64     def status(self):
65         """returns stack state as a string"""
66         heat_client = self.heat_client
67         stack = heat_client.stacks.get(self.uuid)
68         return stack.stack_status
69
70
71 class HeatStack(HeatObject):
72     """Represents a Heat stack (deployed template) """
73     stacks = []
74
75     def __init__(self, name):
76         super(HeatStack, self).__init__()
77         self.uuid = None
78         self.name = name
79         self.outputs = None
80         HeatStack.stacks.append(self)
81
82     @staticmethod
83     def stacks_exist():
84         """check if any stack has been deployed"""
85         return len(HeatStack.stacks) > 0
86
87     def _delete(self):
88         """deletes a stack from the target cloud using heat"""
89         if self.uuid is None:
90             return
91
92         log.info("Deleting stack '%s', uuid:%s", self.name, self.uuid)
93         heat = self.heat_client
94         template = heat.stacks.get(self.uuid)
95         start_time = time.time()
96         template.delete()
97
98         for status in iter(self.status, u'DELETE_COMPLETE'):
99             log.debug("stack state %s", status)
100             if status == u'DELETE_FAILED':
101                 raise RuntimeError(
102                     heat.stacks.get(self.uuid).stack_status_reason)
103
104             time.sleep(2)
105
106         end_time = time.time()
107         log.info("Deleted stack '%s' in %d secs", self.name,
108                  end_time - start_time)
109         self.uuid = None
110
111     def delete(self, block=True, retries=3):
112         """deletes a stack in the target cloud using heat (with retry)
113         Sometimes delete fail with "InternalServerError" and the next attempt
114         succeeds. So it is worthwhile to test a couple of times.
115         """
116         if self.uuid is None:
117             return
118
119         if not block:
120             self._delete()
121             return
122
123         for _ in range(retries):
124             try:
125                 self._delete()
126                 break
127             except RuntimeError as err:
128                 log.warning(err.args)
129                 time.sleep(2)
130
131         # if still not deleted try once more and let it fail everything
132         if self.uuid is not None:
133             self._delete()
134
135         HeatStack.stacks.remove(self)
136
137     @staticmethod
138     def delete_all():
139         for stack in HeatStack.stacks[:]:
140             stack.delete()
141
142     def update(self):
143         """update a stack"""
144         raise RuntimeError("not implemented")
145
146
147 class HeatTemplate(HeatObject):
148     """Describes a Heat template and a method to deploy template to a stack"""
149
150     DESCRIPTION_TEMPLATE = """\
151 Stack built by the yardstick framework for %s on host %s %s.
152 All referred generated resources are prefixed with the template
153 name (i.e. %s).\
154 """
155
156     def _init_template(self):
157         timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
158         self._template = {
159             'heat_template_version': '2013-05-23',
160             'description': self.DESCRIPTION_TEMPLATE % (
161                 getpass.getuser(),
162                 socket.gethostname(),
163                 timestamp,
164                 self.name
165             ),
166             'resources': {},
167             'outputs': {}
168         }
169
170         # short hand for resources part of template
171         self.resources = self._template['resources']
172
173     def __init__(self, name, template_file=None, heat_parameters=None):
174         super(HeatTemplate, self).__init__()
175         self.name = name
176         self.state = "NOT_CREATED"
177         self.keystone_client = None
178         self.heat_parameters = {}
179
180         # heat_parameters is passed to heat in stack create, empty dict when
181         # yardstick creates the template (no get_param in resources part)
182         if heat_parameters:
183             self.heat_parameters = heat_parameters
184
185         if template_file:
186             with open(template_file) as stream:
187                 print("Parsing external template:", template_file)
188                 template_str = stream.read()
189             self._template = template_format.parse(template_str)
190             self._parameters = heat_parameters
191         else:
192             self._init_template()
193
194         # holds results of requested output after deployment
195         self.outputs = {}
196
197         log.debug("template object '%s' created", name)
198
199     def add_flavor(self, name, vcpus=1, ram=1024, disk=1, ephemeral=0,
200                    is_public=True, rxtx_factor=1.0, swap=0,
201                    extra_specs=None):
202         """add to the template a Flavor description"""
203         if name is None:
204             name = 'auto'
205         log.debug("adding Nova::Flavor '%s' vcpus '%d' ram '%d' disk '%d' " +
206                   "ephemeral '%d' is_public '%s' rxtx_factor '%d' " +
207                   "swap '%d' extra_specs '%s' ",
208                   name, vcpus, ram, disk, ephemeral, is_public,
209                   rxtx_factor, swap, str(extra_specs))
210
211         if extra_specs:
212             assert isinstance(extra_specs, collections.Mapping)
213
214         self.resources[name] = {
215             'type': 'OS::Nova::Flavor',
216             'properties': {'name': name,
217                            'disk': disk,
218                            'vcpus': vcpus,
219                            'swap': swap,
220                            'flavorid': name,
221                            'rxtx_factor': rxtx_factor,
222                            'ram': ram,
223                            'is_public': is_public,
224                            'ephemeral': ephemeral,
225                            'extra_specs': extra_specs}
226         }
227
228         self._template['outputs'][name] = {
229             'description': 'Flavor %s ID' % name,
230             'value': {'get_resource': name}
231         }
232
233     def add_volume(self, name, size=10):
234         """add to the template a volume description"""
235         log.debug("adding Cinder::Volume '%s' size '%d' ", name, size)
236
237         self.resources[name] = {
238             'type': 'OS::Cinder::Volume',
239             'properties': {'name': name,
240                            'size': size}
241         }
242
243         self._template['outputs'][name] = {
244             'description': 'Volume %s ID' % name,
245             'value': {'get_resource': name}
246         }
247
248     def add_volume_attachment(self, server_name, volume_name, mountpoint=None):
249         """add to the template an association of volume to instance"""
250         log.debug("adding Cinder::VolumeAttachment server '%s' volume '%s' ", server_name,
251                   volume_name)
252
253         name = "%s-%s" % (server_name, volume_name)
254
255         volume_id = op_utils.get_volume_id(volume_name)
256         if not volume_id:
257             volume_id = {'get_resource': volume_name}
258         self.resources[name] = {
259             'type': 'OS::Cinder::VolumeAttachment',
260             'properties': {'instance_uuid': {'get_resource': server_name},
261                            'volume_id': volume_id}
262         }
263
264         if mountpoint:
265             self.resources[name]['properties']['mountpoint'] = mountpoint
266
267     def add_network(self, name, physical_network='physnet1', provider=None,
268                     segmentation_id=None, port_security_enabled=None, network_type=None):
269         """add to the template a Neutron Net"""
270         log.debug("adding Neutron::Net '%s'", name)
271         if provider is None:
272             self.resources[name] = {
273                 'type': 'OS::Neutron::Net',
274                 'properties': {
275                     'name': name,
276                 }
277             }
278         else:
279             self.resources[name] = {
280                 'type': 'OS::Neutron::ProviderNet',
281                 'properties': {
282                     'name': name,
283                     'network_type': 'flat' if network_type is None else network_type,
284                     'physical_network': physical_network,
285                 },
286             }
287             if segmentation_id:
288                 self.resources[name]['properties']['segmentation_id'] = segmentation_id
289                 if network_type is None:
290                     self.resources[name]['properties']['network_type'] = 'vlan'
291         # if port security is not defined then don't add to template:
292         # some deployments don't have port security plugin installed
293         if port_security_enabled is not None:
294             self.resources[name]['properties']['port_security_enabled'] = port_security_enabled
295
296     def add_server_group(self, name, policies):     # pragma: no cover
297         """add to the template a ServerGroup"""
298         log.debug("adding Nova::ServerGroup '%s'", name)
299         policies = policies if isinstance(policies, list) else [policies]
300         self.resources[name] = {
301             'type': 'OS::Nova::ServerGroup',
302             'properties': {'name': name,
303                            'policies': policies}
304         }
305
306     def add_subnet(self, name, network, cidr, enable_dhcp='true', gateway_ip=None):
307         """add to the template a Neutron Subnet
308         """
309         log.debug("adding Neutron::Subnet '%s' in network '%s', cidr '%s'",
310                   name, network, cidr)
311         self.resources[name] = {
312             'type': 'OS::Neutron::Subnet',
313             'depends_on': network,
314             'properties': {
315                 'name': name,
316                 'cidr': cidr,
317                 'network_id': {'get_resource': network},
318                 'enable_dhcp': enable_dhcp,
319             }
320         }
321         if gateway_ip == 'null':
322             self.resources[name]['properties']['gateway_ip'] = None
323         elif gateway_ip is not None:
324             self.resources[name]['properties']['gateway_ip'] = gateway_ip
325
326         self._template['outputs'][name] = {
327             'description': 'subnet %s ID' % name,
328             'value': {'get_resource': name}
329         }
330         self._template['outputs'][name + "-cidr"] = {
331             'description': 'subnet %s cidr' % name,
332             'value': {'get_attr': [name, 'cidr']}
333         }
334         self._template['outputs'][name + "-gateway_ip"] = {
335             'description': 'subnet %s gateway_ip' % name,
336             'value': {'get_attr': [name, 'gateway_ip']}
337         }
338
339     def add_router(self, name, ext_gw_net, subnet_name):
340         """add to the template a Neutron Router and interface"""
341         log.debug("adding Neutron::Router:'%s', gw-net:'%s'", name, ext_gw_net)
342         self.resources[name] = {
343             'type': 'OS::Neutron::Router',
344             'depends_on': [subnet_name],
345             'properties': {
346                 'name': name,
347                 'external_gateway_info': {
348                     'network': ext_gw_net
349                 }
350             }
351         }
352
353     def add_router_interface(self, name, router_name, subnet_name):
354         """add to the template a Neutron RouterInterface and interface"""
355         log.debug("adding Neutron::RouterInterface '%s' router:'%s', "
356                   "subnet:'%s'", name, router_name, subnet_name)
357         self.resources[name] = {
358             'type': 'OS::Neutron::RouterInterface',
359             'depends_on': [router_name, subnet_name],
360             'properties': {
361                 'router_id': {'get_resource': router_name},
362                 'subnet_id': {'get_resource': subnet_name}
363             }
364         }
365
366     def add_port(self, name, network_name, subnet_name, vnic_type, sec_group_id=None,
367                  provider=None, allowed_address_pairs=None):
368         """add to the template a named Neutron Port
369         """
370         log.debug("adding Neutron::Port '%s', network:'%s', subnet:'%s', vnic_type:'%s', "
371                   "secgroup:%s", name, network_name, subnet_name, vnic_type, sec_group_id)
372         self.resources[name] = {
373             'type': 'OS::Neutron::Port',
374             'depends_on': [subnet_name],
375             'properties': {
376                 'name': name,
377                 'binding:vnic_type': vnic_type,
378                 'fixed_ips': [{'subnet': {'get_resource': subnet_name}}],
379                 'network_id': {'get_resource': network_name},
380                 'replacement_policy': 'AUTO',
381             }
382         }
383
384         if provider == PROVIDER_SRIOV:
385             self.resources[name]['properties']['binding:vnic_type'] = \
386                 'direct'
387
388         if sec_group_id:
389             self.resources[name]['depends_on'].append(sec_group_id)
390             self.resources[name]['properties']['security_groups'] = \
391                 [sec_group_id]
392
393         if allowed_address_pairs:
394             self.resources[name]['properties'][
395                 'allowed_address_pairs'] = allowed_address_pairs
396
397         self._template['outputs'][name] = {
398             'description': 'Address for interface %s' % name,
399             'value': {'get_attr': [name, 'fixed_ips', 0, 'ip_address']}
400         }
401         self._template['outputs'][name + "-subnet_id"] = {
402             'description': 'Address for interface %s' % name,
403             'value': {'get_attr': [name, 'fixed_ips', 0, 'subnet_id']}
404         }
405         self._template['outputs'][name + "-mac_address"] = {
406             'description': 'MAC Address for interface %s' % name,
407             'value': {'get_attr': [name, 'mac_address']}
408         }
409         self._template['outputs'][name + "-device_id"] = {
410             'description': 'Device ID for interface %s' % name,
411             'value': {'get_attr': [name, 'device_id']}
412         }
413         self._template['outputs'][name + "-network_id"] = {
414             'description': 'Network ID for interface %s' % name,
415             'value': {'get_attr': [name, 'network_id']}
416         }
417
418     def add_floating_ip(self, name, network_name, port_name, router_if_name,
419                         secgroup_name=None):
420         """add to the template a Nova FloatingIP resource
421         see: https://bugs.launchpad.net/heat/+bug/1299259
422         """
423         log.debug("adding Nova::FloatingIP '%s', network '%s', port '%s', "
424                   "rif '%s'", name, network_name, port_name, router_if_name)
425
426         self.resources[name] = {
427             'type': 'OS::Nova::FloatingIP',
428             'depends_on': [port_name, router_if_name],
429             'properties': {
430                 'pool': network_name
431             }
432         }
433
434         if secgroup_name:
435             self.resources[name]["depends_on"].append(secgroup_name)
436
437         self._template['outputs'][name] = {
438             'description': 'floating ip %s' % name,
439             'value': {'get_attr': [name, 'ip']}
440         }
441
442     def add_floating_ip_association(self, name, floating_ip_name, port_name):
443         """add to the template a Nova FloatingIP Association resource
444         """
445         log.debug("adding Nova::FloatingIPAssociation '%s', server '%s', "
446                   "floating_ip '%s'", name, port_name, floating_ip_name)
447
448         self.resources[name] = {
449             'type': 'OS::Neutron::FloatingIPAssociation',
450             'depends_on': [port_name],
451             'properties': {
452                 'floatingip_id': {'get_resource': floating_ip_name},
453                 'port_id': {'get_resource': port_name}
454             }
455         }
456
457     def add_keypair(self, name, key_uuid):
458         """add to the template a Nova KeyPair"""
459         log.debug("adding Nova::KeyPair '%s'", name)
460         self.resources[name] = {
461             'type': 'OS::Nova::KeyPair',
462             'properties': {
463                 'name': name,
464                 # resource_string returns bytes, so we must decode to unicode
465                 'public_key': encodeutils.safe_decode(
466                     pkg_resources.resource_string(
467                         'yardstick.resources',
468                         'files/yardstick_key-' +
469                         get_short_key_uuid(key_uuid) + '.pub'),
470                     'utf-8')
471             }
472         }
473
474     def add_servergroup(self, name, policy):
475         """add to the template a Nova ServerGroup"""
476         log.debug("adding Nova::ServerGroup '%s', policy '%s'", name, policy)
477         if policy not in ["anti-affinity", "affinity"]:
478             raise ValueError(policy)
479
480         self.resources[name] = {
481             'type': 'OS::Nova::ServerGroup',
482             'properties': {
483                 'name': name,
484                 'policies': [policy]
485             }
486         }
487
488         self._template['outputs'][name] = {
489             'description': 'ID Server Group %s' % name,
490             'value': {'get_resource': name}
491         }
492
493     def add_security_group(self, name):
494         """add to the template a Neutron SecurityGroup"""
495         log.debug("adding Neutron::SecurityGroup '%s'", name)
496         self.resources[name] = {
497             'type': 'OS::Neutron::SecurityGroup',
498             'properties': {
499                 'name': name,
500                 'description': "Group allowing icmp and upd/tcp on all ports",
501                 'rules': [
502                     {'remote_ip_prefix': '0.0.0.0/0',
503                      'protocol': 'tcp',
504                      'port_range_min': '1',
505                      'port_range_max': '65535'},
506                     {'remote_ip_prefix': '0.0.0.0/0',
507                      'protocol': 'udp',
508                      'port_range_min': '1',
509                      'port_range_max': '65535'},
510                     {'remote_ip_prefix': '0.0.0.0/0',
511                      'protocol': 'icmp'}
512                 ]
513             }
514         }
515
516         self._template['outputs'][name] = {
517             'description': 'ID of Security Group',
518             'value': {'get_resource': name}
519         }
520
521     def add_server(self, name, image, flavor, flavors, ports=None,
522                    networks=None, scheduler_hints=None, user=None,
523                    key_name=None, user_data=None, metadata=None,
524                    additional_properties=None):
525         """add to the template a Nova Server"""
526         log.debug("adding Nova::Server '%s', image '%s', flavor '%s', "
527                   "ports %s", name, image, flavor, ports)
528
529         self.resources[name] = {
530             'type': 'OS::Nova::Server',
531             'depends_on': []
532         }
533
534         server_properties = {
535             'name': name,
536             'image': image,
537             'flavor': {},
538             'networks': []  # list of dictionaries
539         }
540
541         if flavor in flavors:
542             self.resources[name]['depends_on'].append(flavor)
543             server_properties["flavor"] = {'get_resource': flavor}
544         else:
545             server_properties["flavor"] = flavor
546
547         if user:
548             server_properties['admin_user'] = user
549
550         if key_name:
551             self.resources[name]['depends_on'].append(key_name)
552             server_properties['key_name'] = {'get_resource': key_name}
553
554         if ports:
555             self.resources[name]['depends_on'].extend(ports)
556             for port in ports:
557                 server_properties['networks'].append(
558                     {'port': {'get_resource': port}}
559                 )
560
561         if networks:
562             for i, _ in enumerate(networks):
563                 server_properties['networks'].append({'network': networks[i]})
564
565         if scheduler_hints:
566             server_properties['scheduler_hints'] = scheduler_hints
567
568         if user_data:
569             server_properties['user_data'] = user_data
570
571         if metadata:
572             assert isinstance(metadata, collections.Mapping)
573             server_properties['metadata'] = metadata
574
575         if additional_properties:
576             assert isinstance(additional_properties, collections.Mapping)
577             for prop in additional_properties:
578                 server_properties[prop] = additional_properties[prop]
579
580         server_properties['config_drive'] = True
581
582         self.resources[name]['properties'] = server_properties
583
584         self._template['outputs'][name] = {
585             'description': 'VM UUID',
586             'value': {'get_resource': name}
587         }
588
589     HEAT_WAIT_LOOP_INTERVAL = 2
590     HEAT_CREATE_COMPLETE_STATUS = u'CREATE_COMPLETE'
591
592     def create(self, block=True, timeout=3600):
593         """
594         creates a template in the target cloud using heat
595         returns a dict with the requested output values from the template
596
597         :param block: Wait for Heat create to finish
598         :type block: bool
599         :param: timeout: timeout in seconds for Heat create, default 3600s
600         :type timeout: int
601         """
602         log.info("Creating stack '%s'", self.name)
603
604         # create stack early to support cleanup, e.g. ctrl-c while waiting
605         stack = HeatStack(self.name)
606
607         heat_client = self.heat_client
608         start_time = time.time()
609         stack.uuid = self.uuid = heat_client.stacks.create(
610             stack_name=self.name, template=self._template,
611             parameters=self.heat_parameters)['stack']['id']
612
613         if not block:
614             self.outputs = stack.outputs = {}
615             end_time = time.time()
616             log.info("Created stack '%s' in %.3e secs",
617                      self.name, end_time - start_time)
618             return stack
619
620         time_limit = start_time + timeout
621         for status in iter(self.status, self.HEAT_CREATE_COMPLETE_STATUS):
622             log.debug("stack state %s", status)
623             if status == u'CREATE_FAILED':
624                 stack_status_reason = heat_client.stacks.get(self.uuid).stack_status_reason
625                 heat_client.stacks.delete(self.uuid)
626                 raise RuntimeError(stack_status_reason)
627             if time.time() > time_limit:
628                 raise RuntimeError("Heat stack create timeout")
629
630             time.sleep(self.HEAT_WAIT_LOOP_INTERVAL)
631
632         end_time = time.time()
633         outputs = heat_client.stacks.get(self.uuid).outputs
634         log.info("Created stack '%s' in %.3e secs",
635                  self.name, end_time - start_time)
636
637         # keep outputs as unicode
638         self.outputs = {output["output_key"]: output["output_value"] for output
639                         in outputs}
640
641         stack.outputs = self.outputs
642         return stack