1
# Copyright 2010 OpenStack LLC.
2
# Copyright 2011 Piston Cloud Computing, Inc
5
# Licensed under the Apache License, Version 2.0 (the "License"); you may
6
# not use this file except in compliance with the License. You may obtain
7
# a copy of the License at
9
# http://www.apache.org/licenses/LICENSE-2.0
11
# Unless required by applicable law or agreed to in writing, software
12
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
13
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
14
# License for the specific language governing permissions and limitations
24
from xml.dom import minidom
26
from nova.api.openstack import common
27
from nova.api.openstack.compute import ips
28
from nova.api.openstack.compute.views import servers as views_servers
29
from nova.api.openstack import wsgi
30
from nova.api.openstack import xmlutil
31
from nova import compute
32
from nova.compute import instance_types
33
from nova import exception
34
from nova import flags
35
from nova.openstack.common import importutils
36
from nova.openstack.common import log as logging
37
from nova.openstack.common.rpc import common as rpc_common
38
from nova.openstack.common import timeutils
39
from nova import utils
42
LOG = logging.getLogger(__name__)
47
fault = xmlutil.SubTemplateElement(elem, 'fault', selector='fault')
50
msg = xmlutil.SubTemplateElement(fault, 'message')
52
det = xmlutil.SubTemplateElement(fault, 'details')
56
def make_server(elem, detailed=False):
61
elem.set('userId', 'user_id')
62
elem.set('tenantId', 'tenant_id')
66
elem.set('accessIPv4')
67
elem.set('accessIPv6')
70
elem.set('reservation_id')
73
image = xmlutil.SubTemplateElement(elem, 'image', selector='image')
75
xmlutil.make_links(image, 'links')
78
flavor = xmlutil.SubTemplateElement(elem, 'flavor', selector='flavor')
80
xmlutil.make_links(flavor, 'links')
85
# Attach metadata node
86
elem.append(common.MetadataTemplate())
88
# Attach addresses node
89
elem.append(ips.AddressesTemplate())
91
xmlutil.make_links(elem, 'links')
94
server_nsmap = {None: xmlutil.XMLNS_V11, 'atom': xmlutil.XMLNS_ATOM}
97
class ServerTemplate(xmlutil.TemplateBuilder):
99
root = xmlutil.TemplateElement('server', selector='server')
100
make_server(root, detailed=True)
101
return xmlutil.MasterTemplate(root, 1, nsmap=server_nsmap)
104
class MinimalServersTemplate(xmlutil.TemplateBuilder):
106
root = xmlutil.TemplateElement('servers')
107
elem = xmlutil.SubTemplateElement(root, 'server', selector='servers')
109
xmlutil.make_links(root, 'servers_links')
110
return xmlutil.MasterTemplate(root, 1, nsmap=server_nsmap)
113
class ServersTemplate(xmlutil.TemplateBuilder):
115
root = xmlutil.TemplateElement('servers')
116
elem = xmlutil.SubTemplateElement(root, 'server', selector='servers')
117
make_server(elem, detailed=True)
118
return xmlutil.MasterTemplate(root, 1, nsmap=server_nsmap)
121
class ServerAdminPassTemplate(xmlutil.TemplateBuilder):
123
root = xmlutil.TemplateElement('server')
124
root.set('adminPass')
125
return xmlutil.SlaveTemplate(root, 1, nsmap=server_nsmap)
128
def FullServerTemplate():
129
master = ServerTemplate()
130
master.attach(ServerAdminPassTemplate())
134
class CommonDeserializer(wsgi.MetadataXMLDeserializer):
135
"""Common deserializer to handle xml-formatted server create requests.
137
Handles standard server attributes as well as optional metadata
138
and personality attributes
141
metadata_deserializer = common.MetadataXMLDeserializer()
143
def _extract_personality(self, server_node):
144
"""Marshal the personality attribute of a parsed request."""
145
node = self.find_first_child_named(server_node, "personality")
148
for file_node in self.find_children_named(node, "file"):
150
if file_node.hasAttribute("path"):
151
item["path"] = file_node.getAttribute("path")
152
item["contents"] = self.extract_text(file_node)
153
personality.append(item)
158
def _extract_server(self, node):
159
"""Marshal the server attribute of a parsed request."""
161
server_node = self.find_first_child_named(node, 'server')
163
attributes = ["name", "imageRef", "flavorRef", "adminPass",
164
"accessIPv4", "accessIPv6", "key_name",
165
"availability_zone", "min_count", "max_count"]
166
for attr in attributes:
167
if server_node.getAttribute(attr):
168
server[attr] = server_node.getAttribute(attr)
170
res_id = server_node.getAttribute('return_reservation_id')
172
server['return_reservation_id'] = utils.bool_from_str(res_id)
174
scheduler_hints = self._extract_scheduler_hints(server_node)
176
server['OS-SCH-HNT:scheduler_hints'] = scheduler_hints
178
metadata_node = self.find_first_child_named(server_node, "metadata")
179
if metadata_node is not None:
180
server["metadata"] = self.extract_metadata(metadata_node)
182
user_data_node = self.find_first_child_named(server_node, "user_data")
183
if user_data_node is not None:
184
server["user_data"] = self.extract_text(user_data_node)
186
personality = self._extract_personality(server_node)
187
if personality is not None:
188
server["personality"] = personality
190
networks = self._extract_networks(server_node)
191
if networks is not None:
192
server["networks"] = networks
194
security_groups = self._extract_security_groups(server_node)
195
if security_groups is not None:
196
server["security_groups"] = security_groups
198
# NOTE(vish): this is not namespaced in json, so leave it without a
200
block_device_mapping = self._extract_block_device_mapping(server_node)
201
if block_device_mapping is not None:
202
server["block_device_mapping"] = block_device_mapping
204
# NOTE(vish): Support this incorrect version because it was in the code
205
# base for a while and we don't want to accidentally break
206
# anyone that might be using it.
207
auto_disk_config = server_node.getAttribute('auto_disk_config')
209
server['OS-DCF:diskConfig'] = utils.bool_from_str(auto_disk_config)
211
auto_disk_config = server_node.getAttribute('OS-DCF:diskConfig')
213
server['OS-DCF:diskConfig'] = utils.bool_from_str(auto_disk_config)
217
def _extract_block_device_mapping(self, server_node):
218
"""Marshal the block_device_mapping node of a parsed request"""
219
node = self.find_first_child_named(server_node, "block_device_mapping")
221
block_device_mapping = []
222
for child in self.extract_elements(node):
223
if child.nodeName != "mapping":
226
attributes = ["volume_id", "snapshot_id", "device_name",
227
"virtual_name", "volume_size"]
228
for attr in attributes:
229
value = child.getAttribute(attr)
231
mapping[attr] = value
232
attributes = ["delete_on_termination", "no_device"]
233
for attr in attributes:
234
value = child.getAttribute(attr)
236
mapping[attr] = utils.bool_from_str(value)
237
block_device_mapping.append(mapping)
238
return block_device_mapping
242
def _extract_scheduler_hints(self, server_node):
243
"""Marshal the scheduler hints attribute of a parsed request"""
244
node = self.find_first_child_named_in_namespace(server_node,
245
"http://docs.openstack.org/compute/ext/scheduler-hints/api/v2",
249
for child in self.extract_elements(node):
250
scheduler_hints.setdefault(child.nodeName, [])
251
value = self.extract_text(child).strip()
252
scheduler_hints[child.nodeName].append(value)
253
return scheduler_hints
257
def _extract_networks(self, server_node):
258
"""Marshal the networks attribute of a parsed request."""
259
node = self.find_first_child_named(server_node, "networks")
262
for network_node in self.find_children_named(node,
265
if network_node.hasAttribute("uuid"):
266
item["uuid"] = network_node.getAttribute("uuid")
267
if network_node.hasAttribute("fixed_ip"):
268
item["fixed_ip"] = network_node.getAttribute("fixed_ip")
269
if network_node.hasAttribute("port"):
270
item["port"] = network_node.getAttribute("port")
271
networks.append(item)
276
def _extract_security_groups(self, server_node):
277
"""Marshal the security_groups attribute of a parsed request."""
278
node = self.find_first_child_named(server_node, "security_groups")
281
for sg_node in self.find_children_named(node, "security_group"):
283
name = self.find_attribute_or_element(sg_node, 'name')
286
security_groups.append(item)
287
return security_groups
292
class ActionDeserializer(CommonDeserializer):
293
"""Deserializer to handle xml-formatted server action requests.
295
Handles standard server attributes as well as optional metadata
296
and personality attributes
299
def default(self, string):
300
dom = minidom.parseString(string)
301
action_node = dom.childNodes[0]
302
action_name = action_node.tagName
304
action_deserializer = {
305
'createImage': self._action_create_image,
306
'changePassword': self._action_change_password,
307
'reboot': self._action_reboot,
308
'rebuild': self._action_rebuild,
309
'resize': self._action_resize,
310
'confirmResize': self._action_confirm_resize,
311
'revertResize': self._action_revert_resize,
312
}.get(action_name, super(ActionDeserializer, self).default)
314
action_data = action_deserializer(action_node)
316
return {'body': {action_name: action_data}}
318
def _action_create_image(self, node):
319
return self._deserialize_image_action(node, ('name',))
321
def _action_change_password(self, node):
322
if not node.hasAttribute("adminPass"):
323
raise AttributeError("No adminPass was specified in request")
324
return {"adminPass": node.getAttribute("adminPass")}
326
def _action_reboot(self, node):
327
if not node.hasAttribute("type"):
328
raise AttributeError("No reboot type was specified in request")
329
return {"type": node.getAttribute("type")}
331
def _action_rebuild(self, node):
333
if node.hasAttribute("name"):
334
name = node.getAttribute("name")
336
raise AttributeError("Name cannot be blank")
337
rebuild['name'] = name
339
if node.hasAttribute("auto_disk_config"):
340
rebuild['auto_disk_config'] = node.getAttribute("auto_disk_config")
342
metadata_node = self.find_first_child_named(node, "metadata")
343
if metadata_node is not None:
344
rebuild["metadata"] = self.extract_metadata(metadata_node)
346
personality = self._extract_personality(node)
347
if personality is not None:
348
rebuild["personality"] = personality
350
if not node.hasAttribute("imageRef"):
351
raise AttributeError("No imageRef was specified in request")
352
rebuild["imageRef"] = node.getAttribute("imageRef")
354
if node.hasAttribute("adminPass"):
355
rebuild["adminPass"] = node.getAttribute("adminPass")
357
if node.hasAttribute("accessIPv4"):
358
rebuild["accessIPv4"] = node.getAttribute("accessIPv4")
360
if node.hasAttribute("accessIPv6"):
361
rebuild["accessIPv6"] = node.getAttribute("accessIPv6")
365
def _action_resize(self, node):
368
if node.hasAttribute("flavorRef"):
369
resize["flavorRef"] = node.getAttribute("flavorRef")
371
raise AttributeError("No flavorRef was specified in request")
373
if node.hasAttribute("auto_disk_config"):
374
resize['auto_disk_config'] = node.getAttribute("auto_disk_config")
378
def _action_confirm_resize(self, node):
381
def _action_revert_resize(self, node):
384
def _deserialize_image_action(self, node, allowed_attributes):
386
for attribute in allowed_attributes:
387
value = node.getAttribute(attribute)
389
data[attribute] = value
390
metadata_node = self.find_first_child_named(node, 'metadata')
391
if metadata_node is not None:
392
metadata = self.metadata_deserializer.extract_metadata(
394
data['metadata'] = metadata
398
class CreateDeserializer(CommonDeserializer):
399
"""Deserializer to handle xml-formatted server create requests.
401
Handles standard server attributes as well as optional metadata
402
and personality attributes
405
def default(self, string):
406
"""Deserialize an xml-formatted server create request."""
407
dom = minidom.parseString(string)
408
server = self._extract_server(dom)
409
return {'body': {'server': server}}
412
class Controller(wsgi.Controller):
413
"""The Server API base controller class for the OpenStack API."""
415
_view_builder_class = views_servers.ViewBuilder
418
def _add_location(robj):
420
if 'server' not in robj.obj:
423
link = filter(lambda l: l['rel'] == 'self',
424
robj.obj['server']['links'])
426
robj['Location'] = link[0]['href'].encode('utf-8')
431
def __init__(self, ext_mgr=None, **kwargs):
432
super(Controller, self).__init__(**kwargs)
433
self.compute_api = compute.API()
434
self.ext_mgr = ext_mgr
435
self.quantum_attempted = False
437
@wsgi.serializers(xml=MinimalServersTemplate)
438
def index(self, req):
439
"""Returns a list of server names and ids for a given user."""
441
servers = self._get_servers(req, is_detail=False)
442
except exception.Invalid as err:
443
raise exc.HTTPBadRequest(explanation=str(err))
444
except exception.NotFound:
445
raise exc.HTTPNotFound()
448
@wsgi.serializers(xml=ServersTemplate)
449
def detail(self, req):
450
"""Returns a list of server details for a given user."""
452
servers = self._get_servers(req, is_detail=True)
453
except exception.Invalid as err:
454
raise exc.HTTPBadRequest(explanation=str(err))
455
except exception.NotFound as err:
456
raise exc.HTTPNotFound()
459
def _add_instance_faults(self, ctxt, instances):
460
faults = self.compute_api.get_instance_faults(ctxt, instances)
461
if faults is not None:
462
for instance in instances:
463
faults_list = faults.get(instance['uuid'], [])
465
instance['fault'] = faults_list[0]
471
def _get_servers(self, req, is_detail):
472
"""Returns a list of servers, based on any search options specified."""
475
search_opts.update(req.GET)
477
context = req.environ['nova.context']
478
remove_invalid_options(context, search_opts,
479
self._get_server_search_options())
481
# Verify search by 'status' contains a valid status.
482
# Convert it to filter by vm_state for compute_api.
483
status = search_opts.pop('status', None)
484
if status is not None:
485
state = common.vm_state_from_status(status)
487
msg = _('Invalid server status: %(status)s') % locals()
488
raise exc.HTTPBadRequest(explanation=msg)
489
search_opts['vm_state'] = state
491
if 'changes-since' in search_opts:
493
parsed = timeutils.parse_isotime(search_opts['changes-since'])
495
msg = _('Invalid changes-since value')
496
raise exc.HTTPBadRequest(explanation=msg)
497
search_opts['changes-since'] = parsed
499
# By default, compute's get_all() will return deleted instances.
500
# If an admin hasn't specified a 'deleted' search option, we need
501
# to filter out deleted instances by setting the filter ourselves.
502
# ... Unless 'changes-since' is specified, because 'changes-since'
503
# should return recently deleted images according to the API spec.
505
if 'deleted' not in search_opts:
506
if 'changes-since' not in search_opts:
507
# No 'changes-since', so we only want non-deleted servers
508
search_opts['deleted'] = False
510
if search_opts.get("vm_state") == "deleted":
512
search_opts['deleted'] = True
514
msg = _("Only administrators may list deleted instances")
515
raise exc.HTTPBadRequest(explanation=msg)
517
# NOTE(dprince) This prevents computes' get_all() from returning
518
# instances from multiple tenants when an admin accounts is used.
519
# By default non-admin accounts are always limited to project/user
520
# both here and in the compute API.
521
if not context.is_admin or (context.is_admin and 'all_tenants'
523
if context.project_id:
524
search_opts['project_id'] = context.project_id
526
search_opts['user_id'] = context.user_id
528
limit, marker = common.get_limit_and_marker(req)
530
instance_list = self.compute_api.get_all(context,
531
search_opts=search_opts,
534
except exception.MarkerNotFound as e:
535
msg = _('marker [%s] not found') % marker
536
raise webob.exc.HTTPBadRequest(explanation=msg)
539
self._add_instance_faults(context, instance_list)
540
response = self._view_builder.detail(req, instance_list)
542
response = self._view_builder.index(req, instance_list)
543
req.cache_db_instances(instance_list)
546
def _get_server(self, context, req, instance_uuid):
547
"""Utility function for looking up an instance by uuid."""
549
instance = self.compute_api.get(context, instance_uuid)
550
except exception.NotFound:
551
raise exc.HTTPNotFound()
552
req.cache_db_instance(instance)
555
def _validate_server_name(self, value):
556
if not isinstance(value, basestring):
557
msg = _("Server name is not a string or unicode")
558
raise exc.HTTPBadRequest(explanation=msg)
560
if not value.strip():
561
msg = _("Server name is an empty string")
562
raise exc.HTTPBadRequest(explanation=msg)
564
if not len(value) < 256:
565
msg = _("Server name must be less than 256 characters.")
566
raise exc.HTTPBadRequest(explanation=msg)
568
def _get_injected_files(self, personality):
569
"""Create a list of injected files from the personality attribute.
571
At this time, injected_files must be formatted as a list of
572
(file_path, file_content) pairs for compatibility with the
573
underlying compute service.
577
for item in personality:
580
contents = item['contents']
581
except KeyError as key:
582
expl = _('Bad personality format: missing %s') % key
583
raise exc.HTTPBadRequest(explanation=expl)
585
expl = _('Bad personality format')
586
raise exc.HTTPBadRequest(explanation=expl)
587
contents = self._decode_base64(contents)
589
expl = _('Personality content for %s cannot be decoded') % path
590
raise exc.HTTPBadRequest(explanation=expl)
591
injected_files.append((path, contents))
592
return injected_files
594
def _is_quantum_v2(self):
595
# NOTE(dprince): quantumclient is not a requirement
596
if self.quantum_attempted:
597
return self.have_quantum
600
self.quantum_attempted = True
601
from nova.network.quantumv2 import api as quantum_api
602
self.have_quantum = issubclass(
603
importutils.import_class(FLAGS.network_api_class),
606
self.have_quantum = False
608
return self.have_quantum
610
def _get_requested_networks(self, requested_networks):
611
"""Create a list of requested networks from the networks attribute."""
613
for network in requested_networks:
615
port_id = network.get('port', None)
618
if not self._is_quantum_v2():
619
# port parameter is only for qunatum v2.0
620
msg = _("Unknown argment : port")
621
raise exc.HTTPBadRequest(explanation=msg)
622
if not utils.is_uuid_like(port_id):
623
msg = _("Bad port format: port uuid is "
624
"not in proper format "
626
raise exc.HTTPBadRequest(explanation=msg)
628
network_uuid = network['uuid']
630
if not port_id and not utils.is_uuid_like(network_uuid):
631
br_uuid = network_uuid.split('-', 1)[-1]
632
if not utils.is_uuid_like(br_uuid):
633
msg = _("Bad networks format: network uuid is "
634
"not in proper format "
635
"(%s)") % network_uuid
636
raise exc.HTTPBadRequest(explanation=msg)
638
#fixed IP address is optional
639
#if the fixed IP address is not provided then
640
#it will use one of the available IP address from the network
641
address = network.get('fixed_ip', None)
642
if address is not None and not utils.is_valid_ipv4(address):
643
msg = _("Invalid fixed IP address (%s)") % address
644
raise exc.HTTPBadRequest(explanation=msg)
646
# For quantumv2, requestd_networks
647
# should be tuple of (network_uuid, fixed_ip, port_id)
648
if self._is_quantum_v2():
649
networks.append((network_uuid, address, port_id))
651
# check if the network id is already present in the list,
652
# we don't want duplicate networks to be passed
654
for id, ip in networks:
655
if id == network_uuid:
656
expl = (_("Duplicate networks"
657
" (%s) are not allowed") %
659
raise exc.HTTPBadRequest(explanation=expl)
660
networks.append((network_uuid, address))
661
except KeyError as key:
662
expl = _('Bad network format: missing %s') % key
663
raise exc.HTTPBadRequest(explanation=expl)
665
expl = _('Bad networks format')
666
raise exc.HTTPBadRequest(explanation=expl)
670
# NOTE(vish): Without this regex, b64decode will happily
671
# ignore illegal bytes in the base64 encoded
673
B64_REGEX = re.compile('^(?:[A-Za-z0-9+\/]{4})*'
674
'(?:[A-Za-z0-9+\/]{2}=='
675
'|[A-Za-z0-9+\/]{3}=)?$')
677
def _decode_base64(self, data):
678
data = re.sub(r'\s', '', data)
679
if not self.B64_REGEX.match(data):
682
return base64.b64decode(data)
686
def _validate_user_data(self, user_data):
687
"""Check if the user_data is encoded properly."""
690
if self._decode_base64(user_data) is None:
691
expl = _('Userdata content cannot be decoded')
692
raise exc.HTTPBadRequest(explanation=expl)
694
def _validate_access_ipv4(self, address):
696
socket.inet_aton(address)
698
expl = _('accessIPv4 is not proper IPv4 format')
699
raise exc.HTTPBadRequest(explanation=expl)
701
def _validate_access_ipv6(self, address):
703
socket.inet_pton(socket.AF_INET6, address)
705
expl = _('accessIPv6 is not proper IPv6 format')
706
raise exc.HTTPBadRequest(explanation=expl)
708
@wsgi.serializers(xml=ServerTemplate)
709
def show(self, req, id):
710
"""Returns server details by server id."""
712
context = req.environ['nova.context']
713
instance = self.compute_api.get(context, id)
714
req.cache_db_instance(instance)
715
self._add_instance_faults(context, [instance])
716
return self._view_builder.show(req, instance)
717
except exception.NotFound:
718
raise exc.HTTPNotFound()
721
@wsgi.serializers(xml=FullServerTemplate)
722
@wsgi.deserializers(xml=CreateDeserializer)
723
def create(self, req, body):
724
"""Creates a new server for a given user."""
725
if not self.is_valid_body(body, 'server'):
726
raise exc.HTTPUnprocessableEntity()
728
context = req.environ['nova.context']
729
server_dict = body['server']
730
password = self._get_server_admin_password(server_dict)
732
if not 'name' in server_dict:
733
msg = _("Server name is not defined")
734
raise exc.HTTPBadRequest(explanation=msg)
736
name = server_dict['name']
737
self._validate_server_name(name)
740
image_href = self._image_ref_from_req_data(body)
741
image_href = self._image_uuid_from_href(image_href)
743
personality = server_dict.get('personality')
745
if self.ext_mgr.is_loaded('os-config-drive'):
746
config_drive = server_dict.get('config_drive')
750
injected_files = self._get_injected_files(personality)
753
if self.ext_mgr.is_loaded('os-security-groups'):
754
security_groups = server_dict.get('security_groups')
755
if security_groups is not None:
756
sg_names = [sg['name'] for sg in security_groups
759
sg_names.append('default')
761
sg_names = list(set(sg_names))
763
requested_networks = None
764
if self.ext_mgr.is_loaded('os-networks'):
765
requested_networks = server_dict.get('networks')
767
if requested_networks is not None:
768
requested_networks = self._get_requested_networks(
771
(access_ip_v4, ) = server_dict.get('accessIPv4'),
772
if access_ip_v4 is not None:
773
self._validate_access_ipv4(access_ip_v4)
775
(access_ip_v6, ) = server_dict.get('accessIPv6'),
776
if access_ip_v6 is not None:
777
self._validate_access_ipv6(access_ip_v6)
780
flavor_id = self._flavor_id_from_req_data(body)
781
except ValueError as error:
782
msg = _("Invalid flavorRef provided.")
783
raise exc.HTTPBadRequest(explanation=msg)
785
# optional openstack extensions:
787
if self.ext_mgr.is_loaded('os-keypairs'):
788
key_name = server_dict.get('key_name')
791
if self.ext_mgr.is_loaded('os-user-data'):
792
user_data = server_dict.get('user_data')
793
self._validate_user_data(user_data)
795
availability_zone = None
796
if self.ext_mgr.is_loaded('os-availability-zone'):
797
availability_zone = server_dict.get('availability_zone')
799
block_device_mapping = None
800
if self.ext_mgr.is_loaded('os-volumes'):
801
block_device_mapping = server_dict.get('block_device_mapping', [])
802
for bdm in block_device_mapping:
803
if 'delete_on_termination' in bdm:
804
bdm['delete_on_termination'] = utils.bool_from_str(
805
bdm['delete_on_termination'])
808
# min_count and max_count are optional. If they exist, they may come
809
# in as strings. Verify that they are valid integers and > 0.
810
# Also, we want to default 'min_count' to 1, and default
811
# 'max_count' to be 'min_count'.
814
if self.ext_mgr.is_loaded('os-multiple-create'):
815
ret_resv_id = server_dict.get('return_reservation_id', False)
816
min_count = server_dict.get('min_count', 1)
817
max_count = server_dict.get('max_count', min_count)
820
min_count = int(min_count)
822
raise webob.exc.HTTPBadRequest(_('min_count must be an '
825
raise webob.exc.HTTPBadRequest(_('min_count must be > 0'))
828
max_count = int(max_count)
830
raise webob.exc.HTTPBadRequest(_('max_count must be an '
833
raise webob.exc.HTTPBadRequest(_('max_count must be > 0'))
835
if min_count > max_count:
836
raise webob.exc.HTTPBadRequest(_('min_count must be <= max_count'))
838
auto_disk_config = False
839
if self.ext_mgr.is_loaded('OS-DCF'):
840
auto_disk_config = server_dict.get('auto_disk_config')
843
if self.ext_mgr.is_loaded('OS-SCH-HNT'):
844
scheduler_hints = server_dict.get('scheduler_hints', {})
847
_get_inst_type = instance_types.get_instance_type_by_flavor_id
848
inst_type = _get_inst_type(flavor_id, read_deleted="no")
850
(instances, resv_id) = self.compute_api.create(context,
854
display_description=name,
856
metadata=server_dict.get('metadata', {}),
857
access_ip_v4=access_ip_v4,
858
access_ip_v6=access_ip_v6,
859
injected_files=injected_files,
860
admin_password=password,
863
requested_networks=requested_networks,
864
security_group=sg_names,
866
availability_zone=availability_zone,
867
config_drive=config_drive,
868
block_device_mapping=block_device_mapping,
869
auto_disk_config=auto_disk_config,
870
scheduler_hints=scheduler_hints)
871
except exception.QuotaError as error:
872
raise exc.HTTPRequestEntityTooLarge(explanation=unicode(error),
873
headers={'Retry-After': 0})
874
except exception.InstanceTypeMemoryTooSmall as error:
875
raise exc.HTTPBadRequest(explanation=unicode(error))
876
except exception.InstanceTypeNotFound as error:
877
raise exc.HTTPBadRequest(explanation=unicode(error))
878
except exception.InstanceTypeDiskTooSmall as error:
879
raise exc.HTTPBadRequest(explanation=unicode(error))
880
except exception.InvalidMetadata as error:
881
raise exc.HTTPBadRequest(explanation=unicode(error))
882
except exception.InvalidMetadataSize as error:
883
raise exc.HTTPRequestEntityTooLarge(explanation=unicode(error))
884
except exception.ImageNotFound as error:
885
msg = _("Can not find requested image")
886
raise exc.HTTPBadRequest(explanation=msg)
887
except exception.FlavorNotFound as error:
888
msg = _("Invalid flavorRef provided.")
889
raise exc.HTTPBadRequest(explanation=msg)
890
except exception.KeypairNotFound as error:
891
msg = _("Invalid key_name provided.")
892
raise exc.HTTPBadRequest(explanation=msg)
893
except exception.SecurityGroupNotFound as error:
894
raise exc.HTTPBadRequest(explanation=unicode(error))
895
except rpc_common.RemoteError as err:
896
msg = "%(err_type)s: %(err_msg)s" % {'err_type': err.exc_type,
897
'err_msg': err.value}
898
raise exc.HTTPBadRequest(explanation=msg)
899
except UnicodeDecodeError as error:
900
msg = "UnicodeError: %s" % unicode(error)
901
raise exc.HTTPBadRequest(explanation=msg)
902
# Let the caller deal with unhandled exceptions.
904
# If the caller wanted a reservation_id, return it
906
# NOTE(treinish): XML serialization will not work without a root
907
# selector of 'server' however JSON return is not expecting a server
909
if ret_resv_id and (req.get_content_type() == 'application/xml'):
910
return {'server': {'reservation_id': resv_id}}
912
return {'reservation_id': resv_id}
914
req.cache_db_instances(instances)
915
server = self._view_builder.create(req, instances[0])
917
if '_is_precooked' in server['server'].keys():
918
del server['server']['_is_precooked']
920
if FLAGS.enable_instance_password:
921
server['server']['adminPass'] = password
923
robj = wsgi.ResponseObject(server)
925
return self._add_location(robj)
927
def _delete(self, context, req, instance_uuid):
928
instance = self._get_server(context, req, instance_uuid)
929
if FLAGS.reclaim_instance_interval:
930
self.compute_api.soft_delete(context, instance)
932
self.compute_api.delete(context, instance)
934
@wsgi.serializers(xml=ServerTemplate)
935
def update(self, req, id, body):
936
"""Update server then pass on to version-specific controller."""
937
if not self.is_valid_body(body, 'server'):
938
raise exc.HTTPUnprocessableEntity()
940
ctxt = req.environ['nova.context']
943
if 'name' in body['server']:
944
name = body['server']['name']
945
self._validate_server_name(name)
946
update_dict['display_name'] = name.strip()
948
if 'accessIPv4' in body['server']:
949
access_ipv4 = body['server']['accessIPv4']
950
if access_ipv4 is None:
953
self._validate_access_ipv4(access_ipv4)
954
update_dict['access_ip_v4'] = access_ipv4.strip()
956
if 'accessIPv6' in body['server']:
957
access_ipv6 = body['server']['accessIPv6']
958
if access_ipv6 is None:
961
self._validate_access_ipv6(access_ipv6)
962
update_dict['access_ip_v6'] = access_ipv6.strip()
964
if 'auto_disk_config' in body['server']:
965
auto_disk_config = utils.bool_from_str(
966
body['server']['auto_disk_config'])
967
update_dict['auto_disk_config'] = auto_disk_config
969
if 'hostId' in body['server']:
970
msg = _("HostId cannot be updated.")
971
raise exc.HTTPBadRequest(explanation=msg)
974
instance = self.compute_api.get(ctxt, id)
975
req.cache_db_instance(instance)
976
self.compute_api.update(ctxt, instance, **update_dict)
977
except exception.NotFound:
978
raise exc.HTTPNotFound()
980
instance.update(update_dict)
982
self._add_instance_faults(ctxt, [instance])
983
return self._view_builder.show(req, instance)
986
@wsgi.serializers(xml=FullServerTemplate)
987
@wsgi.deserializers(xml=ActionDeserializer)
988
@wsgi.action('confirmResize')
989
def _action_confirm_resize(self, req, id, body):
990
context = req.environ['nova.context']
991
instance = self._get_server(context, req, id)
993
self.compute_api.confirm_resize(context, instance)
994
except exception.MigrationNotFound:
995
msg = _("Instance has not been resized.")
996
raise exc.HTTPBadRequest(explanation=msg)
997
except exception.InstanceInvalidState as state_error:
998
common.raise_http_conflict_for_instance_invalid_state(state_error,
1000
except Exception, e:
1001
LOG.exception(_("Error in confirm-resize %s"), e)
1002
raise exc.HTTPBadRequest()
1003
return exc.HTTPNoContent()
1006
@wsgi.serializers(xml=FullServerTemplate)
1007
@wsgi.deserializers(xml=ActionDeserializer)
1008
@wsgi.action('revertResize')
1009
def _action_revert_resize(self, req, id, body):
1010
context = req.environ['nova.context']
1011
instance = self._get_server(context, req, id)
1013
self.compute_api.revert_resize(context, instance)
1014
except exception.MigrationNotFound:
1015
msg = _("Instance has not been resized.")
1016
raise exc.HTTPBadRequest(explanation=msg)
1017
except exception.InstanceInvalidState as state_error:
1018
common.raise_http_conflict_for_instance_invalid_state(state_error,
1020
except Exception, e:
1021
LOG.exception(_("Error in revert-resize %s"), e)
1022
raise exc.HTTPBadRequest()
1023
return webob.Response(status_int=202)
1026
@wsgi.serializers(xml=FullServerTemplate)
1027
@wsgi.deserializers(xml=ActionDeserializer)
1028
@wsgi.action('reboot')
1029
def _action_reboot(self, req, id, body):
1030
if 'reboot' in body and 'type' in body['reboot']:
1031
valid_reboot_types = ['HARD', 'SOFT']
1032
reboot_type = body['reboot']['type'].upper()
1033
if not valid_reboot_types.count(reboot_type):
1034
msg = _("Argument 'type' for reboot is not HARD or SOFT")
1036
raise exc.HTTPBadRequest(explanation=msg)
1038
msg = _("Missing argument 'type' for reboot")
1040
raise exc.HTTPBadRequest(explanation=msg)
1042
context = req.environ['nova.context']
1043
instance = self._get_server(context, req, id)
1046
self.compute_api.reboot(context, instance, reboot_type)
1047
except exception.InstanceInvalidState as state_error:
1048
common.raise_http_conflict_for_instance_invalid_state(state_error,
1050
except Exception, e:
1051
LOG.exception(_("Error in reboot %s"), e, instance=instance)
1052
raise exc.HTTPUnprocessableEntity()
1053
return webob.Response(status_int=202)
1055
def _resize(self, req, instance_id, flavor_id, **kwargs):
1056
"""Begin the resize process with given instance/flavor."""
1057
context = req.environ["nova.context"]
1058
instance = self._get_server(context, req, instance_id)
1061
self.compute_api.resize(context, instance, flavor_id, **kwargs)
1062
except exception.FlavorNotFound:
1063
msg = _("Unable to locate requested flavor.")
1064
raise exc.HTTPBadRequest(explanation=msg)
1065
except exception.CannotResizeToSameFlavor:
1066
msg = _("Resize requires a flavor change.")
1067
raise exc.HTTPBadRequest(explanation=msg)
1068
except exception.InstanceInvalidState as state_error:
1069
common.raise_http_conflict_for_instance_invalid_state(state_error,
1072
return webob.Response(status_int=202)
1075
def delete(self, req, id):
1076
"""Destroys a server."""
1078
self._delete(req.environ['nova.context'], req, id)
1079
except exception.NotFound:
1080
raise exc.HTTPNotFound()
1081
except exception.InstanceInvalidState as state_error:
1082
common.raise_http_conflict_for_instance_invalid_state(state_error,
1085
def _image_ref_from_req_data(self, data):
1087
return unicode(data['server']['imageRef'])
1088
except (TypeError, KeyError):
1089
msg = _("Missing imageRef attribute")
1090
raise exc.HTTPBadRequest(explanation=msg)
1092
def _image_uuid_from_href(self, image_href):
1093
# If the image href was generated by nova api, strip image_href
1094
# down to an id and use the default glance connection params
1095
image_uuid = image_href.split('/').pop()
1097
if not utils.is_uuid_like(image_uuid):
1098
msg = _("Invalid imageRef provided.")
1099
raise exc.HTTPBadRequest(explanation=msg)
1103
def _flavor_id_from_req_data(self, data):
1105
flavor_ref = data['server']['flavorRef']
1106
except (TypeError, KeyError):
1107
msg = _("Missing flavorRef attribute")
1108
raise exc.HTTPBadRequest(explanation=msg)
1110
return common.get_id_from_href(flavor_ref)
1113
@wsgi.serializers(xml=FullServerTemplate)
1114
@wsgi.deserializers(xml=ActionDeserializer)
1115
@wsgi.action('changePassword')
1116
def _action_change_password(self, req, id, body):
1117
context = req.environ['nova.context']
1118
if (not 'changePassword' in body
1119
or not 'adminPass' in body['changePassword']):
1120
msg = _("No adminPass was specified")
1121
raise exc.HTTPBadRequest(explanation=msg)
1122
password = body['changePassword']['adminPass']
1123
if not isinstance(password, basestring):
1124
msg = _("Invalid adminPass")
1125
raise exc.HTTPBadRequest(explanation=msg)
1126
server = self._get_server(context, req, id)
1127
self.compute_api.set_admin_password(context, server, password)
1128
return webob.Response(status_int=202)
1130
def _validate_metadata(self, metadata):
1131
"""Ensure that we can work with the metadata given."""
1133
metadata.iteritems()
1134
except AttributeError:
1135
msg = _("Unable to parse metadata key/value pairs.")
1137
raise exc.HTTPBadRequest(explanation=msg)
1140
@wsgi.serializers(xml=FullServerTemplate)
1141
@wsgi.deserializers(xml=ActionDeserializer)
1142
@wsgi.action('resize')
1143
def _action_resize(self, req, id, body):
1144
"""Resizes a given instance to the flavor size requested."""
1146
flavor_ref = body["resize"]["flavorRef"]
1148
msg = _("Resize request has invalid 'flavorRef' attribute.")
1149
raise exc.HTTPBadRequest(explanation=msg)
1150
except (KeyError, TypeError):
1151
msg = _("Resize requests require 'flavorRef' attribute.")
1152
raise exc.HTTPBadRequest(explanation=msg)
1155
if 'auto_disk_config' in body['resize']:
1156
kwargs['auto_disk_config'] = body['resize']['auto_disk_config']
1158
return self._resize(req, id, flavor_ref, **kwargs)
1161
@wsgi.serializers(xml=FullServerTemplate)
1162
@wsgi.deserializers(xml=ActionDeserializer)
1163
@wsgi.action('rebuild')
1164
def _action_rebuild(self, req, id, body):
1165
"""Rebuild an instance with the given attributes."""
1167
body = body['rebuild']
1168
except (KeyError, TypeError):
1169
raise exc.HTTPBadRequest(_("Invalid request body"))
1172
image_href = body["imageRef"]
1173
except (KeyError, TypeError):
1174
msg = _("Could not parse imageRef from request.")
1175
raise exc.HTTPBadRequest(explanation=msg)
1177
image_href = self._image_uuid_from_href(image_href)
1180
password = body['adminPass']
1181
except (KeyError, TypeError):
1182
password = utils.generate_password(FLAGS.password_length)
1184
context = req.environ['nova.context']
1185
instance = self._get_server(context, req, id)
1188
'personality': 'files_to_inject',
1189
'name': 'display_name',
1190
'accessIPv4': 'access_ip_v4',
1191
'accessIPv6': 'access_ip_v6',
1192
'metadata': 'metadata',
1193
'auto_disk_config': 'auto_disk_config',
1196
if 'accessIPv4' in body:
1197
self._validate_access_ipv4(body['accessIPv4'])
1199
if 'accessIPv6' in body:
1200
self._validate_access_ipv6(body['accessIPv6'])
1203
self._validate_server_name(body['name'])
1207
for request_attribute, instance_attribute in attr_map.items():
1209
kwargs[instance_attribute] = body[request_attribute]
1210
except (KeyError, TypeError):
1213
self._validate_metadata(kwargs.get('metadata', {}))
1215
if 'files_to_inject' in kwargs:
1216
personality = kwargs['files_to_inject']
1217
kwargs['files_to_inject'] = self._get_injected_files(personality)
1220
self.compute_api.rebuild(context,
1225
except exception.InstanceInvalidState as state_error:
1226
common.raise_http_conflict_for_instance_invalid_state(state_error,
1228
except exception.InstanceNotFound:
1229
msg = _("Instance could not be found")
1230
raise exc.HTTPNotFound(explanation=msg)
1231
except exception.InvalidMetadata as error:
1232
raise exc.HTTPBadRequest(explanation=unicode(error))
1233
except exception.InvalidMetadataSize as error:
1234
raise exc.HTTPRequestEntityTooLarge(explanation=unicode(error))
1235
except exception.ImageNotFound:
1236
msg = _("Cannot find image for rebuild")
1237
raise exc.HTTPBadRequest(explanation=msg)
1238
except exception.InstanceTypeMemoryTooSmall as error:
1239
raise exc.HTTPBadRequest(explanation=unicode(error))
1240
except exception.InstanceTypeDiskTooSmall as error:
1241
raise exc.HTTPBadRequest(explanation=unicode(error))
1243
instance = self._get_server(context, req, id)
1245
self._add_instance_faults(context, [instance])
1246
view = self._view_builder.show(req, instance)
1248
# Add on the adminPass attribute since the view doesn't do it
1249
# unless instance passwords are disabled
1250
if FLAGS.enable_instance_password:
1251
view['server']['adminPass'] = password
1253
robj = wsgi.ResponseObject(view)
1254
return self._add_location(robj)
1257
@wsgi.serializers(xml=FullServerTemplate)
1258
@wsgi.deserializers(xml=ActionDeserializer)
1259
@wsgi.action('createImage')
1260
@common.check_snapshots_enabled
1261
def _action_create_image(self, req, id, body):
1262
"""Snapshot a server instance."""
1263
context = req.environ['nova.context']
1264
entity = body.get("createImage", {})
1266
image_name = entity.get("name")
1269
msg = _("createImage entity requires name attribute")
1270
raise exc.HTTPBadRequest(explanation=msg)
1273
metadata = entity.get('metadata', {})
1274
common.check_img_metadata_properties_quota(context, metadata)
1276
props.update(metadata)
1278
msg = _("Invalid metadata")
1279
raise exc.HTTPBadRequest(explanation=msg)
1281
instance = self._get_server(context, req, id)
1283
bdms = self.compute_api.get_instance_bdms(context, instance)
1286
if self.compute_api.is_volume_backed_instance(context, instance,
1288
img = instance['image_ref']
1289
src_image = self.compute_api.image_service.show(context, img)
1290
image_meta = dict(src_image)
1292
image = self.compute_api.snapshot_volume_backed(
1297
extra_properties=props)
1299
image = self.compute_api.snapshot(context,
1302
extra_properties=props)
1303
except exception.InstanceInvalidState as state_error:
1304
common.raise_http_conflict_for_instance_invalid_state(state_error,
1307
# build location of newly-created image entity
1308
image_id = str(image['id'])
1309
image_ref = os.path.join(req.application_url,
1314
resp = webob.Response(status_int=202)
1315
resp.headers['Location'] = image_ref
1318
def _get_server_admin_password(self, server):
1319
"""Determine the admin password for a server on creation."""
1321
password = server['adminPass']
1322
self._validate_admin_password(password)
1324
password = utils.generate_password(FLAGS.password_length)
1326
raise exc.HTTPBadRequest(explanation=_("Invalid adminPass"))
1330
def _validate_admin_password(self, password):
1331
if not isinstance(password, basestring):
1334
def _get_server_search_options(self):
1335
"""Return server search options allowed by non-admin."""
1336
return ('reservation_id', 'name', 'status', 'image', 'flavor',
1340
def create_resource(ext_mgr):
1341
return wsgi.Resource(Controller(ext_mgr))
1344
def remove_invalid_options(context, search_options, allowed_search_options):
1345
"""Remove search options that are not valid for non-admin API/context."""
1346
if context.is_admin:
1349
# Otherwise, strip out all unknown options
1350
unknown_options = [opt for opt in search_options
1351
if opt not in allowed_search_options]
1352
unk_opt_str = ", ".join(unknown_options)
1353
log_msg = _("Removing options '%(unk_opt_str)s' from query") % locals()
1355
for opt in unknown_options:
1356
search_options.pop(opt, None)