Merge "Update Yardstick README file"
[yardstick.git] / yardstick / orchestrator / heat.py
1 ##############################################################################
2 # Copyright (c) 2015 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 import time
13 import datetime
14 import getpass
15 import socket
16 import logging
17 import pkg_resources
18 import json
19
20 import heatclient
21
22 from yardstick.common import template_format
23 import yardstick.common.openstack_utils as op_utils
24
25
26 log = logging.getLogger(__name__)
27
28
29 class HeatObject(object):
30     ''' base class for template and stack'''
31     def __init__(self):
32         self._heat_client = None
33         self.uuid = None
34
35     def _get_heat_client(self):
36         '''returns a heat client instance'''
37
38         if self._heat_client is None:
39             sess = op_utils.get_session()
40             heat_endpoint = op_utils.get_endpoint(service_type='orchestration')
41             self._heat_client = heatclient.client.Client(
42                 op_utils.get_heat_api_version(),
43                 endpoint=heat_endpoint, session=sess)
44
45         return self._heat_client
46
47     def status(self):
48         '''returns stack state as a string'''
49         heat = self._get_heat_client()
50         stack = heat.stacks.get(self.uuid)
51         return getattr(stack, 'stack_status')
52
53
54 class HeatStack(HeatObject):
55     ''' Represents a Heat stack (deployed template) '''
56     stacks = []
57
58     def __init__(self, name):
59         super(HeatStack, self).__init__()
60         self.uuid = None
61         self.name = name
62         self.outputs = None
63         HeatStack.stacks.append(self)
64
65     @staticmethod
66     def stacks_exist():
67         '''check if any stack has been deployed'''
68         return len(HeatStack.stacks) > 0
69
70     def _delete(self):
71         '''deletes a stack from the target cloud using heat'''
72         if self.uuid is None:
73             return
74
75         log.info("Deleting stack '%s', uuid:%s", self.name, self.uuid)
76         heat = self._get_heat_client()
77         template = heat.stacks.get(self.uuid)
78         start_time = time.time()
79         template.delete()
80         status = self.status()
81
82         while status != u'DELETE_COMPLETE':
83             log.debug("stack state %s", status)
84             if status == u'DELETE_FAILED':
85                 raise RuntimeError(
86                     heat.stacks.get(self.uuid).stack_status_reason)
87
88             time.sleep(2)
89             status = self.status()
90
91         end_time = time.time()
92         log.info("Deleted stack '%s' in %d secs", self.name,
93                  end_time - start_time)
94         self.uuid = None
95
96     def delete(self, block=True, retries=3):
97         '''deletes a stack in the target cloud using heat (with retry)
98         Sometimes delete fail with "InternalServerError" and the next attempt
99         succeeds. So it is worthwhile to test a couple of times.
100         '''
101         if self.uuid is None:
102             return
103
104         if not block:
105             self._delete()
106             return
107
108         i = 0
109         while i < retries:
110             try:
111                 self._delete()
112                 break
113             except RuntimeError as err:
114                 log.warn(err.args)
115                 time.sleep(2)
116             i += 1
117
118         # if still not deleted try once more and let it fail everything
119         if self.uuid is not None:
120             self._delete()
121
122         HeatStack.stacks.remove(self)
123
124     @staticmethod
125     def delete_all():
126         for stack in HeatStack.stacks[:]:
127             stack.delete()
128
129     def update(self):
130         '''update a stack'''
131         raise RuntimeError("not implemented")
132
133
134 class HeatTemplate(HeatObject):
135     '''Describes a Heat template and a method to deploy template to a stack'''
136
137     def _init_template(self):
138         self._template = {}
139         self._template['heat_template_version'] = '2013-05-23'
140
141         timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
142         self._template['description'] = \
143             '''Stack built by the yardstick framework for %s on host %s %s.
144             All referred generated resources are prefixed with the template
145             name (i.e. %s).''' % (getpass.getuser(), socket.gethostname(),
146                                   timestamp, self.name)
147
148         # short hand for resources part of template
149         self.resources = self._template['resources'] = {}
150
151         self._template['outputs'] = {}
152
153     def __init__(self, name, template_file=None, heat_parameters=None):
154         super(HeatTemplate, self).__init__()
155         self.name = name
156         self.state = "NOT_CREATED"
157         self.keystone_client = None
158         self.heat_client = None
159         self.heat_parameters = {}
160
161         # heat_parameters is passed to heat in stack create, empty dict when
162         # yardstick creates the template (no get_param in resources part)
163         if heat_parameters:
164             self.heat_parameters = heat_parameters
165
166         if template_file:
167             with open(template_file) as stream:
168                 print "Parsing external template:", template_file
169                 template_str = stream.read()
170                 self._template = template_format.parse(template_str)
171             self._parameters = heat_parameters
172         else:
173             self._init_template()
174
175         # holds results of requested output after deployment
176         self.outputs = {}
177
178         log.debug("template object '%s' created", name)
179
180     def add_network(self, name):
181         '''add to the template a Neutron Net'''
182         log.debug("adding Neutron::Net '%s'", name)
183         self.resources[name] = {
184             'type': 'OS::Neutron::Net',
185             'properties': {'name': name}
186         }
187
188     def add_subnet(self, name, network, cidr):
189         '''add to the template a Neutron Subnet'''
190         log.debug("adding Neutron::Subnet '%s' in network '%s', cidr '%s'",
191                   name, network, cidr)
192         self.resources[name] = {
193             'type': 'OS::Neutron::Subnet',
194             'depends_on': network,
195             'properties': {
196                 'name': name,
197                 'cidr': cidr,
198                 'network_id': {'get_resource': network}
199             }
200         }
201
202         self._template['outputs'][name] = {
203             'description': 'subnet %s ID' % name,
204             'value': {'get_resource': name}
205         }
206
207     def add_router(self, name, ext_gw_net, subnet_name):
208         '''add to the template a Neutron Router and interface'''
209         log.debug("adding Neutron::Router:'%s', gw-net:'%s'", name, ext_gw_net)
210
211         self.resources[name] = {
212             'type': 'OS::Neutron::Router',
213             'depends_on': [subnet_name],
214             'properties': {
215                 'name': name,
216                 'external_gateway_info': {
217                     'network': ext_gw_net
218                 }
219             }
220         }
221
222     def add_router_interface(self, name, router_name, subnet_name):
223         '''add to the template a Neutron RouterInterface and interface'''
224         log.debug("adding Neutron::RouterInterface '%s' router:'%s', "
225                   "subnet:'%s'", name, router_name, subnet_name)
226
227         self.resources[name] = {
228             'type': 'OS::Neutron::RouterInterface',
229             'depends_on': [router_name, subnet_name],
230             'properties': {
231                 'router_id': {'get_resource': router_name},
232                 'subnet_id': {'get_resource': subnet_name}
233             }
234         }
235
236     def add_port(self, name, network_name, subnet_name, sec_group_id=None):
237         '''add to the template a named Neutron Port'''
238         log.debug("adding Neutron::Port '%s', network:'%s', subnet:'%s', "
239                   "secgroup:%s", name, network_name, subnet_name, sec_group_id)
240         self.resources[name] = {
241             'type': 'OS::Neutron::Port',
242             'depends_on': [subnet_name],
243             'properties': {
244                 'name': name,
245                 'fixed_ips': [{'subnet': {'get_resource': subnet_name}}],
246                 'network_id': {'get_resource': network_name},
247                 'replacement_policy': 'AUTO',
248             }
249         }
250
251         if sec_group_id:
252             self.resources[name]['depends_on'].append(sec_group_id)
253             self.resources[name]['properties']['security_groups'] = \
254                 [sec_group_id]
255
256         self._template['outputs'][name] = {
257             'description': 'Address for interface %s' % name,
258             'value': {'get_attr': [name, 'fixed_ips', 0, 'ip_address']}
259         }
260
261     def add_floating_ip(self, name, network_name, port_name, router_if_name,
262                         secgroup_name=None):
263         '''add to the template a Nova FloatingIP resource
264         see: https://bugs.launchpad.net/heat/+bug/1299259
265         '''
266         log.debug("adding Nova::FloatingIP '%s', network '%s', port '%s', "
267                   "rif '%s'", name, network_name, port_name, router_if_name)
268
269         self.resources[name] = {
270             'type': 'OS::Nova::FloatingIP',
271             'depends_on': [port_name, router_if_name],
272             'properties': {
273                 'pool': network_name
274             }
275         }
276
277         if secgroup_name:
278             self.resources[name]["depends_on"].append(secgroup_name)
279
280         self._template['outputs'][name] = {
281             'description': 'floating ip %s' % name,
282             'value': {'get_attr': [name, 'ip']}
283         }
284
285     def add_floating_ip_association(self, name, floating_ip_name, port_name):
286         '''add to the template a Nova FloatingIP Association resource
287         '''
288         log.debug("adding Nova::FloatingIPAssociation '%s', server '%s', "
289                   "floating_ip '%s'", name, port_name, floating_ip_name)
290
291         self.resources[name] = {
292             'type': 'OS::Neutron::FloatingIPAssociation',
293             'depends_on': [port_name],
294             'properties': {
295                 'floatingip_id': {'get_resource': floating_ip_name},
296                 'port_id': {'get_resource': port_name}
297             }
298         }
299
300     def add_keypair(self, name):
301         '''add to the template a Nova KeyPair'''
302         log.debug("adding Nova::KeyPair '%s'", name)
303         self.resources[name] = {
304             'type': 'OS::Nova::KeyPair',
305             'properties': {
306                 'name': name,
307                 'public_key': pkg_resources.resource_string(
308                     'yardstick.resources', 'files/yardstick_key.pub')
309             }
310         }
311
312     def add_servergroup(self, name, policy):
313         '''add to the template a Nova ServerGroup'''
314         log.debug("adding Nova::ServerGroup '%s', policy '%s'", name, policy)
315         if policy not in ["anti-affinity", "affinity"]:
316             raise ValueError(policy)
317
318         self.resources[name] = {
319             'type': 'OS::Nova::ServerGroup',
320             'properties': {
321                 'name': name,
322                 'policies': [policy]
323             }
324         }
325
326         self._template['outputs'][name] = {
327             'description': 'ID Server Group %s' % name,
328             'value': {'get_resource': name}
329         }
330
331     def add_security_group(self, name):
332         '''add to the template a Neutron SecurityGroup'''
333         log.debug("adding Neutron::SecurityGroup '%s'", name)
334         self.resources[name] = {
335             'type': 'OS::Neutron::SecurityGroup',
336             'properties': {
337                 'name': name,
338                 'description': "Group allowing icmp and upd/tcp on all ports",
339                 'rules': [
340                     {'remote_ip_prefix': '0.0.0.0/0',
341                      'protocol': 'tcp',
342                      'port_range_min': '1',
343                      'port_range_max': '65535'},
344                     {'remote_ip_prefix': '0.0.0.0/0',
345                      'protocol': 'udp',
346                      'port_range_min': '1',
347                      'port_range_max': '65535'},
348                     {'remote_ip_prefix': '0.0.0.0/0',
349                      'protocol': 'icmp'}
350                 ]
351             }
352         }
353
354         self._template['outputs'][name] = {
355             'description': 'ID of Security Group',
356             'value': {'get_resource': name}
357         }
358
359     def add_server(self, name, image, flavor, ports=None, networks=None,
360                    scheduler_hints=None, user=None, key_name=None,
361                    user_data=None, metadata=None, additional_properties=None):
362         '''add to the template a Nova Server'''
363         log.debug("adding Nova::Server '%s', image '%s', flavor '%s', "
364                   "ports %s", name, image, flavor, ports)
365
366         self.resources[name] = {
367             'type': 'OS::Nova::Server'
368         }
369
370         server_properties = {
371             'name': name,
372             'image': image,
373             'flavor': flavor,
374             'networks': []  # list of dictionaries
375         }
376
377         if user:
378             server_properties['admin_user'] = user
379
380         if key_name:
381             self.resources[name]['depends_on'] = [key_name]
382             server_properties['key_name'] = {'get_resource': key_name}
383
384         if ports:
385             self.resources[name]['depends_on'] = ports
386
387             for port in ports:
388                 server_properties['networks'].append(
389                     {'port': {'get_resource': port}}
390                 )
391
392         if networks:
393             for i in range(len(networks)):
394                 server_properties['networks'].append({'network': networks[i]})
395
396         if scheduler_hints:
397             server_properties['scheduler_hints'] = scheduler_hints
398
399         if user_data:
400             server_properties['user_data'] = user_data
401
402         if metadata:
403             assert type(metadata) is dict
404             server_properties['metadata'] = metadata
405
406         if additional_properties:
407             assert type(additional_properties) is dict
408             for prop in additional_properties:
409                 server_properties[prop] = additional_properties[prop]
410
411         server_properties['config_drive'] = True
412
413         self.resources[name]['properties'] = server_properties
414
415         self._template['outputs'][name] = {
416             'description': 'VM UUID',
417             'value': {'get_resource': name}
418         }
419
420     def create(self, block=True):
421         '''creates a template in the target cloud using heat
422         returns a dict with the requested output values from the template'''
423         log.info("Creating stack '%s'", self.name)
424
425         # create stack early to support cleanup, e.g. ctrl-c while waiting
426         stack = HeatStack(self.name)
427
428         heat = self._get_heat_client()
429         json_template = json.dumps(self._template)
430         start_time = time.time()
431         stack.uuid = self.uuid = heat.stacks.create(
432             stack_name=self.name, template=json_template,
433             parameters=self.heat_parameters)['stack']['id']
434
435         status = self.status()
436
437         if block:
438             while status != u'CREATE_COMPLETE':
439                 log.debug("stack state %s", status)
440                 if status == u'CREATE_FAILED':
441                     raise RuntimeError(getattr(heat.stacks.get(self.uuid),
442                                                'stack_status_reason'))
443
444                 time.sleep(2)
445                 status = self.status()
446
447             end_time = time.time()
448             outputs = getattr(heat.stacks.get(self.uuid), 'outputs')
449
450         for output in outputs:
451             self.outputs[output["output_key"].encode("ascii")] = \
452                 output["output_value"].encode("ascii")
453
454         log.info("Created stack '%s' in %d secs",
455                  self.name, end_time - start_time)
456
457         stack.outputs = self.outputs
458         return stack