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

« back to all changes in this revision

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

  • Committer: Package Import Robot
  • Author(s): Chuck Short, Yolanda Robla, Chuck Short
  • Date: 2013-07-22 16:22:29 UTC
  • mfrom: (1.1.2)
  • Revision ID: package-import@ubuntu.com-20130722162229-zzvfu40id94ii0hc
Tags: 2013.2~b2-0ubuntu1
[ Yolanda Robla ]
* debian/tests: added autopkg tests

[ Chuck Short ]
* New upstream release
* debian/control:
  - Add python-pbr to build-depends.
  - Add python-d2to to build-depends.
  - Dropped python-argparse.
  - Add python-six to build-depends.
  - Dropped python-sendfile.
  - Dropped python-nose.
  - Added testrepository.
  - Added python-testtools.
* debian/rules: Run testrepository instead of nosetets.
* debian/patches/removes-lxml-version-limitation-from-pip-requires.patch: Dropped
  no longer needed.
* debian/patches/fix-package-version-detection-when-building-doc.patch: Dropped
  no longer needed.

Show diffs side-by-side

added added

removed removed

Lines of Context:
20
20
import pkgutil
21
21
from urlparse import urlparse
22
22
 
23
 
import eventlet
24
23
from oslo.config import cfg
25
24
 
26
25
from heat.engine import clients
27
26
from heat.engine import resource
 
27
from heat.engine import scheduler
 
28
from heat.engine.resources import volume
 
29
 
28
30
from heat.common import exception
 
31
from heat.engine.resources.network_interface import NetworkInterface
29
32
 
30
33
from heat.openstack.common import log as logging
 
34
from heat.openstack.common import uuidutils
31
35
 
32
36
logger = logging.getLogger(__name__)
33
37
 
60
64
 
61
65
 
62
66
class Instance(resource.Resource):
63
 
    # AWS does not require KeyName and InstanceType but we seem to
 
67
    # AWS does not require InstanceType but Heat does because the nova
 
68
    # create api call requires a flavor
