1
# vim: tabstop=4 shiftwidth=4 softtabstop=4
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
8
# http://www.apache.org/licenses/LICENSE-2.0
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
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
24
logger = logging.getLogger(__name__)
27
class Server(resource.Resource):
29
block_mapping_schema = {
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')},
38
'Description': _('The ID of the volume to boot from. Only one of '
39
'volume_id or snapshot_id should be provided')},
42
'Description': _('The ID of the snapshot to create a volume '
46
'Description': _('The size of the volume, in GB. It is safe to '
47
'leave this blank and have the Compute service '
49
'delete_on_termination': {
51
'Description': _('Indicate whether the volume should be deleted '
52
'when the server is terminated')}
58
'Description': _('ID of network to create a port on')},
61
'Description': _('Fixed IP address to specify for the port '
62
'created on the requested network')},
65
'Description': _('ID of an existing port to associate with '
72
'Description': _('Optional server name')},
75
'Description': _('The ID or name of the image to boot with')},
76
'block_device_mapping': {
78
'Description': _('Block device mappings for this server'),
81
'Schema': block_mapping_schema
86
'Description': _('The ID or name of the flavor to boot onto'),
88
'flavor_update_policy': {
90
'Description': _('Policy on how to apply a flavor update; either '
91
'by requesting a server resize or by replacing '
94
'AllowedValues': ['RESIZE', 'REPLACE']},
97
'Description': _('Name of keypair to inject into the server')},
98
'availability_zone': {
100
'Description': _('Name of the availability zone for server '
104
'Description': _('List of security group names')},
107
'Description': _('An ordered list of nics to be '
108
'added to this server, with information about '
109
'connected networks, fixed ips, port etc'),
112
'Schema': networks_schema
117
'Description': _('Arbitrary key-value pairs specified by the '
118
'client to help boot a server')},
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 '
127
'Description': _('User data script to be executed by cloud-init')},
130
'Description': _('A UUID for the set of servers being requested'),
131
'Implemented': False},
134
'Description': _('value for config drive either boolean, or '
136
'Implemented': False},
137
# diskConfig translates to API attribute OS-DCF:diskConfig
138
# hence the camel case instead of underscore to separate the words
141
'Description': _('Control how the disk is partitioned when the '
142
'server is created'),
143
'AllowedValues': ['AUTO', 'MANUAL']}
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 '
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 '
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 '
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'),
167
update_allowed_keys = ('Metadata', 'Properties')
168
update_allowed_properties = ('flavor', 'flavor_update_policy')
170
def __init__(self, name, json_snippet, stack):
171
super(Server, self).__init__(name, json_snippet, stack)
172
self.mime_string = None
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
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']
185
key_name = self.properties['key_name']
187
# confirm keypair exists
188
nova_utils.get_keypair(self.nova(), key_name)
190
image = self.properties.get('image')
192
image = nova_utils.get_image_id(self.nova(), image)
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')
206
server = self.nova().servers.create(
207
name=self.physical_resource_name(),
211
security_groups=security_groups,
212
userdata=self.get_mime_string(userdata),
214
scheduler_hints=scheduler_hints,
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)
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)
229
def check_create_complete(self, server):
230
return self._check_active(server)
232
def _check_active(self, server):
234
if server.status != 'ACTIVE':
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:
241
elif server.status == 'ACTIVE':
243
elif server.status == 'ERROR':
244
exc = exception.Error(_('Creation of server %s failed.') %
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))
255
def _build_block_device_mapping(bdm):
261
if mapping.get('snapshot_id'):
262
mapping_parts.append(mapping.get('snapshot_id'))
263
mapping_parts.append('snap')
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')):
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
278
def _build_nics(networks):
284
for net_data in networks:
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)
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', [])
306
if name == 'first_public_address':
307
public = server.networks.get('public', [])
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
320
def handle_update(self, json_snippet, tmpl_diff, prop_diff):
321
if 'Metadata' in tmpl_diff:
322
self.metadata = tmpl_diff['Metadata']
324
if 'flavor' in prop_diff:
326
flavor_update_policy = (
327
prop_diff.get('flavor_update_policy') or
328
self.properties.get('flavor_update_policy'))
330
if flavor_update_policy == 'REPLACE':
331
raise resource.UpdateReplace(self.name)
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,
342
def check_update_complete(self, checker):
343
return checker.step() if checker is not None else True
345
def metadata_update(self, new_metadata=None):
347
Refresh the metadata if new_metadata is None
349
if new_metadata is None:
350
self.metadata = self.parsed_template('Metadata')
354
Validate any of the provided params
356
super(Server, self).validate()
358
# check validity of key
359
key_name = self.properties.get('key_name', None)
361
nova_utils.get_keypair(self.nova(), key_name)
363
# make sure the image exists if specified.
364
image = self.properties.get('image', None)
366
nova_utils.get_image_id(self.nova(), image)
368
# TODO(sbaker) confirm block_device_mapping is populated
369
# for boot-by-volume (see LP bug #1215267)
372
def handle_delete(self):
374
Delete a server, blocking until it is disposed by OpenStack
376
if self.resource_id is None:
380
server = self.nova().servers.get(self.resource_id)
381
except clients.novaclient.exceptions.NotFound:
384
delete = scheduler.TaskRunner(nova_utils.delete_server, server)
385
delete(wait_time=0.2)
387
self.resource_id = None
389
def handle_suspend(self):
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
395
if self.resource_id is None:
396
raise exception.Error(_('Cannot suspend %s, resource_id not set') %
400
server = self.nova().servers.get(self.resource_id)
401
except clients.novaclient.exceptions.NotFound:
402
raise exception.NotFound(_('Failed to find server %s') %
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
411
def check_suspend_complete(self, cookie):
412
server, suspend_runner = cookie
414
if not suspend_runner.started():
415
suspend_runner.start()
417
if suspend_runner.done():
418
if server.status == 'SUSPENDED':
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 +
426
return server.status == 'SUSPENDED'
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))
434
def handle_resume(self):
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
440
if self.resource_id is None:
441
raise exception.Error(_('Cannot resume %s, resource_id not set') %
445
server = self.nova().servers.get(self.resource_id)
446
except clients.novaclient.exceptions.NotFound:
447
raise exception.NotFound(_('Failed to find server %s') %
450
logger.debug('resuming server %s' % self.resource_id)
454
def check_resume_complete(self, server):
455
return self._check_active(server)
458
def resource_mapping():
460
'OS::Nova::Server': Server,