/
api.py
6892 lines (6063 loc) · 318 KB
/
api.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
# Copyright 2010 United States Government as represented by the
# Administrator of the National Aeronautics and Space Administration.
# Copyright 2011 Piston Cloud Computing, Inc.
# Copyright 2012-2013 Red Hat, Inc.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
"""Handles all requests relating to compute resources (e.g. guest VMs,
networking and storage of VMs, and compute hosts on which they run)."""
import collections
import functools
import re
import typing as ty
from castellan import key_manager
import os_traits
from oslo_log import log as logging
from oslo_messaging import exceptions as oslo_exceptions
from oslo_serialization import base64 as base64utils
from oslo_utils import excutils
from oslo_utils import strutils
from oslo_utils import timeutils
from oslo_utils import units
from oslo_utils import uuidutils
from nova.accelerator import cyborg
from nova import availability_zones
from nova import block_device
from nova.compute import flavors
from nova.compute import instance_actions
from nova.compute import instance_list
from nova.compute import migration_list
from nova.compute import power_state
from nova.compute import rpcapi as compute_rpcapi
from nova.compute import task_states
from nova.compute import utils as compute_utils
from nova.compute.utils import wrap_instance_event
from nova.compute import vm_states
from nova import conductor
import nova.conf
from nova import context as nova_context
from nova import crypto
from nova.db.api import api as api_db_api
from nova.db.main import api as main_db_api
from nova import exception
from nova import exception_wrapper
from nova.i18n import _
from nova.image import glance
from nova.limit import local as local_limit
from nova.limit import placement as placement_limits
from nova.network import constants
from nova.network import model as network_model
from nova.network import neutron
from nova.network import security_group_api
from nova import objects
from nova.objects import block_device as block_device_obj
from nova.objects import external_event as external_event_obj
from nova.objects import fields as fields_obj
from nova.objects import image_meta as image_meta_obj
from nova.objects import keypair as keypair_obj
from nova.objects import quotas as quotas_obj
from nova.objects import service as service_obj
from nova.pci import request as pci_request
from nova.policies import servers as servers_policies
from nova.policies import shelve as shelve_policies
import nova.policy
from nova import profiler
from nova import rpc
from nova.scheduler.client import query
from nova.scheduler.client import report
from nova.scheduler import utils as scheduler_utils
from nova import servicegroup
from nova import utils
from nova.virt import hardware
from nova.volume import cinder
LOG = logging.getLogger(__name__)
# NOTE(gibi): legacy notification used compute as a service but these
# calls still run on the client side of the compute service which is
# nova-api. By setting the binary to nova-api below, we can make sure
# that the new versioned notifications has the right publisher_id but the
# legacy notifications does not change.
wrap_exception = functools.partial(
exception_wrapper.wrap_exception, service='compute', binary='nova-api')
CONF = nova.conf.CONF
AGGREGATE_ACTION_UPDATE = 'Update'
AGGREGATE_ACTION_UPDATE_META = 'UpdateMeta'
AGGREGATE_ACTION_DELETE = 'Delete'
AGGREGATE_ACTION_ADD = 'Add'
MIN_COMPUTE_SYNC_COMPUTE_STATUS_DISABLED = 38
MIN_COMPUTE_CROSS_CELL_RESIZE = 47
MIN_COMPUTE_SAME_HOST_COLD_MIGRATE = 48
# TODO(huaqiang): Remove in Wallaby
MIN_VER_NOVA_COMPUTE_MIXED_POLICY = 52
SUPPORT_ACCELERATOR_SERVICE_FOR_REBUILD = 53
SUPPORT_VNIC_TYPE_ACCELERATOR = 57
MIN_COMPUTE_BOOT_WITH_EXTENDED_RESOURCE_REQUEST = 58
MIN_COMPUTE_MOVE_WITH_EXTENDED_RESOURCE_REQUEST = 59
MIN_COMPUTE_INT_ATTACH_WITH_EXTENDED_RES_REQ = 60
SUPPORT_VNIC_TYPE_REMOTE_MANAGED = 61
MIN_COMPUTE_VDPA_ATTACH_DETACH = 62
MIN_COMPUTE_VDPA_HOTPLUG_LIVE_MIGRATION = 63
# FIXME(danms): Keep a global cache of the cells we find the
# first time we look. This needs to be refreshed on a timer or
# trigger.
CELLS = []
def check_instance_state(vm_state=None, task_state=(None,),
must_have_launched=True):
"""Decorator to check VM and/or task state before entry to API functions.
If the instance is in the wrong state, or has not been successfully
started at least once the wrapper will raise an exception.
"""
if vm_state is not None and not isinstance(vm_state, set):
vm_state = set(vm_state)
if task_state is not None and not isinstance(task_state, set):
task_state = set(task_state)
def outer(f):
@functools.wraps(f)
def inner(self, context, instance, *args, **kw):
if vm_state is not None and instance.vm_state not in vm_state:
raise exception.InstanceInvalidState(
attr='vm_state',
instance_uuid=instance.uuid,
state=instance.vm_state,
method=f.__name__)
if (task_state is not None and
instance.task_state not in task_state):
raise exception.InstanceInvalidState(
attr='task_state',
instance_uuid=instance.uuid,
state=instance.task_state,
method=f.__name__)
if must_have_launched and not instance.launched_at:
raise exception.InstanceInvalidState(
attr='launched_at',
instance_uuid=instance.uuid,
state=instance.launched_at,
method=f.__name__)
return f(self, context, instance, *args, **kw)
return inner
return outer
def _set_or_none(q):
return q if q is None or isinstance(q, set) else set(q)
def reject_instance_state(vm_state=None, task_state=None):
"""Decorator. Raise InstanceInvalidState if instance is in any of the
given states.
"""
vm_state = _set_or_none(vm_state)
task_state = _set_or_none(task_state)
def outer(f):
@functools.wraps(f)
def inner(self, context, instance, *args, **kw):
_InstanceInvalidState = functools.partial(
exception.InstanceInvalidState,
instance_uuid=instance.uuid,
method=f.__name__)
if vm_state is not None and instance.vm_state in vm_state:
raise _InstanceInvalidState(
attr='vm_state', state=instance.vm_state)
if task_state is not None and instance.task_state in task_state:
raise _InstanceInvalidState(
attr='task_state', state=instance.task_state)
return f(self, context, instance, *args, **kw)
return inner
return outer
def check_instance_host(check_is_up=False):
"""Validate the instance.host before performing the operation.
At a minimum this method will check that the instance.host is set.
:param check_is_up: If True, check that the instance.host status is UP
or MAINTENANCE (disabled but not down).
:raises: InstanceNotReady if the instance.host is not set
:raises: ServiceUnavailable if check_is_up=True and the instance.host
compute service status is not UP or MAINTENANCE
"""
def outer(function):
@functools.wraps(function)
def wrapped(self, context, instance, *args, **kwargs):
if not instance.host:
raise exception.InstanceNotReady(instance_id=instance.uuid)
if check_is_up:
# Make sure the source compute service is not down otherwise we
# cannot proceed.
service = [
service for service in instance.services
if service.binary == 'nova-compute'][0]
if not self.servicegroup_api.service_is_up(service):
# ComputeServiceUnavailable would make more sense here but
# we do not want to leak hostnames to end users.
raise exception.ServiceUnavailable()
return function(self, context, instance, *args, **kwargs)
return wrapped
return outer
def check_instance_lock(function):
@functools.wraps(function)
def inner(self, context, instance, *args, **kwargs):
if instance.locked and not context.is_admin:
raise exception.InstanceIsLocked(instance_uuid=instance.uuid)
return function(self, context, instance, *args, **kwargs)
return inner
def reject_sev_instances(operation):
"""Reject requests to decorated function if instance has SEV enabled.
Raise OperationNotSupportedForSEV if instance has SEV enabled.
"""
def outer(f):
@functools.wraps(f)
def inner(self, context, instance, *args, **kw):
if hardware.get_mem_encryption_constraint(instance.flavor,
instance.image_meta):
raise exception.OperationNotSupportedForSEV(
instance_uuid=instance.uuid,
operation=operation)
return f(self, context, instance, *args, **kw)
return inner
return outer
def reject_vtpm_instances(operation):
"""Reject requests to decorated function if instance has vTPM enabled.
Raise OperationNotSupportedForVTPM if instance has vTPM enabled.
"""
def outer(f):
@functools.wraps(f)
def inner(self, context, instance, *args, **kw):
if hardware.get_vtpm_constraint(
instance.flavor, instance.image_meta,
):
raise exception.OperationNotSupportedForVTPM(
instance_uuid=instance.uuid, operation=operation)
return f(self, context, instance, *args, **kw)
return inner
return outer
def reject_vdpa_instances(operation, until=None):
"""Reject requests to decorated function if instance has vDPA interfaces.
Raise OperationNotSupportedForVDPAInterfaces if operations involves one or
more vDPA interfaces.
"""
def outer(f):
@functools.wraps(f)
def inner(self, context, instance, *args, **kw):
if any(
vif['vnic_type'] == network_model.VNIC_TYPE_VDPA
for vif in instance.get_network_info()
):
reject = True
if until is not None:
min_ver = objects.service.get_minimum_version_all_cells(
nova_context.get_admin_context(), ['nova-compute']
)
if min_ver >= until:
reject = False
if reject:
raise exception.OperationNotSupportedForVDPAInterface(
instance_uuid=instance.uuid, operation=operation
)
return f(self, context, instance, *args, **kw)
return inner
return outer
def load_cells():
global CELLS
if not CELLS:
CELLS = objects.CellMappingList.get_all(
nova_context.get_admin_context())
LOG.debug('Found %(count)i cells: %(cells)s',
dict(count=len(CELLS),
cells=','.join([c.identity for c in CELLS])))
if not CELLS:
LOG.error('No cells are configured, unable to continue')
def _get_image_meta_obj(image_meta_dict):
try:
image_meta = objects.ImageMeta.from_dict(image_meta_dict)
except ValueError as e:
# there must be invalid values in the image meta properties so
# consider this an invalid request
msg = _('Invalid image metadata. Error: %s') % str(e)
raise exception.InvalidRequest(msg)
return image_meta
def block_accelerators(until_service=None):
def inner(func):
@functools.wraps(func)
def wrapper(self, context, instance, *args, **kwargs):
# NOTE(brinzhang): Catch a request operating a mixed instance,
# make sure all nova-compute services have been upgraded and
# support the accelerators.
dp_name = instance.flavor.extra_specs.get('accel:device_profile')
service_support = False
if not dp_name:
service_support = True
elif until_service:
min_version = objects.service.get_minimum_version_all_cells(
nova_context.get_admin_context(), ['nova-compute'])
if min_version >= until_service:
service_support = True
if not service_support:
raise exception.ForbiddenWithAccelerators()
return func(self, context, instance, *args, **kwargs)
return wrapper
return inner
def block_port_accelerators():
def inner(func):
@functools.wraps(func)
def wrapper(self, context, instance, *args, **kwargs):
# Catch a request operating a instance with accelerators
# attach to ports.
nw_info = instance.get_network_info()
for vif in nw_info:
vnic_type = vif['vnic_type']
if vnic_type in (network_model.VNIC_TYPE_ACCELERATOR_DIRECT,
network_model.VNIC_TYPE_ACCELERATOR_DIRECT_PHYSICAL):
raise exception.ForbiddenPortsWithAccelerator()
return func(self, context, instance, *args, **kwargs)
return wrapper
return inner
def block_extended_resource_request(function):
@functools.wraps(function)
def inner(self, context, instance, *args, **kwargs):
if self.network_api.instance_has_extended_resource_request(
instance.uuid
):
version = service_obj.get_minimum_version_all_cells(
context, ["nova-compute"])
if version < MIN_COMPUTE_MOVE_WITH_EXTENDED_RESOURCE_REQUEST:
raise exception.ExtendedResourceRequestOldCompute()
return function(self, context, instance, *args, **kwargs)
return inner
@profiler.trace_cls("compute_api")
class API:
"""API for interacting with the compute manager."""
_sentinel = object()
def __init__(self, image_api=None, network_api=None, volume_api=None):
self.image_api = image_api or glance.API()
self.network_api = network_api or neutron.API()
self.volume_api = volume_api or cinder.API()
self.compute_rpcapi = compute_rpcapi.ComputeAPI()
self.compute_task_api = conductor.ComputeTaskAPI()
self.servicegroup_api = servicegroup.API()
self.host_api = HostAPI(self.compute_rpcapi, self.servicegroup_api)
self.notifier = rpc.get_notifier('compute')
if CONF.ephemeral_storage_encryption.enabled:
self.key_manager = key_manager.API()
# Help us to record host in EventReporter
self.host = CONF.host
def _record_action_start(self, context, instance, action):
objects.InstanceAction.action_start(context, instance.uuid,
action, want_result=False)
def _check_injected_file_quota(self, context, injected_files):
"""Enforce quota limits on injected files.
Raises a OverQuota if any limit is exceeded.
"""
if not injected_files:
return
# Check number of files first
try:
objects.Quotas.limit_check(context,
injected_files=len(injected_files))
local_limit.enforce_api_limit(local_limit.INJECTED_FILES,
len(injected_files))
except exception.OnsetFileLimitExceeded:
raise
except exception.OverQuota:
raise exception.OnsetFileLimitExceeded()
# OK, now count path and content lengths; we're looking for
# the max...
max_path = 0
max_content = 0
for path, content in injected_files:
max_path = max(max_path, len(path))
max_content = max(max_content, len(content))
try:
objects.Quotas.limit_check(context,
injected_file_path_bytes=max_path,
injected_file_content_bytes=max_content)
# TODO(johngarbutt) we can simplify the except clause when
# the above legacy quota check is removed.
local_limit.enforce_api_limit(
local_limit.INJECTED_FILES_PATH, max_path)
local_limit.enforce_api_limit(
local_limit.INJECTED_FILES_CONTENT, max_content)
except exception.OnsetFilePathLimitExceeded:
raise
except exception.OnsetFileContentLimitExceeded:
raise
except exception.OverQuota as exc:
# Favor path limit over content limit for reporting
# purposes
if 'injected_file_path_bytes' in exc.kwargs['overs']:
raise exception.OnsetFilePathLimitExceeded(
allowed=exc.kwargs['quotas']['injected_file_path_bytes'])
else:
raise exception.OnsetFileContentLimitExceeded(
allowed=exc.kwargs['quotas']['injected_file_content_bytes'])
def _check_metadata_properties_quota(self, context, metadata=None):
"""Enforce quota limits on metadata properties."""
if not metadata:
return
if not isinstance(metadata, dict):
msg = (_("Metadata type should be dict."))
raise exception.InvalidMetadata(reason=msg)
num_metadata = len(metadata)
try:
objects.Quotas.limit_check(context, metadata_items=num_metadata)
local_limit.enforce_api_limit(
local_limit.SERVER_METADATA_ITEMS, num_metadata)
except exception.MetadataLimitExceeded:
raise
except exception.OverQuota as exc:
quota_metadata = exc.kwargs['quotas']['metadata_items']
raise exception.MetadataLimitExceeded(allowed=quota_metadata)
# Because metadata is stored in the DB, we hard-code the size limits
# In future, we may support more variable length strings, so we act
# as if this is quota-controlled for forwards compatibility.
# Those are only used in V2 API, from V2.1 API, those checks are
# validated at API layer schema validation.
for k, v in metadata.items():
try:
utils.check_string_length(v)
utils.check_string_length(k, min_length=1)
except exception.InvalidInput as e:
raise exception.InvalidMetadata(reason=e.format_message())
if len(k) > 255:
msg = _("Metadata property key greater than 255 characters")
raise exception.InvalidMetadataSize(reason=msg)
if len(v) > 255:
msg = _("Metadata property value greater than 255 characters")
raise exception.InvalidMetadataSize(reason=msg)
def _check_requested_secgroups(self, context, secgroups):
"""Check if the security group requested exists and belongs to
the project.
:param context: The nova request context.
:type context: nova.context.RequestContext
:param secgroups: list of requested security group names
:type secgroups: list
:returns: list of requested security group UUIDs; note that 'default'
is a special case and will be unmodified if it's requested.
"""
security_groups = []
for secgroup in secgroups:
# NOTE(sdague): default is handled special
if secgroup == "default":
security_groups.append(secgroup)
continue
secgroup_uuid = security_group_api.validate_name(context, secgroup)
security_groups.append(secgroup_uuid)
return security_groups
def _check_requested_networks(self, context, requested_networks,
max_count):
"""Check if the networks requested belongs to the project
and the fixed IP address for each network provided is within
same the network block
"""
if requested_networks is not None:
if requested_networks.no_allocate:
# If the network request was specifically 'none' meaning don't
# allocate any networks, we just return the number of requested
# instances since quotas don't change at all.
return max_count
# NOTE(danms): Temporary transition
requested_networks = requested_networks.as_tuples()
return self.network_api.validate_networks(context, requested_networks,
max_count)
def _handle_kernel_and_ramdisk(self, context, kernel_id, ramdisk_id,
image):
"""Choose kernel and ramdisk appropriate for the instance.
The kernel and ramdisk can be chosen in one of two ways:
1. Passed in with create-instance request.
2. Inherited from image metadata.
If inherited from image metadata, and if that image metadata value is
set to 'nokernel', both kernel and ramdisk will default to None.
"""
# Inherit from image if not specified
image_properties = image.get('properties', {})
if kernel_id is None:
kernel_id = image_properties.get('kernel_id')
if ramdisk_id is None:
ramdisk_id = image_properties.get('ramdisk_id')
# Force to None if kernel_id indicates that a kernel is not to be used
if kernel_id == 'nokernel':
kernel_id = None
ramdisk_id = None
# Verify kernel and ramdisk exist (fail-fast)
if kernel_id is not None:
kernel_image = self.image_api.get(context, kernel_id)
# kernel_id could have been a URI, not a UUID, so to keep behaviour
# from before, which leaked that implementation detail out to the
# caller, we return the image UUID of the kernel image and ramdisk
# image (below) and not any image URIs that might have been
# supplied.
# TODO(jaypipes): Get rid of this silliness once we move to a real
# Image object and hide all of that stuff within nova.image.glance
kernel_id = kernel_image['id']
if ramdisk_id is not None:
ramdisk_image = self.image_api.get(context, ramdisk_id)
ramdisk_id = ramdisk_image['id']
return kernel_id, ramdisk_id
@staticmethod
def parse_availability_zone(context, availability_zone):
# NOTE(vish): We have a legacy hack to allow admins to specify hosts
# via az using az:host:node. It might be nice to expose an
# api to specify specific hosts to force onto, but for
# now it just supports this legacy hack.
# NOTE(deva): It is also possible to specify az::node, in which case
# the host manager will determine the correct host.
forced_host = None
forced_node = None
if availability_zone and ':' in availability_zone:
c = availability_zone.count(':')
if c == 1:
availability_zone, forced_host = availability_zone.split(':')
elif c == 2:
if '::' in availability_zone:
availability_zone, forced_node = \
availability_zone.split('::')
else:
availability_zone, forced_host, forced_node = \
availability_zone.split(':')
else:
raise exception.InvalidInput(
reason="Unable to parse availability_zone")
if not availability_zone:
availability_zone = CONF.default_schedule_zone
return availability_zone, forced_host, forced_node
def _ensure_auto_disk_config_is_valid(self, auto_disk_config_img,
auto_disk_config, image):
auto_disk_config_disabled = \
utils.is_auto_disk_config_disabled(auto_disk_config_img)
if auto_disk_config_disabled and auto_disk_config:
raise exception.AutoDiskConfigDisabledByImage(image=image)
def _inherit_properties_from_image(self, image, auto_disk_config):
image_properties = image.get('properties', {})
auto_disk_config_img = \
utils.get_auto_disk_config_from_image_props(image_properties)
self._ensure_auto_disk_config_is_valid(auto_disk_config_img,
auto_disk_config,
image.get("id"))
if auto_disk_config is None:
auto_disk_config = strutils.bool_from_string(auto_disk_config_img)
return {
'os_type': image_properties.get('os_type'),
'architecture': image_properties.get('architecture'),
'vm_mode': image_properties.get('vm_mode'),
'auto_disk_config': auto_disk_config
}
def _check_config_drive(self, config_drive):
if config_drive:
try:
bool_val = strutils.bool_from_string(config_drive,
strict=True)
except ValueError:
raise exception.ConfigDriveInvalidValue(option=config_drive)
else:
bool_val = False
# FIXME(comstud): Bug ID 1193438 filed for this. This looks silly,
# but this is because the config drive column is a String. False
# is represented by using an empty string. And for whatever
# reason, we rely on the DB to cast True to a String.
return True if bool_val else ''
def _validate_flavor_image(
self, context, image_id, image, flavor, root_bdm, validate_numa=True,
):
"""Validate the flavor and image.
This is called from the API service to ensure that the flavor
extra-specs and image properties are self-consistent and compatible
with each other.
:param context: A context.RequestContext
:param image_id: UUID of the image
:param image: a dict representation of the image including properties,
enforces the image status is active.
:param flavor: Flavor object
:param root_bdm: BlockDeviceMapping for root disk. Will be None for
the resize case.
:param validate_numa: Flag to indicate whether or not to validate
the NUMA-related metadata.
:raises: Many different possible exceptions. See
api.openstack.compute.servers.INVALID_FLAVOR_IMAGE_EXCEPTIONS
for the full list.
"""
if image and image['status'] != 'active':
raise exception.ImageNotActive(image_id=image_id)
self._validate_flavor_image_nostatus(
context, image, flavor, root_bdm, validate_numa)
@staticmethod
def _detect_nonbootable_image_from_properties(image_id, image):
"""Check image for a property indicating it's nonbootable.
This is called from the API service to ensure that there are
no known image properties indicating that this image is of a
type that we do not support booting from.
Currently the only such property is 'cinder_encryption_key_id'.
:param image_id: UUID of the image
:param image: a dict representation of the image including properties
:raises: ImageUnacceptable if the image properties indicate
that booting this image is not supported
"""
if not image:
return
image_properties = image.get('properties', {})
# NOTE(lyarwood) Skip this check when image_id is None indicating that
# the instance is booting from a volume that was itself initially
# created from an image. As such we don't care if
# cinder_encryption_key_id was against the original image as we are now
# booting from an encrypted volume.
if image_properties.get('cinder_encryption_key_id') and image_id:
reason = _('Direct booting of an image uploaded from an '
'encrypted volume is unsupported.')
raise exception.ImageUnacceptable(image_id=image_id,
reason=reason)
@staticmethod
def _validate_flavor_image_nostatus(
context, image, flavor, root_bdm, validate_numa=True,
validate_pci=False,
):
"""Validate the flavor and image.
This is called from the API service to ensure that the flavor
extra-specs and image properties are self-consistent and compatible
with each other.
:param context: A context.RequestContext
:param image: a dict representation of the image including properties
:param flavor: Flavor object
:param root_bdm: BlockDeviceMapping for root disk. Will be None for
the resize case.
:param validate_numa: Flag to indicate whether or not to validate
the NUMA-related metadata.
:param validate_pci: Flag to indicate whether or not to validate
the PCI-related metadata.
:raises: Many different possible exceptions. See
api.openstack.compute.servers.INVALID_FLAVOR_IMAGE_EXCEPTIONS
for the full list.
"""
if not image:
return
image_properties = image.get('properties', {})
config_drive_option = image_properties.get(
'img_config_drive', 'optional')
if config_drive_option not in ['optional', 'mandatory']:
raise exception.InvalidImageConfigDrive(
config_drive=config_drive_option)
if flavor['memory_mb'] < int(image.get('min_ram') or 0):
raise exception.FlavorMemoryTooSmall()
# Image min_disk is in gb, size is in bytes. For sanity, have them both
# in bytes.
image_min_disk = int(image.get('min_disk') or 0) * units.Gi
image_size = int(image.get('size') or 0)
# Target disk is a volume. Don't check flavor disk size because it
# doesn't make sense, and check min_disk against the volume size.
if root_bdm is not None and root_bdm.is_volume:
# There are 2 possibilities here:
#
# 1. The target volume already exists but bdm.volume_size is not
# yet set because this method is called before
# _bdm_validate_set_size_and_instance during server create.
# 2. The target volume doesn't exist, in which case the bdm will
# contain the intended volume size
#
# Note that rebuild also calls this method with potentially a new
# image but you can't rebuild a volume-backed server with a new
# image (yet).
#
# Cinder does its own check against min_disk, so if the target
# volume already exists this has already been done and we don't
# need to check it again here. In this case, volume_size may not be
# set on the bdm.
#
# If we're going to create the volume, the bdm will contain
# volume_size. Therefore we should check it if it exists. This will
# still be checked again by cinder when the volume is created, but
# that will not happen until the request reaches a host. By
# checking it here, the user gets an immediate and useful failure
# indication.
#
# The third possibility is that we have failed to consider
# something, and there are actually more than 2 possibilities. In
# this case cinder will still do the check at volume creation time.
# The behaviour will still be correct, but the user will not get an
# immediate failure from the api, and will instead have to
# determine why the instance is in an error state with a task of
# block_device_mapping.
#
# We could reasonably refactor this check into _validate_bdm at
# some future date, as the various size logic is already split out
# in there.
dest_size = root_bdm.volume_size
if dest_size is not None:
dest_size *= units.Gi
if image_min_disk > dest_size:
raise exception.VolumeSmallerThanMinDisk(
volume_size=dest_size, image_min_disk=image_min_disk)
# Target disk is a local disk whose size is taken from the flavor
else:
dest_size = flavor['root_gb'] * units.Gi
# NOTE(johannes): root_gb is allowed to be 0 for legacy reasons
# since libvirt interpreted the value differently than other
# drivers. A value of 0 means don't check size.
if dest_size != 0:
if image_size > dest_size:
raise exception.FlavorDiskSmallerThanImage(
flavor_size=dest_size, image_size=image_size)
if image_min_disk > dest_size:
raise exception.FlavorDiskSmallerThanMinDisk(
flavor_size=dest_size, image_min_disk=image_min_disk)
else:
# The user is attempting to create a server with a 0-disk
# image-backed flavor, which can lead to issues with a large
# image consuming an unexpectedly large amount of local disk
# on the compute host. Check to see if the deployment will
# allow that.
if not context.can(
servers_policies.ZERO_DISK_FLAVOR, fatal=False):
raise exception.BootFromVolumeRequiredForZeroDiskFlavor()
API._validate_flavor_image_numa_pci(
image, flavor, validate_numa=validate_numa,
validate_pci=validate_pci)
# TODO(huaqiang): Remove in Wallaby when there is no nova-compute node
# having a version prior to Victoria.
@staticmethod
def _check_compute_service_for_mixed_instance(numa_topology):
"""Check if the nova-compute service is ready to support mixed instance
when the CPU allocation policy is 'mixed'.
"""
# No need to check the instance with no NUMA topology associated with.
if numa_topology is None:
return
# No need to check if instance CPU policy is not 'mixed'
if numa_topology.cpu_policy != fields_obj.CPUAllocationPolicy.MIXED:
return
# Catch a request creating a mixed instance, make sure all nova-compute
# service have been upgraded and support the mixed policy.
minimal_version = objects.service.get_minimum_version_all_cells(
nova_context.get_admin_context(), ['nova-compute'])
if minimal_version < MIN_VER_NOVA_COMPUTE_MIXED_POLICY:
raise exception.MixedInstanceNotSupportByComputeService()
@staticmethod
def _validate_flavor_image_numa_pci(
image, flavor, validate_numa=True, validate_pci=False,
):
"""Validate the flavor and image NUMA/PCI values.
This is called from the API service to ensure that the flavor
extra-specs and image properties are self-consistent and compatible
with each other.
:param image: a dict representation of the image including properties
:param flavor: Flavor object
:param validate_numa: Flag to indicate whether or not to validate
the NUMA-related metadata.
:param validate_pci: Flag to indicate whether or not to validate
the PCI-related metadata.
:raises: Many different possible exceptions. See
api.openstack.compute.servers.INVALID_FLAVOR_IMAGE_EXCEPTIONS
for the full list.
"""
image_meta = _get_image_meta_obj(image)
# Only validate values of flavor/image so the return results of
# following 'get' functions are not used.
hardware.get_mem_encryption_constraint(flavor, image_meta)
hardware.get_pmu_constraint(flavor, image_meta)
hardware.get_number_of_serial_ports(flavor, image_meta)
hardware.get_realtime_cpu_constraint(flavor, image_meta)
hardware.get_cpu_topology_constraints(flavor, image_meta)
hardware.get_vif_multiqueue_constraint(flavor, image_meta)
if validate_numa:
hardware.numa_get_constraints(flavor, image_meta)
if validate_pci:
pci_request.get_pci_requests_from_flavor(flavor)
def _get_image_defined_bdms(self, flavor, image_meta, root_device_name):
image_properties = image_meta.get('properties', {})
# Get the block device mappings defined by the image.
image_defined_bdms = image_properties.get('block_device_mapping', [])
legacy_image_defined = not image_properties.get('bdm_v2', False)
image_mapping = image_properties.get('mappings', [])
if legacy_image_defined:
image_defined_bdms = block_device.from_legacy_mapping(
image_defined_bdms, None, root_device_name)
else:
image_defined_bdms = list(map(block_device.BlockDeviceDict,
image_defined_bdms))
if image_mapping:
image_mapping = self._prepare_image_mapping(flavor, image_mapping)
image_defined_bdms = self._merge_bdms_lists(
image_mapping, image_defined_bdms)
return image_defined_bdms
def _get_flavor_defined_bdms(self, flavor, block_device_mapping):
flavor_defined_bdms = []
have_ephemeral_bdms = any(filter(
block_device.new_format_is_ephemeral, block_device_mapping))
have_swap_bdms = any(filter(
block_device.new_format_is_swap, block_device_mapping))
if flavor.get('ephemeral_gb') and not have_ephemeral_bdms:
flavor_defined_bdms.append(
block_device.create_blank_bdm(flavor['ephemeral_gb']))
if flavor.get('swap') and not have_swap_bdms:
flavor_defined_bdms.append(
block_device.create_blank_bdm(flavor['swap'], 'swap'))
return flavor_defined_bdms
def _merge_bdms_lists(self, overridable_mappings, overrider_mappings):
"""Override any block devices from the first list by device name
:param overridable_mappings: list which items are overridden
:param overrider_mappings: list which items override
:returns: A merged list of bdms
"""
device_names = set(bdm['device_name'] for bdm in overrider_mappings
if bdm['device_name'])
return (overrider_mappings +
[bdm for bdm in overridable_mappings
if bdm['device_name'] not in device_names])
def _check_and_transform_bdm(
self, context, base_options, flavor, image_meta, min_count, max_count,
block_device_mapping, legacy_bdm,
):
# NOTE (ndipanov): Assume root dev name is 'vda' if not supplied.
# It's needed for legacy conversion to work.
root_device_name = (base_options.get('root_device_name') or 'vda')
image_ref = base_options.get('image_ref', '')
# If the instance is booted by image and has a volume attached,
# the volume cannot have the same device name as root_device_name
if image_ref:
for bdm in block_device_mapping:
if (bdm.get('destination_type') == 'volume' and
block_device.strip_dev(bdm.get(
'device_name')) == root_device_name):
msg = _('The volume cannot be assigned the same device'
' name as the root device %s') % root_device_name
raise exception.InvalidRequest(msg)
image_defined_bdms = self._get_image_defined_bdms(
flavor, image_meta, root_device_name)
root_in_image_bdms = (
block_device.get_root_bdm(image_defined_bdms) is not None)
if legacy_bdm:
block_device_mapping = block_device.from_legacy_mapping(
block_device_mapping, image_ref, root_device_name,
no_root=root_in_image_bdms)
elif root_in_image_bdms:
# NOTE (ndipanov): client will insert an image mapping into the v2
# block_device_mapping, but if there is a bootable device in image
# mappings - we need to get rid of the inserted image
# NOTE (gibi): another case is when a server is booted with an
# image to bdm mapping where the image only contains a bdm to a
# snapshot. In this case the other image to bdm mapping
# contains an unnecessary device with boot_index == 0.
# Also in this case the image_ref is None as we are booting from
# an image to volume bdm.
def not_image_and_root_bdm(bdm):
return not (bdm.get('boot_index') == 0 and
bdm.get('source_type') == 'image')
block_device_mapping = list(
filter(not_image_and_root_bdm, block_device_mapping))
block_device_mapping = self._merge_bdms_lists(
image_defined_bdms, block_device_mapping)
if min_count > 1 or max_count > 1:
if any(map(lambda bdm: bdm['source_type'] == 'volume',
block_device_mapping)):
msg = _('Cannot attach one or more volumes to multiple'
' instances')
raise exception.InvalidRequest(msg)
block_device_mapping += self._get_flavor_defined_bdms(
flavor, block_device_mapping)