64
69
    tags_schema = {'Key': {'Type': 'String',
65
70
                           'Required': True},
66
71
                   'Value': {'Type': 'String',
85
90
                         'RamDiskId': {'Type': 'String',
86
91
                                       'Implemented': False},
87
92
                         'SecurityGroups': {'Type': 'List'},
88
 
                         'SecurityGroupIds': {'Type': 'List',
89
 
                                              'Implemented': False},
 
93
                         'SecurityGroupIds': {'Type': 'List'},
90
94
                         'NetworkInterfaces': {'Type': 'List'},
91
95
                         'SourceDestCheck': {'Type': 'Boolean',
92
96
                                             'Implemented': False},
93
 
                         'SubnetId': {'Type': 'String',
94
 
                                      'Implemented': False},
 
97
                         'SubnetId': {'Type': 'String'},
95
98
                         'Tags': {'Type': 'List',
96
99
                                  'Schema': {'Type': 'Map',
97
100
                                             'Schema': tags_schema}},
106
109
                         'UserData': {'Type': 'String'},
107
110
                         'Volumes': {'Type': 'List'}}
108
111
 
 
112
    attributes_schema = {'AvailabilityZone': ('The Availability Zone where the'
 
113
                                              ' specified instance is '
 
114
                                              'launched.'),
 
115
                         'PrivateDnsName': ('Private DNS name of the specified'
 
116
                                            ' instance.'),
 
117
                         'PublicDnsName': ('Public DNS name of the specified '
 
118
                                           'instance.'),
 
119
                         'PrivateIp': ('Private IP address of the specified '
 
120
                                       'instance.'),
 
121
                         'PublicIp': ('Public IP address of the specified '
 
122
                                      'instance.')}
 
123
 
109
124
    # template keys supported for handle_update, note trailing comma
110
125
    # is required for a single item to get a tuple not a string
111
126
    update_allowed_keys = ('Metadata',)
112
127
 
 
128
    _deferred_server_statuses = ['BUILD',
 
129
                                 'HARD_REBOOT',
 
130
                                 'PASSWORD',
 
131
                                 'REBOOT',
 
132
                                 'RESCUE',
 
133
                                 'RESIZE',
 
134
                                 'REVERT_RESIZE',
 
135
                                 'SHUTOFF',
 
136
                                 'SUSPENDED',
 
137
                                 'VERIFY_RESIZE']
 
138
 
113
139
    def __init__(self, name, json_snippet, stack):
114
140
        super(Instance, self).__init__(name, json_snippet, stack)
115
141
        self.ipaddress = None
116
142
        self.mime_string = None
117
 
        self._server_status = None
118
143
 
119
144
    def _set_ipaddress(self, networks):
120
145
        '''
122
147
        '''
123
148
        # Just record the first ipaddress
124
149
        for n in networks:
125
 
            self.ipaddress = networks[n][0]
126
 
            break
 
150
            if len(networks[n]) > 0:
 
151
                self.ipaddress = networks[n][0]
 
152
                break
127
153
 
128
154
    def _ipaddress(self):
129
155
        '''
139
165
 
140
166
        return self.ipaddress or '0.0.0.0'
141
167
 
142
 
    def FnGetAtt(self, key):
 
168
    def _resolve_attribute(self, name):
143
169
        res = None
144
 
        if key == 'AvailabilityZone':
 
170
        if name == 'AvailabilityZone':
145
171
            res = self.properties['AvailabilityZone']
146
 
        elif key == 'PublicIp':
147
 
            res = self._ipaddress()
148
 
        elif key == 'PrivateIp':
149
 
            res = self._ipaddress()
150
 
        elif key == 'PublicDnsName':
151
 
            res = self._ipaddress()
152
 
        elif key == 'PrivateDnsName':
153
 
            res = self._ipaddress()
154
 
        else:
155
 
            raise exception.InvalidTemplateAttribute(resource=self.name,
156
 
                                                     key=key)
 
172
        elif name in ['PublicIp', 'PrivateIp', 'PublicDnsName',
 
173
                      'PrivateDnsName']:
 
174
            res = self._ipaddress()
157
175
 
158
 
        logger.info('%s.GetAtt(%s) == %s' % (self.name, key, res))
159
 
        return unicode(res)
 
176
        logger.info('%s._resolve_attribute(%s) == %s' % (self.name, name, res))
 
177
        return unicode(res) if res else None
160
178
 
161
179
    def _build_userdata(self, userdata):
162
180
        if not self.mime_string:
179
197
            attachments = [(read_cloudinit_file('config'), 'cloud-config'),
180
198
                           (read_cloudinit_file('boothook.sh'), 'boothook.sh',
181
199
                            'cloud-boothook'),
182
 
                           (read_cloudinit_file('part-handler.py'),
 
200
                           (read_cloudinit_file('part_handler.py'),
183
201
                            'part-handler.py'),
184
202
                           (userdata, 'cfn-userdata', 'x-cfninitdata'),
185
203
                           (read_cloudinit_file('loguserdata.py'),
221
239
 
222
240
        return self.mime_string
223
241
 
224
 
    @staticmethod
225
 
    def _build_nics(network_interfaces):
226
 
        if not network_interfaces:
227
 
            return None
228
 
 
229
 
        nics = []
230
 
        for nic in network_interfaces:
231
 
            if isinstance(nic, basestring):
232
 
                nics.append({
233
 
                    'NetworkInterfaceId': nic,
234
 
                    'DeviceIndex': len(nics)})
235
 
            else:
236
 
                nics.append(nic)
237
 
        sorted_nics = sorted(nics, key=lambda nic: int(nic['DeviceIndex']))
238
 
 
239
 
        return [{'port-id': nic['NetworkInterfaceId']} for nic in sorted_nics]
 
242
    def _build_nics(self, network_interfaces, subnet_id=None):
 
243
 
 
244
        nics = None
 
245
 
 
246
        if network_interfaces:
 
247
            unsorted_nics = []
 
248
            for entry in network_interfaces:
 
249
                nic = (entry
 
250
                       if not isinstance(entry, basestring)
 
251
                       else {'NetworkInterfaceId': entry,
 
252
                             'DeviceIndex': len(unsorted_nics)})
 
253
                unsorted_nics.append(nic)
 
254
            sorted_nics = sorted(unsorted_nics,
 
255
                                 key=lambda nic: int(nic['DeviceIndex']))
 
256
            nics = [{'port-id': nic['NetworkInterfaceId']}
 
257
                    for nic in sorted_nics]
 
258
        else:
 
259
            # if SubnetId property in Instance, ensure subnet exists
 
260
            if subnet_id:
 
261
                quantumclient = self.quantum()
 
262
                network_id = NetworkInterface.network_id_from_subnet_id(
 
263
                    quantumclient, subnet_id)
 
264
                # if subnet verified, create a port to use this subnet
 
265
                # if port is not created explicitly, nova will choose
 
266
                # the first subnet in the given network.
 
267
                if network_id:
 
268
                    fixed_ip = {'subnet_id': subnet_id}
 
269
                    props = {
 
270
                        'admin_state_up': True,
 
271
                        'network_id': network_id,
 
272
                        'fixed_ips': [fixed_ip]
 
273
                    }
 
274
                    port = quantumclient.create_port({'port': props})['port']
 
275
                    nics = [{'port-id': port['id']}]
 
276
 
 
277
        return nics
 
278
 
 
279
    def _get_security_groups(self):
 
280
        security_groups = []
 
281
        for property in ('SecurityGroups', 'SecurityGroupIds'):
 
282
            if self.properties.get(property) is not None:
 
283
                for sg in self.properties.get(property):
 
284
                    security_groups.append(sg)
 
285
        if not security_groups:
 
286
            security_groups = None
 
287
        return security_groups
240
288
 
241
289
    def handle_create(self):
242
 
        if self.properties.get('SecurityGroups') is None:
243
 
            security_groups = None
244
 
        else:
245
 
            security_groups = [self.physical_resource_name_find(sg)
246
 
                               for sg in self.properties.get('SecurityGroups')]
 
290
        security_groups = self._get_security_groups()
247
291
 
248
292
        userdata = self.properties['UserData'] or ''
249
293
        flavor = self.properties['InstanceType']
255
299
            raise exception.UserKeyPairMissing(key_name=key_name)
256
300
 
257
301
        image_name = self.properties['ImageId']
258
 
        image_id = None
259
 
        image_list = self.nova().images.list()
260
 
        for o in image_list:
261
 
            if o.name == image_name:
262
 
                image_id = o.id
263
 
                break
264
302
 
265
 
        if image_id is None:
266
 
            logger.info("Image %s was not found in glance" % image_name)
267
 
            raise exception.ImageNotFound(image_name=image_name)
 
303
        image_id = self._get_image_id(image_name)
268
304
 
269
305
        flavor_id = None
270
306
        flavor_list = self.nova().flavors.list()
289
325
        else:
290
326
            scheduler_hints = None
291
327
 
292
 
        nics = self._build_nics(self.properties['NetworkInterfaces'])
 
328
        nics = self._build_nics(self.properties['NetworkInterfaces'],
 
329
                                subnet_id=self.properties['SubnetId'])
293
330
 
294
331
        server_userdata = self._build_userdata(userdata)
295
332
        server = None
311
348
            if server is not None:
312
349
                self.resource_id_set(server.id)
313
350
 
314
 
        self._server_status = server.status
315
 
 
316
 
    def check_active(self):
317
 
        if self._server_status == 'ACTIVE':
318
 
            return True
319
 
 
320
 
        server = self.nova().servers.get(self.resource_id)
321
 
        self._server_status = server.status
322
 
        if server.status == 'BUILD':
323
 
            return False
324
 
        if server.status == 'ACTIVE':
325
 
            self._set_ipaddress(server.networks)
326
 
            self.attach_volumes()
327
 
            return True
 
351
        return server, scheduler.TaskRunner(self._attach_volumes_task())
 
352
 
 
353
    def _attach_volumes_task(self):
 
354
        attach_tasks = (volume.VolumeAttachTask(self.stack,
 
355
                                                self.resource_id,
 
356
                                                volume_id,
 
357
                                                device)
 
358
                        for volume_id, device in self.volumes())
 
359
        return scheduler.PollingTaskGroup(attach_tasks)
 
360
 
 
361
    def check_create_complete(self, cookie):
 
362
        return self._check_active(cookie)
 
363
 
 
364
    def _check_active(self, cookie):
 
365
        server, volume_attach = cookie
 
366
 
 
367
        if not volume_attach.started():
 
368
            if server.status != 'ACTIVE':
 
369
                server.get()
 
370
 
 
371
            # Some clouds append extra (STATUS) strings to the status
 
372
            short_server_status = server.status.split('(')[0]
 
373
            if short_server_status in self._deferred_server_statuses:
 
374
                return False
 
375
            elif server.status == 'ACTIVE':
 
376
                self._set_ipaddress(server.networks)
 
377
                volume_attach.start()
 
378
                return volume_attach.done()
 
379
            else:
 
380
                raise exception.Error('%s instance[%s] status[%s]' %
 
381
                                      ('nova reported unexpected',
 
382
                                       self.name, server.status))
328
383
        else:
329
 
            raise exception.Error('%s instance[%s] status[%s]' %
330
 
                                  ('nova reported unexpected',
331
 
                                   self.name, server.status))
332
 
 
333
 
    def attach_volumes(self):
334
 
        if not self.properties['Volumes']:
335
 
            return
336
 
        server_id = self.resource_id
337
 
        for vol in self.properties['Volumes']:
338
 
            if 'DeviceId' in vol:
339
 
                dev = vol['DeviceId']
340
 
            else:
341
 
                dev = vol['Device']
342
 
            self.stack.clients.attach_volume_to_instance(server_id,
343
 
                                                         vol['VolumeId'],
344
 
                                                         dev)
345
 
 
346
 
    def detach_volumes(self):
347
 
        server_id = self.resource_id
348
 
        for vol in self.properties['Volumes']:
349
 
            self.stack.clients.detach_volume_from_instance(server_id,
350
 
                                                           vol['VolumeId'])
351
 
 
352
 
    def handle_update(self, json_snippet):
353
 
        status = self.UPDATE_REPLACE
354
 
        try:
355
 
            tmpl_diff = self.update_template_diff(json_snippet)
356
 
        except NotImplementedError:
357
 
            return self.UPDATE_REPLACE
358
 
 
359
 
        for k in tmpl_diff:
360
 
            if k == 'Metadata':
361
 
                self.metadata = json_snippet.get('Metadata', {})
362
 
                status = self.UPDATE_COMPLETE
363
 
            else:
364
 
                return self.UPDATE_REPLACE
365
 
 
366
 
        return status
 
384
            return volume_attach.step()
 
385
 
 
386
    def volumes(self):
 
387
        """
 
388
        Return an iterator over (volume_id, device) tuples for all volumes
 
389
        that should be attached to this instance.
 
390
        """
 
391
        volumes = self.properties['Volumes']
 
392
        if volumes is None:
 
393
            return []
 
394
 
 
395
        return ((vol['VolumeId'], vol['Device']) for vol in volumes)
 
396
 
 
397
    def handle_update(self, json_snippet, tmpl_diff, prop_diff):
 
398
        if 'Metadata' in tmpl_diff:
 
399
            self.metadata = tmpl_diff.get('Metadata', {})
367
400
 
368
401
    def metadata_update(self, new_metadata=None):
369
402
        '''
381
414
            return res
382
415
 
383
416
        # check validity of key
384
 
        try:
385
 
            key_name = self.properties['KeyName']
386
 
            if key_name is None:
387
 
                return
388
 
        except ValueError:
389
 
            return
390
 
        else:
 
417
        key_name = self.properties.get('KeyName', None)
 
418
        if key_name:
391
419
            keypairs = self.nova().keypairs.list()
392
 
            for k in keypairs:
393
 
                if k.name == key_name:
394
 
                    return
395
 
        return {'Error':
396
 
                'Provided KeyName is not registered with nova'}
 
420
            if not any(k.name == key_name for k in keypairs):
 
421
                return {'Error':
 
422
                        'Provided KeyName is not registered with nova'}
 
423
 
 
424
        # check validity of security groups vs. network interfaces
 
425
        security_groups = self._get_security_groups()
 
426
        if security_groups and self.properties.get('NetworkInterfaces'):
 
427
            return {'Error':
 
428
                    'Cannot define both SecurityGroups/SecurityGroupIds and '
 
429
                    'NetworkInterfaces properties.'}
 
430
 
 
431
        # make sure the image exists.
 
432
        image_identifier = self.properties['ImageId']
 
433
        try:
 
434
            self._get_image_id(image_identifier)
 
435
        except exception.ImageNotFound:
 
436
            return {'Error': 'Image %s was not found in glance' %
 
437
                    image_identifier}
 
438
        except exception.NoUniqueImageFound:
 
439
            return {'Error': 'Multiple images were found with name %s' %
 
440
                    image_identifier}
 
441
 
 
442
        return
 
443
 
 
444
    def _delete_server(self, server):
 
445
        '''
 
446
        Return a co-routine that deletes the server and waits for it to
 
447
        disappear from Nova.
 
448
        '''
 
449
        server.delete()
 
450
 
 
451
        while True:
 
452
            yield
 
453
 
 
454
            try:
 
455
                server.get()
 
456
            except clients.novaclient.exceptions.NotFound:
 
457
                break
 
458
 
 
459
    def _detach_volumes_task(self):
 
460
        '''
 
461
        Detach volumes from the instance
 
462
        '''
 
463
        detach_tasks = (volume.VolumeDetachTask(self.stack,
 
464
                                                self.resource_id,
 
465
                                                volume_id)
 
466
                        for volume_id, device in self.volumes())
 
467
        return scheduler.PollingTaskGroup(detach_tasks)
397
468
 
398
469
    def handle_delete(self):
399
470
        '''
402
473
        if self.resource_id is None:
403
474
            return
404
475
 
405
 
        if self.properties['Volumes']:
406
 
            self.detach_volumes()
 
476
        scheduler.TaskRunner(self._detach_volumes_task())()
407
477
 
408
478
        try:
409
479
            server = self.nova().servers.get(self.resource_id)
410
480
        except clients.novaclient.exceptions.NotFound:
411
481
            pass
412
482
        else:
413
 
            server.delete()
414
 
            while True:
415
 
                try:
416
 
                    server.get()
417
 
                except clients.novaclient.exceptions.NotFound:
418
 
                    break
419
 
                eventlet.sleep(0.2)
 
483
            delete = scheduler.TaskRunner(self._delete_server, server)
 
484
            delete(wait_time=0.2)
 
485
 
420
486
        self.resource_id = None
421
487
 
 
488
    def _get_image_id(self, image_identifier):
 
489
        image_id = None
 
490
        if uuidutils.is_uuid_like(image_identifier):
 
491
            try:
 
492
                image_id = self.nova().images.get(image_identifier).id
 
493
            except clients.novaclient.exceptions.NotFound:
 
494
                logger.info("Image %s was not found in glance"
 
495
                            % image_identifier)
 
496
                raise exception.ImageNotFound(image_name=image_identifier)
 
497
        else:
 
498
            try:
 
499
                image_list = self.nova().images.list()
 
500
            except clients.novaclient.exceptions.ClientException as ex:
 
501
                raise exception.ServerError(message=str(ex))
 
502
            image_names = dict(
 
503
                (o.id, o.name)
 
504
                for o in image_list if o.name == image_identifier)
 
505
            if len(image_names) == 0:
 
506
                logger.info("Image %s was not found in glance" %
 
507
                            image_identifier)
 
508
                raise exception.ImageNotFound(image_name=image_identifier)
 
509
            elif len(image_names) > 1:
 
510
                logger.info("Mulitple images %s were found in glance with name"
 
511
                            % image_identifier)
 
512
                raise exception.NoUniqueImageFound(image_name=image_identifier)
 
513
            image_id = image_names.popitem()[0]
 
514
        return image_id
 
515
 
 
516
    def handle_suspend(self):
 
517
        '''
 
518
        Suspend an instance - note we do not wait for the SUSPENDED state,
 
519
        this is polled for by check_suspend_complete in a similar way to the
 
520
        create logic so we can take advantage of coroutines
 
521
        '''
 
522
        if self.resource_id is None:
 
523
            raise exception.Error(_('Cannot suspend %s, resource_id not set') %
 
524
                                  self.name)
 
525
 
 
526
        try:
 
527
            server = self.nova().servers.get(self.resource_id)
 
528
        except clients.novaclient.exceptions.NotFound:
 
529
            raise exception.NotFound(_('Failed to find instance %s') %
 
530
                                     self.resource_id)
 
531
        else:
 
532
            logger.debug("suspending instance %s" % self.resource_id)
 
533
            # We want the server.suspend to happen after the volume
 
534
            # detachement has finished, so pass both tasks and the server
 
535
            suspend_runner = scheduler.TaskRunner(server.suspend)
 
536
            volumes_runner = scheduler.TaskRunner(self._detach_volumes_task())
 
537
            return server, suspend_runner, volumes_runner
 
538
 
 
539
    def check_suspend_complete(self, cookie):
 
540
        server, suspend_runner, volumes_runner = cookie
 
541
 
 
542
        if not volumes_runner.started():
 
543
            volumes_runner.start()
 
544
 
 
545
        if volumes_runner.done():
 
546
            if not suspend_runner.started():
 
547
                suspend_runner.start()
 
548
 
 
549
            if suspend_runner.done():
 
550
                if server.status == 'SUSPENDED':
 
551
                    return True
 
552
 
 
553
                server.get()
 
554
                logger.debug("%s check_suspend_complete status = %s" %
 
555
                             (self.name, server.status))
 
556
                if server.status in list(self._deferred_server_statuses +
 
557
                                         ['ACTIVE']):
 
558
                    return server.status == 'SUSPENDED'
 
559
                else:
 
560
                    raise exception.Error(_(' nova reported unexpected '
 
561
                                            'instance[%(instance)s] '
 
562
                                            'status[%(status)s]') %
 
563
                                          {'instance': self.name,
 
564
                                           'status': server.status})
 
565
            else:
 
566
                suspend_runner.step()
 
567
        else:
 
568
            return volumes_runner.step()
 
569
 
 
570
    def handle_resume(self):
 
571
        '''
 
572
        Resume an instance - note we do not wait for the ACTIVE state,
 
573
        this is polled for by check_resume_complete in a similar way to the
 
574
        create logic so we can take advantage of coroutines
 
575
        '''
 
576
        if self.resource_id is None:
 
577
            raise exception.Error(_('Cannot resume %s, resource_id not set') %
 
578
                                  self.name)
 
579
 
 
580
        try:
 
581
            server = self.nova().servers.get(self.resource_id)
 
582
        except clients.novaclient.exceptions.NotFound:
 
583
            raise exception.NotFound(_('Failed to find instance %s') %
 
584
                                     self.resource_id)
 
585
        else:
 
586
            logger.debug("resuming instance %s" % self.resource_id)
 
587
            server.resume()
 
588
            return server, scheduler.TaskRunner(self._attach_volumes_task())
 
589
 
 
590
    def check_resume_complete(self, cookie):
 
591
        return self._check_active(cookie)
 
592
 
422
593
 
423
594
def resource_mapping():
424
595
    return {