1# Licensed under the Apache License, Version 2.0 (the "License"); you may 2# not use this file except in compliance with the License. You may obtain 3# a copy of the License at 4# 5# http://www.apache.org/licenses/LICENSE-2.0 6# 7# Unless required by applicable law or agreed to in writing, software 8# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 9# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 10# License for the specific language governing permissions and limitations 11# under the License. 12 13import collections 14import enum 15 16from openstack.baremetal.v1 import _common 17from openstack import exceptions 18from openstack import resource 19from openstack import utils 20 21 22class ValidationResult: 23 """Result of a single interface validation. 24 25 :ivar result: Result of a validation, ``True`` for success, ``False`` for 26 failure, ``None`` for unsupported interface. 27 :ivar reason: If ``result`` is ``False`` or ``None``, explanation of 28 the result. 29 """ 30 31 def __init__(self, result, reason): 32 self.result = result 33 self.reason = reason 34 35 36class PowerAction(enum.Enum): 37 """Mapping from an action to a target power state.""" 38 39 POWER_ON = 'power on' 40 """Power on the node.""" 41 42 POWER_OFF = 'power off' 43 """Power off the node (using hard power off).""" 44 REBOOT = 'rebooting' 45 """Reboot the node (using hard power off).""" 46 47 SOFT_POWER_OFF = 'soft power off' 48 """Power off the node using soft power off.""" 49 50 SOFT_REBOOT = 'soft rebooting' 51 """Reboot the node using soft power off.""" 52 53 54class WaitResult(collections.namedtuple('WaitResult', 55 ['success', 'failure', 'timeout'])): 56 """A named tuple representing a result of waiting for several nodes. 57 58 Each component is a list of :class:`~openstack.baremetal.v1.node.Node` 59 objects: 60 61 :ivar ~.success: a list of :class:`~openstack.baremetal.v1.node.Node` 62 objects that reached the state. 63 :ivar ~.timeout: a list of :class:`~openstack.baremetal.v1.node.Node` 64 objects that reached timeout. 65 :ivar ~.failure: a list of :class:`~openstack.baremetal.v1.node.Node` 66 objects that hit a failure. 67 """ 68 __slots__ = () 69 70 71class Node(_common.ListMixin, resource.Resource): 72 73 resources_key = 'nodes' 74 base_path = '/nodes' 75 76 # capabilities 77 allow_create = True 78 allow_fetch = True 79 allow_commit = True 80 allow_delete = True 81 allow_list = True 82 allow_patch = True 83 commit_method = 'PATCH' 84 commit_jsonpatch = True 85 86 _query_mapping = resource.QueryParameters( 87 'associated', 'conductor_group', 'driver', 'fault', 88 'provision_state', 'resource_class', 89 fields={'type': _common.fields_type}, 90 instance_id='instance_uuid', 91 is_maintenance='maintenance', 92 ) 93 94 # Ability to change boot_mode and secure_boot, introduced in 1.76 (Xena). 95 _max_microversion = '1.76' 96 97 # Properties 98 #: The UUID of the allocation associated with this node. Added in API 99 #: microversion 1.52. 100 allocation_id = resource.Body("allocation_uuid") 101 #: A string or UUID of the tenant who owns the baremetal node. Added in API 102 #: microversion 1.50. 103 owner = resource.Body("owner") 104 #: The current boot mode state (uefi/bios). Added in API microversion 1.75. 105 boot_mode = resource.Body("boot_mode") 106 #: The UUID of the chassis associated wit this node. Can be empty or None. 107 chassis_id = resource.Body("chassis_uuid") 108 #: The current clean step. 109 clean_step = resource.Body("clean_step") 110 #: Hostname of the conductor currently handling this ndoe. Added in API 111 # microversion 1.49. 112 conductor = resource.Body("conductor") 113 #: Conductor group this node is managed by. Added in API microversion 1.46. 114 conductor_group = resource.Body("conductor_group") 115 #: Timestamp at which the node was last updated. 116 created_at = resource.Body("created_at") 117 #: The current deploy step. Added in API microversion 1.44. 118 deploy_step = resource.Body("deploy_step") 119 #: The name of the driver. 120 driver = resource.Body("driver") 121 #: All the metadata required by the driver to manage this node. List of 122 #: fields varies between drivers, and can be retrieved from the 123 #: :class:`openstack.baremetal.v1.driver.Driver` resource. 124 driver_info = resource.Body("driver_info", type=dict) 125 #: Internal metadata set and stored by node's driver. This is read-only. 126 driver_internal_info = resource.Body("driver_internal_info", type=dict) 127 #: A set of one or more arbitrary metadata key and value pairs. 128 extra = resource.Body("extra") 129 #: Fault type that caused the node to enter maintenance mode. 130 #: Introduced in API microversion 1.42. 131 fault = resource.Body("fault") 132 #: The UUID of the node resource. 133 id = resource.Body("uuid", alternate_id=True) 134 #: Information used to customize the deployed image, e.g. size of root 135 #: partition, config drive in the form of base64 encoded string and other 136 #: metadata. 137 instance_info = resource.Body("instance_info") 138 #: UUID of the nova instance associated with this node. 139 instance_id = resource.Body("instance_uuid") 140 #: Override enabling of automated cleaning. Added in API microversion 1.47. 141 is_automated_clean_enabled = resource.Body("automated_clean", type=bool) 142 #: Whether console access is enabled on this node. 143 is_console_enabled = resource.Body("console_enabled", type=bool) 144 #: Whether node is currently in "maintenance mode". Nodes put into 145 #: maintenance mode are removed from the available resource pool. 146 is_maintenance = resource.Body("maintenance", type=bool) 147 # Whether the node is protected from undeploying. Added in API microversion 148 # 1.48. 149 is_protected = resource.Body("protected", type=bool) 150 #: Whether the node is marked for retirement. Added in API microversion 151 #: 1.61. 152 is_retired = resource.Body("retired", type=bool) 153 #: Whether the node is currently booted with secure boot turned on. 154 #: Added in API microversion 1.75. 155 is_secure_boot = resource.Body("secure_boot", type=bool) 156 #: Any error from the most recent transaction that started but failed to 157 #: finish. 158 last_error = resource.Body("last_error") 159 #: A list of relative links, including self and bookmark links. 160 links = resource.Body("links", type=list) 161 #: user settable description of the reason why the node was placed into 162 #: maintenance mode. 163 maintenance_reason = resource.Body("maintenance_reason") 164 #: Human readable identifier for the node. May be undefined. Certain words 165 #: are reserved. Added in API microversion 1.5 166 name = resource.Body("name") 167 #: Links to the collection of ports on this node. 168 ports = resource.Body("ports", type=list) 169 #: Links to the collection of portgroups on this node. Available since 170 #: API microversion 1.24. 171 port_groups = resource.Body("portgroups", type=list) 172 #: The current power state. Usually "power on" or "power off", but may be 173 #: "None" if service is unable to determine the power state. 174 power_state = resource.Body("power_state") 175 #: Physical characteristics of the node. Content populated by the service 176 #: during inspection. 177 properties = resource.Body("properties", type=dict) 178 # The reason why this node is protected. Added in API microversion 1.48. 179 protected_reason = resource.Body("protected_reason") 180 #: The current provisioning state of the node. 181 provision_state = resource.Body("provision_state") 182 #: The reason why the node is marked for retirement. Added in API 183 #: microversion 1.61. 184 retired_reason = resource.Body("retired_reason") 185 #: The current RAID configuration of the node. 186 raid_config = resource.Body("raid_config") 187 #: The name of an service conductor host which is holding a lock on this 188 #: node, if a lock is held. 189 reservation = resource.Body("reservation") 190 #: A string to be used by external schedulers to identify this node as a 191 #: unit of a specific type of resource. Added in API microversion 1.21. 192 resource_class = resource.Body("resource_class") 193 #: Links to the collection of states. 194 states = resource.Body("states", type=list) 195 #: The requested state if a provisioning action has been requested. For 196 #: example, ``AVAILABLE``, ``DEPLOYING``, ``DEPLOYWAIT``, ``DEPLOYING``, 197 #: ``ACTIVE`` etc. 198 target_provision_state = resource.Body("target_provision_state") 199 #: The requested state during a state transition. 200 target_power_state = resource.Body("target_power_state") 201 #: The requested RAID configuration of the node which will be applied when 202 #: the node next transitions through the CLEANING state. 203 target_raid_config = resource.Body("target_raid_config") 204 #: Traits of the node. Introduced in API microversion 1.37. 205 traits = resource.Body("traits", type=list) 206 #: Timestamp at which the node was last updated. 207 updated_at = resource.Body("updated_at") 208 209 # Hardware interfaces grouped together for convenience. 210 211 #: BIOS interface to use when setting BIOS properties of the node. 212 #: Introduced in API microversion 1.40. 213 bios_interface = resource.Body("bios_interface") 214 #: Boot interface to use when configuring boot of the node. 215 #: Introduced in API microversion 1.31. 216 boot_interface = resource.Body("boot_interface") 217 #: Console interface to use when working with serial console. 218 #: Introduced in API microversion 1.31. 219 console_interface = resource.Body("console_interface") 220 #: Deploy interface to use when deploying the node. 221 #: Introduced in API microversion 1.31. 222 deploy_interface = resource.Body("deploy_interface") 223 #: Inspect interface to use when inspecting the node. 224 #: Introduced in API microversion 1.31. 225 inspect_interface = resource.Body("inspect_interface") 226 #: Management interface to use for management actions on the node. 227 #: Introduced in API microversion 1.31. 228 management_interface = resource.Body("management_interface") 229 #: Network interface provider to use when plumbing the network connections 230 #: for this node. Introduced in API microversion 1.20. 231 network_interface = resource.Body("network_interface") 232 #: Power interface to use for power actions on the node. 233 #: Introduced in API microversion 1.31. 234 power_interface = resource.Body("power_interface") 235 #: RAID interface to use for configuring RAID on the node. 236 #: Introduced in API microversion 1.31. 237 raid_interface = resource.Body("raid_interface") 238 #: Rescue interface to use for rescuing of the node. 239 #: Introduced in API microversion 1.38. 240 rescue_interface = resource.Body("rescue_interface") 241 #: Storage interface to use when attaching remote storage. 242 #: Introduced in API microversion 1.33. 243 storage_interface = resource.Body("storage_interface") 244 #: Vendor interface to use for vendor-specific actions on the node. 245 #: Introduced in API microversion 1.31. 246 vendor_interface = resource.Body("vendor_interface") 247 248 def _consume_body_attrs(self, attrs): 249 if 'provision_state' in attrs and attrs['provision_state'] is None: 250 # API version 1.1 uses None instead of "available". Make it 251 # consistent. 252 attrs['provision_state'] = 'available' 253 return super(Node, self)._consume_body_attrs(attrs) 254 255 def create(self, session, *args, **kwargs): 256 """Create a remote resource based on this instance. 257 258 The overridden version is capable of handling the populated 259 ``provision_state`` field of one of three values: ``enroll``, 260 ``manageable`` or ``available``. The default is currently 261 ``available``, since it's the only state supported by all API versions. 262 263 Note that Bare Metal API 1.4 is required for ``manageable`` and 264 1.11 is required for ``enroll``. 265 266 :param session: The session to use for making this request. 267 :type session: :class:`~keystoneauth1.adapter.Adapter` 268 269 :return: This :class:`Resource` instance. 270 :raises: ValueError if the Node's ``provision_state`` is not one of 271 ``None``, ``enroll``, ``manageable`` or ``available``. 272 :raises: :exc:`~openstack.exceptions.NotSupported` if 273 the ``provision_state`` cannot be reached with any API version 274 supported by the server. 275 """ 276 expected_provision_state = self.provision_state 277 if expected_provision_state is None: 278 expected_provision_state = 'available' 279 280 if expected_provision_state not in ('enroll', 281 'manageable', 282 'available'): 283 raise ValueError( 284 "Node's provision_state must be one of 'enroll', " 285 "'manageable' or 'available' for creation, got %s" % 286 expected_provision_state) 287 288 session = self._get_session(session) 289 # Verify that the requested provision state is reachable with the API 290 # version we are going to use. 291 try: 292 expected_version = _common.STATE_VERSIONS[expected_provision_state] 293 except KeyError: 294 pass 295 else: 296 self._assert_microversion_for( 297 session, 'create', expected_version, 298 error_message="Cannot create a node with initial provision " 299 "state %s" % expected_provision_state) 300 301 # Ironic cannot set provision_state itself, so marking it as unchanged 302 self._clean_body_attrs({'provision_state'}) 303 super(Node, self).create(session, *args, **kwargs) 304 305 if (self.provision_state == 'enroll' 306 and expected_provision_state != 'enroll'): 307 self.set_provision_state(session, 'manage', wait=True) 308 309 if (self.provision_state == 'manageable' 310 and expected_provision_state == 'available'): 311 self.set_provision_state(session, 'provide', wait=True) 312 313 if (self.provision_state == 'available' 314 and expected_provision_state == 'manageable'): 315 self.set_provision_state(session, 'manage', wait=True) 316 317 return self 318 319 def commit(self, session, *args, **kwargs): 320 """Commit the state of the instance to the remote resource. 321 322 :param session: The session to use for making this request. 323 :type session: :class:`~keystoneauth1.adapter.Adapter` 324 325 :return: This :class:`Node` instance. 326 """ 327 # These fields have to be set through separate API. 328 if ('maintenance_reason' in self._body.dirty 329 or 'maintenance' in self._body.dirty): 330 if not self.is_maintenance and self.maintenance_reason: 331 if 'maintenance' in self._body.dirty: 332 self.maintenance_reason = None 333 else: 334 raise ValueError('Maintenance reason cannot be set when ' 335 'maintenance is False') 336 if self.is_maintenance: 337 self._do_maintenance_action( 338 session, 'put', {'reason': self.maintenance_reason}) 339 else: 340 # This corresponds to setting maintenance=False and 341 # maintenance_reason=None in the same request. 342 self._do_maintenance_action(session, 'delete') 343 344 self._clean_body_attrs({'maintenance', 'maintenance_reason'}) 345 if not self.requires_commit: 346 # Other fields are not updated, re-fetch the node to reflect 347 # the new status. 348 return self.fetch(session) 349 350 return super(Node, self).commit(session, *args, **kwargs) 351 352 def set_provision_state(self, session, target, config_drive=None, 353 clean_steps=None, rescue_password=None, 354 wait=False, timeout=None, deploy_steps=None): 355 """Run an action modifying this node's provision state. 356 357 This call is asynchronous, it will return success as soon as the Bare 358 Metal service acknowledges the request. 359 360 :param session: The session to use for making this request. 361 :type session: :class:`~keystoneauth1.adapter.Adapter` 362 :param target: Provisioning action, e.g. ``active``, ``provide``. 363 See the Bare Metal service documentation for available actions. 364 :param config_drive: Config drive to pass to the node, only valid 365 for ``active` and ``rebuild`` targets. You can use functions from 366 :mod:`openstack.baremetal.configdrive` to build it. 367 :param clean_steps: Clean steps to execute, only valid for ``clean`` 368 target. 369 :param rescue_password: Password for the rescue operation, only valid 370 for ``rescue`` target. 371 :param wait: Whether to wait for the target state to be reached. 372 :param timeout: Timeout (in seconds) to wait for the target state to be 373 reached. If ``None``, wait without timeout. 374 :param deploy_steps: Deploy steps to execute, only valid for ``active`` 375 and ``rebuild`` target. 376 377 :return: This :class:`Node` instance. 378 :raises: ValueError if ``config_drive``, ``clean_steps``, 379 ``deploy_steps`` or ``rescue_password`` are provided with an 380 invalid ``target``. 381 :raises: :class:`~openstack.exceptions.ResourceFailure` if the node 382 reaches an error state while waiting for the state. 383 :raises: :class:`~openstack.exceptions.ResourceTimeout` if timeout 384 is reached while waiting for the state. 385 """ 386 session = self._get_session(session) 387 388 version = None 389 if target in _common.PROVISIONING_VERSIONS: 390 version = '1.%d' % _common.PROVISIONING_VERSIONS[target] 391 392 if config_drive: 393 # Some config drive actions require a higher version. 394 if isinstance(config_drive, dict): 395 version = _common.CONFIG_DRIVE_DICT_VERSION 396 elif target == 'rebuild': 397 version = _common.CONFIG_DRIVE_REBUILD_VERSION 398 399 if deploy_steps: 400 version = _common.DEPLOY_STEPS_VERSION 401 402 version = self._assert_microversion_for(session, 'commit', version) 403 404 body = {'target': target} 405 if config_drive: 406 if target not in ('active', 'rebuild'): 407 raise ValueError('Config drive can only be provided with ' 408 '"active" and "rebuild" targets') 409 # Not a typo - ironic accepts "configdrive" (without underscore) 410 body['configdrive'] = config_drive 411 412 if clean_steps is not None: 413 if target != 'clean': 414 raise ValueError('Clean steps can only be provided with ' 415 '"clean" target') 416 body['clean_steps'] = clean_steps 417 418 if deploy_steps is not None: 419 if target not in ('active', 'rebuild'): 420 raise ValueError('Deploy steps can only be provided with ' 421 '"deploy" and "rebuild" target') 422 body['deploy_steps'] = deploy_steps 423 424 if rescue_password is not None: 425 if target != 'rescue': 426 raise ValueError('Rescue password can only be provided with ' 427 '"rescue" target') 428 body['rescue_password'] = rescue_password 429 430 if wait: 431 try: 432 expected_state = _common.EXPECTED_STATES[target] 433 except KeyError: 434 raise ValueError('For target %s the expected state is not ' 435 'known, cannot wait for it' % target) 436 437 request = self._prepare_request(requires_id=True) 438 request.url = utils.urljoin(request.url, 'states', 'provision') 439 response = session.put( 440 request.url, json=body, 441 headers=request.headers, microversion=version, 442 retriable_status_codes=_common.RETRIABLE_STATUS_CODES) 443 444 msg = ("Failed to set provision state for bare metal node {node} " 445 "to {target}".format(node=self.id, target=target)) 446 exceptions.raise_from_response(response, error_message=msg) 447 448 if wait: 449 return self.wait_for_provision_state(session, 450 expected_state, 451 timeout=timeout) 452 else: 453 return self.fetch(session) 454 455 def wait_for_power_state(self, session, expected_state, timeout=None): 456 """Wait for the node to reach the expected power state. 457 458 :param session: The session to use for making this request. 459 :type session: :class:`~keystoneauth1.adapter.Adapter` 460 :param expected_state: The expected power state to reach. 461 :param timeout: If ``wait`` is set to ``True``, specifies how much (in 462 seconds) to wait for the expected state to be reached. The value of 463 ``None`` (the default) means no client-side timeout. 464 465 :return: This :class:`Node` instance. 466 :raises: :class:`~openstack.exceptions.ResourceTimeout` on timeout. 467 """ 468 for count in utils.iterate_timeout( 469 timeout, 470 "Timeout waiting for node %(node)s to reach " 471 "power state '%(state)s'" % {'node': self.id, 472 'state': expected_state}): 473 self.fetch(session) 474 if self.power_state == expected_state: 475 return self 476 477 session.log.debug( 478 'Still waiting for node %(node)s to reach power state ' 479 '"%(target)s", the current state is "%(state)s"', 480 {'node': self.id, 'target': expected_state, 481 'state': self.power_state}) 482 483 def wait_for_provision_state(self, session, expected_state, timeout=None, 484 abort_on_failed_state=True): 485 """Wait for the node to reach the expected state. 486 487 :param session: The session to use for making this request. 488 :type session: :class:`~keystoneauth1.adapter.Adapter` 489 :param expected_state: The expected provisioning state to reach. 490 :param timeout: If ``wait`` is set to ``True``, specifies how much (in 491 seconds) to wait for the expected state to be reached. The value of 492 ``None`` (the default) means no client-side timeout. 493 :param abort_on_failed_state: If ``True`` (the default), abort waiting 494 if the node reaches a failure state which does not match the 495 expected one. Note that the failure state for ``enroll`` -> 496 ``manageable`` transition is ``enroll`` again. 497 498 :return: This :class:`Node` instance. 499 :raises: :class:`~openstack.exceptions.ResourceFailure` if the node 500 reaches an error state and ``abort_on_failed_state`` is ``True``. 501 :raises: :class:`~openstack.exceptions.ResourceTimeout` on timeout. 502 """ 503 for count in utils.iterate_timeout( 504 timeout, 505 "Timeout waiting for node %(node)s to reach " 506 "target state '%(state)s'" % {'node': self.id, 507 'state': expected_state}): 508 self.fetch(session) 509 if self._check_state_reached(session, expected_state, 510 abort_on_failed_state): 511 return self 512 513 session.log.debug( 514 'Still waiting for node %(node)s to reach state ' 515 '"%(target)s", the current state is "%(state)s"', 516 {'node': self.id, 'target': expected_state, 517 'state': self.provision_state}) 518 519 def wait_for_reservation(self, session, timeout=None): 520 """Wait for a lock on the node to be released. 521 522 Bare metal nodes in ironic have a reservation lock that 523 is used to represent that a conductor has locked the node 524 while performing some sort of action, such as changing 525 configuration as a result of a machine state change. 526 527 This lock can occur during power syncronization, and prevents 528 updates to objects attached to the node, such as ports. 529 530 Note that nothing prevents a conductor from acquiring the lock again 531 after this call returns, so it should be treated as best effort. 532 533 Returns immediately if there is no reservation on the node. 534 535 :param session: The session to use for making this request. 536 :type session: :class:`~keystoneauth1.adapter.Adapter` 537 :param timeout: How much (in seconds) to wait for the lock to be 538 released. The value of ``None`` (the default) means no timeout. 539 540 :return: This :class:`Node` instance. 541 """ 542 if self.reservation is None: 543 return self 544 545 for count in utils.iterate_timeout( 546 timeout, 547 "Timeout waiting for the lock to be released on node %s" % 548 self.id): 549 self.fetch(session) 550 if self.reservation is None: 551 return self 552 553 session.log.debug( 554 'Still waiting for the lock to be released on node ' 555 '%(node)s, currently locked by conductor %(host)s', 556 {'node': self.id, 'host': self.reservation}) 557 558 def _check_state_reached(self, session, expected_state, 559 abort_on_failed_state=True): 560 """Wait for the node to reach the expected state. 561 562 :param session: The session to use for making this request. 563 :type session: :class:`~keystoneauth1.adapter.Adapter` 564 :param expected_state: The expected provisioning state to reach. 565 :param abort_on_failed_state: If ``True`` (the default), abort waiting 566 if the node reaches a failure state which does not match the 567 expected one. Note that the failure state for ``enroll`` -> 568 ``manageable`` transition is ``enroll`` again. 569 570 :return: ``True`` if the target state is reached 571 :raises: :class:`~openstack.exceptions.ResourceFailure` if the node 572 reaches an error state and ``abort_on_failed_state`` is ``True``. 573 """ 574 # NOTE(dtantsur): microversion 1.2 changed None to available 575 if (self.provision_state == expected_state 576 or (expected_state == 'available' 577 and self.provision_state is None)): 578 return True 579 elif not abort_on_failed_state: 580 return False 581 582 if (self.provision_state.endswith(' failed') 583 or self.provision_state == 'error'): 584 raise exceptions.ResourceFailure( 585 "Node %(node)s reached failure state \"%(state)s\"; " 586 "the last error is %(error)s" % 587 {'node': self.id, 'state': self.provision_state, 588 'error': self.last_error}) 589 # Special case: a failure state for "manage" transition can be 590 # "enroll" 591 elif (expected_state == 'manageable' 592 and self.provision_state == 'enroll' and self.last_error): 593 raise exceptions.ResourceFailure( 594 "Node %(node)s could not reach state manageable: " 595 "failed to verify management credentials; " 596 "the last error is %(error)s" % 597 {'node': self.id, 'error': self.last_error}) 598 599 def set_power_state(self, session, target, wait=False, timeout=None): 600 """Run an action modifying this node's power state. 601 602 This call is asynchronous, it will return success as soon as the Bare 603 Metal service acknowledges the request. 604 605 :param session: The session to use for making this request. 606 :type session: :class:`~keystoneauth1.adapter.Adapter` 607 :param target: Target power state, as a :class:`PowerAction` or 608 a string. 609 :param wait: Whether to wait for the expected power state to be 610 reached. 611 :param timeout: Timeout (in seconds) to wait for the target state to be 612 reached. If ``None``, wait without timeout. 613 """ 614 if isinstance(target, PowerAction): 615 target = target.value 616 if wait: 617 try: 618 expected = _common.EXPECTED_POWER_STATES[target] 619 except KeyError: 620 raise ValueError("Cannot use target power state %s with wait, " 621 "the expected state is not known" % target) 622 623 session = self._get_session(session) 624 625 if target.startswith("soft "): 626 version = '1.27' 627 else: 628 version = None 629 630 version = self._assert_microversion_for(session, 'commit', version) 631 632 # TODO(dtantsur): server timeout support 633 body = {'target': target} 634 635 request = self._prepare_request(requires_id=True) 636 request.url = utils.urljoin(request.url, 'states', 'power') 637 response = session.put( 638 request.url, json=body, 639 headers=request.headers, microversion=version, 640 retriable_status_codes=_common.RETRIABLE_STATUS_CODES) 641 642 msg = ("Failed to set power state for bare metal node {node} " 643 "to {target}".format(node=self.id, target=target)) 644 exceptions.raise_from_response(response, error_message=msg) 645 646 if wait: 647 self.wait_for_power_state(session, expected, timeout=timeout) 648 649 def attach_vif(self, session, vif_id, retry_on_conflict=True): 650 """Attach a VIF to the node. 651 652 The exact form of the VIF ID depends on the network interface used by 653 the node. In the most common case it is a Network service port 654 (NOT a Bare Metal port) ID. A VIF can only be attached to one node 655 at a time. 656 657 :param session: The session to use for making this request. 658 :type session: :class:`~keystoneauth1.adapter.Adapter` 659 :param string vif_id: Backend-specific VIF ID. 660 :param retry_on_conflict: Whether to retry HTTP CONFLICT errors. 661 This can happen when either the VIF is already used on a node or 662 the node is locked. Since the latter happens more often, the 663 default value is True. 664 :return: ``None`` 665 :raises: :exc:`~openstack.exceptions.NotSupported` if the server 666 does not support the VIF API. 667 """ 668 session = self._get_session(session) 669 version = self._assert_microversion_for( 670 session, 'commit', _common.VIF_VERSION, 671 error_message=("Cannot use VIF attachment API")) 672 673 request = self._prepare_request(requires_id=True) 674 request.url = utils.urljoin(request.url, 'vifs') 675 body = {'id': vif_id} 676 retriable_status_codes = _common.RETRIABLE_STATUS_CODES 677 if not retry_on_conflict: 678 retriable_status_codes = set(retriable_status_codes) - {409} 679 response = session.post( 680 request.url, json=body, 681 headers=request.headers, microversion=version, 682 retriable_status_codes=retriable_status_codes) 683 684 msg = ("Failed to attach VIF {vif} to bare metal node {node}" 685 .format(node=self.id, vif=vif_id)) 686 exceptions.raise_from_response(response, error_message=msg) 687 688 def detach_vif(self, session, vif_id, ignore_missing=True): 689 """Detach a VIF from the node. 690 691 The exact form of the VIF ID depends on the network interface used by 692 the node. In the most common case it is a Network service port 693 (NOT a Bare Metal port) ID. 694 695 :param session: The session to use for making this request. 696 :type session: :class:`~keystoneauth1.adapter.Adapter` 697 :param string vif_id: Backend-specific VIF ID. 698 :param bool ignore_missing: When set to ``False`` 699 :class:`~openstack.exceptions.ResourceNotFound` will be 700 raised when the VIF does not exist. Otherwise, ``False`` 701 is returned. 702 :return: ``True`` if the VIF was detached, otherwise ``False``. 703 :raises: :exc:`~openstack.exceptions.NotSupported` if the server 704 does not support the VIF API. 705 """ 706 session = self._get_session(session) 707 version = self._assert_microversion_for( 708 session, 'commit', _common.VIF_VERSION, 709 error_message=("Cannot use VIF attachment API")) 710 711 request = self._prepare_request(requires_id=True) 712 request.url = utils.urljoin(request.url, 'vifs', vif_id) 713 response = session.delete( 714 request.url, headers=request.headers, microversion=version, 715 retriable_status_codes=_common.RETRIABLE_STATUS_CODES) 716 717 if ignore_missing and response.status_code == 400: 718 session.log.debug( 719 'VIF %(vif)s was already removed from node %(node)s', 720 {'vif': vif_id, 'node': self.id}) 721 return False 722 723 msg = ("Failed to detach VIF {vif} from bare metal node {node}" 724 .format(node=self.id, vif=vif_id)) 725 exceptions.raise_from_response(response, error_message=msg) 726 return True 727 728 def list_vifs(self, session): 729 """List IDs of VIFs attached to the node. 730 731 The exact form of the VIF ID depends on the network interface used by 732 the node. In the most common case it is a Network service port 733 (NOT a Bare Metal port) ID. 734 735 :param session: The session to use for making this request. 736 :type session: :class:`~keystoneauth1.adapter.Adapter` 737 :return: List of VIF IDs as strings. 738 :raises: :exc:`~openstack.exceptions.NotSupported` if the server 739 does not support the VIF API. 740 """ 741 session = self._get_session(session) 742 version = self._assert_microversion_for( 743 session, 'fetch', _common.VIF_VERSION, 744 error_message=("Cannot use VIF attachment API")) 745 746 request = self._prepare_request(requires_id=True) 747 request.url = utils.urljoin(request.url, 'vifs') 748 response = session.get( 749 request.url, headers=request.headers, microversion=version) 750 751 msg = ("Failed to list VIFs attached to bare metal node {node}" 752 .format(node=self.id)) 753 exceptions.raise_from_response(response, error_message=msg) 754 return [vif['id'] for vif in response.json()['vifs']] 755 756 def validate(self, session, required=('boot', 'deploy', 'power')): 757 """Validate required information on a node. 758 759 :param session: The session to use for making this request. 760 :type session: :class:`~keystoneauth1.adapter.Adapter` 761 :param required: List of interfaces that are required to pass 762 validation. The default value is the list of minimum required 763 interfaces for provisioning. 764 765 :return: dict mapping interface names to :class:`ValidationResult` 766 objects. 767 :raises: :exc:`~openstack.exceptions.ValidationException` if validation 768 fails for a required interface. 769 """ 770 session = self._get_session(session) 771 version = self._get_microversion_for(session, 'fetch') 772 773 request = self._prepare_request(requires_id=True) 774 request.url = utils.urljoin(request.url, 'validate') 775 response = session.get(request.url, headers=request.headers, 776 microversion=version) 777 778 msg = ("Failed to validate node {node}".format(node=self.id)) 779 exceptions.raise_from_response(response, error_message=msg) 780 result = response.json() 781 782 if required: 783 failed = [ 784 '%s (%s)' % (key, value.get('reason', 'no reason')) 785 for key, value in result.items() 786 if key in required and not value.get('result') 787 ] 788 789 if failed: 790 raise exceptions.ValidationException( 791 'Validation failed for required interfaces of node {node}:' 792 ' {failures}'.format(node=self.id, 793 failures=', '.join(failed))) 794 795 return {key: ValidationResult(value.get('result'), value.get('reason')) 796 for key, value in result.items()} 797 798 def set_maintenance(self, session, reason=None): 799 """Enable maintenance mode on the node. 800 801 :param session: The session to use for making this request. 802 :type session: :class:`~keystoneauth1.adapter.Adapter` 803 :param reason: Optional reason for maintenance. 804 :return: This :class:`Node` instance. 805 """ 806 self._do_maintenance_action(session, 'put', {'reason': reason}) 807 return self.fetch(session) 808 809 def unset_maintenance(self, session): 810 """Disable maintenance mode on the node. 811 812 :param session: The session to use for making this request. 813 :type session: :class:`~keystoneauth1.adapter.Adapter` 814 :return: This :class:`Node` instance. 815 """ 816 self._do_maintenance_action(session, 'delete') 817 return self.fetch(session) 818 819 def _do_maintenance_action(self, session, verb, body=None): 820 session = self._get_session(session) 821 version = self._get_microversion_for(session, 'commit') 822 request = self._prepare_request(requires_id=True) 823 request.url = utils.urljoin(request.url, 'maintenance') 824 response = getattr(session, verb)( 825 request.url, json=body, 826 headers=request.headers, microversion=version) 827 msg = ("Failed to change maintenance mode for node {node}" 828 .format(node=self.id)) 829 exceptions.raise_from_response(response, error_message=msg) 830 831 def set_boot_device(self, session, boot_device, persistent=False): 832 """Set node boot device 833 834 :param session: The session to use for making this request. 835 :param boot_device: Boot device to assign to the node. 836 :param persistent: If the boot device change is maintained after node 837 reboot 838 :return: The updated :class:`~openstack.baremetal.v1.node.Node` 839 """ 840 session = self._get_session(session) 841 version = self._get_microversion_for(session, 'commit') 842 request = self._prepare_request(requires_id=True) 843 request.url = utils.urljoin(request.url, 'management', 'boot_device') 844 845 body = {'boot_device': boot_device, 'persistent': persistent} 846 847 response = session.put( 848 request.url, json=body, 849 headers=request.headers, microversion=version, 850 retriable_status_codes=_common.RETRIABLE_STATUS_CODES) 851 852 msg = ("Failed to set boot device for node {node}" 853 .format(node=self.id)) 854 exceptions.raise_from_response(response, error_message=msg) 855 856 def set_boot_mode(self, session, target): 857 """Make a request to change node's boot mode 858 859 This call is asynchronous, it will return success as soon as the Bare 860 Metal service acknowledges the request. 861 862 :param session: The session to use for making this request. 863 :param target: Boot mode to set for node, one of either 'uefi'/'bios'. 864 :raises: ValueError if ``target`` is not one of 'uefi or 'bios'. 865 """ 866 session = self._get_session(session) 867 version = utils.pick_microversion(session, 868 _common.CHANGE_BOOT_MODE_VERSION) 869 request = self._prepare_request(requires_id=True) 870 request.url = utils.urljoin(request.url, 'states', 'boot_mode') 871 if target not in ('uefi', 'bios'): 872 raise ValueError("Unrecognized boot mode %s." 873 "Boot mode should be one of 'uefi' or 'bios'." 874 % target) 875 body = {'target': target} 876 877 response = session.put( 878 request.url, json=body, 879 headers=request.headers, microversion=version, 880 retriable_status_codes=_common.RETRIABLE_STATUS_CODES) 881 882 msg = ("Failed to change boot mode for node {node}" 883 .format(node=self.id)) 884 exceptions.raise_from_response(response, error_message=msg) 885 886 def set_secure_boot(self, session, target): 887 """Make a request to change node's secure boot state 888 889 This call is asynchronous, it will return success as soon as the Bare 890 Metal service acknowledges the request. 891 892 :param session: The session to use for making this request. 893 :param bool target: Boolean indicating secure boot state to set. 894 True/False corresponding to 'on'/'off' respectively. 895 :raises: ValueError if ``target`` is not boolean. 896 """ 897 session = self._get_session(session) 898 version = utils.pick_microversion(session, 899 _common.CHANGE_BOOT_MODE_VERSION) 900 request = self._prepare_request(requires_id=True) 901 request.url = utils.urljoin(request.url, 'states', 'secure_boot') 902 if not isinstance(target, bool): 903 raise ValueError("Invalid target %s. It should be True or False " 904 "corresponding to secure boot state 'on' or 'off'" 905 % target) 906 body = {'target': target} 907 908 response = session.put( 909 request.url, json=body, 910 headers=request.headers, microversion=version, 911 retriable_status_codes=_common.RETRIABLE_STATUS_CODES) 912 913 msg = ("Failed to change secure boot state for {node}" 914 .format(node=self.id)) 915 exceptions.raise_from_response(response, error_message=msg) 916 917 def add_trait(self, session, trait): 918 """Add a trait to a node. 919 920 :param session: The session to use for making this request. 921 :param trait: The trait to add to the node. 922 :returns: The updated :class:`~openstack.baremetal.v1.node.Node` 923 """ 924 session = self._get_session(session) 925 version = utils.pick_microversion(session, '1.37') 926 request = self._prepare_request(requires_id=True) 927 request.url = utils.urljoin(request.url, 'traits', trait) 928 response = session.put( 929 request.url, json=None, 930 headers=request.headers, microversion=version, 931 retriable_status_codes=_common.RETRIABLE_STATUS_CODES) 932 933 msg = ("Failed to add trait {trait} for node {node}" 934 .format(trait=trait, node=self.id)) 935 exceptions.raise_from_response(response, error_message=msg) 936 937 self.traits = list(set(self.traits or ()) | {trait}) 938 939 def remove_trait(self, session, trait, ignore_missing=True): 940 """Remove a trait from a node. 941 942 :param session: The session to use for making this request. 943 :param trait: The trait to remove from the node. 944 :param bool ignore_missing: When set to ``False`` 945 :class:`~openstack.exceptions.ResourceNotFound` will be 946 raised when the trait does not exist. 947 Otherwise, ``False`` is returned. 948 :returns: The updated :class:`~openstack.baremetal.v1.node.Node` 949 """ 950 session = self._get_session(session) 951 version = utils.pick_microversion(session, '1.37') 952 request = self._prepare_request(requires_id=True) 953 request.url = utils.urljoin(request.url, 'traits', trait) 954 955 response = session.delete( 956 request.url, headers=request.headers, microversion=version, 957 retriable_status_codes=_common.RETRIABLE_STATUS_CODES) 958 959 if ignore_missing or response.status_code == 400: 960 session.log.debug( 961 'Trait %(trait)s was already removed from node %(node)s', 962 {'trait': trait, 'node': self.id}) 963 return False 964 965 msg = ("Failed to remove trait {trait} from bare metal node {node}" 966 .format(node=self.id, trait=trait)) 967 exceptions.raise_from_response(response, error_message=msg) 968 969 self.traits = list(set(self.traits) - {trait}) 970 971 return True 972 973 def set_traits(self, session, traits): 974 """Set traits for a node. 975 976 Removes any existing traits and adds the traits passed in to this 977 method. 978 979 :param session: The session to use for making this request. 980 :param traits: list of traits to add to the node. 981 :returns: The updated :class:`~openstack.baremetal.v1.node.Node` 982 """ 983 session = self._get_session(session) 984 version = utils.pick_microversion(session, '1.37') 985 request = self._prepare_request(requires_id=True) 986 request.url = utils.urljoin(request.url, 'traits') 987 988 body = {'traits': traits} 989 990 response = session.put( 991 request.url, json=body, 992 headers=request.headers, microversion=version, 993 retriable_status_codes=_common.RETRIABLE_STATUS_CODES) 994 995 msg = ("Failed to set traits for node {node}" 996 .format(node=self.id)) 997 exceptions.raise_from_response(response, error_message=msg) 998 999 self.traits = traits 1000 1001 def call_vendor_passthru(self, session, verb, method, body=None): 1002 """Call a vendor passthru method. 1003 1004 :param session: The session to use for making this request. 1005 :param verb: The HTTP verb, one of GET, SET, POST, DELETE. 1006 :param method: The method to call using vendor_passthru. 1007 :param body: The JSON body in the HTTP call. 1008 :returns: The HTTP response. 1009 """ 1010 session = self._get_session(session) 1011 version = self._get_microversion_for(session, 'commit') 1012 request = self._prepare_request(requires_id=True) 1013 request.url = utils.urljoin(request.url, 'vendor_passthru?method={}' 1014 .format(method)) 1015 1016 call = getattr(session, verb.lower()) 1017 response = call( 1018 request.url, json=body, 1019 headers=request.headers, microversion=version, 1020 retriable_status_codes=_common.RETRIABLE_STATUS_CODES) 1021 1022 msg = ("Failed to call vendor_passthru for node {node}, verb {verb}" 1023 " and method {method}" 1024 .format(node=self.id, verb=verb, method=method)) 1025 exceptions.raise_from_response(response, error_message=msg) 1026 1027 return response 1028 1029 def list_vendor_passthru(self, session): 1030 """List vendor passthru methods. 1031 1032 :param session: The session to use for making this request. 1033 :returns: The HTTP response. 1034 """ 1035 session = self._get_session(session) 1036 version = self._get_microversion_for(session, 'fetch') 1037 request = self._prepare_request(requires_id=True) 1038 request.url = utils.urljoin(request.url, 'vendor_passthru/methods') 1039 1040 response = session.get( 1041 request.url, headers=request.headers, microversion=version, 1042 retriable_status_codes=_common.RETRIABLE_STATUS_CODES) 1043 1044 msg = ("Failed to list vendor_passthru methods for node {node}" 1045 .format(node=self.id)) 1046 exceptions.raise_from_response(response, error_message=msg) 1047 1048 return response.json() 1049 1050 def patch(self, session, patch=None, prepend_key=True, has_body=True, 1051 retry_on_conflict=None, base_path=None, reset_interfaces=None): 1052 1053 if reset_interfaces is not None: 1054 # The id cannot be dirty for an commit 1055 self._body._dirty.discard("id") 1056 1057 # Only try to update if we actually have anything to commit. 1058 if not patch and not self.requires_commit: 1059 return self 1060 1061 if not self.allow_patch: 1062 raise exceptions.MethodNotSupported(self, "patch") 1063 1064 session = self._get_session(session) 1065 microversion = self._assert_microversion_for( 1066 session, 'commit', _common.RESET_INTERFACES_VERSION) 1067 params = [('reset_interfaces', reset_interfaces)] 1068 1069 request = self._prepare_request(requires_id=True, 1070 prepend_key=prepend_key, 1071 base_path=base_path, patch=True, 1072 params=params) 1073 1074 if patch: 1075 request.body += self._convert_patch(patch) 1076 1077 return self._commit(session, request, 'PATCH', microversion, 1078 has_body=has_body, 1079 retry_on_conflict=retry_on_conflict) 1080 1081 else: 1082 return super(Node, self).patch(session, patch=patch, 1083 retry_on_conflict=retry_on_conflict) 1084 1085 1086NodeDetail = Node 1087