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