~ubuntu-branches/ubuntu/trusty/heat/trusty

« back to all changes in this revision

Viewing changes to heat/engine/resources/server.py

  • Committer: Package Import Robot
  • Author(s): Chuck Short, Chuck Short, Adam Gandelman
  • Date: 2013-09-08 21:51:19 UTC
  • mfrom: (1.1.4)
  • Revision ID: package-import@ubuntu.com-20130908215119-r939tu4aumqgdrkx
Tags: 2013.2~b3-0ubuntu1
[ Chuck Short ]
* New upstream release.
* debian/control: Add python-netaddr as build-dep.
* debian/heat-common.install: Remove heat-boto and associated man-page
* debian/heat-common.install: Remove heat-cfn and associated man-page
* debian/heat-common.install: Remove heat-watch and associated man-page
* debian/patches/fix-sqlalchemy-0.8.patch: Dropped

[ Adam Gandelman ]
* debian/patches/default-kombu.patch: Dropped.
* debian/patches/default-sqlite.patch: Refreshed.
* debian/*.install, rules: Install heat.conf.sample as common
  config file in heat-common. Drop other per-package configs, they
  are no longer used.
* debian/rules: Clean pbr .egg from build dir if it exists.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# vim: tabstop=4 shiftwidth=4 softtabstop=4
 
2
 
 
3
#
 
4
#    Licensed under the Apache License, Version 2.0 (the "License"); you may
 
5
#    not use this file except in compliance with the License. You may obtain
 
6
#    a copy of the License at
 
7
#
 
8
#         http://www.apache.org/licenses/LICENSE-2.0
 
9
#
 
10
#    Unless required by applicable law or agreed to in writing, software
 
11
#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 
12
#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 
13
#    License for the specific language governing permissions and limitations
 
14
#    under the License.
 
15
 
 
16
from heat.common import exception
 
17
from heat.engine import clients
 
18
from heat.engine import scheduler
 
19
from heat.engine.resources import nova_utils
 
20
from heat.engine import resource
 
21
from heat.openstack.common.gettextutils import _
 
22
from heat.openstack.common import log as logging
 
23
 
 
24
logger = logging.getLogger(__name__)
 
25
 
 
26
 
 
27
class Server(resource.Resource):
 
28
 
 
29
    block_mapping_schema = {
 
30
        'device_name': {
 
31
            'Type': 'String',
 
32
            'Required': True,
 
33
            'Description': _('A device name where the volume will be '
 
34
                             'attached in the system at /dev/device_name. '
 
35
                             'This value is typically vda')},
 
36
        'volume_id': {
 
37
            'Type': 'String',
 
38
            'Description': _('The ID of the volume to boot from. Only one of '
 
39
                             'volume_id or snapshot_id should be provided')},
 
40
        'snapshot_id': {
 
41
            'Type': 'String',
 
42
            'Description': _('The ID of the snapshot to create a volume '
 
43
                             'from')},
 
44
        'volume_size': {
 
45
            'Type': 'String',
 
46
            'Description': _('The size of the volume, in GB. It is safe to '
 
47
                             'leave this blank and have the Compute service '
 
48
                             'infer the size')},
 
49
        'delete_on_termination': {
 
50
            'Type': 'Boolean',
 
51
            'Description': _('Indicate whether the volume should be deleted '
 
52
                             'when the server is terminated')}
 
53
    }
 
54
 
 
55
    networks_schema = {
 
56
        'uuid': {
 
57
            'Type': 'String',
 
58
            'Description': _('ID of network to create a port on')},
 
59
        'fixed_ip': {
 
60
            'Type': 'String',
 
61
            'Description': _('Fixed IP address to specify for the port '
 
62
                             'created on the requested network')},
 
63
        'port': {
 
64
            'Type': 'String',
 
65
            'Description': _('ID of an existing port to associate with '
 
66
                             'this server')},
 
67
    }
 
68
 
 
69
    properties_schema = {
 
70
        'name': {
 
71
            'Type': 'String',
 
72
            'Description': _('Optional server name')},
 
73
        'image': {
 
74
            'Type': 'String',
 
75
            'Description': _('The ID or name of the image to boot with')},
 
76
        'block_device_mapping': {
 
77
            'Type': 'List',
 
78
            'Description': _('Block device mappings for this server'),
 
79
            'Schema': {
 
80
                'Type': 'Map',
 
81
                'Schema': block_mapping_schema
 
82
            }
 
83
        },
 
84
        'flavor': {
 
85
            'Type': 'String',
 
86
            'Description': _('The ID or name of the flavor to boot onto'),
 
87
            'Required': True},
 
88
        'flavor_update_policy': {
 
89
            'Type': 'String',
 
90
            'Description': _('Policy on how to apply a flavor update; either '
 
91
                             'by requesting a server resize or by replacing '
 
92
                             'the entire server'),
 
93
            'Default': 'RESIZE',
 
94
            'AllowedValues': ['RESIZE', 'REPLACE']},
 
95
        'key_name': {
 
96
            'Type': 'String',
 
97
            'Description': _('Name of keypair to inject into the server')},
 
98
        'availability_zone': {
 
99
            'Type': 'String',
 
100
            'Description': _('Name of the availability zone for server '
 
101
                             'placement')},
 
102
        'security_groups': {
 
103
            'Type': 'List',
 
104
            'Description': _('List of security group names')},
 
105
        'networks': {
 
106
            'Type': 'List',
 
107
            'Description': _('An ordered list of nics to be '
 
108
                             'added to this server, with information about '
 
109
                             'connected networks, fixed ips, port etc'),
 
110
            'Schema': {
 
111
                'Type': 'Map',
 
112
                'Schema': networks_schema
 
113
            }
 
114
        },
 
115
        'scheduler_hints': {
 
116
            'Type': 'Map',
 
117
            'Description': _('Arbitrary key-value pairs specified by the '
 
118
                             'client to help boot a server')},
 
119
        'metadata': {
 
120
            'Type': 'Map',
 
121
            'Description': _('Arbitrary key/value metadata to store for this '
 
122
                             'server. A maximum of five entries is allowed, '
 
123
                             'and both keys and values must be 255 characters '
 
124
                             'or less')},
 
125
        'user_data': {
 
126
            'Type': 'String',
 
127
            'Description': _('User data script to be executed by cloud-init')},
 
128
        'reservation_id': {
 
129
            'Type': 'String',
 
130
            'Description': _('A UUID for the set of servers being requested'),
 
131
            'Implemented': False},
 
132
        'config_drive': {
 
133
            'Type': 'String',
 
134
            'Description': _('value for config drive either boolean, or '
 
135
                             'volume-id'),
 
136
            'Implemented': False},
 
137
        # diskConfig translates to API attribute OS-DCF:diskConfig
 
138
        # hence the camel case instead of underscore to separate the words
 
139
        'diskConfig': {
 
140
            'Type': 'String',
 
141
            'Description': _('Control how the disk is partitioned when the '
 
142
                             'server is created'),
 
143
            'AllowedValues': ['AUTO', 'MANUAL']}
 
144
    }
 
145
 
 
146
    attributes_schema = {
 
147
        'show': _('A dict of all server details as returned by the API'),
 
148
        'addresses': _('A dict of all network addresses as returned by '
 
149
                       'the API'),
 
150
        'networks': _('A dict of assigned network addresses of the form: '
 
151
                      '{"public": [ip1, ip2...], "private": [ip3, ip4]}'),
 
152
        'first_private_address': _('Convenience attribute to fetch the first '
 
153
                                   'assigned private network address, or an '
 
154
                                   'empty string if nothing has been assigned '
 
155
                                   'at this time'),
 
156
        'first_public_address': _('Convenience attribute to fetch the first '
 
157
                                  'assigned public network address, or an '
 
158
                                  'empty string if nothing has been assigned '
 
159
                                  'at this time'),
 
160
        'instance_name': _('AWS compatible instance name'),
 
161
        'accessIPv4': _('The manually assigned alternative public IPv4 '
 
162
                        'address of the server'),
 
163
        'accessIPv6': _('The manually assigned alternative public IPv6 '
 
164
                        'address of the server'),
 
165
    }
 
166
 
 
167
    update_allowed_keys = ('Metadata', 'Properties')
 
168
    update_allowed_properties = ('flavor', 'flavor_update_policy')
 
169
 
 
170
    def __init__(self, name, json_snippet, stack):
 
171
        super(Server, self).__init__(name, json_snippet, stack)
 
172
        self.mime_string = None
 
173
 
 
174
    def get_mime_string(self, userdata):
 
175
        if not self.mime_string:
 
176
            self.mime_string = nova_utils.build_userdata(self, userdata)
 
177
        return self.mime_string
 
178
 
 
179
    def handle_create(self):
 
180
        security_groups = self.properties.get('security_groups', [])
 
181
        userdata = self.properties.get('user_data', '')
 
182
        flavor = self.properties['flavor']
 
183
        availability_zone = self.properties['availability_zone']
 
184
 
 
185
        key_name = self.properties['key_name']
 
186
        if key_name:
 
187
            # confirm keypair exists
 
188
            nova_utils.get_keypair(self.nova(), key_name)
 
189
 
 
190
        image = self.properties.get('image')
 
191
        if image:
 
192
            image = nova_utils.get_image_id(self.nova(), image)
 
193
 
 
194
        flavor_id = nova_utils.get_flavor_id(self.nova(), flavor)
 
195
        instance_meta = self.properties.get('metadata')
 
196
        scheduler_hints = self.properties.get('scheduler_hints')
 
197
        nics = self._build_nics(self.properties.get('networks'))
 
198
        block_device_mapping = self._build_block_device_mapping(
 
199
            self.properties.get('block_device_mapping'))
 
200
        reservation_id = self.properties.get('reservation_id')
 
201
        config_drive = self.properties.get('config_drive')
 
202
        disk_config = self.properties.get('diskConfig')
 
203
 
 
204
        server = None
 
205
        try:
 
206
            server = self.nova().servers.create(
 
207
                name=self.physical_resource_name(),
 
208
                image=image,
 
209
                flavor=flavor_id,
 
210
                key_name=key_name,
 
211
                security_groups=security_groups,
 
212
                userdata=self.get_mime_string(userdata),
 
213
                meta=instance_meta,
 
214
                scheduler_hints=scheduler_hints,
 
215
                nics=nics,
 
216
                availability_zone=availability_zone,
 
217
                block_device_mapping=block_device_mapping,
 
218
                reservation_id=reservation_id,
 
219
                config_drive=config_drive,
 
220
                disk_config=disk_config)
 
221
        finally:
 
222
            # Avoid a race condition where the thread could be cancelled
 
223
            # before the ID is stored
 
224
            if server is not None:
 
225
                self.resource_id_set(server.id)
 
226
 
 
227
        return server
 
228
 
 
229
    def check_create_complete(self, server):
 
230
        return self._check_active(server)
 
231
 
 
232
    def _check_active(self, server):
 
233
 
 
234
        if server.status != 'ACTIVE':
 
235
            server.get()
 
236
 
 
237
        # Some clouds append extra (STATUS) strings to the status
 
238
        short_server_status = server.status.split('(')[0]
 
239
        if short_server_status in nova_utils.deferred_server_statuses:
 
240
            return False
 
241
        elif server.status == 'ACTIVE':
 
242
            return True
 
243
        elif server.status == 'ERROR':
 
244
            exc = exception.Error(_('Creation of server %s failed.') %
 
245
                                  server.name)
 
246
            raise exc
 
247
        else:
 
248
            exc = exception.Error(_('Creation of server %(server)s failed '
 
249
                                    'with unknown status: %(status)s') %
 
250
                                  dict(server=server.name,
 
251
                                       status=server.status))
 
252
            raise exc
 
253
 
 
254
    @staticmethod
 
255
    def _build_block_device_mapping(bdm):
 
256
        if not bdm:
 
257
            return None
 
258
        bdm_dict = {}
 
259
        for mapping in bdm:
 
260
            mapping_parts = []
 
261
            if mapping.get('snapshot_id'):
 
262
                mapping_parts.append(mapping.get('snapshot_id'))
 
263
                mapping_parts.append('snap')
 
264
            else:
 
265
                mapping_parts.append(mapping.get('volume_id'))
 
266
                mapping_parts.append('')
 
267
            if (mapping.get('volume_size') or
 
268
                    mapping.get('delete_on_termination')):
 
269
 
 
270
                mapping_parts.append(mapping.get('volume_size', 0))
 
271
            if mapping.get('delete_on_termination'):
 
272
                mapping_parts.append(mapping.get('delete_on_termination'))
 
273
            bdm_dict[mapping.get('device_name')] = mapping_parts
 
274
 
 
275
        return bdm_dict
 
276
 
 
277
    @staticmethod
 
278
    def _build_nics(networks):
 
279
        if not networks:
 
280
            return None
 
281
 
 
282
        nics = []
 
283
 
 
284
        for net_data in networks:
 
285
            nic_info = {}
 
286
            if net_data.get('uuid'):
 
287
                nic_info['net-id'] = net_data['uuid']
 
288
            if net_data.get('fixed_ip'):
 
289
                nic_info['v4-fixed-ip'] = net_data['fixed_ip']
 
290
            if net_data.get('port'):
 
291
                nic_info['port-id'] = net_data['port']
 
292
            nics.append(nic_info)
 
293
        return nics
 
294
 
 
295
    def _resolve_attribute(self, name):
 
296
        server = self.nova().servers.get(self.resource_id)
 
297
        if name == 'addresses':
 
298
            return server.addresses
 
299
        if name == 'networks':
 
300
            return server.networks
 
301
        if name == 'first_private_address':
 
302
            private = server.networks.get('private', [])
 
303
            if len(private) > 0:
 
304
                return private[0]
 
305
            return ''
 
306
        if name == 'first_public_address':
 
307
            public = server.networks.get('public', [])
 
308
            if len(public) > 0:
 
309
                return public[0]
 
310
            return ''
 
311
        if name == 'instance_name':
 
312
            return server._info.get('OS-EXT-SRV-ATTR:instance_name')
 
313
        if name == 'accessIPv4':
 
314
            return server.accessIPv4
 
315
        if name == 'accessIPv6':
 
316
            return server.accessIPv6
 
317
        if name == 'show':
 
318
            return server._info
 
319
 
 
320
    def handle_update(self, json_snippet, tmpl_diff, prop_diff):
 
321
        if 'Metadata' in tmpl_diff:
 
322
            self.metadata = tmpl_diff['Metadata']
 
323
 
 
324
        if 'flavor' in prop_diff:
 
325
 
 
326
            flavor_update_policy = (
 
327
                prop_diff.get('flavor_update_policy') or
 
328
                self.properties.get('flavor_update_policy'))
 
329
 
 
330
            if flavor_update_policy == 'REPLACE':
 
331
                raise resource.UpdateReplace(self.name)
 
332
 
 
333
            flavor = prop_diff['flavor']
 
334
            flavor_id = nova_utils.get_flavor_id(self.nova(), flavor)
 
335
            server = self.nova().servers.get(self.resource_id)
 
336
            server.resize(flavor_id)
 
337
            checker = scheduler.TaskRunner(nova_utils.check_resize,
 
338
                                           server, flavor)
 
339
            checker.start()
 
340
            return checker
 
341
 
 
342
    def check_update_complete(self, checker):
 
343
        return checker.step() if checker is not None else True
 
344
 
 
345
    def metadata_update(self, new_metadata=None):
 
346
        '''
 
347
        Refresh the metadata if new_metadata is None
 
348
        '''
 
349
        if new_metadata is None:
 
350
            self.metadata = self.parsed_template('Metadata')
 
351
 
 
352
    def validate(self):
 
353
        '''
 
354
        Validate any of the provided params
 
355
        '''
 
356
        super(Server, self).validate()
 
357
 
 
358
        # check validity of key
 
359
        key_name = self.properties.get('key_name', None)
 
360
        if key_name:
 
361
            nova_utils.get_keypair(self.nova(), key_name)
 
362
 
 
363
        # make sure the image exists if specified.
 
364
        image = self.properties.get('image', None)
 
365
        if image:
 
366
            nova_utils.get_image_id(self.nova(), image)
 
367
        else:
 
368
            # TODO(sbaker) confirm block_device_mapping is populated
 
369
            # for boot-by-volume (see LP bug #1215267)
 
370
            pass
 
371
 
 
372
    def handle_delete(self):
 
373
        '''
 
374
        Delete a server, blocking until it is disposed by OpenStack
 
375
        '''
 
376
        if self.resource_id is None:
 
377
            return
 
378
 
 
379
        try:
 
380
            server = self.nova().servers.get(self.resource_id)
 
381
        except clients.novaclient.exceptions.NotFound:
 
382
            pass
 
383
        else:
 
384
            delete = scheduler.TaskRunner(nova_utils.delete_server, server)
 
385
            delete(wait_time=0.2)
 
386
 
 
387
        self.resource_id = None
 
388
 
 
389
    def handle_suspend(self):
 
390
        '''
 
391
        Suspend a server - note we do not wait for the SUSPENDED state,
 
392
        this is polled for by check_suspend_complete in a similar way to the
 
393
        create logic so we can take advantage of coroutines
 
394
        '''
 
395
        if self.resource_id is None:
 
396
            raise exception.Error(_('Cannot suspend %s, resource_id not set') %
 
397
                                  self.name)
 
398
 
 
399
        try:
 
400
            server = self.nova().servers.get(self.resource_id)
 
401
        except clients.novaclient.exceptions.NotFound:
 
402
            raise exception.NotFound(_('Failed to find server %s') %
 
403
                                     self.resource_id)
 
404
        else:
 
405
            logger.debug('suspending server %s' % self.resource_id)
 
406
            # We want the server.suspend to happen after the volume
 
407
            # detachement has finished, so pass both tasks and the server
 
408
            suspend_runner = scheduler.TaskRunner(server.suspend)
 
409
            return server, suspend_runner
 
410
 
 
411
    def check_suspend_complete(self, cookie):
 
412
        server, suspend_runner = cookie
 
413
 
 
414
        if not suspend_runner.started():
 
415
            suspend_runner.start()
 
416
 
 
417
        if suspend_runner.done():
 
418
            if server.status == 'SUSPENDED':
 
419
                return True
 
420
 
 
421
            server.get()
 
422
            logger.debug('%s check_suspend_complete status = %s' %
 
423
                         (self.name, server.status))
 
424
            if server.status in list(nova_utils.deferred_server_statuses +
 
425
                                     ['ACTIVE']):
 
426
                return server.status == 'SUSPENDED'
 
427
            else:
 
428
                exc = exception.Error(_('Suspend of server %(server)s failed '
 
429
                                        'with unknown status: %(status)s') %
 
430
                                      dict(server=server.name,
 
431
                                           status=server.status))
 
432
                raise exc
 
433
 
 
434
    def handle_resume(self):
 
435
        '''
 
436
        Resume a server - note we do not wait for the ACTIVE state,
 
437
        this is polled for by check_resume_complete in a similar way to the
 
438
        create logic so we can take advantage of coroutines
 
439
        '''
 
440
        if self.resource_id is None:
 
441
            raise exception.Error(_('Cannot resume %s, resource_id not set') %
 
442
                                  self.name)
 
443
 
 
444
        try:
 
445
            server = self.nova().servers.get(self.resource_id)
 
446
        except clients.novaclient.exceptions.NotFound:
 
447
            raise exception.NotFound(_('Failed to find server %s') %
 
448
                                     self.resource_id)
 
449
        else:
 
450
            logger.debug('resuming server %s' % self.resource_id)
 
451
            server.resume()
 
452
            return server
 
453
 
 
454
    def check_resume_complete(self, server):
 
455
        return self._check_active(server)
 
456
 
 
457
 
 
458
def resource_mapping():
 
459
    return {
 
460
        'OS::Nova::Server': Server,
 
461
    }