#    Licensed under the Apache License, Version 2.0 (the "License"); you may
#    not use this file except in compliance with the License. You may obtain
#    a copy of the License at
#
#         http://www.apache.org/licenses/LICENSE-2.0
#
#    Unless required by applicable law or agreed to in writing, software
#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
#    License for the specific language governing permissions and limitations
#    under the License.
"""
Tests for the API /nodes/ methods.
"""

import datetime
from http import client as http_client
import json
import os
import sys
import tempfile
from unittest import mock
from urllib import parse as urlparse
import uuid

import fixtures
from oslo_config import cfg
from oslo_utils import timeutils
from oslo_utils import uuidutils
from testtools import matchers

from ironic.api.controllers import base as api_base
from ironic.api.controllers import v1 as api_v1
from ironic.api.controllers.v1 import node as api_node
from ironic.api.controllers.v1 import notification_utils
from ironic.api.controllers.v1 import utils as api_utils
from ironic.api.controllers.v1 import versions
from ironic.common import boot_devices
from ironic.common import boot_modes
from ironic.common import components
from ironic.common import driver_factory
from ironic.common import exception
from ironic.common import indicator_states
from ironic.common import policy
from ironic.common import states
from ironic.conductor import rpcapi
from ironic.conf import CONF
from ironic.drivers.modules import inspect_utils
from ironic import objects
from ironic.objects import fields as obj_fields
from ironic import tests as tests_root
from ironic.tests.unit.api import base as test_api_base
from ironic.tests.unit.api import utils as test_api_utils
from ironic.tests.unit.db import base as db_base
from ironic.tests.unit.objects import utils as obj_utils


with open(
        os.path.join(
            os.path.dirname(tests_root.__file__),
            'json_samples', 'network_data.json')) as fl:
    NETWORK_DATA = json.load(fl)


class TestListNodes(test_api_base.BaseApiTest):

    def setUp(self):
        super(TestListNodes, self).setUp()
        self.chassis = obj_utils.create_test_chassis(self.context)
        p = mock.patch.object(rpcapi.ConductorAPI, 'get_topic_for',
                              autospec=True)
        self.mock_gtf = p.start()
        self.mock_gtf.return_value = 'test-topic'
        self.addCleanup(p.stop)
        self.mock_get_conductor_for = self.useFixture(
            fixtures.MockPatchObject(rpcapi.ConductorAPI, 'get_conductor_for',
                                     autospec=True)).mock
        self.mock_get_conductor_for.return_value = 'fake.conductor'

    def _create_association_test_nodes(self):
        # create some unassociated nodes
        unassociated_nodes = []
        for id in range(3):
            node = obj_utils.create_test_node(self.context,
                                              uuid=uuidutils.generate_uuid())
            unassociated_nodes.append(node.uuid)

        # created some associated nodes
        associated_nodes = []
        for id in range(4):
            node = obj_utils.create_test_node(
                self.context, uuid=uuidutils.generate_uuid(),
                instance_uuid=uuidutils.generate_uuid())
            associated_nodes.append(node.uuid)
        return {'associated': associated_nodes,
                'unassociated': unassociated_nodes}

    def test_empty(self):
        data = self.get_json('/nodes')
        self.assertEqual([], data['nodes'])

    def test_one(self):
        node = obj_utils.create_test_node(self.context,
                                          chassis_id=self.chassis.id)
        data = self.get_json(
            '/nodes',
            headers={api_base.Version.string: str(api_v1.max_version())})
        self.assertIn('instance_uuid', data['nodes'][0])
        self.assertIn('maintenance', data['nodes'][0])
        self.assertIn('power_state', data['nodes'][0])
        self.assertIn('provision_state', data['nodes'][0])
        self.assertIn('uuid', data['nodes'][0])
        self.assertEqual(node.uuid, data['nodes'][0]["uuid"])
        self.assertNotIn('driver', data['nodes'][0])
        self.assertNotIn('driver_info', data['nodes'][0])
        self.assertNotIn('driver_internal_info', data['nodes'][0])
        self.assertNotIn('extra', data['nodes'][0])
        self.assertNotIn('properties', data['nodes'][0])
        self.assertNotIn('chassis_uuid', data['nodes'][0])
        self.assertNotIn('reservation', data['nodes'][0])
        self.assertNotIn('console_enabled', data['nodes'][0])
        self.assertNotIn('target_power_state', data['nodes'][0])
        self.assertNotIn('target_provision_state', data['nodes'][0])
        self.assertNotIn('provision_updated_at', data['nodes'][0])
        self.assertNotIn('maintenance_reason', data['nodes'][0])
        self.assertNotIn('clean_step', data['nodes'][0])
        self.assertNotIn('raid_config', data['nodes'][0])
        self.assertNotIn('target_raid_config', data['nodes'][0])
        self.assertNotIn('network_interface', data['nodes'][0])
        self.assertNotIn('resource_class', data['nodes'][0])
        for field in api_utils.V31_FIELDS:
            self.assertNotIn(field, data['nodes'][0])
        self.assertNotIn('storage_interface', data['nodes'][0])
        self.assertNotIn('traits', data['nodes'][0])
        # never expose the chassis_id
        self.assertNotIn('chassis_id', data['nodes'][0])
        self.assertNotIn('bios_interface', data['nodes'][0])
        self.assertNotIn('deploy_step', data['nodes'][0])
        self.assertNotIn('conductor_group', data['nodes'][0])
        self.assertNotIn('automated_clean', data['nodes'][0])
        self.assertNotIn('protected', data['nodes'][0])
        self.assertNotIn('protected_reason', data['nodes'][0])
        self.assertNotIn('owner', data['nodes'][0])
        self.assertNotIn('retired', data['nodes'][0])
        self.assertNotIn('retired_reason', data['nodes'][0])
        self.assertNotIn('lessee', data['nodes'][0])
        self.assertNotIn('network_data', data['nodes'][0])
        self.assertNotIn('service_steps', data['nodes'][0])

    @mock.patch.object(policy, 'check', autospec=True)
    @mock.patch.object(policy, 'check_policy', autospec=True)
    def test_one_field_specific_santization(self, mock_check_policy,
                                            mock_check):
        py_ver = sys.version_info
        if py_ver.major == 3 and py_ver.minor == 6:
            self.skipTest('Test fails to work on python 3.6 when '
                          'matching mock.ANY.')
        obj_utils.create_test_node(self.context,
                                   chassis_id=self.chassis.id,
                                   last_error='meow')
        mock_check_policy.return_value = False
        data = self.get_json(
            '/nodes?fields=uuid,provision_state,maintenance,instance_uuid,'
            'last_error',
            headers={api_base.Version.string: str(api_v1.max_version())})
        self.assertIn('uuid', data['nodes'][0])
        self.assertIn('provision_state', data['nodes'][0])
        self.assertIn('maintenance', data['nodes'][0])
        self.assertIn('instance_uuid', data['nodes'][0])
        self.assertNotIn('driver_info', data['nodes'][0])
        mock_check_policy.assert_has_calls([
            mock.call('baremetal:node:get:filter_threshold',
                      mock.ANY, mock.ANY)])
        mock_check.assert_has_calls([
            mock.call('show_password', mock.ANY, mock.ANY),
            mock.call('show_instance_secrets', mock.ANY, mock.ANY),
            # Last error is populated above and should trigger a check.
            mock.call('baremetal:node:get:last_error', mock.ANY, mock.ANY),
            mock.call().__bool__(),
            mock.call().__bool__(),
        ])

    def test_get_one(self):
        node = obj_utils.create_test_node(self.context,
                                          chassis_id=self.chassis.id)
        data = self.get_json(
            '/nodes/%s' % node.uuid,
            headers={api_base.Version.string: str(api_v1.max_version())})
        self.assertEqual(node.uuid, data['uuid'])
        self.assertIn('driver', data)
        self.assertIn('driver_info', data)
        self.assertEqual('******', data['driver_info']['fake_password'])
        self.assertEqual('bar', data['driver_info']['foo'])
        self.assertIn('driver_internal_info', data)
        self.assertIn('instance_info', data)
        self.assertEqual('******', data['instance_info']['configdrive'])
        self.assertEqual('******', data['instance_info']['image_url'])
        self.assertEqual('bar', data['instance_info']['foo'])
        self.assertIn('extra', data)
        self.assertIn('properties', data)
        self.assertIn('chassis_uuid', data)
        self.assertIn('reservation', data)
        self.assertIn('maintenance_reason', data)
        self.assertIn('name', data)
        self.assertIn('inspection_finished_at', data)
        self.assertIn('inspection_started_at', data)
        self.assertIn('clean_step', data)
        self.assertIn('states', data)
        self.assertIn('network_interface', data)
        self.assertIn('resource_class', data)
        for field in api_utils.V31_FIELDS:
            self.assertIn(field, data)
        self.assertIn('storage_interface', data)
        self.assertIn('traits', data)
        # never expose the chassis_id
        self.assertNotIn('chassis_id', data)
        self.assertIn('bios_interface', data)
        self.assertIn('deploy_step', data)
        self.assertIn('conductor_group', data)
        self.assertIn('automated_clean', data)
        self.assertIn('protected', data)
        self.assertIn('protected_reason', data)
        self.assertIn('owner', data)
        self.assertIn('lessee', data)
        self.assertNotIn('allocation_id', data)
        self.assertIn('allocation_uuid', data)
        self.assertIn('service_step', data)

    def test_get_one_configdrive_dict(self):
        fake_instance_info = {
            "configdrive": {'user_data': 'data'},
            "image_url": "http://example.com/test_image_url",
            "foo": "bar",
        }
        node = obj_utils.create_test_node(self.context,
                                          chassis_id=self.chassis.id,
                                          instance_info=fake_instance_info)
        data = self.get_json(
            '/nodes/%s' % node.uuid,
            headers={api_base.Version.string: str(api_v1.max_version())})
        self.assertEqual(node.uuid, data['uuid'])
        self.assertEqual('******', data['driver_info']['fake_password'])
        self.assertEqual('bar', data['driver_info']['foo'])
        self.assertEqual('******', data['instance_info']['configdrive'])
        self.assertEqual('******', data['instance_info']['image_url'])
        self.assertEqual('bar', data['instance_info']['foo'])

    def test_get_one_with_json(self):
        # Test backward compatibility with guess_content_type_from_ext
        node = obj_utils.create_test_node(self.context,
                                          chassis_id=self.chassis.id)
        data = self.get_json(
            '/nodes/%s.json' % node.uuid,
            headers={api_base.Version.string: str(api_v1.max_version())})
        self.assertEqual(node.uuid, data['uuid'])

    def test_get_one_with_json_in_name(self):
        # Test that it is possible to name a node ending with .json
        node = obj_utils.create_test_node(self.context,
                                          name='node.json',
                                          chassis_id=self.chassis.id)
        data = self.get_json(
            '/nodes/%s' % node.name,
            headers={api_base.Version.string: str(api_v1.max_version())})
        self.assertEqual(node.uuid, data['uuid'])

    def test_get_one_with_suffix(self):
        # This tests that we don't mess with mime-like suffixes
        node = obj_utils.create_test_node(self.context,
                                          name='test.1',
                                          chassis_id=self.chassis.id)
        data = self.get_json(
            '/nodes/%s' % node.name,
            headers={api_base.Version.string: str(api_v1.max_version())})
        self.assertEqual(node.uuid, data['uuid'])

    def test_get_one_with_double_json(self):
        # Check that .json is only stripped once
        node = obj_utils.create_test_node(self.context,
                                          name='node.json',
                                          chassis_id=self.chassis.id)
        data = self.get_json(
            '/nodes/%s.json' % node.name,
            headers={api_base.Version.string: str(api_v1.max_version())})
        self.assertEqual(node.uuid, data['uuid'])

    def _test_node_field_hidden_in_lower_version(self, field,
                                                 old_version, new_version):
        node = obj_utils.create_test_node(self.context)
        data = self.get_json(
            '/nodes/%s' % node.uuid,
            headers={api_base.Version.string: old_version})
        self.assertNotIn(field, data)
        data = self.get_json(
            '/nodes/%s' % node.uuid,
            headers={api_base.Version.string: new_version})
        self.assertIn(field, data)

    def test_node_states_field_hidden_in_lower_version(self):
        self._test_node_field_hidden_in_lower_version('states', '1.8', '1.14')

    def test_node_interface_fields_hidden_in_lower_version(self):
        node = obj_utils.create_test_node(self.context)
        data = self.get_json(
            '/nodes/%s' % node.uuid,
            headers={api_base.Version.string: '1.30'})
        for field in api_utils.V31_FIELDS:
            self.assertNotIn(field, data)

    def test_node_storage_interface_hidden_in_lower_version(self):
        self._test_node_field_hidden_in_lower_version('storage_interface',
                                                      '1.32', '1.33')

    def test_node_traits_hidden_in_lower_version(self):
        self._test_node_field_hidden_in_lower_version('traits', '1.36', '1.37')

    def test_node_bios_hidden_in_lower_version(self):
        self._test_node_field_hidden_in_lower_version('bios_interface',
                                                      '1.39', '1.40')

    def test_node_inspect_wait_state_between_api_versions(self):
        node = obj_utils.create_test_node(self.context,
                                          provision_state='inspect wait')
        lower_version_data = self.get_json(
            '/nodes/%s' % node.uuid,
            headers={api_base.Version.string: '1.38'})
        self.assertEqual('inspecting', lower_version_data['provision_state'])

        higher_version_data = self.get_json(
            '/nodes/%s' % node.uuid,
            headers={api_base.Version.string: '1.39'})
        self.assertEqual('inspect wait',
                         higher_version_data['provision_state'])

    def test_node_fault_hidden_in_lower_version(self):
        self._test_node_field_hidden_in_lower_version('fault',
                                                      '1.41', '1.42')

    def test_node_deploy_step_hidden_in_lower_version(self):
        self._test_node_field_hidden_in_lower_version('deploy_step',
                                                      '1.43', '1.44')

    def test_node_conductor_group_hidden_in_lower_version(self):
        self._test_node_field_hidden_in_lower_version('conductor_group',
                                                      '1.45', '1.46')

    def test_node_automated_clean_hidden_in_lower_version(self):
        self._test_node_field_hidden_in_lower_version('automated_clean',
                                                      '1.46', '1.47')

    def test_node_automated_clean_null_field(self):
        node = obj_utils.create_test_node(self.context, automated_clean=None)
        data = self.get_json('/nodes/%s' % node.uuid,
                             headers={api_base.Version.string: '1.47'})
        self.assertIsNone(data['automated_clean'])

    def test_node_automated_clean_true_field(self):
        node = obj_utils.create_test_node(self.context, automated_clean=True)
        data = self.get_json('/nodes/%s' % node.uuid,
                             headers={api_base.Version.string: '1.47'})
        self.assertEqual(data['automated_clean'], True)

    def test_node_automated_clean_false_field(self):
        node = obj_utils.create_test_node(self.context, automated_clean=False)
        data = self.get_json('/nodes/%s' % node.uuid,
                             headers={api_base.Version.string: '1.47'})
        self.assertEqual(data['automated_clean'], False)

    def test_node_protected_hidden_in_lower_version(self):
        self._test_node_field_hidden_in_lower_version('protected',
                                                      '1.47', '1.48')

    def test_node_protected_reason_hidden_in_lower_version(self):
        self._test_node_field_hidden_in_lower_version('protected_reason',
                                                      '1.47', '1.48')

    def test_node_conductor_hidden_in_lower_version(self):
        self._test_node_field_hidden_in_lower_version('conductor',
                                                      '1.48', '1.49')

    def test_node_protected(self):
        for value in (True, False):
            node = obj_utils.create_test_node(self.context, protected=value,
                                              provision_state='active',
                                              uuid=uuidutils.generate_uuid())
            data = self.get_json('/nodes/%s' % node.uuid,
                                 headers={api_base.Version.string: '1.48'})
            self.assertIs(data['protected'], value)
            self.assertIsNone(data['protected_reason'])

    def test_node_protected_with_reason(self):
        node = obj_utils.create_test_node(self.context, protected=True,
                                          provision_state='active',
                                          protected_reason='reason!')
        data = self.get_json('/nodes/%s' % node.uuid,
                             headers={api_base.Version.string: '1.48'})
        self.assertTrue(data['protected'])
        self.assertEqual('reason!', data['protected_reason'])

    def test_node_owner_hidden_in_lower_version(self):
        self._test_node_field_hidden_in_lower_version('owner',
                                                      '1.49', '1.50')

    def test_node_owner_null_field(self):
        node = obj_utils.create_test_node(self.context, owner=None)
        data = self.get_json('/nodes/%s' % node.uuid,
                             headers={api_base.Version.string: '1.50'})
        self.assertIsNone(data['owner'])

    def test_node_owner_present(self):
        node = obj_utils.create_test_node(self.context,
                                          owner="akindofmagic")
        data = self.get_json('/nodes/%s' % node.uuid,
                             headers={api_base.Version.string: '1.50'})
        self.assertEqual(data['owner'], "akindofmagic")

    def test_node_description_null_field(self):
        node = obj_utils.create_test_node(self.context, description=None)
        data = self.get_json('/nodes/%s' % node.uuid,
                             headers={api_base.Version.string: '1.51'})
        self.assertIsNone(data['description'])

    def test_node_retired_hidden_in_lower_version(self):
        self._test_node_field_hidden_in_lower_version('retired',
                                                      '1.60', '1.61')

    def test_node_retired_reason_hidden_in_lower_version(self):
        self._test_node_field_hidden_in_lower_version('retired_reason',
                                                      '1.60', '1.61')

    def test_node_retired(self):
        for value in (True, False):
            node = obj_utils.create_test_node(self.context, retired=value,
                                              provision_state='active',
                                              uuid=uuidutils.generate_uuid())
            data = self.get_json('/nodes/%s' % node.uuid,
                                 headers={api_base.Version.string: '1.61'})
            self.assertIs(data['retired'], value)
            self.assertIsNone(data['retired_reason'])

    def test_node_retired_with_reason(self):
        node = obj_utils.create_test_node(self.context, retired=True,
                                          provision_state='active',
                                          retired_reason='warranty expired')
        data = self.get_json('/nodes/%s' % node.uuid,
                             headers={api_base.Version.string: '1.61'})
        self.assertTrue(data['retired'])
        self.assertEqual('warranty expired', data['retired_reason'])

    def test_node_lessee_hidden_in_lower_version(self):
        self._test_node_field_hidden_in_lower_version('lessee',
                                                      '1.64', '1.65')

    def test_node_lessee_null_field(self):
        node = obj_utils.create_test_node(self.context, lessee=None)
        data = self.get_json('/nodes/%s' % node.uuid,
                             headers={api_base.Version.string: '1.65'})
        self.assertIsNone(data['lessee'])

    def test_node_lessee_present(self):
        node = obj_utils.create_test_node(self.context,
                                          lessee="some-lucky-project")
        data = self.get_json('/nodes/%s' % node.uuid,
                             headers={api_base.Version.string: '1.65'})
        self.assertEqual(data['lessee'], "some-lucky-project")

    def test_node_network_data_hidden_in_lower_version(self):
        self._test_node_field_hidden_in_lower_version('network_data',
                                                      '1.65', '1.66')

    def test_node_network_data(self):
        node = obj_utils.create_test_node(
            self.context, network_data=NETWORK_DATA,
            provision_state='active',
            uuid=uuidutils.generate_uuid())
        data = self.get_json('/nodes/%s' % node.uuid,
                             headers={api_base.Version.string: '1.66'})
        self.assertEqual(data['network_data'], NETWORK_DATA)

    def test_node_boot_mode_hidden_in_lower_version(self):
        self._test_node_field_hidden_in_lower_version('boot_mode',
                                                      '1.74', '1.75')

    def test_node_secure_boot_hidden_in_lower_version(self):
        self._test_node_field_hidden_in_lower_version('secure_boot',
                                                      '1.74', '1.75')

    def test_node_boot_mode_null_field(self):
        node = obj_utils.create_test_node(self.context)
        data = self.get_json('/nodes/%s' % node.uuid,
                             headers={api_base.Version.string: '1.75'})
        self.assertIsNone(data['boot_mode'])
        self.assertIsNone(data['secure_boot'])

    def test_node_boot_mode(self):
        for value in ('bios', 'uefi'):
            node = obj_utils.create_test_node(self.context,
                                              boot_mode=value,
                                              uuid=uuidutils.generate_uuid())
            data = self.get_json('/nodes/%s' % node.uuid,
                                 headers={api_base.Version.string: '1.75'})
            self.assertEqual(data['boot_mode'], value)
            self.assertIsNone(data['secure_boot'])

    def test_node_secure_boot(self):
        for value in (True, False):
            node = obj_utils.create_test_node(self.context,
                                              boot_mode='uefi',
                                              secure_boot=value,
                                              uuid=uuidutils.generate_uuid())
            data = self.get_json('/nodes/%s' % node.uuid,
                                 headers={api_base.Version.string: '1.75'})
            self.assertEqual(data['boot_mode'], 'uefi')
            self.assertEqual(data['secure_boot'], value)

    def test_get_one_custom_fields(self):
        node = obj_utils.create_test_node(self.context,
                                          chassis_id=self.chassis.id)
        fields = 'extra,instance_info'
        data = self.get_json(
            '/nodes/%s?fields=%s' % (node.uuid, fields),
            headers={api_base.Version.string: str(api_v1.max_version())})
        # We always append "links"
        self.assertCountEqual(['extra', 'instance_info', 'links'], data)

    def test_get_collection_custom_fields(self):
        fields = 'uuid,instance_info'
        for i in range(3):
            obj_utils.create_test_node(self.context,
                                       uuid=uuidutils.generate_uuid(),
                                       instance_uuid=uuidutils.generate_uuid())

        data = self.get_json(
            '/nodes?fields=%s' % fields,
            headers={api_base.Version.string: str(api_v1.max_version())})

        self.assertEqual(3, len(data['nodes']))
        for node in data['nodes']:
            # We always append "links"
            self.assertCountEqual(['uuid', 'instance_info', 'links'], node)

    def test_get_collection_fields_for_nova(self):
        # Unit test which explicitly attempts to request traits along with
        # all columns which nova would normally request as part of it's sync
        # process.
        fields = ('uuid,power_state,target_power_state,provision_state,'
                  'target_provision_state,last_error,maintenance,'
                  'instance_uuid,traits,resource_class')
        trait_list = ['CUSTOM_TRAIT1', 'CUSTOM_RAID5']
        for i in range(3):
            node = obj_utils.create_test_node(
                self.context, uuid=uuidutils.generate_uuid(),
                instance_uuid=uuidutils.generate_uuid())
            self.dbapi.set_node_traits(node.id, trait_list, '1.0')

        data = self.get_json(
            '/nodes?fields=%s' % fields,
            headers={api_base.Version.string: str(api_v1.max_version())})

        self.assertEqual(3, len(data['nodes']))
        for node in data['nodes']:
            # We always append "links"
            self.assertCountEqual([
                'uuid', 'power_state', 'target_power_state',
                'provision_state', 'target_provision_state', 'last_error',
                'maintenance', 'instance_uuid', 'traits', 'resource_class',
                'links'], node)
            # Ensure traits exists that have been created on the node.
            # Sorted traits list is an artifact of the data being inserted
            # as distinct rows and then collected back into a list.
            # There is no contract around ordering for the results of traits.
            self.assertEqual(sorted(trait_list, reverse=False), node['traits'])

    def test_get_custom_fields_invalid_fields(self):
        node = obj_utils.create_test_node(self.context,
                                          chassis_id=self.chassis.id)
        fields = 'uuid,spongebob'
        response = self.get_json(
            '/nodes/%s?fields=%s' % (node.uuid, fields),
            headers={api_base.Version.string: str(api_v1.max_version())},
            expect_errors=True)
        self.assertEqual(http_client.BAD_REQUEST, response.status_int)
        self.assertEqual('application/json', response.content_type)
        self.assertIn('spongebob', response.json['error_message'])

    def test_get_custom_fields_invalid_api_version(self):
        node = obj_utils.create_test_node(self.context,
                                          chassis_id=self.chassis.id)
        fields = 'uuid,extra'
        response = self.get_json(
            '/nodes/%s?fields=%s' % (node.uuid, fields),
            headers={api_base.Version.string: str(api_v1.min_version())},
            expect_errors=True)
        self.assertEqual(http_client.NOT_ACCEPTABLE, response.status_int)

    def test_get_one_custom_fields_show_password(self):
        node = obj_utils.create_test_node(self.context,
                                          chassis_id=self.chassis.id,
                                          driver_info={'fake_password': 'bar'})
        fields = 'driver_info'
        data = self.get_json(
            '/nodes/%s?fields=%s' % (node.uuid, fields),
            headers={api_base.Version.string: str(api_v1.max_version())})
        # We always append "links"
        self.assertCountEqual(['driver_info', 'links'], data)
        self.assertEqual('******', data['driver_info']['fake_password'])

    def test_get_one_with_deleted_chassis(self):
        node = obj_utils.create_test_node(self.context,
                                          chassis_id=self.chassis.id)
        with mock.patch.object(self.dbapi,
                               'get_chassis_by_id',
                               autospec=True) as mock_gc:
            # Explicitly return a chassis not found, and make sure the API
            # hides this from the API consumer as this is likely just an
            # in-flight deletion across multiple DB sessions or different
            # API surfaces (or, just slow DB replication.)
            mock_gc.side_effect = exception.ChassisNotFound(
                chassis=self.chassis.id)
            data = self.get_json(
                '/nodes/%s' % node.uuid,
                headers={api_base.Version.string: str(api_v1.max_version())})
        self.assertIsNone(data['chassis_uuid'])

    def test_get_network_interface_fields_invalid_api_version(self):
        node = obj_utils.create_test_node(self.context,
                                          chassis_id=self.chassis.id)
        fields = 'network_interface'
        response = self.get_json(
            '/nodes/%s?fields=%s' % (node.uuid, fields),
            headers={api_base.Version.string: str('1.19')},
            expect_errors=True)
        self.assertEqual(http_client.NOT_ACCEPTABLE, response.status_int)

    def test_get_network_interface_fields(self):
        node = obj_utils.create_test_node(self.context,
                                          chassis_id=self.chassis.id)
        fields = 'network_interface'
        response = self.get_json(
            '/nodes/%s?fields=%s' % (node.uuid, fields),
            headers={api_base.Version.string: str(api_v1.max_version())})
        self.assertIn('network_interface', response)

    def test_get_all_interface_fields_invalid_api_version(self):
        node = obj_utils.create_test_node(self.context,
                                          chassis_id=self.chassis.id)
        fields_arg = ','.join(api_utils.V31_FIELDS)
        response = self.get_json(
            '/nodes/%s?fields=%s' % (node.uuid, fields_arg),
            headers={api_base.Version.string: '1.30'},
            expect_errors=True)
        self.assertEqual(http_client.NOT_ACCEPTABLE, response.status_int)

    def test_get_all_interface_fields(self):
        node = obj_utils.create_test_node(self.context,
                                          chassis_id=self.chassis.id)
        fields_arg = ','.join(api_utils.V31_FIELDS)
        response = self.get_json(
            '/nodes/%s?fields=%s' % (node.uuid, fields_arg),
            headers={api_base.Version.string: str(api_v1.max_version())})
        for field in api_utils.V31_FIELDS:
            self.assertIn(field, response)

    def test_get_storage_interface_fields_invalid_api_version(self):
        node = obj_utils.create_test_node(self.context,
                                          chassis_id=self.chassis.id)
        fields = 'storage_interface'
        response = self.get_json(
            '/nodes/%s?fields=%s' % (node.uuid, fields),
            headers={api_base.Version.string: '1.32'},
            expect_errors=True)
        self.assertEqual(http_client.NOT_ACCEPTABLE, response.status_int)

    def test_get_storage_interface_fields(self):
        node = obj_utils.create_test_node(self.context,
                                          chassis_id=self.chassis.id)
        fields = 'storage_interface'
        response = self.get_json(
            '/nodes/%s?fields=%s' % (node.uuid, fields),
            headers={api_base.Version.string: str(api_v1.max_version())})
        self.assertIn('storage_interface', response)

    def test_get_traits_fields_invalid_api_version(self):
        node = obj_utils.create_test_node(self.context,
                                          chassis_id=self.chassis.id)
        fields = 'traits'
        response = self.get_json(
            '/nodes/%s?fields=%s' % (node.uuid, fields),
            headers={api_base.Version.string: '1.36'},
            expect_errors=True)
        self.assertEqual(http_client.NOT_ACCEPTABLE, response.status_int)

    def test_get_traits_fields(self):
        node = obj_utils.create_test_node(self.context,
                                          chassis_id=self.chassis.id)
        fields = 'traits'
        response = self.get_json(
            '/nodes/%s?fields=%s' % (node.uuid, fields),
            headers={api_base.Version.string: str(api_v1.max_version())})
        self.assertIn('traits', response)

    def test_get_conductor_group_fields_invalid_api_version(self):
        node = obj_utils.create_test_node(self.context,
                                          chassis_id=self.chassis.id)
        fields = 'conductor_group'
        response = self.get_json(
            '/nodes/%s?fields=%s' % (node.uuid, fields),
            headers={api_base.Version.string: '1.45'},
            expect_errors=True)
        self.assertEqual(http_client.NOT_ACCEPTABLE, response.status_int)

    def test_get_conductor_group_fields(self):
        node = obj_utils.create_test_node(self.context,
                                          chassis_id=self.chassis.id)
        fields = 'conductor_group'
        response = self.get_json(
            '/nodes/%s?fields=%s' % (node.uuid, fields),
            headers={api_base.Version.string: '1.46'})
        self.assertIn('conductor_group', response)

    def test_get_automated_clean_fields(self):
        node = obj_utils.create_test_node(self.context,
                                          automated_clean=True)
        fields = 'automated_clean'
        response = self.get_json('/nodes/%s?fields=%s' % (node.uuid, fields),
                                 headers={api_base.Version.string: '1.47'})
        self.assertIn('automated_clean', response)

    def test_get_protected_fields(self):
        node = obj_utils.create_test_node(self.context,
                                          protected=True)
        response = self.get_json('/nodes/%s?fields=%s' %
                                 (node.uuid, 'protected'),
                                 headers={api_base.Version.string: '1.48'})
        self.assertIn('protected', response)

    def test_get_conductor_field_invalid_api_version(self):
        node = obj_utils.create_test_node(self.context,
                                          chassis_id=self.chassis.id)
        fields = 'conductor'
        response = self.get_json(
            '/nodes/%s?fields=%s' % (node.uuid, fields),
            headers={api_base.Version.string: '1.48'},
            expect_errors=True)
        self.assertEqual(http_client.NOT_ACCEPTABLE, response.status_int)

    def test_get_conductor_field(self):
        node = obj_utils.create_test_node(self.context,
                                          chassis_id=self.chassis.id)
        fields = 'conductor'
        response = self.get_json(
            '/nodes/%s?fields=%s' % (node.uuid, fields),
            headers={api_base.Version.string: '1.49'})
        self.assertIn('conductor', response)

    def test_get_owner_fields(self):
        node = obj_utils.create_test_node(self.context, owner='fred')
        fields = 'owner'
        response = self.get_json('/nodes/%s?fields=%s' % (node.uuid, fields),
                                 headers={api_base.Version.string: '1.50'})
        self.assertIn('owner', response)

    def test_get_description_field(self):
        node = obj_utils.create_test_node(self.context,
                                          description='useful piece')
        fields = 'description'
        response = self.get_json('/nodes/%s?fields=%s' % (node.uuid, fields),
                                 headers={api_base.Version.string: '1.51'})
        self.assertIn('description', response)

    def test_get_lessee_field(self):
        node = obj_utils.create_test_node(self.context,
                                          lessee='some-lucky-project')
        fields = 'lessee'
        response = self.get_json('/nodes/%s?fields=%s' % (node.uuid, fields),
                                 headers={api_base.Version.string: '1.65'})
        self.assertIn('lessee', response)

    def test_get_with_allocation(self):
        allocation = obj_utils.create_test_allocation(self.context)
        node = obj_utils.create_test_node(self.context,
                                          allocation_id=allocation.id)
        fields = 'allocation_uuid'
        response = self.get_json('/nodes/%s?fields=%s' % (node.uuid, fields),
                                 headers={api_base.Version.string: '1.52'})
        self.assertEqual(allocation.uuid, response['allocation_uuid'])

    def test_get_retired_fields(self):
        node = obj_utils.create_test_node(self.context,
                                          retired=True)
        response = self.get_json('/nodes/%s?fields=%s' %
                                 (node.uuid, 'retired'),
                                 headers={api_base.Version.string: '1.61'})
        self.assertIn('retired', response)

    def test_get_one_with_no_agent_secret(self):
        node = obj_utils.create_test_node(
            self.context,
            driver_internal_info={'agent_secret_token': 'abcdefg'})
        response = self.get_json('/nodes/%s' % (node.uuid),
                                 headers={api_base.Version.string: '1.52'})
        token_value = response['driver_internal_info']['agent_secret_token']
        self.assertEqual('******', token_value)

    def test_detail(self):
        node = obj_utils.create_test_node(self.context,
                                          chassis_id=self.chassis.id)
        data = self.get_json(
            '/nodes/detail',
            headers={api_base.Version.string: str(api_v1.max_version())})
        self.assertEqual(node.uuid, data['nodes'][0]["uuid"])
        self.assertIn('name', data['nodes'][0])
        self.assertIn('driver', data['nodes'][0])
        self.assertIn('driver_info', data['nodes'][0])
        self.assertIn('extra', data['nodes'][0])
        self.assertIn('properties', data['nodes'][0])
        self.assertIn('chassis_uuid', data['nodes'][0])
        self.assertIn('reservation', data['nodes'][0])
        self.assertIn('maintenance', data['nodes'][0])
        self.assertIn('console_enabled', data['nodes'][0])
        self.assertIn('target_power_state', data['nodes'][0])
        self.assertIn('target_provision_state', data['nodes'][0])
        self.assertIn('provision_updated_at', data['nodes'][0])
        self.assertIn('inspection_finished_at', data['nodes'][0])
        self.assertIn('inspection_started_at', data['nodes'][0])
        self.assertIn('raid_config', data['nodes'][0])
        self.assertIn('target_raid_config', data['nodes'][0])
        self.assertIn('network_interface', data['nodes'][0])
        self.assertIn('resource_class', data['nodes'][0])
        for field in api_utils.V31_FIELDS:
            self.assertIn(field, data['nodes'][0])
        self.assertIn('storage_interface', data['nodes'][0])
        self.assertIn('traits', data['nodes'][0])
        self.assertIn('conductor_group', data['nodes'][0])
        self.assertIn('automated_clean', data['nodes'][0])
        self.assertIn('protected', data['nodes'][0])
        self.assertIn('protected_reason', data['nodes'][0])
        self.assertIn('owner', data['nodes'][0])
        self.assertIn('lessee', data['nodes'][0])
        # never expose the chassis_id
        self.assertNotIn('chassis_id', data['nodes'][0])
        self.assertNotIn('allocation_id', data['nodes'][0])
        self.assertIn('allocation_uuid', data['nodes'][0])
        self.assertIn('retired', data['nodes'][0])
        self.assertIn('retired_reason', data['nodes'][0])
        self.assertIn('network_data', data['nodes'][0])

    def test_detail_snmpv3(self):
        driver_info = {
            'snmp_version': 3,
            'snmp_user': 'test-user',
            'snmp_auth_protocol': 'sha',
            'snmp_auth_key': 'test-auth-key',
            'snmp_priv_protocol': 'aes',
            'snmp_priv_key': 'test-priv-key'
        }
        sanitized_driver_info = driver_info.copy()
        sanitized_driver_info['snmp_auth_key'] = '******'
        sanitized_driver_info['snmp_priv_key'] = '******'

        node = obj_utils.create_test_node(self.context,
                                          chassis_id=self.chassis.id,
                                          driver_info=driver_info)
        data = self.get_json(
            '/nodes/detail',
            headers={api_base.Version.string: str(api_v1.max_version())})
        self.assertEqual(node.uuid, data['nodes'][0]["uuid"])
        self.assertIn('name', data['nodes'][0])
        self.assertIn('driver', data['nodes'][0])
        self.assertIn('driver_info', data['nodes'][0])
        self.assertEqual(sanitized_driver_info,
                         data['nodes'][0]['driver_info'])
        self.assertIn('extra', data['nodes'][0])
        self.assertIn('properties', data['nodes'][0])
        self.assertIn('chassis_uuid', data['nodes'][0])
        self.assertIn('reservation', data['nodes'][0])
        self.assertIn('maintenance', data['nodes'][0])
        self.assertIn('console_enabled', data['nodes'][0])
        self.assertIn('target_power_state', data['nodes'][0])
        self.assertIn('target_provision_state', data['nodes'][0])
        self.assertIn('provision_updated_at', data['nodes'][0])
        self.assertIn('inspection_finished_at', data['nodes'][0])
        self.assertIn('inspection_started_at', data['nodes'][0])
        self.assertIn('raid_config', data['nodes'][0])
        self.assertIn('target_raid_config', data['nodes'][0])
        self.assertIn('network_interface', data['nodes'][0])
        self.assertIn('resource_class', data['nodes'][0])
        for field in api_utils.V31_FIELDS:
            self.assertIn(field, data['nodes'][0])
        self.assertIn('storage_interface', data['nodes'][0])
        self.assertIn('traits', data['nodes'][0])
        self.assertIn('conductor_group', data['nodes'][0])
        self.assertIn('automated_clean', data['nodes'][0])
        self.assertIn('protected', data['nodes'][0])
        self.assertIn('protected_reason', data['nodes'][0])
        self.assertIn('owner', data['nodes'][0])
        self.assertIn('lessee', data['nodes'][0])
        # never expose the chassis_id
        self.assertNotIn('chassis_id', data['nodes'][0])
        self.assertNotIn('allocation_id', data['nodes'][0])
        self.assertIn('allocation_uuid', data['nodes'][0])
        self.assertIn('retired', data['nodes'][0])
        self.assertIn('retired_reason', data['nodes'][0])
        self.assertIn('network_data', data['nodes'][0])

    def test_detail_instance_uuid(self):
        instance_uuid = '6eccd391-961c-4da5-b3c5-e2fa5cfbbd9d'
        node = obj_utils.create_test_node(
            self.context,
            instance_uuid=instance_uuid)
        data = self.get_json(
            '/nodes/detail?instance_uuid=%s' % instance_uuid,
            headers={api_base.Version.string: str(api_v1.max_version())})
        self.assertEqual(1, len(data['nodes']))
        self.assertEqual(node.uuid, data['nodes'][0]["uuid"])
        expected_fields = [
            'name', 'driver', 'driver_info', 'extra', 'chassis_uuid',
            'reservation', 'maintenance', 'console_enabled',
            'target_power_state', 'target_provision_state',
            'provision_updated_at', 'inspection_finished_at',
            'inspection_started_at', 'raid_config', 'target_raid_config',
            'network_interface', 'resource_class', 'owner', 'lessee',
            'storage_interface', 'traits', 'automated_clean',
            'conductor_group', 'protected', 'protected_reason',
            'retired', 'retired_reason', 'allocation_uuid', 'network_data'
        ]

        for field in expected_fields:
            self.assertIn(field, data['nodes'][0])
        for field in api_utils.V31_FIELDS:
            self.assertIn(field, data['nodes'][0])
        # never expose the chassis_id
        self.assertNotIn('chassis_id', data['nodes'][0])
        self.assertNotIn('allocation_id', data['nodes'][0])
        # no pagination marker should be present
        self.assertNotIn('next', data)

    @mock.patch.object(policy, 'authorize', spec=True)
    def test_detail_instance_uuid_project_not_match(self, mock_authorize):
        def mock_authorize_function(rule, target, creds):
            if rule == 'baremetal:node:list_all':
                raise exception.HTTPForbidden(resource='fake')
            return True
        mock_authorize.side_effect = mock_authorize_function

        instance_uuid = '6eccd391-961c-4da5-b3c5-e2fa5cfbbd9d'
        requestor_uuid = '46c0bf8a-846d-49a5-9724-5a61a5efa6bf'
        obj_utils.create_test_node(
            self.context,
            owner='97879042-c0bf-4216-882a-66a7cbf2bd74',
            instance_uuid=instance_uuid)
        data = self.get_json(
            '/nodes/detail?instance_uuid=%s' % instance_uuid,
            headers={'X-Project-ID': requestor_uuid,
                     api_base.Version.string: str(api_v1.max_version())})
        self.assertEqual(0, len(data['nodes']))

    @mock.patch.object(policy, 'authorize', spec=True)
    def test_detail_instance_uuid_project_match(self, mock_authorize):
        def mock_authorize_function(rule, target, creds):
            if rule == 'baremetal:node:list_all':
                raise exception.HTTPForbidden(resource='fake')
            return True
        mock_authorize.side_effect = mock_authorize_function

        instance_uuid = '6eccd391-961c-4da5-b3c5-e2fa5cfbbd9d'
        requestor_uuid = '46c0bf8a-846d-49a5-9724-5a61a5efa6bf'
        node = obj_utils.create_test_node(
            self.context,
            owner=requestor_uuid,
            instance_uuid=instance_uuid)
        data = self.get_json(
            '/nodes/detail?instance_uuid=%s' % instance_uuid,
            headers={'X-Project-ID': requestor_uuid,
                     api_base.Version.string: str(api_v1.max_version())})
        self.assertEqual(1, len(data['nodes']))
        # Assert we did get the node and it matched.
        self.assertEqual(node.uuid, data['nodes'][0]["uuid"])
        self.assertEqual(node.owner, data['nodes'][0]["owner"])

    def test_detail_using_query(self):
        node = obj_utils.create_test_node(self.context,
                                          chassis_id=self.chassis.id)
        data = self.get_json(
            '/nodes?detail=True',
            headers={api_base.Version.string: str(api_v1.max_version())})
        self.assertEqual(node.uuid, data['nodes'][0]["uuid"])
        self.assertIn('name', data['nodes'][0])
        self.assertIn('driver', data['nodes'][0])
        self.assertIn('driver_info', data['nodes'][0])
        self.assertIn('extra', data['nodes'][0])
        self.assertIn('properties', data['nodes'][0])
        self.assertIn('chassis_uuid', data['nodes'][0])
        self.assertIn('reservation', data['nodes'][0])
        self.assertIn('maintenance', data['nodes'][0])
        self.assertIn('console_enabled', data['nodes'][0])
        self.assertIn('target_power_state', data['nodes'][0])
        self.assertIn('target_provision_state', data['nodes'][0])
        self.assertIn('provision_updated_at', data['nodes'][0])
        self.assertIn('inspection_finished_at', data['nodes'][0])
        self.assertIn('inspection_started_at', data['nodes'][0])
        self.assertIn('raid_config', data['nodes'][0])
        self.assertIn('target_raid_config', data['nodes'][0])
        self.assertIn('network_interface', data['nodes'][0])
        self.assertIn('resource_class', data['nodes'][0])
        self.assertIn('conductor_group', data['nodes'][0])
        self.assertIn('automated_clean', data['nodes'][0])
        self.assertIn('protected', data['nodes'][0])
        self.assertIn('protected_reason', data['nodes'][0])
        self.assertIn('owner', data['nodes'][0])
        self.assertIn('lessee', data['nodes'][0])
        for field in api_utils.V31_FIELDS:
            self.assertIn(field, data['nodes'][0])
        # never expose the chassis_id
        self.assertNotIn('chassis_id', data['nodes'][0])
        self.assertIn('retired', data['nodes'][0])
        self.assertIn('retired_reason', data['nodes'][0])
        self.assertIn('network_data', data['nodes'][0])

    def test_detail_query_false(self):
        obj_utils.create_test_node(self.context)
        data1 = self.get_json(
            '/nodes',
            headers={api_base.Version.string: str(api_v1.max_version())})
        data2 = self.get_json(
            '/nodes?detail=False',
            headers={api_base.Version.string: str(api_v1.max_version())})
        self.assertEqual(data1['nodes'], data2['nodes'])

    def test_detail_using_query_false_and_fields(self):
        obj_utils.create_test_node(self.context)
        data = self.get_json(
            '/nodes?detail=False&fields=name',
            headers={api_base.Version.string: str(api_v1.max_version())})
        self.assertIn('name', data['nodes'][0])
        self.assertNotIn('uuid', data['nodes'][0])

    def test_detail_using_query_and_fields(self):
        obj_utils.create_test_node(self.context,
                                   chassis_id=self.chassis.id)
        response = self.get_json(
            '/nodes?detail=True&fields=name',
            headers={api_base.Version.string: str(api_v1.max_version())},
            expect_errors=True)
        self.assertEqual(http_client.BAD_REQUEST, response.status_int)

    def test_detail_using_query_old_version(self):
        obj_utils.create_test_node(self.context,
                                   chassis_id=self.chassis.id)
        response = self.get_json(
            '/nodes?detail=True',
            headers={api_base.Version.string: str(api_v1.min_version())},
            expect_errors=True)
        self.assertEqual(http_client.BAD_REQUEST, response.status_int)

    def test_detail_against_single(self):
        node = obj_utils.create_test_node(self.context)
        response = self.get_json('/nodes/%s/detail' % node.uuid,
                                 expect_errors=True)
        self.assertEqual(http_client.NOT_FOUND, response.status_int)

    @mock.patch.object(policy, 'authorize', spec=True)
    def test_detail_forbidden(self, mock_authorize):
        def mock_authorize_function(rule, target, creds):
            raise exception.HTTPForbidden(resource='fake')
        mock_authorize.side_effect = mock_authorize_function

        response = self.get_json('/nodes/detail', expect_errors=True,
                                 headers={
                                     api_base.Version.string: '1.50',
                                     'X-Project-Id': '12345'
                                 })
        self.assertEqual(http_client.FORBIDDEN, response.status_int)

    @mock.patch.object(policy, 'authorize', spec=True)
    def test_detail_list_all_forbidden_no_project(self, mock_authorize):
        def mock_authorize_function(rule, target, creds):
            if rule == 'baremetal:node:list_all':
                raise exception.HTTPForbidden(resource='fake')
            return True
        mock_authorize.side_effect = mock_authorize_function

        response = self.get_json('/nodes/detail', expect_errors=True,
                                 headers={
                                     api_base.Version.string: '1.49',
                                 })
        self.assertEqual(http_client.FORBIDDEN, response.status_int)

    @mock.patch.object(policy, 'authorize', spec=True)
    def test_detail_list_all_forbid_project_mismatch(self, mock_authorize):
        def mock_authorize_function(rule, target, creds):
            if rule == 'baremetal:node:list_all':
                raise exception.HTTPForbidden(resource='fake')
            return True
        mock_authorize.side_effect = mock_authorize_function

        response = self.get_json('/nodes/detail?project=54321',
                                 expect_errors=True,
                                 headers={
                                     api_base.Version.string: '1.50',
                                     'X-Project-Id': '12345'
                                 })
        self.assertEqual(http_client.FORBIDDEN, response.status_int)

    @mock.patch.object(policy, 'authorize', spec=True)
    def test_detail_list_all_forbidden(self, mock_authorize):
        def mock_authorize_function(rule, target, creds):
            if rule == 'baremetal:node:list_all':
                raise exception.HTTPForbidden(resource='fake')
            return True
        mock_authorize.side_effect = mock_authorize_function

        nodes = []
        for id in range(3):
            node = obj_utils.create_test_node(self.context,
                                              uuid=uuidutils.generate_uuid(),
                                              owner='12345')
            nodes.append(node.uuid)
        for id in range(3):
            node = obj_utils.create_test_node(self.context,
                                              uuid=uuidutils.generate_uuid(),
                                              lessee='12345')
            nodes.append(node.uuid)
        for id in range(2):
            node = obj_utils.create_test_node(self.context,
                                              uuid=uuidutils.generate_uuid(),
                                              owner='54321')
        for id in range(2):
            node = obj_utils.create_test_node(self.context,
                                              uuid=uuidutils.generate_uuid(),
                                              lessee='54321')

        data = self.get_json('/nodes/detail', headers={
            api_base.Version.string: '1.65',
            'X-Project-Id': '12345'})
        self.assertEqual(len(nodes), len(data['nodes']))

        uuids = [n['uuid'] for n in data['nodes']]
        self.assertEqual(sorted(nodes), sorted(uuids))

    def test_mask_available_state(self):
        node = obj_utils.create_test_node(self.context,
                                          provision_state=states.AVAILABLE)

        data = self.get_json(
            '/nodes/%s' % node.uuid,
            headers={api_base.Version.string: str(api_v1.min_version())})
        self.assertEqual(states.NOSTATE, data['provision_state'])

        data = self.get_json('/nodes/%s' % node.uuid,
                             headers={api_base.Version.string: "1.2"})
        self.assertEqual(states.AVAILABLE, data['provision_state'])

    def test_hide_fields_in_newer_versions_driver_internal(self):
        node = obj_utils.create_test_node(self.context,
                                          driver_internal_info={"foo": "bar"})
        data = self.get_json(
            '/nodes/%s' % node.uuid,
            headers={api_base.Version.string: str(api_v1.min_version())})
        self.assertNotIn('driver_internal_info', data)

        data = self.get_json('/nodes/%s' % node.uuid,
                             headers={api_base.Version.string: "1.3"})
        self.assertEqual({"foo": "bar"}, data['driver_internal_info'])

    def test_hide_fields_in_newer_versions_name(self):
        node = obj_utils.create_test_node(self.context,
                                          name="fish")
        data = self.get_json('/nodes/%s' % node.uuid,
                             headers={api_base.Version.string: "1.4"})
        self.assertNotIn('name', data)

        data = self.get_json('/nodes/%s' % node.uuid,
                             headers={api_base.Version.string: "1.5"})
        self.assertEqual('fish', data['name'])

    def test_hide_fields_in_newer_versions_inspection(self):
        some_time = datetime.datetime(2015, 3, 18, 19, 20)
        node = obj_utils.create_test_node(self.context,
                                          inspection_started_at=some_time)
        data = self.get_json(
            '/nodes/%s' % node.uuid,
            headers={api_base.Version.string: str(api_v1.min_version())})
        self.assertNotIn('inspection_finished_at', data)
        self.assertNotIn('inspection_started_at', data)

        data = self.get_json('/nodes/%s' % node.uuid,
                             headers={api_base.Version.string: "1.6"})
        started = timeutils.parse_isotime(
            data['inspection_started_at']).replace(tzinfo=None)
        self.assertEqual(some_time, started)
        self.assertIsNone(data['inspection_finished_at'])

    def test_hide_fields_in_newer_versions_clean_step(self):
        node = obj_utils.create_test_node(self.context,
                                          clean_step={"foo": "bar"})
        data = self.get_json(
            '/nodes/%s' % node.uuid,
            headers={api_base.Version.string: str(api_v1.min_version())})
        self.assertNotIn('clean_step', data)

        data = self.get_json('/nodes/%s' % node.uuid,
                             headers={api_base.Version.string: "1.7"})
        self.assertEqual({"foo": "bar"}, data['clean_step'])

    def test_hide_fields_in_newer_versions_network_interface(self):
        node = obj_utils.create_test_node(self.context,
                                          network_interface='flat')
        data = self.get_json(
            '/nodes/detail', headers={api_base.Version.string: '1.19'})
        self.assertNotIn('network_interface', data['nodes'][0])
        new_data = self.get_json(
            '/nodes/detail', headers={api_base.Version.string: '1.20'})
        self.assertEqual(node.network_interface,
                         new_data['nodes'][0]["network_interface"])

    def test_hide_fields_in_newer_versions_resource_class(self):
        node = obj_utils.create_test_node(self.context,
                                          resource_class='foo')
        data = self.get_json(
            '/nodes/detail', headers={api_base.Version.string: '1.20'})
        self.assertNotIn('resource_class', data['nodes'][0])
        new_data = self.get_json(
            '/nodes/detail', headers={api_base.Version.string: '1.21'})
        self.assertEqual(node.resource_class,
                         new_data['nodes'][0]["resource_class"])

    def test_hide_fields_in_newer_versions_interface_fields(self):
        node = obj_utils.create_test_node(self.context)
        data = self.get_json(
            '/nodes/detail', headers={api_base.Version.string: '1.30'})
        for field in api_utils.V31_FIELDS:
            self.assertNotIn(field, data['nodes'][0])
        new_data = self.get_json(
            '/nodes/detail', headers={api_base.Version.string: '1.31'})
        for field in api_utils.V31_FIELDS:
            self.assertEqual(getattr(node, field),
                             new_data['nodes'][0][field])

    def test_hide_fields_in_newer_versions_volume(self):
        node = obj_utils.create_test_node(self.context)
        data = self.get_json(
            '/nodes/%s' % node.uuid,
            headers={api_base.Version.string: '1.31'})
        self.assertNotIn('volume', data)

        data = self.get_json('/nodes/%s' % node.uuid,
                             headers={api_base.Version.string: "1.32"})
        self.assertIn('volume', data)

    def test_hide_fields_in_newer_versions_storage_interface(self):
        node = obj_utils.create_test_node(self.context,
                                          storage_interface='cinder')
        data = self.get_json(
            '/nodes/detail', headers={api_base.Version.string: '1.32'})
        self.assertNotIn('storage_interface', data['nodes'][0])
        new_data = self.get_json(
            '/nodes/detail', headers={api_base.Version.string: '1.33'})
        self.assertEqual(node.storage_interface,
                         new_data['nodes'][0]["storage_interface"])

    def test_hide_fields_in_newer_versions_traits(self):
        node = obj_utils.create_test_node(self.context)
        objects.TraitList.create(self.context, node.id, ['CUSTOM_1'])
        node.refresh()

        data = self.get_json(
            '/nodes/detail', headers={api_base.Version.string: '1.36'})
        self.assertNotIn('traits', data['nodes'][0])
        new_data = self.get_json(
            '/nodes/detail', headers={api_base.Version.string: '1.37'})
        self.assertEqual(['CUSTOM_1'], new_data['nodes'][0]["traits"])

    def test_hide_fields_in_newer_versions_description(self):
        node = obj_utils.create_test_node(self.context,
                                          description="useful piece")
        data = self.get_json('/nodes/%s' % node.uuid,
                             headers={api_base.Version.string: "1.50"})
        self.assertNotIn('description', data)

        data = self.get_json('/nodes/%s' % node.uuid,
                             headers={api_base.Version.string: "1.51"})
        self.assertEqual('useful piece', data['description'])

    def test_many(self):
        nodes = []
        for id in range(5):
            node = obj_utils.create_test_node(self.context,
                                              uuid=uuidutils.generate_uuid())
            nodes.append(node.uuid)
        data = self.get_json('/nodes')
        self.assertEqual(len(nodes), len(data['nodes']))

        uuids = [n['uuid'] for n in data['nodes']]
        self.assertEqual(sorted(nodes), sorted(uuids))

    def test_many_have_names(self):
        nodes = []
        node_names = []
        for id in range(5):
            name = 'node-%s' % id
            node = obj_utils.create_test_node(self.context,
                                              uuid=uuidutils.generate_uuid(),
                                              name=name)
            nodes.append(node.uuid)
            node_names.append(name)
        data = self.get_json('/nodes',
                             headers={api_base.Version.string: "1.5"})
        names = [n['name'] for n in data['nodes']]
        self.assertEqual(len(nodes), len(data['nodes']))
        self.assertEqual(sorted(node_names), sorted(names))

    @mock.patch.object(policy, 'authorize', spec=True)
    def test_many_forbidden(self, mock_authorize):
        def mock_authorize_function(rule, target, creds):
            raise exception.HTTPForbidden(resource='fake')
        mock_authorize.side_effect = mock_authorize_function

        response = self.get_json('/nodes', expect_errors=True,
                                 headers={
                                     api_base.Version.string: '1.50',
                                     'X-Project-Id': '12345'
                                 })
        self.assertEqual(http_client.FORBIDDEN, response.status_int)

    @mock.patch.object(policy, 'authorize', spec=True)
    def test_many_list_all_forbidden_no_project(self, mock_authorize):
        def mock_authorize_function(rule, target, creds):
            if rule == 'baremetal:node:list_all':
                raise exception.HTTPForbidden(resource='fake')
            return True
        mock_authorize.side_effect = mock_authorize_function

        response = self.get_json('/nodes', expect_errors=True,
                                 headers={
                                     api_base.Version.string: '1.49',
                                 })
        self.assertEqual(http_client.FORBIDDEN, response.status_int)

    @mock.patch.object(policy, 'authorize', spec=True)
    def test_many_list_all_forbid_project_mismatch(self, mock_authorize):
        def mock_authorize_function(rule, target, creds):
            if rule == 'baremetal:node:list_all':
                raise exception.HTTPForbidden(resource='fake')
            return True
        mock_authorize.side_effect = mock_authorize_function

        response = self.get_json('/nodes?project=54321',
                                 expect_errors=True,
                                 headers={
                                     api_base.Version.string: '1.50',
                                     'X-Project-Id': '12345'
                                 })
        self.assertEqual(http_client.FORBIDDEN, response.status_int)

    @mock.patch.object(policy, 'authorize', spec=True)
    def test_many_list_all_forbidden(self, mock_authorize):
        def mock_authorize_function(rule, target, creds):
            if rule == 'baremetal:node:list_all':
                raise exception.HTTPForbidden(resource='fake')
            return True
        mock_authorize.side_effect = mock_authorize_function

        nodes = []
        for id in range(3):
            node = obj_utils.create_test_node(self.context,
                                              uuid=uuidutils.generate_uuid(),
                                              owner='12345')
            nodes.append(node.uuid)
        for id in range(3):
            node = obj_utils.create_test_node(self.context,
                                              uuid=uuidutils.generate_uuid(),
                                              lessee='12345')
            nodes.append(node.uuid)
        for id in range(2):
            node = obj_utils.create_test_node(self.context,
                                              uuid=uuidutils.generate_uuid(),
                                              owner='54321')
        for id in range(2):
            node = obj_utils.create_test_node(self.context,
                                              uuid=uuidutils.generate_uuid(),
                                              lessee='54321')

        data = self.get_json('/nodes', headers={
            api_base.Version.string: '1.65',
            'X-Project-Id': '12345'})
        self.assertEqual(len(nodes), len(data['nodes']))

        uuids = [n['uuid'] for n in data['nodes']]
        self.assertEqual(sorted(nodes), sorted(uuids))

    def _test_links(self, public_url=None):
        cfg.CONF.set_override('public_endpoint', public_url, 'api')
        uuid = uuidutils.generate_uuid()
        obj_utils.create_test_node(self.context, uuid=uuid)
        data = self.get_json('/nodes/%s' % uuid)
        self.assertIn('links', data)
        self.assertEqual(2, len(data['links']))
        self.assertIn(uuid, data['links'][0]['href'])
        for link in data['links']:
            bookmark = link['rel'] == 'bookmark'
            self.assertTrue(self.validate_link(link['href'],
                            bookmark=bookmark))

        if public_url is not None:
            expected = [{'href': '%s/v1/nodes/%s' % (public_url, uuid),
                         'rel': 'self'},
                        {'href': '%s/nodes/%s' % (public_url, uuid),
                         'rel': 'bookmark'}]
            for i in expected:
                self.assertIn(i, data['links'])

    def test_links(self):
        self._test_links()

    def test_links_public_url(self):
        self._test_links(public_url='http://foo')

    def test_collection_links(self):
        nodes = []
        for id in range(5):
            node = obj_utils.create_test_node(self.context,
                                              uuid=uuidutils.generate_uuid())
            nodes.append(node.uuid)
        data = self.get_json('/nodes/?limit=3')
        self.assertEqual(3, len(data['nodes']))

        next_marker = data['nodes'][-1]['uuid']
        self.assertIn(next_marker, data['next'])
        self.assertIn('nodes', data['next'])

    def test_collection_links_default_limit(self):
        cfg.CONF.set_override('max_limit', 3, 'api')
        nodes = []
        for id in range(5):
            node = obj_utils.create_test_node(self.context,
                                              uuid=uuidutils.generate_uuid())
            nodes.append(node.uuid)
        data = self.get_json('/nodes')
        self.assertEqual(3, len(data['nodes']))

        next_marker = data['nodes'][-1]['uuid']
        self.assertIn(next_marker, data['next'])
        self.assertIn('nodes', data['next'])

    def test_collection_links_custom_fields(self):
        fields = 'driver_info,uuid'
        cfg.CONF.set_override('max_limit', 3, 'api')
        nodes = []
        for id in range(5):
            node = obj_utils.create_test_node(self.context,
                                              uuid=uuidutils.generate_uuid(),
                                              driver_info={'fake': 'value'},
                                              properties={'fake': 'bar'})
            nodes.append(node.uuid)
        data = self.get_json(
            '/nodes?fields=%s' % fields,
            headers={api_base.Version.string: str(api_v1.max_version())})
        self.assertEqual(3, len(data['nodes']))

        next_marker = data['nodes'][-1]['uuid']
        self.assertIn(next_marker, data['next'])
        self.assertIn('fields', data['next'])
        self.assertIn('nodes', data['next'])

    def test_get_collection_pagination_no_uuid(self):
        fields = 'name'
        limit = 2
        nodes = []
        for id_ in range(3):
            node = obj_utils.create_test_node(
                self.context,
                uuid=uuidutils.generate_uuid())
            nodes.append(node)

        data = self.get_json(
            '/nodes?fields=%s&limit=%s' % (fields, limit),
            headers={api_base.Version.string: str(api_v1.max_version())})

        self.assertEqual(limit, len(data['nodes']))
        self.assertIn('marker=%s' % nodes[limit - 1].uuid, data['next'])
        self.assertIn('nodes', data['next'])

    def test_collection_links_instance_uuid_param(self):
        cfg.CONF.set_override('max_limit', 1, 'api')
        nodes = []
        for id in range(2):
            node = obj_utils.create_test_node(
                self.context,
                uuid=uuidutils.generate_uuid(),
                instance_uuid=uuidutils.generate_uuid(),
                resource_class='tst_resource')
            nodes.append(node)

        query_str = 'instance_uuid=%s' % nodes[0].instance_uuid
        data = self.get_json('/nodes?%s' % query_str)
        self.assertEqual(1, len(data['nodes']))
        self.assertNotIn('next', data)

    def test_sort_key(self):
        nodes = []
        for id in range(3):
            node = obj_utils.create_test_node(self.context,
                                              uuid=uuidutils.generate_uuid())
            nodes.append(node.uuid)
        data = self.get_json('/nodes?sort_key=uuid')
        uuids = [n['uuid'] for n in data['nodes']]
        self.assertEqual(sorted(nodes), uuids)

    def test_sort_key_invalid(self):
        invalid_keys_list = ['foo', 'properties', 'driver_info', 'extra',
                             'instance_info', 'driver_internal_info',
                             'clean_step', 'traits']
        headers = {api_base.Version.string: str(api_v1.max_version())}
        for invalid_key in invalid_keys_list:
            response = self.get_json('/nodes?sort_key=%s' % invalid_key,
                                     headers=headers,
                                     expect_errors=True)
            self.assertEqual(http_client.BAD_REQUEST, response.status_int)
            self.assertEqual('application/json', response.content_type)
            self.assertIn(invalid_key, response.json['error_message'])

    def _test_sort_key_allowed(self, detail=False):
        node_uuids = []
        for id in range(3, 0, -1):
            node = obj_utils.create_test_node(self.context,
                                              uuid=uuidutils.generate_uuid(),
                                              resource_class='rc_%s' % id)
            node_uuids.append(node.uuid)
        node_uuids.reverse()
        headers = {'X-OpenStack-Ironic-API-Version': '1.21'}
        detail_str = '/detail' if detail else ''
        data = self.get_json('/nodes%s?sort_key=resource_class' % detail_str,
                             headers=headers)
        data_uuids = [n['uuid'] for n in data['nodes']]
        self.assertEqual(node_uuids, data_uuids)

    def test_sort_key_allowed(self):
        self._test_sort_key_allowed()

    def test_detail_sort_key_allowed(self):
        self._test_sort_key_allowed(detail=True)

    def _test_sort_key_not_allowed(self, detail=False):
        headers = {'X-OpenStack-Ironic-API-Version': '1.20'}
        detail_str = '/detail' if detail else ''
        resp = self.get_json('/nodes%s?sort_key=resource_class' % detail_str,
                             headers=headers, expect_errors=True)
        self.assertEqual(http_client.NOT_ACCEPTABLE, resp.status_int)
        self.assertEqual('application/json', resp.content_type)

    def test_sort_key_not_allowed(self):
        self._test_sort_key_not_allowed()

    def test_detail_sort_key_not_allowed(self):
        self._test_sort_key_not_allowed(detail=True)

    def test_ports_subresource_link(self):
        node = obj_utils.create_test_node(self.context)
        data = self.get_json('/nodes/%s' % node.uuid)
        self.assertIn('ports', data)

    def test_portgroups_subresource(self):
        node = obj_utils.create_test_node(self.context)
        headers = {'X-OpenStack-Ironic-API-Version': '1.24'}
        for id_ in range(2):
            obj_utils.create_test_portgroup(self.context, node_id=node.id,
                                            name="pg-%s" % id_,
                                            uuid=uuidutils.generate_uuid(),
                                            address='52:54:00:cf:2d:3%s' % id_)

        data = self.get_json('/nodes/%s/portgroups' % node.uuid,
                             headers=headers)
        self.assertEqual(2, len(data['portgroups']))
        self.assertNotIn('next', data)

        # Test collection pagination
        data = self.get_json('/nodes/%s/portgroups?limit=1' % node.uuid,
                             headers=headers)
        self.assertEqual(1, len(data['portgroups']))
        self.assertIn('next', data)
        self.assertIn('portgroups', data['next'])

    def test_portgroups_subresource_link(self):
        node = obj_utils.create_test_node(self.context)
        data = self.get_json(
            '/nodes/%s' % node.uuid,
            headers={'X-OpenStack-Ironic-API-Version': '1.24'})
        self.assertIn('portgroups', data)

    def test_portgroups_subresource_link_hidden_for_older_versions(self):
        node = obj_utils.create_test_node(self.context)
        data = self.get_json(
            '/nodes/%s' % node.uuid,
            headers={'X-OpenStack-Ironic-API-Version': '1.20'})
        self.assertNotIn('portgroups', data)

    def test_portgroups_subresource_old_api_version(self):
        node = obj_utils.create_test_node(self.context)
        response = self.get_json(
            '/nodes/%s/portgroups' % node.uuid, expect_errors=True,
            headers={'X-OpenStack-Ironic-API-Version': '1.23'})
        self.assertEqual(http_client.NOT_FOUND, response.status_int)

    def test_ports_subresource(self):
        node = obj_utils.create_test_node(self.context)

        for id_ in range(2):
            obj_utils.create_test_port(self.context, node_id=node.id,
                                       uuid=uuidutils.generate_uuid(),
                                       address='52:54:00:cf:2d:3%s' % id_)

        data = self.get_json('/nodes/%s/ports' % node.uuid)
        self.assertEqual(2, len(data['ports']))
        self.assertNotIn('next', data)

        # Test collection pagination
        data = self.get_json('/nodes/%s/ports?limit=1' % node.uuid)
        self.assertEqual(1, len(data['ports']))
        self.assertIn('next', data)
        self.assertIn('ports', data['next'])

    def test_ports_subresource_noid(self):
        node = obj_utils.create_test_node(self.context)
        obj_utils.create_test_port(self.context, node_id=node.id)
        # No node id specified
        response = self.get_json('/nodes/ports', expect_errors=True)
        self.assertEqual(http_client.NOT_FOUND, response.status_int)

    def test_ports_subresource_node_not_found(self):
        non_existent_uuid = 'eeeeeeee-cccc-aaaa-bbbb-cccccccccccc'
        response = self.get_json('/nodes/%s/ports' % non_existent_uuid,
                                 expect_errors=True)
        self.assertEqual(http_client.NOT_FOUND, response.status_int)

    def test_ports_subresource_invalid_ident(self):
        invalid_ident = '123 123'
        response = self.get_json('/nodes/%s/ports' % invalid_ident,
                                 expect_errors=True)
        self.assertEqual(http_client.BAD_REQUEST, response.status_int)
        self.assertIn('Expected UUID or name for node',
                      response.json['error_message'])

    def test_ports_subresource_via_portgroups_subres_not_allowed(self):
        node = obj_utils.create_test_node(self.context)
        pg = obj_utils.create_test_portgroup(self.context,
                                             node_id=node.id)
        response = self.get_json('/nodes/%s/portgroups/%s/ports' % (
            node.uuid, pg.uuid), expect_errors=True,
            headers={api_base.Version.string: '1.24'})
        self.assertEqual(http_client.FORBIDDEN, response.status_int)

    def test_volume_subresource_link(self):
        node = obj_utils.create_test_node(self.context)
        data = self.get_json(
            '/nodes/%s' % node.uuid,
            headers={api_base.Version.string: '1.32'})
        self.assertIn('volume', data)

    def test_volume_subresource(self):
        node = obj_utils.create_test_node(self.context)
        data = self.get_json('/nodes/%s/volume' % node.uuid,
                             headers={api_base.Version.string: '1.32'})
        self.assertIn('connectors', data)
        self.assertIn('targets', data)
        self.assertIn('/volume/connectors',
                      data['connectors'][0]['href'])
        self.assertIn('/volume/connectors',
                      data['connectors'][1]['href'])
        self.assertIn('/volume/targets',
                      data['targets'][0]['href'])
        self.assertIn('/volume/targets',
                      data['targets'][1]['href'])

    def test_volume_subresource_invalid_api_version(self):
        node = obj_utils.create_test_node(self.context)
        response = self.get_json('/nodes/%s/volume' % node.uuid,
                                 headers={api_base.Version.string: '1.31'},
                                 expect_errors=True)
        self.assertEqual(http_client.NOT_FOUND, response.status_int)

    def test_volume_connectors_subresource(self):
        node = obj_utils.create_test_node(self.context)

        for id_ in range(2):
            obj_utils.create_test_volume_connector(
                self.context, node_id=node.id, uuid=uuidutils.generate_uuid(),
                connector_id='test-connector_id-%s' % id_)

        data = self.get_json(
            '/nodes/%s/volume/connectors' % node.uuid,
            headers={api_base.Version.string: str(api_v1.max_version())})
        self.assertEqual(2, len(data['connectors']))
        self.assertNotIn('next', data)

        # Test collection pagination
        data = self.get_json(
            '/nodes/%s/volume/connectors?limit=1' % node.uuid,
            headers={api_base.Version.string: str(api_v1.max_version())})
        self.assertEqual(1, len(data['connectors']))
        self.assertIn('next', data)
        self.assertIn('volume/connectors', data['next'])

    def test_volume_connectors_subresource_noid(self):
        node = obj_utils.create_test_node(self.context)
        obj_utils.create_test_volume_connector(self.context, node_id=node.id)
        # No node_id specified.
        response = self.get_json(
            '/nodes/volume/connectors',
            expect_errors=True,
            headers={api_base.Version.string: str(api_v1.max_version())})
        self.assertEqual(http_client.NOT_FOUND, response.status_int)

    def test_volume_connectors_subresource_node_not_found(self):
        non_existent_uuid = 'eeeeeeee-cccc-aaaa-bbbb-cccccccccccc'
        response = self.get_json(
            '/nodes/%s/volume/connectors' % non_existent_uuid,
            expect_errors=True,
            headers={api_base.Version.string: str(api_v1.max_version())})
        self.assertEqual(http_client.NOT_FOUND, response.status_int)

    def test_volume_targets_subresource(self):
        node = obj_utils.create_test_node(self.context)

        for id_ in range(2):
            obj_utils.create_test_volume_target(
                self.context, node_id=node.id, uuid=uuidutils.generate_uuid(),
                boot_index=id_)

        data = self.get_json(
            '/nodes/%s/volume/targets' % node.uuid,
            headers={api_base.Version.string: str(api_v1.max_version())})
        self.assertEqual(2, len(data['targets']))
        self.assertNotIn('next', data)

        # Test collection pagination
        data = self.get_json(
            '/nodes/%s/volume/targets?limit=1' % node.uuid,
            headers={api_base.Version.string: str(api_v1.max_version())})
        self.assertEqual(1, len(data['targets']))
        self.assertIn('next', data)
        self.assertIn('volume/target', data['next'])

    def test_volume_targets_subresource_noid(self):
        node = obj_utils.create_test_node(self.context)
        obj_utils.create_test_volume_target(self.context, node_id=node.id)
        # No node_id specified.
        response = self.get_json(
            '/nodes/volume/targets',
            expect_errors=True,
            headers={api_base.Version.string: str(api_v1.max_version())})
        self.assertEqual(http_client.NOT_FOUND, response.status_int)

    def test_volume_targets_subresource_node_not_found(self):
        non_existent_uuid = 'eeeeeeee-cccc-aaaa-bbbb-cccccccccccc'
        response = self.get_json(
            '/nodes/%s/volume/targets' % non_existent_uuid,
            expect_errors=True,
            headers={api_base.Version.string: str(api_v1.max_version())})
        self.assertEqual(http_client.NOT_FOUND, response.status_int)

    @mock.patch.object(timeutils, 'utcnow', autospec=True)
    def _test_node_states(self, mock_utcnow, api_version=None):
        fake_state = 'fake-state'
        fake_error = 'fake-error'
        fake_config = '{"foo": "bar"}'
        test_time = datetime.datetime(2000, 1, 1, 0, 0)
        mock_utcnow.return_value = test_time
        node = obj_utils.create_test_node(self.context,
                                          power_state=fake_state,
                                          target_power_state=fake_state,
                                          provision_state=fake_state,
                                          target_provision_state=fake_state,
                                          provision_updated_at=test_time,
                                          raid_config=fake_config,
                                          target_raid_config=fake_config,
                                          last_error=fake_error)
        headers = {}
        if api_version:
            headers = {api_base.Version.string: api_version}
        data = self.get_json('/nodes/%s/states' % node.uuid, headers=headers)
        self.assertEqual(fake_state, data['power_state'])
        self.assertEqual(fake_state, data['target_power_state'])
        self.assertEqual(fake_state, data['provision_state'])
        self.assertEqual(fake_state, data['target_provision_state'])
        prov_up_at = timeutils.parse_isotime(
            data['provision_updated_at']).replace(tzinfo=None)
        self.assertEqual(test_time, prov_up_at)
        self.assertEqual(fake_error, data['last_error'])
        self.assertFalse(data['console_enabled'])
        return data

    def test_node_states(self):
        self._test_node_states()

    def test_node_states_raid(self):
        data = self._test_node_states(api_version="1.12")
        self.assertEqual({'foo': 'bar'}, data['raid_config'])
        self.assertEqual({'foo': 'bar'}, data['target_raid_config'])

    @mock.patch.object(timeutils, 'utcnow', autospec=True)
    def test_node_states_by_name(self, mock_utcnow):
        fake_state = 'fake-state'
        fake_error = 'fake-error'
        test_time = datetime.datetime(1971, 3, 9, 0, 0)
        mock_utcnow.return_value = test_time
        node = obj_utils.create_test_node(self.context,
                                          name='eggs',
                                          power_state=fake_state,
                                          target_power_state=fake_state,
                                          provision_state=fake_state,
                                          target_provision_state=fake_state,
                                          provision_updated_at=test_time,
                                          last_error=fake_error)
        data = self.get_json('/nodes/%s/states' % node.name,
                             headers={api_base.Version.string: "1.5"})
        self.assertEqual(fake_state, data['power_state'])
        self.assertEqual(fake_state, data['target_power_state'])
        self.assertEqual(fake_state, data['provision_state'])
        self.assertEqual(fake_state, data['target_provision_state'])
        prov_up_at = timeutils.parse_isotime(
            data['provision_updated_at']).replace(tzinfo=None)
        self.assertEqual(test_time, prov_up_at)
        self.assertEqual(fake_error, data['last_error'])
        self.assertFalse(data['console_enabled'])

    def test_node_states_boot_mode(self):
        for value in ('bios', 'uefi'):
            node = obj_utils.create_test_node(self.context,
                                              boot_mode=value,
                                              uuid=uuidutils.generate_uuid())
            data = self.get_json('/nodes/%s/states' % node.uuid,
                                 headers={api_base.Version.string: '1.75'})
            self.assertEqual(data['boot_mode'], value)
            self.assertIsNone(data['secure_boot'])

    def test_node_states_secure_boot(self):
        for value in (True, False):
            node = obj_utils.create_test_node(self.context,
                                              boot_mode='uefi',
                                              secure_boot=value,
                                              uuid=uuidutils.generate_uuid())
            data = self.get_json('/nodes/%s/states' % node.uuid,
                                 headers={api_base.Version.string: '1.75'})
            self.assertEqual(data['boot_mode'], 'uefi')
            self.assertEqual(data['secure_boot'], value)

    def _test_node_states_subfield_hidden_in_lower_version(self, field,
                                                           old_version,
                                                           new_version):
        node = obj_utils.create_test_node(self.context)
        data = self.get_json(
            '/nodes/%s/states' % node.uuid,
            headers={api_base.Version.string: old_version})
        self.assertNotIn(field, data)
        data = self.get_json(
            '/nodes/%s/states' % node.uuid,
            headers={api_base.Version.string: new_version})
        self.assertIn(field, data)

    def test_node_states_boot_mode_hidden_in_lower_version(self):
        self._test_node_states_subfield_hidden_in_lower_version('boot_mode',
                                                                '1.74',
                                                                '1.75')

    def test_node_states_secure_boot_hidden_in_lower_version(self):
        self._test_node_states_subfield_hidden_in_lower_version('secure_boot',
                                                                '1.74',
                                                                '1.75')

    def test_node_by_instance_uuid(self):
        node = obj_utils.create_test_node(
            self.context,
            uuid=uuidutils.generate_uuid(),
            instance_uuid=uuidutils.generate_uuid())
        instance_uuid = node.instance_uuid

        data = self.get_json('/nodes?instance_uuid=%s' % instance_uuid,
                             headers={api_base.Version.string: "1.5"})

        self.assertThat(data['nodes'], matchers.HasLength(1))
        self.assertEqual(node['instance_uuid'],
                         data['nodes'][0]["instance_uuid"])

    def test_node_by_instance_uuid_wrong_uuid(self):
        obj_utils.create_test_node(
            self.context, uuid=uuidutils.generate_uuid(),
            instance_uuid=uuidutils.generate_uuid())
        wrong_uuid = uuidutils.generate_uuid()

        data = self.get_json('/nodes?instance_uuid=%s' % wrong_uuid)

        self.assertThat(data['nodes'], matchers.HasLength(0))

    def test_node_by_instance_uuid_invalid_uuid(self):
        response = self.get_json('/nodes?instance_uuid=fake',
                                 expect_errors=True)

        self.assertEqual('application/json', response.content_type)
        self.assertEqual(http_client.BAD_REQUEST, response.status_code)

    def test_associated_nodes_insensitive(self):
        associated_nodes = (self
                            ._create_association_test_nodes()['associated'])

        data = self.get_json('/nodes?associated=true')
        data1 = self.get_json('/nodes?associated=True')

        uuids = [n['uuid'] for n in data['nodes']]
        uuids1 = [n['uuid'] for n in data1['nodes']]
        self.assertEqual(sorted(associated_nodes), sorted(uuids1))
        self.assertEqual(sorted(associated_nodes), sorted(uuids))

    def test_associated_nodes_error(self):
        self._create_association_test_nodes()
        response = self.get_json('/nodes?associated=blah', expect_errors=True)
        self.assertEqual('application/json', response.content_type)
        self.assertEqual(http_client.BAD_REQUEST, response.status_code)
        self.assertTrue(response.json['error_message'])

    def test_unassociated_nodes_insensitive(self):
        unassociated_nodes = (
            self._create_association_test_nodes()['unassociated'])

        data = self.get_json('/nodes?associated=false')
        data1 = self.get_json('/nodes?associated=FALSE')

        uuids = [n['uuid'] for n in data['nodes']]
        uuids1 = [n['uuid'] for n in data1['nodes']]
        self.assertEqual(sorted(unassociated_nodes), sorted(uuids1))
        self.assertEqual(sorted(unassociated_nodes), sorted(uuids))

    def test_unassociated_nodes_with_limit(self):
        unassociated_nodes = (
            self._create_association_test_nodes()['unassociated'])

        data = self.get_json('/nodes?associated=False&limit=2')

        self.assertThat(data['nodes'], matchers.HasLength(2))
        self.assertIn(data['nodes'][0]['uuid'], unassociated_nodes)

    def test_next_link_with_association(self):
        self._create_association_test_nodes()
        data = self.get_json('/nodes/?limit=3&associated=True')
        self.assertThat(data['nodes'], matchers.HasLength(3))
        self.assertIn('associated=True', data['next'])
        self.assertIn('nodes', data['next'])

    def test_detail_with_association_filter(self):
        associated_nodes = (self
                            ._create_association_test_nodes()['associated'])
        data = self.get_json('/nodes/detail?associated=true')
        self.assertIn('driver', data['nodes'][0])
        self.assertEqual(len(associated_nodes), len(data['nodes']))

    def test_next_link_with_association_with_detail(self):
        self._create_association_test_nodes()
        data = self.get_json('/nodes/detail?limit=3&associated=true')
        self.assertThat(data['nodes'], matchers.HasLength(3))
        self.assertIn('driver', data['nodes'][0])
        self.assertIn('associated=True', data['next'])
        self.assertIn('nodes', data['next'])

    def test_detail_with_instance_uuid(self):
        node = obj_utils.create_test_node(
            self.context,
            uuid=uuidutils.generate_uuid(),
            instance_uuid=uuidutils.generate_uuid(),
            chassis_id=self.chassis.id)
        instance_uuid = node.instance_uuid

        data = self.get_json('/nodes/detail?instance_uuid=%s' % instance_uuid)

        self.assertEqual(node['instance_uuid'],
                         data['nodes'][0]["instance_uuid"])
        self.assertIn('driver', data['nodes'][0])
        self.assertIn('driver_info', data['nodes'][0])
        self.assertIn('extra', data['nodes'][0])
        self.assertIn('properties', data['nodes'][0])
        self.assertIn('chassis_uuid', data['nodes'][0])
        # never expose the chassis_id
        self.assertNotIn('chassis_id', data['nodes'][0])

    def test_maintenance_nodes(self):
        nodes = []
        for id in range(5):
            node = obj_utils.create_test_node(self.context,
                                              uuid=uuidutils.generate_uuid(),
                                              maintenance=id % 2)
            nodes.append(node)

        data = self.get_json('/nodes?maintenance=true')
        uuids = [n['uuid'] for n in data['nodes']]
        test_uuids_1 = [n.uuid for n in nodes if n.maintenance]
        self.assertEqual(sorted(test_uuids_1), sorted(uuids))

        data = self.get_json('/nodes?maintenance=false')
        uuids = [n['uuid'] for n in data['nodes']]
        test_uuids_0 = [n.uuid for n in nodes if not n.maintenance]
        self.assertEqual(sorted(test_uuids_0), sorted(uuids))

    def test_maintenance_nodes_error(self):
        response = self.get_json('/nodes?associated=true&maintenance=blah',
                                 expect_errors=True)
        self.assertEqual('application/json', response.content_type)
        self.assertEqual(http_client.BAD_REQUEST, response.status_code)
        self.assertTrue(response.json['error_message'])

    def test_maintenance_nodes_associated(self):
        self._create_association_test_nodes()
        node = obj_utils.create_test_node(
            self.context,
            instance_uuid=uuidutils.generate_uuid(),
            maintenance=True)

        data = self.get_json('/nodes?associated=true&maintenance=false')
        uuids = [n['uuid'] for n in data['nodes']]
        self.assertNotIn(node.uuid, uuids)
        data = self.get_json('/nodes?associated=true&maintenance=true')
        uuids = [n['uuid'] for n in data['nodes']]
        self.assertIn(node.uuid, uuids)
        data = self.get_json('/nodes?associated=true&maintenance=TruE')
        uuids = [n['uuid'] for n in data['nodes']]
        self.assertIn(node.uuid, uuids)

    def test_get_nodes_by_provision_state(self):
        node = obj_utils.create_test_node(self.context,
                                          uuid=uuidutils.generate_uuid(),
                                          provision_state=states.AVAILABLE)
        node1 = obj_utils.create_test_node(self.context,
                                           uuid=uuidutils.generate_uuid(),
                                           provision_state=states.DEPLOYING)

        data = self.get_json('/nodes?provision_state=available',
                             headers={api_base.Version.string: "1.9"})
        uuids = [n['uuid'] for n in data['nodes']]
        self.assertIn(node.uuid, uuids)
        self.assertNotIn(node1.uuid, uuids)
        data = self.get_json('/nodes?provision_state=deploying',
                             headers={api_base.Version.string: "1.9"})
        uuids = [n['uuid'] for n in data['nodes']]
        self.assertIn(node1.uuid, uuids)
        self.assertNotIn(node.uuid, uuids)

    def test_get_nodes_by_invalid_provision_state(self):
        response = self.get_json('/nodes?provision_state=test',
                                 headers={api_base.Version.string: "1.9"},
                                 expect_errors=True)
        self.assertEqual('application/json', response.content_type)
        self.assertEqual(http_client.BAD_REQUEST, response.status_code)
        self.assertTrue(response.json['error_message'])

    def test_get_nodes_by_provision_state_not_allowed(self):
        response = self.get_json('/nodes?provision_state=test',
                                 headers={api_base.Version.string: "1.8"},
                                 expect_errors=True)
        self.assertEqual('application/json', response.content_type)
        self.assertEqual(http_client.NOT_ACCEPTABLE, response.status_code)
        self.assertTrue(response.json['error_message'])

    def test_get_nodes_by_driver(self):
        node = obj_utils.create_test_node(self.context,
                                          uuid=uuidutils.generate_uuid(),
                                          driver='ipmi')
        node1 = obj_utils.create_test_node(self.context,
                                           uuid=uuidutils.generate_uuid(),
                                           driver='fake-hardware')

        data = self.get_json('/nodes?driver=ipmi',
                             headers={api_base.Version.string: "1.16"})
        uuids = [n['uuid'] for n in data['nodes']]
        self.assertIn(node.uuid, uuids)
        self.assertNotIn(node1.uuid, uuids)
        data = self.get_json('/nodes?driver=fake-hardware',
                             headers={api_base.Version.string: "1.16"})
        uuids = [n['uuid'] for n in data['nodes']]
        self.assertIn(node1.uuid, uuids)
        self.assertNotIn(node.uuid, uuids)

    def test_get_nodes_by_invalid_driver(self):
        data = self.get_json('/nodes?driver=test',
                             headers={api_base.Version.string: "1.16"})
        self.assertEqual(0, len(data['nodes']))

    def test_get_nodes_by_driver_invalid_api_version(self):
        response = self.get_json(
            '/nodes?driver=fake',
            headers={api_base.Version.string: str(api_v1.min_version())},
            expect_errors=True)
        self.assertEqual(http_client.NOT_ACCEPTABLE, response.status_code)
        self.assertTrue(response.json['error_message'])

    def _test_get_nodes_by_resource_class(self, detail=False):
        if detail:
            base_url = '/nodes/detail?resource_class=%s'
        else:
            base_url = '/nodes?resource_class=%s'

        node = obj_utils.create_test_node(self.context,
                                          uuid=uuidutils.generate_uuid(),
                                          resource_class='foo')
        node1 = obj_utils.create_test_node(self.context,
                                           uuid=uuidutils.generate_uuid(),
                                           resource_class='bar')

        data = self.get_json(base_url % 'foo',
                             headers={api_base.Version.string: "1.21"})
        uuids = [n['uuid'] for n in data['nodes']]
        self.assertIn(node.uuid, uuids)
        self.assertNotIn(node1.uuid, uuids)
        data = self.get_json(base_url % 'bar',
                             headers={api_base.Version.string: "1.21"})
        uuids = [n['uuid'] for n in data['nodes']]
        self.assertIn(node1.uuid, uuids)
        self.assertNotIn(node.uuid, uuids)

    def test_get_nodes_by_resource_class(self):
        self._test_get_nodes_by_resource_class(detail=False)

    def test_get_nodes_by_resource_class_detail(self):
        self._test_get_nodes_by_resource_class(detail=True)

    def _test_get_nodes_by_invalid_resource_class(self, detail=False):
        if detail:
            base_url = '/nodes/detail?resource_class=%s'
        else:
            base_url = '/nodes?resource_class=%s'

        data = self.get_json(base_url % 'test',
                             headers={api_base.Version.string: "1.21"})
        self.assertEqual(0, len(data['nodes']))

    def test_get_nodes_by_invalid_resource_class(self):
        self._test_get_nodes_by_invalid_resource_class(detail=False)

    def test_get_nodes_by_invalid_resource_class_detail(self):
        self._test_get_nodes_by_invalid_resource_class(detail=True)

    def _test_get_nodes_by_resource_class_invalid_api_version(self,
                                                              detail=False):
        if detail:
            base_url = '/nodes/detail?resource_class=%s'
        else:
            base_url = '/nodes?resource_class=%s'

        response = self.get_json(
            base_url % 'fake',
            headers={api_base.Version.string: str(api_v1.min_version())},
            expect_errors=True)
        self.assertEqual(http_client.NOT_ACCEPTABLE, response.status_code)
        self.assertTrue(response.json['error_message'])

    def test_get_nodes_by_resource_class_invalid_api_version(self):
        self._test_get_nodes_by_resource_class_invalid_api_version(
            detail=False)

    def test_get_nodes_by_resource_class_invalid_api_version_detail(self):
        self._test_get_nodes_by_resource_class_invalid_api_version(detail=True)

    def _test_get_nodes_by_traits_not_allowed(self, detail=False):
        if detail:
            base_url = '/nodes/detail?traits=%s'
        else:
            base_url = '/nodes?traits=%s'

        response = self.get_json(
            base_url % 'CUSTOM_TRAIT_1',
            headers={api_base.Version.string: str(api_v1.max_version())},
            expect_errors=True)
        self.assertEqual(http_client.BAD_REQUEST, response.status_code)
        self.assertTrue(response.json['error_message'])

    def test_get_nodes_by_traits_not_allowed(self):
        self._test_get_nodes_by_traits_not_allowed(detail=False)

    def test_get_nodes_by_traits_not_allowed_detail(self):
        self._test_get_nodes_by_traits_not_allowed(detail=True)

    def test_get_nodes_by_fault(self):
        node1 = obj_utils.create_test_node(self.context,
                                           uuid=uuidutils.generate_uuid(),
                                           fault='power failure')
        node2 = obj_utils.create_test_node(self.context,
                                           uuid=uuidutils.generate_uuid(),
                                           fault="clean failure")

        for base_url in ('/nodes', '/nodes/detail'):
            data = self.get_json(base_url + '?fault=power failure',
                                 headers={api_base.Version.string: "1.42"})
            uuids = [n['uuid'] for n in data['nodes']]
            self.assertIn(node1.uuid, uuids)
            self.assertNotIn(node2.uuid, uuids)
            data = self.get_json(base_url + '?fault=clean failure',
                                 headers={api_base.Version.string: "1.42"})
            uuids = [n['uuid'] for n in data['nodes']]
            self.assertIn(node2.uuid, uuids)
            self.assertNotIn(node1.uuid, uuids)

    def test_get_nodes_by_fault_with_invalid_fault(self):
        for url in ('/nodes?fault=somefake',
                    '/nodes/detail?fault=somefake'):
            response = self.get_json(
                url, headers={api_base.Version.string: "1.42"},
                expect_errors=True)
            self.assertEqual('application/json', response.content_type)
            self.assertEqual(http_client.BAD_REQUEST, response.status_code)
            self.assertTrue(response.json['error_message'])

    def test_get_nodes_by_fault_not_allowed(self):
        for url in ('/nodes?fault=power failure',
                    '/nodes/detail?fault=power failure'):
            response = self.get_json(
                url, headers={api_base.Version.string: "1.41"},
                expect_errors=True)
            self.assertEqual('application/json', response.content_type)
            self.assertEqual(http_client.NOT_ACCEPTABLE, response.status_code)
            self.assertTrue(response.json['error_message'])

    def test_get_nodes_by_conductor_group(self):
        node1 = obj_utils.create_test_node(self.context,
                                           uuid=uuidutils.generate_uuid(),
                                           conductor_group='group1')
        node2 = obj_utils.create_test_node(self.context,
                                           uuid=uuidutils.generate_uuid(),
                                           conductor_group='group2')

        for base_url in ('/nodes', '/nodes/detail'):
            data = self.get_json(base_url + '?conductor_group=group1',
                                 headers={api_base.Version.string: "1.46"})
            uuids = [n['uuid'] for n in data['nodes']]
            self.assertIn(node1.uuid, uuids)
            self.assertNotIn(node2.uuid, uuids)
            data = self.get_json(base_url + '?conductor_group=group2',
                                 headers={api_base.Version.string: "1.46"})
            uuids = [n['uuid'] for n in data['nodes']]
            self.assertIn(node2.uuid, uuids)
            self.assertNotIn(node1.uuid, uuids)

    def test_get_nodes_by_conductor_group_not_allowed(self):
        for url in ('/nodes?conductor_group=group1',
                    '/nodes/detail?conductor_group=group1'):
            response = self.get_json(
                url, headers={api_base.Version.string: "1.45"},
                expect_errors=True)
            self.assertEqual('application/json', response.content_type)
            self.assertEqual(http_client.NOT_ACCEPTABLE, response.status_code)
            self.assertTrue(response.json['error_message'])

    def test_get_nodes_by_conductor_not_allowed(self):
        response = self.get_json('/nodes?conductor=rocky.rocks',
                                 headers={api_base.Version.string: "1.48"},
                                 expect_errors=True)
        self.assertEqual('application/json', response.content_type)
        self.assertEqual(http_client.NOT_ACCEPTABLE, response.status_code)
        self.assertTrue(response.json['error_message'])

    def test_get_nodes_by_conductor(self):
        node1 = obj_utils.create_test_node(self.context,
                                           uuid=uuidutils.generate_uuid())
        node2 = obj_utils.create_test_node(self.context,
                                           uuid=uuidutils.generate_uuid())

        response = self.get_json('/nodes?conductor=rocky.rocks',
                                 headers={api_base.Version.string: "1.49"})
        uuids = [n['uuid'] for n in response['nodes']]
        self.assertFalse(uuids)

        response = self.get_json('/nodes?conductor=fake.conductor',
                                 headers={api_base.Version.string: "1.49"})
        uuids = [n['uuid'] for n in response['nodes']]
        self.assertEqual(2, len(uuids))
        self.assertIn(node1.uuid, uuids)
        self.assertIn(node2.uuid, uuids)

        self.mock_get_conductor_for.side_effect = ['rocky.rocks',
                                                   'fake.conductor']
        response = self.get_json('/nodes?conductor=fake.conductor',
                                 headers={api_base.Version.string: "1.49"})
        uuids = [n['uuid'] for n in response['nodes']]
        self.assertEqual(1, len(uuids))
        self.assertNotIn(node1.uuid, uuids)
        self.assertIn(node2.uuid, uuids)

    def test_get_nodes_by_conductor_no_valid_host(self):
        obj_utils.create_test_node(self.context,
                                   uuid=uuidutils.generate_uuid())

        self.mock_get_conductor_for.side_effect = exception.NoValidHost(
            reason='hey a conductor just goes vacation')
        response = self.get_json('/nodes?conductor=like.shadows',
                                 headers={api_base.Version.string: "1.49"})
        self.assertEqual([], response['nodes'])

        self.mock_get_conductor_for.side_effect = exception.TemporaryFailure(
            reason='this must be conductor strike')
        response = self.get_json('/nodes?conductor=like.shadows',
                                 headers={api_base.Version.string: "1.49"})
        self.assertEqual([], response['nodes'])

        self.mock_get_conductor_for.side_effect = exception.IronicException(
            'Some unexpected thing happened')
        response = self.get_json('/nodes?conductor=fake.conductor',
                                 headers={api_base.Version.string: "1.49"},
                                 expect_errors=True)
        self.assertIn('Some unexpected thing happened',
                      response.json['error_message'])

    def test_get_nodes_by_owner(self):
        node1 = obj_utils.create_test_node(self.context,
                                           uuid=uuidutils.generate_uuid(),
                                           owner='fred')
        node2 = obj_utils.create_test_node(self.context,
                                           uuid=uuidutils.generate_uuid(),
                                           owner='bob')

        for base_url in ('/nodes', '/nodes/detail'):
            data = self.get_json(base_url + '?owner=fred',
                                 headers={api_base.Version.string: "1.50"})
            uuids = [n['uuid'] for n in data['nodes']]
            self.assertIn(node1.uuid, uuids)
            self.assertNotIn(node2.uuid, uuids)
            data = self.get_json(base_url + '?owner=bob',
                                 headers={api_base.Version.string: "1.50"})
            uuids = [n['uuid'] for n in data['nodes']]
            self.assertIn(node2.uuid, uuids)
            self.assertNotIn(node1.uuid, uuids)

    def test_get_nodes_by_owner_not_allowed(self):
        for url in ('/nodes?owner=fred',
                    '/nodes/detail?owner=fred'):
            response = self.get_json(
                url, headers={api_base.Version.string: "1.48"},
                expect_errors=True)
            self.assertEqual('application/json', response.content_type)
            self.assertEqual(http_client.NOT_ACCEPTABLE, response.status_code)
            self.assertTrue(response.json['error_message'])

    def test_get_nodes_by_description(self):
        node1 = obj_utils.create_test_node(self.context,
                                           uuid=uuidutils.generate_uuid(),
                                           description='some cats here')
        node2 = obj_utils.create_test_node(self.context,
                                           uuid=uuidutils.generate_uuid(),
                                           description='some dogs there')
        data = self.get_json('/nodes?description_contains=cat',
                             headers={api_base.Version.string: '1.51'})
        uuids = [n['uuid'] for n in data['nodes']]
        self.assertIn(node1.uuid, uuids)
        self.assertNotIn(node2.uuid, uuids)

        data = self.get_json('/nodes?description_contains=dog',
                             headers={api_base.Version.string: '1.51'})
        uuids = [n['uuid'] for n in data['nodes']]
        self.assertIn(node2.uuid, uuids)
        self.assertNotIn(node1.uuid, uuids)

    def test_get_nodes_by_lessee(self):
        node1 = obj_utils.create_test_node(self.context,
                                           uuid=uuidutils.generate_uuid(),
                                           lessee='project1')
        node2 = obj_utils.create_test_node(self.context,
                                           uuid=uuidutils.generate_uuid(),
                                           lessee='project2')

        for base_url in ('/nodes', '/nodes/detail'):
            data = self.get_json(base_url + '?lessee=project1',
                                 headers={api_base.Version.string: "1.65"})
            uuids = [n['uuid'] for n in data['nodes']]
            self.assertIn(node1.uuid, uuids)
            self.assertNotIn(node2.uuid, uuids)
            data = self.get_json(base_url + '?lessee=project2',
                                 headers={api_base.Version.string: "1.65"})
            uuids = [n['uuid'] for n in data['nodes']]
            self.assertIn(node2.uuid, uuids)
            self.assertNotIn(node1.uuid, uuids)

    def test_get_nodes_by_lessee_not_allowed(self):
        for url in ('/nodes?lessee=project1',
                    '/nodes/detail?lessee=project1'):
            response = self.get_json(
                url, headers={api_base.Version.string: "1.64"},
                expect_errors=True)
            self.assertEqual('application/json', response.content_type)
            self.assertEqual(http_client.NOT_ACCEPTABLE, response.status_code)
            self.assertTrue(response.json['error_message'])

    def test_get_console_information(self):
        node = obj_utils.create_test_node(self.context)
        expected_console_info = {'test': 'test-data'}
        expected_data = {'console_enabled': True,
                         'console_info': expected_console_info}
        with mock.patch.object(rpcapi.ConductorAPI,
                               'get_console_information',
                               autospec=True) as mock_gci:
            mock_gci.return_value = expected_console_info
            data = self.get_json('/nodes/%s/states/console' % node.uuid)
            self.assertEqual(expected_data, data)
            mock_gci.assert_called_once_with(
                mock.ANY, mock.ANY, node.uuid, 'test-topic')

    @mock.patch.object(rpcapi.ConductorAPI, 'get_console_information',
                       autospec=True)
    def test_get_console_information_by_name(self, mock_gci):
        node = obj_utils.create_test_node(self.context, name='spam')
        expected_console_info = {'test': 'test-data'}
        expected_data = {'console_enabled': True,
                         'console_info': expected_console_info}
        mock_gci.return_value = expected_console_info
        data = self.get_json('/nodes/%s/states/console' % node.name,
                             headers={api_base.Version.string: "1.5"})
        self.assertEqual(expected_data, data)
        mock_gci.assert_called_once_with(
            mock.ANY, mock.ANY, node.uuid, 'test-topic')

    def test_get_console_information_console_disabled(self):
        node = obj_utils.create_test_node(self.context)
        expected_data = {'console_enabled': False,
                         'console_info': None}
        with mock.patch.object(rpcapi.ConductorAPI,
                               'get_console_information',
                               autospec=True) as mock_gci:
            mock_gci.side_effect = (
                exception.NodeConsoleNotEnabled(node=node.uuid))
            data = self.get_json('/nodes/%s/states/console' % node.uuid)
            self.assertEqual(expected_data, data)
            mock_gci.assert_called_once_with(
                mock.ANY, mock.ANY, node.uuid, 'test-topic')

    def test_get_console_information_not_supported(self):
        node = obj_utils.create_test_node(self.context)
        with mock.patch.object(rpcapi.ConductorAPI,
                               'get_console_information',
                               autospec=True) as mock_gci:
            mock_gci.side_effect = exception.UnsupportedDriverExtension(
                extension='console', driver='test-driver')
            ret = self.get_json('/nodes/%s/states/console' % node.uuid,
                                expect_errors=True)
            self.assertEqual(http_client.BAD_REQUEST, ret.status_code)
            mock_gci.assert_called_once_with(
                mock.ANY, mock.ANY, node.uuid, 'test-topic')

    @mock.patch.object(rpcapi.ConductorAPI, 'get_boot_device',
                       autospec=True)
    def test_get_boot_device(self, mock_gbd):
        node = obj_utils.create_test_node(self.context)
        expected_data = {'boot_device': boot_devices.PXE, 'persistent': True}
        mock_gbd.return_value = expected_data
        data = self.get_json('/nodes/%s/management/boot_device' % node.uuid)
        self.assertEqual(expected_data, data)
        mock_gbd.assert_called_once_with(
            mock.ANY, mock.ANY, node.uuid, 'test-topic')

    @mock.patch.object(rpcapi.ConductorAPI, 'get_boot_device',
                       autospec=True)
    def test_get_boot_device_by_name(self, mock_gbd):
        node = obj_utils.create_test_node(self.context, name='spam')
        expected_data = {'boot_device': boot_devices.PXE, 'persistent': True}
        mock_gbd.return_value = expected_data
        data = self.get_json('/nodes/%s/management/boot_device' % node.name,
                             headers={api_base.Version.string: "1.5"})
        self.assertEqual(expected_data, data)
        mock_gbd.assert_called_once_with(
            mock.ANY, mock.ANY, node.uuid, 'test-topic')

    @mock.patch.object(rpcapi.ConductorAPI, 'get_boot_device',
                       autospec=True)
    def test_get_boot_device_iface_not_supported(self, mock_gbd):
        node = obj_utils.create_test_node(self.context)
        mock_gbd.side_effect = exception.UnsupportedDriverExtension(
            extension='management', driver='test-driver')
        ret = self.get_json('/nodes/%s/management/boot_device' % node.uuid,
                            expect_errors=True)
        self.assertEqual(http_client.BAD_REQUEST, ret.status_code)
        self.assertTrue(ret.json['error_message'])
        mock_gbd.assert_called_once_with(
            mock.ANY, mock.ANY, node.uuid, 'test-topic')

    @mock.patch.object(rpcapi.ConductorAPI, 'get_supported_boot_devices',
                       autospec=True)
    def test_get_supported_boot_devices(self, mock_gsbd):
        mock_gsbd.return_value = [boot_devices.PXE]
        node = obj_utils.create_test_node(self.context)
        data = self.get_json('/nodes/%s/management/boot_device/supported'
                             % node.uuid)
        expected_data = {'supported_boot_devices': [boot_devices.PXE]}
        self.assertEqual(expected_data, data)
        mock_gsbd.assert_called_once_with(
            mock.ANY, mock.ANY, node.uuid, 'test-topic')

    @mock.patch.object(rpcapi.ConductorAPI, 'get_supported_boot_devices',
                       autospec=True)
    def test_get_supported_boot_devices_by_name(self, mock_gsbd):
        mock_gsbd.return_value = [boot_devices.PXE]
        node = obj_utils.create_test_node(self.context, name='spam')
        data = self.get_json(
            '/nodes/%s/management/boot_device/supported' % node.name,
            headers={api_base.Version.string: "1.5"})
        expected_data = {'supported_boot_devices': [boot_devices.PXE]}
        self.assertEqual(expected_data, data)
        mock_gsbd.assert_called_once_with(mock.ANY, mock.ANY, node.uuid,
                                          'test-topic')

    @mock.patch.object(rpcapi.ConductorAPI, 'get_supported_boot_devices',
                       autospec=True)
    def test_get_supported_boot_devices_iface_not_supported(self, mock_gsbd):
        node = obj_utils.create_test_node(self.context)
        mock_gsbd.side_effect = exception.UnsupportedDriverExtension(
            extension='management', driver='test-driver')
        ret = self.get_json('/nodes/%s/management/boot_device/supported' %
                            node.uuid, expect_errors=True)
        self.assertEqual(http_client.BAD_REQUEST, ret.status_code)
        self.assertTrue(ret.json['error_message'])
        mock_gsbd.assert_called_once_with(
            mock.ANY, mock.ANY, node.uuid, 'test-topic')

    @mock.patch.object(rpcapi.ConductorAPI, 'validate_driver_interfaces',
                       autospec=True, return_value={})
    def test_validate_by_uuid_using_deprecated_interface(self, mock_vdi):
        # Note(mrda): The 'node_uuid' interface is deprecated in favour
        # of the 'node' interface
        node = obj_utils.create_test_node(self.context)
        self.get_json('/nodes/validate?node_uuid=%s' % node.uuid)
        mock_vdi.assert_called_once_with(mock.ANY, mock.ANY,
                                         node.uuid, 'test-topic')

    @mock.patch.object(rpcapi.ConductorAPI, 'validate_driver_interfaces',
                       autospec=True, return_value={})
    def test_validate_by_uuid(self, mock_vdi):
        node = obj_utils.create_test_node(self.context)
        self.get_json('/nodes/validate?node=%s' % node.uuid,
                      headers={api_base.Version.string: "1.5"})
        mock_vdi.assert_called_once_with(mock.ANY, mock.ANY,
                                         node.uuid, 'test-topic')

    @mock.patch.object(rpcapi.ConductorAPI, 'validate_driver_interfaces',
                       autospec=True)
    def test_validate_by_name_unsupported(self, mock_vdi):
        node = obj_utils.create_test_node(self.context, name='spam')
        ret = self.get_json('/nodes/validate?node=%s' % node.name,
                            expect_errors=True)
        self.assertEqual(http_client.NOT_ACCEPTABLE, ret.status_code)
        self.assertFalse(mock_vdi.called)

    @mock.patch.object(rpcapi.ConductorAPI, 'validate_driver_interfaces',
                       autospec=True, return_value={})
    def test_validate_by_name(self, mock_vdi):
        node = obj_utils.create_test_node(self.context, name='spam')
        self.get_json('/nodes/validate?node=%s' % node.name,
                      headers={api_base.Version.string: "1.5"})
        # note that this should be node.uuid here as we get that from the
        # rpc_node lookup and pass that downwards
        mock_vdi.assert_called_once_with(mock.ANY, mock.ANY,
                                         node.uuid, 'test-topic')

    @mock.patch.object(rpcapi.ConductorAPI, 'get_indicator_state',
                       autospec=True)
    def test_get_indicator_state(self, mock_gis):
        node = obj_utils.create_test_node(self.context)
        expected_data = {
            'state': indicator_states.ON
        }
        mock_gis.return_value = indicator_states.ON
        component = components.SYSTEM
        indicator_id = 'led'
        indicator_name = indicator_id + '@' + component
        data = self.get_json(
            '/nodes/%s/management/indicators'
            '/%s' % (node.uuid, indicator_name))
        self.assertEqual(expected_data, data)
        mock_gis.assert_called_once_with(
            mock.ANY, mock.ANY, node.uuid, component, indicator_id,
            topic='test-topic')

    @mock.patch.object(rpcapi.ConductorAPI, 'get_indicator_state',
                       autospec=True)
    def test_get_indicator_state_versioning(self, mock_gis):
        node = obj_utils.create_test_node(self.context, name='spam')
        expected_data = {
            'state': indicator_states.ON
        }
        mock_gis.return_value = indicator_states.ON
        component = components.SYSTEM
        indicator_id = 'led'
        indicator_name = indicator_id + '@' + component
        data = self.get_json(
            '/nodes/%s/management/indicators'
            '/%s' % (node.uuid, indicator_name),
            headers={api_base.Version.string: "1.63"})
        self.assertEqual(expected_data, data)
        mock_gis.assert_called_once_with(
            mock.ANY, mock.ANY, node.uuid, component, indicator_id,
            topic='test-topic')

    @mock.patch.object(rpcapi.ConductorAPI, 'get_indicator_state',
                       autospec=True)
    def test_get_indicator_state_iface_not_supported(self, mock_gis):
        node = obj_utils.create_test_node(self.context)
        mock_gis.side_effect = exception.UnsupportedDriverExtension(
            extension='management', driver='test-driver')
        component = components.SYSTEM
        indicator_id = 'led'
        indicator_name = indicator_id + '@' + component
        ret = self.get_json(
            '/nodes/%s/management/indicators'
            '/%s' % (node.uuid, indicator_name),
            expect_errors=True)
        self.assertEqual(http_client.BAD_REQUEST, ret.status_code)
        self.assertTrue(ret.json['error_message'])
        mock_gis.assert_called_once_with(
            mock.ANY, mock.ANY, node.uuid, component, indicator_id,
            topic='test-topic')

    @mock.patch.object(rpcapi.ConductorAPI, 'get_supported_indicators',
                       autospec=True)
    def test_get_supported_indicators(self, mock_gsi):
        mock_gsi.return_value = {
            components.CHASSIS: {
                'led': {
                    'readonly': True,
                    'states': [
                        'OFF',
                        'ON'
                    ]
                }
            }
        }
        node = obj_utils.create_test_node(self.context)

        expected_data = {
            'indicators': [
                {'component': 'chassis',
                 'name': 'led@chassis',
                 'readonly': True,
                 'states': ['OFF', 'ON'],
                 'links': [
                     {'href': 'http://localhost/v1/nodes/1be26c0b-03f2-4d2e'
                              '-ae87-c02d7f33c123/management/indicators/'
                              'led@chassis',
                      'rel': 'self'},
                     {'href': 'http://localhost/nodes/1be26c0b-03f2-4d2e-ae'
                              '87-c02d7f33c123/management/indicators/'
                              'led@chassis',
                      'rel': 'bookmark'}]}
            ]
        }

        data = self.get_json('/nodes/%s/management/indicators'
                             % node.uuid)
        self.assertEqual(expected_data, data)
        mock_gsi.assert_called_once_with(
            mock.ANY, mock.ANY, node.uuid, topic='test-topic')

    @mock.patch.object(rpcapi.ConductorAPI, 'get_supported_indicators',
                       autospec=True)
    def test_get_supported_indicators_versioning(self, mock_gsi):
        mock_gsi.return_value = {
            components.CHASSIS: {
                'led': {
                    'readonly': True,
                    'states': [
                        'OFF',
                        'ON'
                    ]
                }
            }
        }
        node = obj_utils.create_test_node(self.context)

        expected_data = {
            'indicators': [
                {'component': 'chassis',
                 'name': 'led@chassis',
                 'readonly': True,
                 'states': ['OFF', 'ON'],
                 'links': [
                     {'href': 'http://localhost/v1/nodes/1be26c0b-03f2-4d2e'
                              '-ae87-c02d7f33c123/management/indicators/'
                              'led@chassis',
                      'rel': 'self'},
                     {'href': 'http://localhost/nodes/1be26c0b-03f2-4d2e-ae'
                              '87-c02d7f33c123/management/indicators/'
                              'led@chassis',
                      'rel': 'bookmark'}]}
            ]
        }

        data = self.get_json('/nodes/%s/management/indicators'
                             % node.uuid,
                             headers={api_base.Version.string: "1.63"})
        self.assertEqual(expected_data, data)
        mock_gsi.assert_called_once_with(
            mock.ANY, mock.ANY, node.uuid, topic='test-topic')

    @mock.patch.object(rpcapi.ConductorAPI, 'get_supported_indicators',
                       autospec=True)
    def test_get_supported_indicators_iface_not_supported(self, mock_gsi):
        node = obj_utils.create_test_node(self.context)
        mock_gsi.side_effect = exception.UnsupportedDriverExtension(
            extension='management', driver='test-driver')
        ret = self.get_json('/nodes/%s/management/indicators' %
                            node.uuid, expect_errors=True)
        self.assertEqual(http_client.BAD_REQUEST, ret.status_code)
        self.assertTrue(ret.json['error_message'])
        mock_gsi.assert_called_once_with(
            mock.ANY, mock.ANY, node.uuid, topic='test-topic')


class TestPatch(test_api_base.BaseApiTest):

    def setUp(self):
        super(TestPatch, self).setUp()
        self.chassis = obj_utils.create_test_chassis(self.context)
        self.node = obj_utils.create_test_node(self.context, name='node-57.1',
                                               chassis_id=self.chassis.id)
        self.node_no_name = obj_utils.create_test_node(
            self.context, uuid='deadbeef-0000-1111-2222-333333333333',
            chassis_id=self.chassis.id)
        self.port = obj_utils.create_test_port(
            self.context,
            uuid='9bb50f13-0b8d-4ade-ad2d-d91fefdef9cc',
            address='00:01:02:03:04:05',
            node_id=self.node.id)
        self.portgroup = obj_utils.create_test_portgroup(
            self.context,
            uuid='9bb50f13-0b8d-4ade-ad2d-d91fefdef9ff',
            address='00:00:00:00:00:ff',
            node_id=self.node.id)
        p = mock.patch.object(rpcapi.ConductorAPI, 'get_topic_for',
                              autospec=True)
        self.mock_gtf = p.start()
        self.mock_gtf.return_value = 'test-topic'
        self.addCleanup(p.stop)
        p = mock.patch.object(rpcapi.ConductorAPI, 'update_node',
                              autospec=True)
        self.mock_update_node = p.start()
        self.addCleanup(p.stop)
        p = mock.patch.object(rpcapi.ConductorAPI, 'change_node_power_state',
                              autospec=True)
        self.mock_cnps = p.start()
        self.addCleanup(p.stop)

    @mock.patch.object(notification_utils, '_emit_api_notification',
                       autospec=True)
    def test_update_ok(self, mock_notify):
        self.mock_update_node.return_value = self.node
        (self
         .mock_update_node
         .return_value
         .updated_at) = "2013-12-03T06:20:41.184720+00:00"
        response = self.patch_json('/nodes/%s' % self.node.uuid,
                                   [{'path': '/instance_uuid',
                                     'value':
                                     'aaaaaaaa-1111-bbbb-2222-cccccccccccc',
                                     'op': 'replace'}])
        self.assertEqual('application/json', response.content_type)
        self.assertEqual(http_client.OK, response.status_code)
        self.assertEqual(self.mock_update_node.return_value.updated_at,
                         timeutils.parse_isotime(response.json['updated_at']))
        self.mock_update_node.assert_called_once_with(
            mock.ANY, mock.ANY, mock.ANY, 'test-topic', None)
        mock_notify.assert_has_calls([mock.call(mock.ANY, mock.ANY, 'update',
                                      obj_fields.NotificationLevel.INFO,
                                      obj_fields.NotificationStatus.START,
                                      chassis_uuid=self.chassis.uuid),
                                      mock.call(mock.ANY, mock.ANY, 'update',
                                      obj_fields.NotificationLevel.INFO,
                                      obj_fields.NotificationStatus.END,
                                      chassis_uuid=self.chassis.uuid)])

    def test_update_by_name_unsupported(self):
        self.mock_update_node.return_value = self.node
        (self
         .mock_update_node
         .return_value
         .updated_at) = "2013-12-03T06:20:41.184720+00:00"
        response = self.patch_json(
            '/nodes/%s' % self.node.name,
            [{'path': '/instance_uuid',
              'value': 'aaaaaaaa-1111-bbbb-2222-cccccccccccc',
              'op': 'replace'}],
            expect_errors=True)
        self.assertEqual(http_client.NOT_FOUND, response.status_code)
        self.assertFalse(self.mock_update_node.called)

    def test_update_ok_by_name(self):
        self.mock_update_node.return_value = self.node
        (self
         .mock_update_node
         .return_value
         .updated_at) = "2013-12-03T06:20:41.184720+00:00"
        response = self.patch_json(
            '/nodes/%s' % self.node.name,
            [{'path': '/instance_uuid',
              'value': 'aaaaaaaa-1111-bbbb-2222-cccccccccccc',
              'op': 'replace'}],
            headers={api_base.Version.string: "1.5"})
        self.assertEqual('application/json', response.content_type)
        self.assertEqual(http_client.OK, response.status_code)
        self.assertEqual(self.mock_update_node.return_value.updated_at,
                         timeutils.parse_isotime(response.json['updated_at']))
        self.mock_update_node.assert_called_once_with(
            mock.ANY, mock.ANY, mock.ANY, 'test-topic', None)

    def test_update_ok_by_name_with_json(self):
        self.mock_update_node.return_value = self.node
        (self
         .mock_update_node
         .return_value
         .updated_at) = "2013-12-03T06:20:41.184720+00:00"
        response = self.patch_json(
            '/nodes/%s.json' % self.node.name,
            [{'path': '/instance_uuid',
              'value': 'aaaaaaaa-1111-bbbb-2222-cccccccccccc',
              'op': 'replace'}],
            headers={api_base.Version.string: "1.5"})
        self.assertEqual('application/json', response.content_type)
        self.assertEqual(http_client.OK, response.status_code)
        self.assertEqual(self.mock_update_node.return_value.updated_at,
                         timeutils.parse_isotime(response.json['updated_at']))
        self.mock_update_node.assert_called_once_with(
            mock.ANY, mock.ANY, mock.ANY, 'test-topic', None)

    def test_update_state(self):
        response = self.patch_json('/nodes/%s' % self.node.uuid,
                                   [{'power_state': 'new state'}],
                                   expect_errors=True)
        self.assertEqual('application/json', response.content_type)
        self.assertEqual(http_client.BAD_REQUEST, response.status_code)
        self.assertTrue(response.json['error_message'])

    @mock.patch.object(notification_utils, '_emit_api_notification',
                       autospec=True)
    def test_update_fails_bad_driver_info(self, mock_notify):
        fake_err = 'Fake Error Message'
        self.mock_update_node.side_effect = (
            exception.InvalidParameterValue(fake_err))

        response = self.patch_json('/nodes/%s' % self.node.uuid,
                                   [{'path': '/driver_info/this',
                                     'value': 'foo',
                                     'op': 'add'},
                                    {'path': '/driver_info/that',
                                     'value': 'bar',
                                     'op': 'add'}],
                                   expect_errors=True)
        self.assertEqual('application/json', response.content_type)
        self.assertEqual(http_client.BAD_REQUEST, response.status_code)

        self.mock_update_node.assert_called_once_with(
            mock.ANY, mock.ANY, mock.ANY, 'test-topic', None)
        mock_notify.assert_has_calls([mock.call(mock.ANY, mock.ANY, 'update',
                                      obj_fields.NotificationLevel.INFO,
                                      obj_fields.NotificationStatus.START,
                                      chassis_uuid=self.chassis.uuid),
                                      mock.call(mock.ANY, mock.ANY, 'update',
                                      obj_fields.NotificationLevel.ERROR,
                                      obj_fields.NotificationStatus.ERROR,
                                      chassis_uuid=self.chassis.uuid)])

    def test_update_fails_bad_driver(self):
        self.mock_gtf.side_effect = exception.NoValidHost('Fake Error')

        response = self.patch_json('/nodes/%s' % self.node.uuid,
                                   [{'path': '/driver',
                                     'value': 'bad-driver',
                                     'op': 'replace'}],
                                   expect_errors=True)

        self.assertEqual('application/json', response.content_type)
        self.assertEqual(http_client.BAD_REQUEST, response.status_code)

    def test_update_with_reset_interfaces(self):
        self.mock_update_node.return_value = self.node
        (self
         .mock_update_node
         .return_value
         .updated_at) = "2013-12-03T06:20:41.184720+00:00"
        response = self.patch_json(
            '/nodes/%s?reset_interfaces=True' % self.node.uuid,
            [{'path': '/driver', 'value': 'ipmi', 'op': 'replace'}],
            headers={api_base.Version.string: "1.45"})
        self.assertEqual('application/json', response.content_type)
        self.assertEqual(http_client.OK, response.status_code)
        self.assertEqual(self.mock_update_node.return_value.updated_at,
                         timeutils.parse_isotime(response.json['updated_at']))
        self.mock_update_node.assert_called_once_with(
            mock.ANY, mock.ANY, mock.ANY, 'test-topic', True)

    def test_reset_interfaces_without_driver(self):
        response = self.patch_json(
            '/nodes/%s?reset_interfaces=True' % self.node.uuid,
            [{'path': '/name', 'value': 'new name', 'op': 'replace'}],
            headers={api_base.Version.string: "1.45"},
            expect_errors=True)
        self.assertEqual('application/json', response.content_type)
        self.assertEqual(http_client.BAD_REQUEST, response.status_code)
        self.assertFalse(self.mock_update_node.called)

    def test_reset_interfaces_not_supported(self):
        response = self.patch_json(
            '/nodes/%s?reset_interfaces=True' % self.node.uuid,
            [{'path': '/driver', 'value': 'ipmi', 'op': 'replace'}],
            expect_errors=True)
        self.assertEqual('application/json', response.content_type)
        self.assertEqual(http_client.NOT_ACCEPTABLE, response.status_code)
        self.assertFalse(self.mock_update_node.called)

    def test_add_ok(self):
        self.mock_update_node.return_value = self.node

        response = self.patch_json('/nodes/%s' % self.node.uuid,
                                   [{'path': '/extra/foo',
                                     'value': 'bar',
                                     'op': 'add'}])
        self.assertEqual('application/json', response.content_type)
        self.assertEqual(http_client.OK, response.status_code)

        self.mock_update_node.assert_called_once_with(
            mock.ANY, mock.ANY, mock.ANY, 'test-topic', None)

    def test_add_root(self):
        self.mock_update_node.return_value = self.node
        response = self.patch_json('/nodes/%s' % self.node.uuid,
                                   [{'path': '/instance_uuid',
                                     'value':
                                     'aaaaaaaa-1111-bbbb-2222-cccccccccccc',
                                     'op': 'add'}])
        self.assertEqual('application/json', response.content_type)
        self.assertEqual(http_client.OK, response.status_code)
        self.mock_update_node.assert_called_once_with(
            mock.ANY, mock.ANY, mock.ANY, 'test-topic', None)

    def test_add_root_non_existent(self):
        response = self.patch_json('/nodes/%s' % self.node.uuid,
                                   [{'path': '/foo', 'value': 'bar',
                                     'op': 'add'}],
                                   expect_errors=True)
        self.assertEqual('application/json', response.content_type)
        self.assertEqual(http_client.BAD_REQUEST, response.status_code)
        self.assertTrue(response.json['error_message'])

    def test_remove_ok(self):
        self.mock_update_node.return_value = self.node

        response = self.patch_json('/nodes/%s' % self.node.uuid,
                                   [{'path': '/extra',
                                     'op': 'remove'}])
        self.assertEqual('application/json', response.content_type)
        self.assertEqual(http_client.OK, response.status_code)

        self.mock_update_node.assert_called_once_with(
            mock.ANY, mock.ANY, mock.ANY, 'test-topic', None)

    def test_remove_non_existent_property_fail(self):
        response = self.patch_json('/nodes/%s' % self.node.uuid,
                                   [{'path': '/extra/non-existent',
                                     'op': 'remove'}],
                                   expect_errors=True)
        self.assertEqual('application/json', response.content_type)
        self.assertEqual(http_client.BAD_REQUEST, response.status_code)
        self.assertTrue(response.json['error_message'])

    def test_update_allowed_in_power_transition(self):
        node = obj_utils.create_test_node(self.context,
                                          uuid=uuidutils.generate_uuid(),
                                          target_power_state=states.POWER_OFF)
        self.mock_update_node.return_value = node
        response = self.patch_json('/nodes/%s' % node.uuid,
                                   [{'path': '/extra/foo',
                                     'value': 'bar',
                                     'op': 'add'}])
        self.assertEqual(http_client.OK, response.status_code)

    def test_update_allowed_in_maintenance(self):
        node = obj_utils.create_test_node(self.context,
                                          uuid=uuidutils.generate_uuid(),
                                          target_power_state=states.POWER_OFF,
                                          maintenance=True)
        self.mock_update_node.return_value = node
        response = self.patch_json('/nodes/%s' % node.uuid,
                                   [{'path': '/instance_uuid',
                                     'op': 'remove'}])
        self.assertEqual(http_client.OK, response.status_code)

    def test_update_in_inspecting_not_allowed(self):
        node = obj_utils.create_test_node(self.context,
                                          uuid=uuidutils.generate_uuid(),
                                          provision_state=states.INSPECTING)
        self.mock_update_node.return_value = node
        response = self.patch_json('/nodes/%s' % node.uuid,
                                   [{'path': '/instance_uuid',
                                     'op': 'remove'}],
                                   headers={api_base.Version.string: "1.39"},
                                   expect_errors=True)
        self.assertEqual(http_client.CONFLICT, response.status_code)

    def test_update_in_inspecting_allowed(self):
        node = obj_utils.create_test_node(self.context,
                                          uuid=uuidutils.generate_uuid(),
                                          provision_state=states.INSPECTING)
        self.mock_update_node.return_value = node
        response = self.patch_json('/nodes/%s' % node.uuid,
                                   [{'path': '/instance_uuid',
                                     'op': 'remove'}],
                                   headers={api_base.Version.string: "1.38"})
        self.assertEqual(http_client.OK, response.status_code)

    def test_add_state_in_deployfail(self):
        node = obj_utils.create_test_node(self.context,
                                          uuid=uuidutils.generate_uuid(),
                                          provision_state=states.DEPLOYFAIL,
                                          target_provision_state=states.ACTIVE)
        self.mock_update_node.return_value = node
        response = self.patch_json('/nodes/%s' % node.uuid,
                                   [{'path': '/extra/foo', 'value': 'bar',
                                     'op': 'add'}])
        self.assertEqual('application/json', response.content_type)
        self.assertEqual(http_client.OK, response.status_code)
        self.mock_update_node.assert_called_once_with(
            mock.ANY, mock.ANY, mock.ANY, 'test-topic', None)

    def test_patch_ports_subresource_no_port_id(self):
        response = self.patch_json('/nodes/%s/ports' % self.node.uuid,
                                   [{'path': '/extra/foo', 'value': 'bar',
                                     'op': 'add'}], expect_errors=True)
        self.assertEqual(http_client.BAD_REQUEST, response.status_int)

    def test_patch_ports_subresource(self):
        response = self.patch_json(
            '/nodes/%s/ports/9bb50f13-0b8d-4ade-ad2d-d91fefdef9cc' %
            self.node.uuid,
            [{'path': '/extra/foo', 'value': 'bar',
              'op': 'add'}], expect_errors=True)
        self.assertEqual(http_client.FORBIDDEN, response.status_int)

    def test_patch_portgroups_subresource(self):
        response = self.patch_json(
            '/nodes/%s/portgroups/9bb50f13-0b8d-4ade-ad2d-d91fefdef9ff' %
            self.node.uuid,
            [{'path': '/extra/foo', 'value': 'bar',
              'op': 'add'}], expect_errors=True,
            headers={'X-OpenStack-Ironic-API-Version': '1.24'})
        self.assertEqual(http_client.FORBIDDEN, response.status_int)

    def test_patch_volume_connectors_subresource_no_connector_id(self):
        response = self.patch_json(
            '/nodes/%s/volume/connectors' % self.node.uuid,
            [{'path': '/extra/foo', 'value': 'bar', 'op': 'add'}],
            expect_errors=True,
            headers={api_base.Version.string: str(api_v1.max_version())})
        self.assertEqual(http_client.BAD_REQUEST, response.status_int)

    def test_patch_volume_connectors_subresource(self):
        connector = (
            obj_utils.create_test_volume_connector(self.context,
                                                   node_id=self.node.id))
        response = self.patch_json(
            '/nodes/%s/volume/connectors/%s' % (self.node.uuid,
                                                connector.uuid),
            [{'path': '/extra/foo', 'value': 'bar', 'op': 'add'}],
            expect_errors=True,
            headers={api_base.Version.string: str(api_v1.max_version())})
        self.assertEqual(http_client.FORBIDDEN, response.status_int)

    def test_patch_volume_targets_subresource(self):
        target = obj_utils.create_test_volume_target(self.context,
                                                     node_id=self.node.id)
        response = self.patch_json(
            '/nodes/%s/volume/targets/%s' % (self.node.uuid,
                                             target.uuid),
            [{'path': '/extra/foo', 'value': 'bar', 'op': 'add'}],
            expect_errors=True,
            headers={api_base.Version.string: str(api_v1.max_version())})
        self.assertEqual(http_client.FORBIDDEN, response.status_int)

    def test_remove_uuid(self):
        response = self.patch_json('/nodes/%s' % self.node.uuid,
                                   [{'path': '/uuid', 'op': 'remove'}],
                                   expect_errors=True)
        self.assertEqual('application/json', response.content_type)
        self.assertEqual(http_client.BAD_REQUEST, response.status_code)
        self.assertTrue(response.json['error_message'])

    def test_add_state_in_cleaning(self):
        node = obj_utils.create_test_node(
            self.context,
            uuid=uuidutils.generate_uuid(),
            provision_state=states.CLEANING,
            target_provision_state=states.AVAILABLE)
        self.mock_update_node.return_value = node
        response = self.patch_json('/nodes/%s' % node.uuid,
                                   [{'path': '/extra/foo', 'value': 'bar',
                                     'op': 'add'}], expect_errors=True)
        self.assertEqual('application/json', response.content_type)
        self.assertEqual(http_client.CONFLICT, response.status_code)
        self.assertTrue(response.json['error_message'])

    def test_remove_mandatory_field(self):
        response = self.patch_json('/nodes/%s' % self.node.uuid,
                                   [{'path': '/driver', 'op': 'remove'}],
                                   expect_errors=True)
        self.assertEqual('application/json', response.content_type)
        self.assertEqual(http_client.BAD_REQUEST, response.status_code)
        self.assertTrue(response.json['error_message'])

    def test_replace_chassis_uuid(self):
        self.mock_update_node.return_value = self.node
        response = self.patch_json('/nodes/%s' % self.node.uuid,
                                   [{'path': '/chassis_uuid',
                                     'value': self.chassis.uuid,
                                     'op': 'replace'}])
        self.assertEqual('application/json', response.content_type)
        self.assertEqual(http_client.OK, response.status_code)

    def test_add_chassis_uuid(self):
        self.mock_update_node.return_value = self.node
        response = self.patch_json('/nodes/%s' % self.node.uuid,
                                   [{'path': '/chassis_uuid',
                                     'value': self.chassis.uuid,
                                     'op': 'add'}])
        self.assertEqual('application/json', response.content_type)
        self.assertEqual(http_client.OK, response.status_code)

    def test_remove_chassis_uuid(self):
        self.mock_update_node.return_value = self.node
        headers = {api_base.Version.string: "1.25"}
        response = self.patch_json('/nodes/%s' % self.node.uuid,
                                   [{'path': '/chassis_uuid',
                                     'op': 'remove'}],
                                   headers=headers)
        self.assertEqual('application/json', response.content_type)
        self.assertEqual(http_client.OK, response.status_code)

    def test_remove_chassis_uuid_invalid_api_version(self):
        self.mock_update_node.return_value = self.node
        headers = {api_base.Version.string: "1.24"}
        response = self.patch_json('/nodes/%s' % self.node.uuid,
                                   [{'path': '/chassis_uuid',
                                     'op': 'remove'}],
                                   headers=headers,
                                   expect_errors=True)
        self.assertEqual('application/json', response.content_type)
        self.assertEqual(http_client.NOT_ACCEPTABLE, response.status_code)
        self.assertTrue(response.json['error_message'])

    @mock.patch('ironic.api.request')  # noqa
    def test__update_changed_fields_lowers_conductor_group(self,
                                                           mock_pecan_req):
        mock_pecan_req.version.minor = versions.MINOR_MAX_VERSION
        controller = api_node.NodesController()

        node_dict = self.node.as_dict()
        node_dict['conductor_group'] = 'NEW-GROUP'

        controller._update_changed_fields(node_dict, self.node)
        self.assertEqual('new-group', self.node.conductor_group)

    @mock.patch('ironic.api.request')  # noqa
    def test__update_changed_fields_remove_chassis_uuid(self, mock_pecan_req):
        mock_pecan_req.version.minor = versions.MINOR_MAX_VERSION
        controller = api_node.NodesController()

        node_dict = self.node.as_dict()
        del node_dict['chassis_id']

        controller._update_changed_fields(node_dict, self.node)
        self.assertIsNone(self.node.chassis_id)

    def test_add_chassis_id(self):
        response = self.patch_json('/nodes/%s' % self.node.uuid,
                                   [{'path': '/chassis_id',
                                     'value': '1',
                                     'op': 'add'}],
                                   expect_errors=True)
        self.assertEqual('application/json', response.content_type)
        self.assertEqual(http_client.BAD_REQUEST, response.status_code)
        self.assertTrue(response.json['error_message'])

    def test_replace_chassis_id(self):
        response = self.patch_json('/nodes/%s' % self.node.uuid,
                                   [{'path': '/chassis_id',
                                     'value': '1',
                                     'op': 'replace'}],
                                   expect_errors=True)
        self.assertEqual('application/json', response.content_type)
        self.assertEqual(http_client.BAD_REQUEST, response.status_code)
        self.assertTrue(response.json['error_message'])

    def test_remove_chassis_id(self):
        response = self.patch_json('/nodes/%s' % self.node.uuid,
                                   [{'path': '/chassis_id',
                                     'op': 'remove'}],
                                   expect_errors=True)
        self.assertEqual('application/json', response.content_type)
        self.assertEqual(http_client.BAD_REQUEST, response.status_code)
        self.assertTrue(response.json['error_message'])

    def test_replace_non_existent_chassis_uuid(self):
        response = self.patch_json('/nodes/%s' % self.node.uuid,
                                   [{'path': '/chassis_uuid',
                                     'value':
                                     'eeeeeeee-dddd-cccc-bbbb-aaaaaaaaaaaa',
                                     'op': 'replace'}], expect_errors=True)
        self.assertEqual('application/json', response.content_type)
        self.assertEqual(http_client.BAD_REQUEST, response.status_code)
        self.assertTrue(response.json['error_message'])

    def test_remove_internal_field(self):
        response = self.patch_json('/nodes/%s' % self.node.uuid,
                                   [{'path': '/last_error', 'op': 'remove'}],
                                   expect_errors=True)
        self.assertEqual('application/json', response.content_type)
        self.assertEqual(http_client.BAD_REQUEST, response.status_code)
        self.assertTrue(response.json['error_message'])

    def test_replace_internal_field(self):
        response = self.patch_json('/nodes/%s' % self.node.uuid,
                                   [{'path': '/power_state', 'op': 'replace',
                                     'value': 'fake-state'}],
                                   expect_errors=True)
        self.assertEqual('application/json', response.content_type)
        self.assertEqual(http_client.BAD_REQUEST, response.status_code)
        self.assertTrue(response.json['error_message'])

    def test_replace_maintenance(self):
        self.mock_update_node.return_value = self.node

        response = self.patch_json('/nodes/%s' % self.node.uuid,
                                   [{'path': '/maintenance', 'op': 'replace',
                                     'value': True}])
        self.assertEqual('application/json', response.content_type)
        self.assertEqual(http_client.OK, response.status_code)

        self.mock_update_node.assert_called_once_with(
            mock.ANY, mock.ANY, mock.ANY, 'test-topic', None)

    def test_replace_maintenance_by_name(self):
        self.mock_update_node.return_value = self.node

        response = self.patch_json(
            '/nodes/%s' % self.node.name,
            [{'path': '/maintenance', 'op': 'replace',
              'value': True}],
            headers={api_base.Version.string: "1.5"})
        self.assertEqual('application/json', response.content_type)
        self.assertEqual(http_client.OK, response.status_code)

        self.mock_update_node.assert_called_once_with(
            mock.ANY, mock.ANY, mock.ANY, 'test-topic', None)

    def test_replace_consoled_enabled(self):
        response = self.patch_json('/nodes/%s' % self.node.uuid,
                                   [{'path': '/console_enabled',
                                     'op': 'replace', 'value': True}],
                                   expect_errors=True)
        self.assertEqual('application/json', response.content_type)
        self.assertEqual(http_client.BAD_REQUEST, response.status_code)
        self.assertTrue(response.json['error_message'])

    def test_replace_provision_updated_at(self):
        test_time = '2000-01-01 00:00:00'
        response = self.patch_json('/nodes/%s' % self.node.uuid,
                                   [{'path': '/provision_updated_at',
                                     'op': 'replace', 'value': test_time}],
                                   expect_errors=True)
        self.assertEqual('application/json', response.content_type)
        self.assertEqual(http_client.BAD_REQUEST, response.status_code)
        self.assertTrue(response.json['error_message'])

    def test_patch_add_name_ok(self):
        self.mock_update_node.return_value = self.node_no_name
        test_name = 'guido-van-rossum'
        response = self.patch_json('/nodes/%s' % self.node_no_name.uuid,
                                   [{'path': '/name',
                                     'op': 'add',
                                     'value': test_name}],
                                   headers={api_base.Version.string: "1.5"})
        self.assertEqual('application/json', response.content_type)
        self.assertEqual(http_client.OK, response.status_code)

    def _patch_add_name_invalid_or_reserved(self, name):
        self.mock_update_node.return_value = self.node_no_name
        response = self.patch_json('/nodes/%s' % self.node_no_name.uuid,
                                   [{'path': '/name',
                                     'op': 'add',
                                     'value': name}],
                                   headers={api_base.Version.string: "1.10"},
                                   expect_errors=True)
        self.assertEqual('application/json', response.content_type)
        self.assertEqual(http_client.BAD_REQUEST, response.status_code)
        self.assertTrue(response.json['error_message'])

    def test_patch_add_name_invalid(self):
        self._patch_add_name_invalid_or_reserved('i am invalid')

    def test_patch_add_name_reserved(self):
        reserved_names = api_utils.get_controller_reserved_names(
            api_node.NodesController)
        for name in reserved_names:
            self._patch_add_name_invalid_or_reserved(name)

    def test_patch_add_name_empty_invalid(self):
        test_name = ''
        response = self.patch_json('/nodes/%s' % self.node_no_name.uuid,
                                   [{'path': '/name',
                                     'op': 'add',
                                     'value': test_name}],
                                   headers={api_base.Version.string: "1.5"},
                                   expect_errors=True)
        self.assertEqual('application/json', response.content_type)
        self.assertEqual(http_client.BAD_REQUEST, response.status_code)
        self.assertTrue(response.json['error_message'])

    def test_patch_add_name_empty_not_acceptable(self):
        test_name = ''
        response = self.patch_json('/nodes/%s' % self.node_no_name.uuid,
                                   [{'path': '/name',
                                     'op': 'add',
                                     'value': test_name}],
                                   expect_errors=True)
        self.assertEqual('application/json', response.content_type)
        self.assertEqual(http_client.NOT_ACCEPTABLE, response.status_code)
        self.assertTrue(response.json['error_message'])

    def test_patch_name_replace_ok(self):
        self.mock_update_node.return_value = self.node
        test_name = 'guido-van-rossum'
        response = self.patch_json('/nodes/%s' % self.node.uuid,
                                   [{'path': '/name',
                                     'op': 'replace',
                                     'value': test_name}],
                                   headers={api_base.Version.string: "1.5"})
        self.assertEqual('application/json', response.content_type)
        self.assertEqual(http_client.OK, response.status_code)

    def test_patch_add_replace_invalid(self):
        self.mock_update_node.return_value = self.node_no_name
        test_name = 'Guido Van Error'
        response = self.patch_json('/nodes/%s' % self.node.uuid,
                                   [{'path': '/name',
                                     'op': 'replace',
                                     'value': test_name}],
                                   headers={api_base.Version.string: "1.5"},
                                   expect_errors=True)
        self.assertEqual('application/json', response.content_type)
        self.assertEqual(http_client.BAD_REQUEST, response.status_code)
        self.assertTrue(response.json['error_message'])

    def test_patch_update_name_twice_both_invalid(self):
        test_name_1 = 'Windows ME'
        test_name_2 = 'Guido Van Error'
        response = self.patch_json('/nodes/%s' % self.node.uuid,
                                   [{'path': '/name',
                                     'op': 'add',
                                     'value': test_name_1},
                                    {'path': '/name',
                                     'op': 'replace',
                                     'value': test_name_2}],
                                   headers={api_base.Version.string: "1.5"},
                                   expect_errors=True)
        self.assertEqual('application/json', response.content_type)
        self.assertEqual(http_client.BAD_REQUEST, response.status_code)
        self.assertIn(test_name_1, response.json['error_message'])

    def test_patch_update_name_twice_second_invalid(self):
        test_name = 'Guido Van Error'
        response = self.patch_json('/nodes/%s' % self.node.uuid,
                                   [{'path': '/name',
                                     'op': 'add',
                                     'value': 'node-0'},
                                    {'path': '/name',
                                     'op': 'replace',
                                     'value': test_name}],
                                   headers={api_base.Version.string: "1.5"},
                                   expect_errors=True)
        self.assertEqual('application/json', response.content_type)
        self.assertEqual(http_client.BAD_REQUEST, response.status_code)
        self.assertIn(test_name, response.json['error_message'])

    def test_patch_duplicate_name(self):
        node = obj_utils.create_test_node(self.context,
                                          uuid=uuidutils.generate_uuid())
        test_name = "this-is-my-node"
        self.mock_update_node.side_effect = exception.DuplicateName(test_name)
        response = self.patch_json('/nodes/%s' % node.uuid,
                                   [{'path': '/name',
                                     'op': 'replace',
                                     'value': test_name}],
                                   headers={api_base.Version.string: "1.5"},
                                   expect_errors=True)
        self.assertEqual('application/json', response.content_type)
        self.assertEqual(http_client.CONFLICT, response.status_code)
        self.assertTrue(response.json['error_message'])

    @mock.patch.object(api_node.NodesController, '_check_names_acceptable',
                       autospec=True)
    def test_patch_name_remove_ok(self, cna_mock):
        self.mock_update_node.return_value = self.node
        response = self.patch_json('/nodes/%s' % self.node.uuid,
                                   [{'path': '/name',
                                     'op': 'remove'}],
                                   headers={api_base.Version.string:
                                            "1.5"})
        self.assertEqual('application/json', response.content_type)
        self.assertEqual(http_client.OK, response.status_code)
        self.assertFalse(cna_mock.called)

    @mock.patch.object(api_utils, 'get_rpc_node', autospec=True)
    def test_patch_update_drive_console_enabled(self, mock_rpc_node):
        self.node.console_enabled = True
        mock_rpc_node.return_value = self.node

        response = self.patch_json('/nodes/%s' % self.node.uuid,
                                   [{'path': '/driver',
                                     'value': 'foo',
                                     'op': 'add'}],
                                   expect_errors=True)
        mock_rpc_node.assert_called_once_with(self.node.uuid)
        self.assertEqual('application/json', response.content_type)
        self.assertEqual(http_client.CONFLICT, response.status_code)
        self.assertTrue(response.json['error_message'])

    def test_update_in_UPDATE_ALLOWED_STATES(self):
        for state in states.UPDATE_ALLOWED_STATES:
            node = obj_utils.create_test_node(
                self.context,
                uuid=uuidutils.generate_uuid(),
                provision_state=state,
                target_provision_state=states.AVAILABLE)

            self.mock_update_node.return_value = node
            response = self.patch_json('/nodes/%s' % node.uuid,
                                       [{'path': '/extra/foo', 'value': 'bar',
                                         'op': 'add'}])
            self.assertEqual('application/json', response.content_type)
            self.assertEqual(http_client.OK, response.status_code)

    def test_update_network_interface(self):
        node = obj_utils.create_test_node(self.context,
                                          uuid=uuidutils.generate_uuid())
        self.mock_update_node.return_value = node
        network_interface = 'flat'
        headers = {api_base.Version.string: str(api_v1.max_version())}
        response = self.patch_json('/nodes/%s' % node.uuid,
                                   [{'path': '/network_interface',
                                     'value': network_interface,
                                     'op': 'add'}],
                                   headers=headers)
        self.assertEqual('application/json', response.content_type)
        self.assertEqual(http_client.OK, response.status_code)

    def test_reset_network_interface(self):
        node = obj_utils.create_test_node(self.context,
                                          uuid=uuidutils.generate_uuid())
        self.mock_update_node.return_value = node
        headers = {api_base.Version.string: str(api_v1.max_version())}
        response = self.patch_json('/nodes/%s' % node.uuid,
                                   [{'path': '/network_interface',
                                     'op': 'remove'}],
                                   headers=headers)
        self.assertEqual('application/json', response.content_type)
        self.assertEqual(http_client.OK, response.status_code)

    def test_update_network_interface_old_api(self):
        node = obj_utils.create_test_node(self.context,
                                          uuid=uuidutils.generate_uuid())
        self.mock_update_node.return_value = node
        network_interface = 'flat'
        headers = {api_base.Version.string: '1.15'}
        response = self.patch_json('/nodes/%s' % node.uuid,
                                   [{'path': '/network_interface',
                                     'value': network_interface,
                                     'op': 'add'}],
                                   headers=headers,
                                   expect_errors=True)
        self.assertEqual('application/json', response.content_type)
        self.assertEqual(http_client.NOT_ACCEPTABLE, response.status_code)

    def test_update_resource_class(self):
        node = obj_utils.create_test_node(self.context,
                                          uuid=uuidutils.generate_uuid())
        self.mock_update_node.return_value = node
        resource_class = 'foo'
        headers = {api_base.Version.string: '1.21'}
        response = self.patch_json('/nodes/%s' % node.uuid,
                                   [{'path': '/resource_class',
                                     'value': resource_class,
                                     'op': 'add'}],
                                   headers=headers)
        self.assertEqual('application/json', response.content_type)
        self.assertEqual(http_client.OK, response.status_code)

    def test_update_resource_class_old_api(self):
        node = obj_utils.create_test_node(self.context,
                                          uuid=uuidutils.generate_uuid())
        self.mock_update_node.return_value = node
        resource_class = 'foo'
        headers = {api_base.Version.string: '1.20'}
        response = self.patch_json('/nodes/%s' % node.uuid,
                                   [{'path': '/resource_class',
                                     'value': resource_class,
                                     'op': 'add'}],
                                   headers=headers,
                                   expect_errors=True)
        self.assertEqual('application/json', response.content_type)
        self.assertEqual(http_client.NOT_ACCEPTABLE, response.status_code)

    def test_update_resource_class_max_length(self):
        node = obj_utils.create_test_node(self.context,
                                          uuid=uuidutils.generate_uuid())
        self.mock_update_node.return_value = node
        resource_class = 'f' * 80
        headers = {api_base.Version.string: '1.21'}
        response = self.patch_json('/nodes/%s' % node.uuid,
                                   [{'path': '/resource_class',
                                     'value': resource_class,
                                     'op': 'add'}],
                                   headers=headers)
        self.assertEqual('application/json', response.content_type)
        self.assertEqual(http_client.OK, response.status_code)

    def test_update_resource_class_too_long(self):
        node = obj_utils.create_test_node(self.context,
                                          uuid=uuidutils.generate_uuid())
        self.mock_update_node.return_value = node
        resource_class = 'f' * 81
        headers = {api_base.Version.string: '1.21'}
        response = self.patch_json('/nodes/%s' % node.uuid,
                                   [{'path': '/resource_class',
                                     'value': resource_class,
                                     'op': 'add'}],
                                   headers=headers,
                                   expect_errors=True)
        self.assertEqual('application/json', response.content_type)
        self.assertEqual(http_client.BAD_REQUEST, response.status_code)

    def test_update_interface_fields(self):
        node = obj_utils.create_test_node(self.context,
                                          uuid=uuidutils.generate_uuid())
        self.mock_update_node.return_value = node
        headers = {api_base.Version.string: str(api_v1.max_version())}
        for field in api_utils.V31_FIELDS:
            response = self.patch_json('/nodes/%s' % node.uuid,
                                       [{'path': '/%s' % field,
                                         'value': 'fake',
                                         'op': 'add'}],
                                       headers=headers)
            self.assertEqual('application/json', response.content_type)
            self.assertEqual(http_client.OK, response.status_code)

    def test_reset_interface_fields(self):
        # Using remove on an interface resets it to its default
        node = obj_utils.create_test_node(self.context,
                                          uuid=uuidutils.generate_uuid())
        self.mock_update_node.return_value = node
        headers = {api_base.Version.string: str(api_v1.max_version())}
        for field in api_utils.V31_FIELDS:
            response = self.patch_json('/nodes/%s' % node.uuid,
                                       [{'path': '/%s' % field,
                                         'op': 'remove'}],
                                       headers=headers)
            self.assertEqual('application/json', response.content_type)
            self.assertEqual(http_client.OK, response.status_code)

    def test_update_interface_fields_bad_version(self):
        node = obj_utils.create_test_node(self.context,
                                          uuid=uuidutils.generate_uuid())
        self.mock_update_node.return_value = node
        headers = {api_base.Version.string: '1.30'}
        for field in api_utils.V31_FIELDS:
            response = self.patch_json('/nodes/%s' % node.uuid,
                                       [{'path': '/%s' % field,
                                         'value': 'fake',
                                         'op': 'add'}],
                                       headers=headers,
                                       expect_errors=True)
            self.assertEqual('application/json', response.content_type)
            self.assertEqual(http_client.NOT_ACCEPTABLE, response.status_code)

    def test_update_storage_interface(self):
        node = obj_utils.create_test_node(self.context,
                                          uuid=uuidutils.generate_uuid())
        self.mock_update_node.return_value = node
        storage_interface = 'cinder'
        headers = {api_base.Version.string: str(api_v1.max_version())}
        response = self.patch_json('/nodes/%s' % node.uuid,
                                   [{'path': '/storage_interface',
                                     'value': storage_interface,
                                     'op': 'add'}],
                                   headers=headers)
        self.assertEqual('application/json', response.content_type)
        self.assertEqual(http_client.OK, response.status_code)

    def test_reset_storage_interface(self):
        node = obj_utils.create_test_node(self.context,
                                          uuid=uuidutils.generate_uuid())
        self.mock_update_node.return_value = node
        headers = {api_base.Version.string: str(api_v1.max_version())}
        response = self.patch_json('/nodes/%s' % node.uuid,
                                   [{'path': '/storage_interface',
                                     'op': 'remove'}],
                                   headers=headers)
        self.assertEqual('application/json', response.content_type)
        self.assertEqual(http_client.OK, response.status_code)

    def test_update_storage_interface_old_api(self):
        node = obj_utils.create_test_node(self.context,
                                          uuid=uuidutils.generate_uuid())
        self.mock_update_node.return_value = node
        storage_interface = 'cinder'
        headers = {api_base.Version.string: '1.32'}
        response = self.patch_json('/nodes/%s' % node.uuid,
                                   [{'path': '/storage_interface',
                                     'value': storage_interface,
                                     'op': 'add'}],
                                   headers=headers,
                                   expect_errors=True)
        self.assertEqual('application/json', response.content_type)
        self.assertEqual(http_client.NOT_ACCEPTABLE, response.status_code)

    def test_update_traits(self):
        node = obj_utils.create_test_node(self.context,
                                          uuid=uuidutils.generate_uuid())
        self.mock_update_node.return_value = node
        headers = {api_base.Version.string: str(api_v1.max_version())}
        response = self.patch_json('/nodes/%s' % node.uuid,
                                   [{'path': '/traits',
                                     'value': ['CUSTOM_1'],
                                     'op': 'add'}],
                                   headers=headers,
                                   expect_errors=True)
        self.assertEqual('application/json', response.content_type)
        self.assertEqual(http_client.BAD_REQUEST, response.status_code)

    def test_patch_fault_forbidden(self):
        node = obj_utils.create_test_node(self.context,
                                          uuid=uuidutils.generate_uuid())
        response = self.patch_json('/nodes/%s' % node.uuid,
                                   [{'path': '/fault',
                                     'op': 'replace',
                                     'value': 'why care'}],
                                   headers={api_base.Version.string: "1.42"},
                                   expect_errors=True)
        self.assertEqual('application/json', response.content_type)
        self.assertEqual(http_client.BAD_REQUEST, response.status_code)
        self.assertTrue(response.json['error_message'])

    def test_patch_deploy_step_forbidden(self):
        node = obj_utils.create_test_node(self.context,
                                          uuid=uuidutils.generate_uuid())
        response = self.patch_json('/nodes/%s' % node.uuid,
                                   [{'path': '/deploy_step',
                                     'op': 'replace',
                                     'value': 'deploy this'}],
                                   headers={api_base.Version.string: "1.44"},
                                   expect_errors=True)
        self.assertEqual('application/json', response.content_type)
        self.assertEqual(http_client.BAD_REQUEST, response.status_code)
        self.assertTrue(response.json['error_message'])

    def test_patch_allocation_uuid_forbidden(self):
        node = obj_utils.create_test_node(self.context,
                                          uuid=uuidutils.generate_uuid())
        response = self.patch_json('/nodes/%s' % node.uuid,
                                   [{'path': '/allocation_uuid',
                                     'op': 'replace',
                                     'value': uuidutils.generate_uuid()}],
                                   headers={api_base.Version.string: "1.52"},
                                   expect_errors=True)
        self.assertEqual('application/json', response.content_type)
        self.assertEqual(http_client.BAD_REQUEST, response.status_code)
        self.assertTrue(response.json['error_message'])

    def test_update_conductor_group(self):
        node = obj_utils.create_test_node(self.context,
                                          uuid=uuidutils.generate_uuid())
        self.mock_update_node.return_value = node
        headers = {api_base.Version.string: '1.46'}
        response = self.patch_json('/nodes/%s' % node.uuid,
                                   [{'path': '/conductor_group',
                                     'value': 'foogroup',
                                     'op': 'add'}],
                                   headers=headers)
        self.assertEqual('application/json', response.content_type)
        self.assertEqual(http_client.OK, response.status_code)

    def test_update_conductor_group_old_api(self):
        node = obj_utils.create_test_node(self.context,
                                          uuid=uuidutils.generate_uuid())
        self.mock_update_node.return_value = node
        headers = {api_base.Version.string: '1.45'}
        response = self.patch_json('/nodes/%s' % node.uuid,
                                   [{'path': '/conductor_group',
                                     'value': 'foogroup',
                                     'op': 'add'}],
                                   headers=headers,
                                   expect_errors=True)
        self.assertEqual('application/json', response.content_type)
        self.assertEqual(http_client.NOT_ACCEPTABLE, response.status_code)

    def test_update_automated_clean(self):
        node = obj_utils.create_test_node(self.context,
                                          uuid=uuidutils.generate_uuid())
        self.mock_update_node.return_value = node
        headers = {api_base.Version.string: '1.47'}
        response = self.patch_json('/nodes/%s' % node.uuid,
                                   [{'path': '/automated_clean',
                                     'value': True,
                                     'op': 'replace'}],
                                   headers=headers)
        self.assertEqual('application/json', response.content_type)
        self.assertEqual(http_client.OK, response.status_code)

    @mock.patch.object(policy, 'authorize', spec=True)
    def test_update_automated_clean_with_false(self, mock_authorize):
        def mock_authorize_function(rule, target, creds):
            if rule == 'baremetal:node:disable_cleaning':
                raise exception.HTTPForbidden(resource='fake')
            return True
        mock_authorize.side_effect = mock_authorize_function

        node = obj_utils.create_test_node(self.context,
                                          uuid=uuidutils.generate_uuid())
        self.mock_update_node.return_value = node
        headers = {api_base.Version.string: '1.47'}
        response = self.patch_json('/nodes/%s' % node.uuid,
                                   [{'path': '/automated_clean',
                                     'value': False,
                                     'op': 'replace'}],
                                   headers=headers,
                                   expect_errors=True)
        self.assertEqual('application/json', response.content_type)
        self.assertEqual(http_client.FORBIDDEN, response.status_code)

    def test_update_automated_clean_old_api(self):
        node = obj_utils.create_test_node(self.context,
                                          uuid=uuidutils.generate_uuid())
        self.mock_update_node.return_value = node
        headers = {api_base.Version.string: '1.46'}
        response = self.patch_json('/nodes/%s' % node.uuid,
                                   [{'path': '/automated_clean',
                                     'value': True,
                                     'op': 'replace'}],
                                   headers=headers,
                                   expect_errors=True)
        self.assertEqual('application/json', response.content_type)
        self.assertEqual(http_client.NOT_ACCEPTABLE, response.status_code)

    def test_update_protected(self):
        node = obj_utils.create_test_node(self.context,
                                          uuid=uuidutils.generate_uuid(),
                                          provision_state='active')
        self.mock_update_node.return_value = node
        headers = {api_base.Version.string: '1.48'}
        response = self.patch_json('/nodes/%s' % node.uuid,
                                   [{'path': '/protected',
                                     'value': True,
                                     'op': 'replace'}],
                                   headers=headers)
        self.assertEqual('application/json', response.content_type)
        self.assertEqual(http_client.OK, response.status_code)

    def test_update_protected_string(self):
        node = obj_utils.create_test_node(self.context,
                                          uuid=uuidutils.generate_uuid(),
                                          provision_state='active')
        self.mock_update_node.return_value = node
        headers = {api_base.Version.string: '1.48'}
        # Patch with valid boolean string
        response = self.patch_json('/nodes/%s' % node.uuid,
                                   [{'path': '/protected',
                                     'value': "True",
                                     'op': 'replace'}],
                                   headers=headers)
        self.assertEqual('application/json', response.content_type)
        self.assertEqual(http_client.OK, response.status_code)

    def test_update_protected_string_invalid(self):
        node = obj_utils.create_test_node(self.context,
                                          uuid=uuidutils.generate_uuid(),
                                          provision_state='active')
        self.mock_update_node.return_value = node
        headers = {api_base.Version.string: '1.48'}
        # Patch with invalid boolean string
        response = self.patch_json('/nodes/%s' % node.uuid,
                                   [{'path': '/protected',
                                     'value': "YeahNahGood",
                                     'op': 'replace'}],
                                   headers=headers,
                                   expect_errors=True)
        self.assertEqual(http_client.BAD_REQUEST, response.status_code)
        self.assertIn("Invalid protected: Unrecognized value 'YeahNahGood'",
                      response.json['error_message'])

    def test_update_protected_remove(self):
        node = obj_utils.create_test_node(self.context,
                                          uuid=uuidutils.generate_uuid(),
                                          provision_state='active')
        self.mock_update_node.return_value = node
        headers = {api_base.Version.string: '1.48'}
        response = self.patch_json('/nodes/%s' % node.uuid,
                                   [{"op": "remove", "path": "/protected"}],
                                   headers=headers)
        self.assertEqual('application/json', response.content_type)
        self.assertEqual(http_client.OK, response.status_code)

    def test_update_protected_with_reason(self):
        node = obj_utils.create_test_node(self.context,
                                          uuid=uuidutils.generate_uuid(),
                                          provision_state='active')
        self.mock_update_node.return_value = node
        headers = {api_base.Version.string: '1.48'}
        response = self.patch_json('/nodes/%s' % node.uuid,
                                   [{'path': '/protected',
                                     'value': True,
                                     'op': 'replace'},
                                    {'path': '/protected_reason',
                                     'value': 'reason!',
                                     'op': 'replace'}],
                                   headers=headers)
        self.assertEqual('application/json', response.content_type)
        self.assertEqual(http_client.OK, response.status_code)

    def test_update_protected_reason(self):
        node = obj_utils.create_test_node(self.context,
                                          uuid=uuidutils.generate_uuid(),
                                          provision_state='active',
                                          protected=True)
        self.mock_update_node.return_value = node
        headers = {api_base.Version.string: '1.48'}
        response = self.patch_json('/nodes/%s' % node.uuid,
                                   [{'path': '/protected_reason',
                                     'value': 'reason!',
                                    'op': 'replace'}],
                                   headers=headers)
        self.assertEqual('application/json', response.content_type)
        self.assertEqual(http_client.OK, response.status_code)

    @mock.patch.object(policy, 'authorize', spec=True)
    def test_update_owner(self, mock_authorize):
        def mock_authorize_function(rule, target, creds):
            # test should not check this policy rule
            if rule == 'baremetal:node:update_owner_provisioned':
                raise exception.HTTPForbidden(resource='fake')
            return True
        mock_authorize.side_effect = mock_authorize_function

        node = obj_utils.create_test_node(self.context,
                                          uuid=uuidutils.generate_uuid())
        self.mock_update_node.return_value = node
        headers = {api_base.Version.string: '1.50'}
        response = self.patch_json('/nodes/%s' % node.uuid,
                                   [{'path': '/owner',
                                     'value': 'meow',
                                     'op': 'replace'}],
                                   headers=headers)
        self.assertEqual('application/json', response.content_type)
        self.assertEqual(http_client.OK, response.status_code)

    def test_update_owner_provisioned(self):
        node = obj_utils.create_test_node(self.context,
                                          uuid=uuidutils.generate_uuid(),
                                          provision_state='active')
        self.mock_update_node.return_value = node
        headers = {api_base.Version.string: '1.50'}
        response = self.patch_json('/nodes/%s' % node.uuid,
                                   [{'path': '/owner',
                                     'value': 'meow',
                                     'op': 'replace'}],
                                   headers=headers)
        self.assertEqual('application/json', response.content_type)
        self.assertEqual(http_client.OK, response.status_code)

    @mock.patch.object(policy, 'authorize', spec=True)
    def test_update_owner_provisioned_forbidden(self, mock_authorize):
        def mock_authorize_function(rule, target, creds):
            if rule == 'baremetal:node:update_owner_provisioned':
                raise exception.HTTPForbidden(resource='fake')
            return True
        mock_authorize.side_effect = mock_authorize_function

        node = obj_utils.create_test_node(self.context,
                                          uuid=uuidutils.generate_uuid(),
                                          provision_state='active')
        self.mock_update_node.return_value = node
        headers = {api_base.Version.string: '1.50'}
        response = self.patch_json('/nodes/%s' % node.uuid,
                                   [{'path': '/owner',
                                     'value': 'meow',
                                     'op': 'replace'}],
                                   headers=headers,
                                   expect_errors=True)
        self.assertEqual('application/json', response.content_type)
        self.assertEqual(http_client.CONFLICT, response.status_code)
        self.assertTrue(response.json['error_message'])

    def test_update_owner_allocation(self):
        allocation = obj_utils.create_test_allocation(self.context)
        node = obj_utils.create_test_node(self.context,
                                          uuid=uuidutils.generate_uuid(),
                                          allocation_id=allocation.id)
        self.mock_update_node.return_value = node
        headers = {api_base.Version.string: '1.50'}
        response = self.patch_json('/nodes/%s' % node.uuid,
                                   [{'path': '/owner',
                                     'value': 'meow',
                                     'op': 'replace'}],
                                   headers=headers)
        self.assertEqual('application/json', response.content_type)
        self.assertEqual(http_client.OK, response.status_code)

    def test_update_owner_allocation_owned(self):
        allocation = obj_utils.create_test_allocation(self.context,
                                                      owner='12345')
        node = obj_utils.create_test_node(self.context,
                                          uuid=uuidutils.generate_uuid(),
                                          allocation_id=allocation.id)
        self.mock_update_node.return_value = node
        headers = {api_base.Version.string: '1.50'}
        response = self.patch_json('/nodes/%s' % node.uuid,
                                   [{'path': '/owner',
                                     'value': 'meow',
                                     'op': 'replace'}],
                                   headers=headers,
                                   expect_errors=True)
        self.assertEqual('application/json', response.content_type)
        self.assertEqual(http_client.CONFLICT, response.status_code)
        self.assertTrue(response.json['error_message'])

    def test_update_protected_old_api(self):
        node = obj_utils.create_test_node(self.context,
                                          uuid=uuidutils.generate_uuid())
        self.mock_update_node.return_value = node
        headers = {api_base.Version.string: '1.47'}
        response = self.patch_json('/nodes/%s' % node.uuid,
                                   [{'path': '/protected',
                                     'value': True,
                                     'op': 'replace'}],
                                   headers=headers,
                                   expect_errors=True)
        self.assertEqual('application/json', response.content_type)
        self.assertEqual(http_client.NOT_ACCEPTABLE, response.status_code)

    def test_patch_conductor_forbidden(self):
        node = obj_utils.create_test_node(self.context,
                                          uuid=uuidutils.generate_uuid())
        response = self.patch_json('/nodes/%s' % node.uuid,
                                   [{'path': '/conductor',
                                     'op': 'replace',
                                     'value': 'why care'}],
                                   headers={api_base.Version.string: "1.49"},
                                   expect_errors=True)
        self.assertEqual('application/json', response.content_type)
        self.assertEqual(http_client.BAD_REQUEST, response.status_code)
        self.assertTrue(response.json['error_message'])

    def test_update_owner_old_api(self):
        node = obj_utils.create_test_node(self.context,
                                          uuid=uuidutils.generate_uuid())
        self.mock_update_node.return_value = node
        headers = {api_base.Version.string: '1.47'}
        response = self.patch_json('/nodes/%s' % node.uuid,
                                   [{'path': '/owner',
                                     'value': 'meow',
                                     'op': 'replace'}],
                                   headers=headers,
                                   expect_errors=True)
        self.assertEqual('application/json', response.content_type)
        self.assertEqual(http_client.NOT_ACCEPTABLE, response.status_code)

    def test_update_description(self):
        node = obj_utils.create_test_node(self.context,
                                          uuid=uuidutils.generate_uuid())
        self.mock_update_node.return_value = node
        headers = {api_base.Version.string: '1.51'}
        response = self.patch_json('/nodes/%s' % node.uuid,
                                   [{'path': '/description',
                                     'value': 'meow',
                                     'op': 'replace'}],
                                   headers=headers)
        self.assertEqual('application/json', response.content_type)
        self.assertEqual(http_client.OK, response.status_code)

    def test_update_description_oversize(self):
        node = obj_utils.create_test_node(self.context,
                                          uuid=uuidutils.generate_uuid())
        desc = '12345678' * 512 + 'last weed'
        self.mock_update_node.return_value = node
        headers = {api_base.Version.string: '1.51'}
        response = self.patch_json('/nodes/%s' % node.uuid,
                                   [{'path': '/description',
                                     'value': desc,
                                     'op': 'replace'}],
                                   headers=headers,
                                   expect_errors=True)
        self.assertEqual('application/json', response.content_type)
        self.assertEqual(http_client.BAD_REQUEST, response.status_code)

    def test_update_lessee(self):
        node = obj_utils.create_test_node(self.context,
                                          uuid=uuidutils.generate_uuid())
        self.mock_update_node.return_value = node
        headers = {api_base.Version.string: '1.65'}
        response = self.patch_json('/nodes/%s' % node.uuid,
                                   [{'path': '/lessee',
                                     'value': 'new-project',
                                     'op': 'replace'}],
                                   headers=headers)
        self.assertEqual('application/json', response.content_type)
        self.assertEqual(http_client.OK, response.status_code)

    def test_update_lessee_old_api(self):
        node = obj_utils.create_test_node(self.context,
                                          uuid=uuidutils.generate_uuid())
        self.mock_update_node.return_value = node
        headers = {api_base.Version.string: '1.64'}
        response = self.patch_json('/nodes/%s' % node.uuid,
                                   [{'path': '/lessee',
                                     'value': 'new-project',
                                     'op': 'replace'}],
                                   headers=headers,
                                   expect_errors=True)
        self.assertEqual('application/json', response.content_type)
        self.assertEqual(http_client.NOT_ACCEPTABLE, response.status_code)

    def test_patch_allocation_forbidden(self):
        node = obj_utils.create_test_node(self.context,
                                          uuid=uuidutils.generate_uuid())
        response = self.patch_json('/nodes/%s' % node.uuid,
                                   [{'path': '/allocation_uuid',
                                     'op': 'replace',
                                     'value': uuidutils.generate_uuid()}],
                                   headers={api_base.Version.string:
                                            str(api_v1.max_version())},
                                   expect_errors=True)
        self.assertEqual('application/json', response.content_type)
        self.assertEqual(http_client.BAD_REQUEST, response.status_code)
        self.assertTrue(response.json['error_message'])

    def test_update_retired(self):
        node = obj_utils.create_test_node(self.context,
                                          uuid=uuidutils.generate_uuid(),
                                          provision_state='active')
        self.mock_update_node.return_value = node
        headers = {api_base.Version.string: '1.61'}
        response = self.patch_json('/nodes/%s' % node.uuid,
                                   [{'path': '/retired',
                                     'value': True,
                                     'op': 'replace'}],
                                   headers=headers)
        self.assertEqual('application/json', response.content_type)
        self.assertEqual(http_client.OK, response.status_code)

    def test_update_retired_remove(self):
        node = obj_utils.create_test_node(self.context,
                                          uuid=uuidutils.generate_uuid(),
                                          provision_state='active')
        self.mock_update_node.return_value = node
        headers = {api_base.Version.string: '1.61'}
        response = self.patch_json('/nodes/%s' % node.uuid,
                                   [{"op": "remove", "path": "/retired"}],
                                   headers=headers)
        self.assertEqual('application/json', response.content_type)
        self.assertEqual(http_client.OK, response.status_code)

    def test_update_retired_with_reason(self):
        node = obj_utils.create_test_node(self.context,
                                          uuid=uuidutils.generate_uuid(),
                                          provision_state='active')
        self.mock_update_node.return_value = node
        headers = {api_base.Version.string: '1.61'}
        response = self.patch_json('/nodes/%s' % node.uuid,
                                   [{'path': '/retired',
                                     'value': True,
                                     'op': 'replace'},
                                    {'path': '/retired_reason',
                                     'value': 'a better reason',
                                     'op': 'replace'}],
                                   headers=headers)
        self.assertEqual('application/json', response.content_type)
        self.assertEqual(http_client.OK, response.status_code)

    def test_update_retired_reason(self):
        node = obj_utils.create_test_node(self.context,
                                          uuid=uuidutils.generate_uuid(),
                                          provision_state='active',
                                          retired=True)
        self.mock_update_node.return_value = node
        headers = {api_base.Version.string: '1.61'}
        response = self.patch_json('/nodes/%s' % node.uuid,
                                   [{'path': '/retired_reason',
                                     'value': 'a better reason',
                                     'op': 'replace'}],
                                   headers=headers)
        self.assertEqual('application/json', response.content_type)
        self.assertEqual(http_client.OK, response.status_code)

    def test_update_retired_old_api(self):
        node = obj_utils.create_test_node(self.context,
                                          uuid=uuidutils.generate_uuid())
        self.mock_update_node.return_value = node
        headers = {api_base.Version.string: '1.60'}
        response = self.patch_json('/nodes/%s' % node.uuid,
                                   [{'path': '/retired',
                                     'value': True,
                                     'op': 'replace'}],
                                   headers=headers,
                                   expect_errors=True)
        self.assertEqual('application/json', response.content_type)
        self.assertEqual(http_client.NOT_ACCEPTABLE, response.status_code)

    def test_update_network_data(self):
        node = obj_utils.create_test_node(self.context,
                                          uuid=uuidutils.generate_uuid(),
                                          provision_state='active')
        self.mock_update_node.return_value = node
        headers = {api_base.Version.string: '1.66'}

        response = self.patch_json('/nodes/%s' % node.uuid,
                                   [{'path': '/network_data',
                                     'value': NETWORK_DATA,
                                     'op': 'replace'}],
                                   headers=headers)
        self.assertEqual('application/json', response.content_type)
        self.assertEqual(http_client.OK, response.status_code)

    def test_update_network_data_old_api(self):
        node = obj_utils.create_test_node(self.context,
                                          uuid=uuidutils.generate_uuid())
        self.mock_update_node.return_value = node
        headers = {api_base.Version.string: '1.62'}

        response = self.patch_json('/nodes/%s' % node.uuid,
                                   [{'path': '/network_data',
                                     'value': NETWORK_DATA,
                                     'op': 'replace'}],
                                   headers=headers,
                                   expect_errors=True)
        self.assertEqual('application/json', response.content_type)
        self.assertEqual(http_client.NOT_ACCEPTABLE, response.status_code)

    def test_update_network_data_wrong_format(self):
        node = obj_utils.create_test_node(self.context,
                                          uuid=uuidutils.generate_uuid())
        self.mock_update_node.return_value = node
        headers = {api_base.Version.string: '1.66'}

        response = self.patch_json('/nodes/%s' % node.uuid,
                                   [{'path': '/network_data',
                                     'value': {'cat': 'meow'},
                                     'op': 'replace'}],
                                   headers=headers,
                                   expect_errors=True)
        self.assertEqual('application/json', response.content_type)
        self.assertEqual(http_client.BAD_REQUEST, response.status_code)

    def test_update_network_data_custom(self):
        custom_schema = {
            'type': 'object',
            'properties': {
                'cat': {'type': 'string'},
            },
        }
        with tempfile.NamedTemporaryFile('wt') as fp:
            json.dump(custom_schema, fp)
            fp.flush()
            self.config(network_data_schema=fp.name, group='api')

            node = obj_utils.create_test_node(self.context,
                                              uuid=uuidutils.generate_uuid(),
                                              provision_state='active')
            self.mock_update_node.return_value = node
            headers = {api_base.Version.string: '1.66'}

            response = self.patch_json('/nodes/%s' % node.uuid,
                                       [{'path': '/network_data',
                                         'value': {'cat': 'meow'},
                                         'op': 'replace'}],
                                       headers=headers)
            self.assertEqual('application/json', response.content_type)
            self.assertEqual(http_client.OK, response.status_code)

    @mock.patch.object(api_utils, 'check_multiple_node_policies_and_retrieve',
                       autospec=True)
    def test_patch_policy_update(self, mock_cmnpar):
        node = obj_utils.create_test_node(self.context,
                                          uuid=uuidutils.generate_uuid())
        mock_cmnpar.return_value = node
        self.mock_update_node.return_value = node
        headers = {api_base.Version.string: str(api_v1.max_version())}
        response = self.patch_json('/nodes/%s' % node.uuid,
                                   [{'path': '/description',
                                     'value': 'foo',
                                     'op': 'replace'}],
                                   headers=headers)
        self.assertEqual('application/json', response.content_type)
        self.assertEqual(http_client.OK, response.status_code)
        mock_cmnpar.assert_called_once_with(
            ['baremetal:node:update'], node.uuid, with_suffix=True)

    @mock.patch.object(api_utils, 'check_multiple_node_policies_and_retrieve',
                       autospec=True)
    def test_patch_policy_update_none(self, mock_cmnpar):
        node = obj_utils.create_test_node(self.context,
                                          uuid=uuidutils.generate_uuid())
        mock_cmnpar.return_value = node
        self.mock_update_node.return_value = node
        headers = {api_base.Version.string: str(api_v1.max_version())}
        response = self.patch_json('/nodes/%s' % node.uuid,
                                   [],
                                   headers=headers)
        self.assertEqual('application/json', response.content_type)
        self.assertEqual(http_client.OK, response.status_code)
        mock_cmnpar.assert_called_once_with(
            ['baremetal:node:update'], node.uuid, with_suffix=True)

    @mock.patch.object(api_utils, 'check_multiple_node_policies_and_retrieve',
                       autospec=True)
    def test_patch_policy_update_extra(self, mock_cmnpar):
        node = obj_utils.create_test_node(self.context,
                                          uuid=uuidutils.generate_uuid())
        mock_cmnpar.return_value = node
        self.mock_update_node.return_value = node
        headers = {api_base.Version.string: str(api_v1.max_version())}
        response = self.patch_json('/nodes/%s' % node.uuid,
                                   [{'path': '/extra/foo',
                                     'value': 'bar',
                                     'op': 'add'}],
                                   headers=headers)
        self.assertEqual('application/json', response.content_type)
        self.assertEqual(http_client.OK, response.status_code)
        mock_cmnpar.assert_called_once_with(
            ['baremetal:node:update_extra'], node.uuid, with_suffix=True)

    @mock.patch.object(api_utils, 'check_multiple_node_policies_and_retrieve',
                       autospec=True)
    def test_patch_policy_update_instance_info(self, mock_cmnpar):
        node = obj_utils.create_test_node(self.context,
                                          uuid=uuidutils.generate_uuid())
        mock_cmnpar.return_value = node
        self.mock_update_node.return_value = node
        headers = {api_base.Version.string: str(api_v1.max_version())}
        response = self.patch_json('/nodes/%s' % node.uuid,
                                   [{'path': '/instance_info/foo',
                                     'value': 'bar',
                                     'op': 'add'}],
                                   headers=headers)
        self.assertEqual('application/json', response.content_type)
        self.assertEqual(http_client.OK, response.status_code)
        mock_cmnpar.assert_called_once_with(
            ['baremetal:node:update_instance_info'],
            node.uuid, with_suffix=True)

    @mock.patch.object(api_utils, 'check_multiple_node_policies_and_retrieve',
                       autospec=True)
    def test_patch_policy_update_generic_and_extra(self, mock_cmnpar):
        node = obj_utils.create_test_node(self.context,
                                          uuid=uuidutils.generate_uuid())
        mock_cmnpar.return_value = node
        self.mock_update_node.return_value = node
        headers = {api_base.Version.string: str(api_v1.max_version())}
        response = self.patch_json('/nodes/%s' % node.uuid,
                                   [{'path': '/description',
                                     'value': 'foo',
                                     'op': 'replace'},
                                    {'path': '/extra/foo',
                                     'value': 'bar',
                                     'op': 'add'}],
                                   headers=headers)
        self.assertEqual('application/json', response.content_type)
        self.assertEqual(http_client.OK, response.status_code)
        mock_cmnpar.assert_called_once_with(
            ['baremetal:node:update_extra', 'baremetal:node:update'],
            node.uuid, with_suffix=True)

    @mock.patch.object(api_utils, 'check_multiple_node_policies_and_retrieve',
                       autospec=True)
    def test_patch_policy_update_generic_and_instance_info(self, mock_cmnpar):
        node = obj_utils.create_test_node(self.context,
                                          uuid=uuidutils.generate_uuid())
        mock_cmnpar.return_value = node
        self.mock_update_node.return_value = node
        headers = {api_base.Version.string: str(api_v1.max_version())}
        response = self.patch_json('/nodes/%s' % node.uuid,
                                   [{'path': '/description',
                                     'value': 'foo',
                                     'op': 'replace'},
                                    {'path': '/instance_info/foo',
                                     'value': 'bar',
                                     'op': 'add'}],
                                   headers=headers)
        self.assertEqual('application/json', response.content_type)
        self.assertEqual(http_client.OK, response.status_code)
        mock_cmnpar.assert_called_once_with(
            ['baremetal:node:update_instance_info', 'baremetal:node:update'],
            node.uuid, with_suffix=True)

    @mock.patch.object(api_utils, 'check_multiple_node_policies_and_retrieve',
                       autospec=True)
    def test_patch_policy_update_extra_and_instance_info(self, mock_cmnpar):
        node = obj_utils.create_test_node(self.context,
                                          uuid=uuidutils.generate_uuid())
        mock_cmnpar.return_value = node
        self.mock_update_node.return_value = node
        headers = {api_base.Version.string: str(api_v1.max_version())}
        response = self.patch_json('/nodes/%s' % node.uuid,
                                   [{'path': '/extra/foo',
                                     'value': 'bar',
                                     'op': 'add'},
                                    {'path': '/instance_info/foo',
                                     'value': 'bar',
                                     'op': 'add'}],
                                   headers=headers)
        self.assertEqual('application/json', response.content_type)
        self.assertEqual(http_client.OK, response.status_code)
        mock_cmnpar.assert_called_once_with(
            ['baremetal:node:update_extra',
             'baremetal:node:update_instance_info'],
            node.uuid, with_suffix=True)

    @mock.patch.object(api_utils, 'check_multiple_node_policies_and_retrieve',
                       autospec=True)
    def test_patch_policy_update_generic_extra_instance_info(
            self, mock_cmnpar):
        node = obj_utils.create_test_node(self.context,
                                          uuid=uuidutils.generate_uuid())
        mock_cmnpar.return_value = node
        self.mock_update_node.return_value = node
        headers = {api_base.Version.string: str(api_v1.max_version())}
        response = self.patch_json('/nodes/%s' % node.uuid,
                                   [{'path': '/description',
                                     'value': 'foo',
                                     'op': 'replace'},
                                    {'path': '/extra/foo',
                                     'value': 'bar',
                                     'op': 'add'},
                                    {'path': '/instance_info/foo',
                                     'value': 'bar',
                                     'op': 'add'}],
                                   headers=headers)
        self.assertEqual('application/json', response.content_type)
        self.assertEqual(http_client.OK, response.status_code)
        mock_cmnpar.assert_called_once_with(
            ['baremetal:node:update_extra',
             'baremetal:node:update_instance_info',
             'baremetal:node:update'],
            node.uuid, with_suffix=True)


def _create_node_locally(node):
    driver_factory.check_and_update_node_interfaces(node)
    node.create()
    return node


@mock.patch.object(rpcapi.ConductorAPI, 'create_node',
                   lambda _api, _ctx, node, _topic: _create_node_locally(node))
class TestPost(test_api_base.BaseApiTest):

    def setUp(self):
        super(TestPost, self).setUp()
        self.chassis = obj_utils.create_test_chassis(self.context)
        p = mock.patch.object(rpcapi.ConductorAPI, 'get_topic_for',
                              autospec=True)
        self.mock_gtf = p.start()
        self.mock_gtf.return_value = 'test-topic'
        self.addCleanup(p.stop)

    @mock.patch.object(timeutils, 'utcnow', autospec=True)
    def _test_create_node(self, mock_utcnow, headers=None,
                          remove_chassis_uuid=False, **kwargs):
        headers = headers or {}
        ndict = test_api_utils.post_get_test_node(**kwargs)
        if remove_chassis_uuid:
            del ndict['chassis_uuid']
        test_time = datetime.datetime(2000, 1, 1, 0, 0)
        mock_utcnow.return_value = test_time
        response = self.post_json('/nodes', ndict,
                                  headers=headers)
        self.assertEqual(http_client.CREATED, response.status_int)
        result = self.get_json('/nodes/%s' % ndict['uuid'],
                               headers=headers)
        self.assertEqual(ndict['uuid'], result['uuid'])
        self.assertFalse(result['updated_at'])
        return_created_at = timeutils.parse_isotime(
            result['created_at']).replace(tzinfo=None)
        self.assertEqual(test_time, return_created_at)
        # Check location header
        self.assertIsNotNone(response.location)
        expected_location = '/v1/nodes/%s' % ndict['uuid']
        self.assertEqual(urlparse.urlparse(response.location).path,
                         expected_location)
        return result

    @mock.patch.object(notification_utils.LOG, 'exception', autospec=True)
    @mock.patch.object(notification_utils.LOG, 'warning', autospec=True)
    def test_create_node(self, mock_warning, mock_exception):
        self._test_create_node()
        self.assertFalse(mock_warning.called)
        self.assertFalse(mock_exception.called)

    def test_create_node_chassis_uuid_always_in_response(self):
        result = self._test_create_node(chassis_uuid=None)
        self.assertIsNone(result['chassis_uuid'])
        result = self._test_create_node(uuid=uuidutils.generate_uuid(),
                                        remove_chassis_uuid=True)
        self.assertIsNone(result['chassis_uuid'])

    def test_create_node_invalid_chassis(self):
        ndict = test_api_utils.post_get_test_node(chassis_uuid=0)
        response = self.post_json('/nodes', ndict, expect_errors=True)
        self.assertEqual(http_client.BAD_REQUEST, response.status_int)
        self.assertEqual('application/json', response.content_type)
        self.assertTrue(response.json['error_message'])

    def test_create_node_explicit_network_interface(self):
        headers = {api_base.Version.string: '1.20'}
        result = self._test_create_node(headers=headers,
                                        network_interface='neutron')
        self.assertEqual('neutron', result['network_interface'])

    def test_create_node_specify_interfaces(self):
        headers = {api_base.Version.string: '1.40'}
        all_interface_fields = api_utils.V31_FIELDS + ['network_interface',
                                                       'rescue_interface',
                                                       'storage_interface',
                                                       'bios_interface']
        for field in all_interface_fields:
            if field == 'network_interface':
                cfg.CONF.set_override('enabled_%ss' % field, ['flat'])
            elif field == 'storage_interface':
                cfg.CONF.set_override('enabled_%ss' % field, ['noop'])
            else:
                cfg.CONF.set_override('enabled_%ss' % field, ['fake'])

        for field in all_interface_fields:
            expected = 'fake'
            if field == 'network_interface':
                expected = 'flat'
            elif field == 'storage_interface':
                expected = 'noop'
            node = {
                'uuid': uuidutils.generate_uuid(),
                field: expected,
                'driver': 'fake-hardware'
            }
            result = self._test_create_node(headers=headers, **node)
            self.assertEqual(expected, result[field])

    def test_create_node_specify_traits(self):
        headers = {api_base.Version.string: str(api_v1.max_version())}
        ndict = test_api_utils.post_get_test_node()
        ndict['traits'] = ['CUSTOM_4']
        response = self.post_json('/nodes', ndict, headers=headers,
                                  expect_errors=True)
        self.assertEqual(http_client.BAD_REQUEST, response.status_int)

    def test_create_node_specify_interfaces_bad_version(self):
        headers = {api_base.Version.string: '1.30'}
        for field in api_utils.V31_FIELDS:
            ndict = test_api_utils.post_get_test_node(**{field: 'fake'})
            response = self.post_json('/nodes', ndict, headers=headers,
                                      expect_errors=True)
            self.assertEqual(http_client.NOT_ACCEPTABLE, response.status_int)

    def test_create_node_explicit_storage_interface(self):
        self.config(enabled_storage_interfaces=['cinder', 'noop', 'fake'])
        headers = {api_base.Version.string: '1.33'}
        result = self._test_create_node(headers=headers,
                                        storage_interface='cinder')
        self.assertEqual('cinder', result['storage_interface'])

    def test_create_node_name_empty_invalid(self):
        ndict = test_api_utils.post_get_test_node(name='')
        response = self.post_json('/nodes', ndict,
                                  headers={api_base.Version.string: "1.10"},
                                  expect_errors=True)
        self.assertEqual(http_client.BAD_REQUEST, response.status_int)
        self.assertEqual('application/json', response.content_type)
        self.assertTrue(response.json['error_message'])

    def test_create_node_name_empty_not_acceptable(self):
        ndict = test_api_utils.post_get_test_node(name='')
        response = self.post_json('/nodes', ndict, expect_errors=True)
        self.assertEqual(http_client.NOT_ACCEPTABLE, response.status_int)
        self.assertEqual('application/json', response.content_type)
        self.assertTrue(response.json['error_message'])

    def test_create_node_reserved_name(self):
        reserved_names = api_utils.get_controller_reserved_names(
            api_node.NodesController)
        for name in reserved_names:
            ndict = test_api_utils.post_get_test_node(name=name)
            response = self.post_json(
                '/nodes', ndict, headers={api_base.Version.string: "1.10"},
                expect_errors=True)
            self.assertEqual(http_client.BAD_REQUEST, response.status_int)
            self.assertEqual('application/json', response.content_type)
            self.assertTrue(response.json['error_message'])

    def test_create_node_default_state_none(self):
        ndict = test_api_utils.post_get_test_node()
        response = self.post_json('/nodes', ndict,
                                  headers={api_base.Version.string: "1.10"})
        self.assertEqual(http_client.CREATED, response.status_int)

        # default state remains NONE/AVAILABLE
        result = self.get_json('/nodes/%s' % ndict['uuid'])
        self.assertEqual(states.NOSTATE, result['provision_state'])
        result = self.get_json('/nodes/%s' % ndict['uuid'],
                               headers={api_base.Version.string: "1.10"})
        self.assertEqual(ndict['uuid'], result['uuid'])
        self.assertEqual(states.AVAILABLE, result['provision_state'])

    def test_create_node_default_state_enroll(self):
        ndict = test_api_utils.post_get_test_node()
        response = self.post_json('/nodes', ndict,
                                  headers={api_base.Version.string: "1.11"})
        self.assertEqual(http_client.CREATED, response.status_int)

        # default state is ENROLL
        result = self.get_json('/nodes/%s' % ndict['uuid'])
        self.assertEqual(ndict['uuid'], result['uuid'])
        self.assertEqual(states.ENROLL, result['provision_state'])

    def test_create_node_no_default_resource_class(self):
        ndict = test_api_utils.post_get_test_node()
        self.post_json('/nodes', ndict)

        # newer version is needed to see the resource_class field
        result = self.get_json('/nodes/%s' % ndict['uuid'],
                               headers={api_base.Version.string: "1.21"})
        self.assertIsNone(result['resource_class'])

    def test_create_node_with_default_resource_class(self):
        self.config(default_resource_class='class1')

        ndict = test_api_utils.post_get_test_node()
        self.post_json('/nodes', ndict)

        # newer version is needed to see the resource_class field
        result = self.get_json('/nodes/%s' % ndict['uuid'],
                               headers={api_base.Version.string: "1.21"})
        self.assertEqual('class1', result['resource_class'])

    def test_create_node_explicit_resource_class(self):
        self.config(default_resource_class='class1')

        ndict = test_api_utils.post_get_test_node(resource_class='class2')
        self.post_json('/nodes', ndict,
                       headers={api_base.Version.string: "1.21"})

        result = self.get_json('/nodes/%s' % ndict['uuid'],
                               headers={api_base.Version.string: "1.21"})
        self.assertEqual('class2', result['resource_class'])

    def test_create_node_with_default_conductor_group(self):
        self.config(default_conductor_group='magic')

        ndict = test_api_utils.post_get_test_node()
        self.post_json('/nodes', ndict)

        # newer version is needed to see the resource_class field
        result = self.get_json('/nodes/%s' % ndict['uuid'],
                               headers={api_base.Version.string: "1.46"})
        self.assertEqual('magic', result['conductor_group'])

    def test_create_node_explicit_default_conductor_group(self):
        self.config(default_conductor_group='meow')
        ndict = test_api_utils.post_get_test_node(conductor_group='mouse')
        self.post_json('/nodes', ndict,
                       headers={api_base.Version.string: "1.46"})

        result = self.get_json('/nodes/%s' % ndict['uuid'],
                               headers={api_base.Version.string: "1.46"})
        self.assertEqual('mouse', result['conductor_group'])

    def test_create_node_doesnt_contain_id(self):
        # FIXME(comstud): I'd like to make this test not use the
        # dbapi, however, no matter what I do when trying to mock
        # Node.create(), the API fails to convert the objects.Node
        # into the API Node object correctly (it leaves all fields
        # as Unset).
        with mock.patch.object(self.dbapi, 'create_node',
                               wraps=self.dbapi.create_node) as cn_mock:
            ndict = test_api_utils.post_get_test_node(extra={'foo': 123})
            self.post_json('/nodes', ndict)
            result = self.get_json('/nodes/%s' % ndict['uuid'])
            self.assertEqual(ndict['extra'], result['extra'])
            cn_mock.assert_called_once_with(mock.ANY)
            # Check that 'id' is not in first arg of positional args
            self.assertNotIn('id', cn_mock.call_args[0][0])

    def test_create_node_specify_conductor_group(self):
        headers = {api_base.Version.string: '1.46'}
        ndict = test_api_utils.post_get_test_node(conductor_group='foo')
        self.post_json('/nodes', ndict, headers=headers)

        result = self.get_json('/nodes/%s' % ndict['uuid'], headers=headers)
        self.assertEqual('foo', result['conductor_group'])

    def test_create_node_specify_conductor_group_bad_version(self):
        headers = {api_base.Version.string: '1.45'}
        ndict = test_api_utils.post_get_test_node(conductor_group='foo')
        response = self.post_json('/nodes', ndict, headers=headers,
                                  expect_errors=True)
        self.assertEqual(http_client.NOT_ACCEPTABLE, response.status_int)

    def _test_jsontype_attributes(self, attr_name):
        kwargs = {attr_name: {'str': 'foo', 'int': 123, 'float': 0.1,
                              'bool': True, 'list': [1, 2], 'none': None,
                              'dict': {'cat': 'meow'}}}
        ndict = test_api_utils.post_get_test_node(**kwargs)
        self.post_json('/nodes', ndict)
        result = self.get_json('/nodes/%s' % ndict['uuid'])
        self.assertEqual(ndict[attr_name], result[attr_name])

    def test_create_node_valid_extra(self):
        self._test_jsontype_attributes('extra')

    def test_create_node_valid_properties(self):
        self._test_jsontype_attributes('properties')

    def test_create_node_valid_driver_info(self):
        self._test_jsontype_attributes('driver_info')

    def _test_vendor_passthru_ok(self, mock_vendor, return_value=None,
                                 is_async=True):
        expected_status = http_client.ACCEPTED if is_async else http_client.OK
        if return_value is None:
            expected_return_value = b''
        else:
            expected_return_value = json.dumps(return_value).encode('utf-8')

        node = obj_utils.create_test_node(self.context)
        info = {'foo': 'bar'}
        mock_vendor.return_value = {'return': return_value,
                                    'async': is_async,
                                    'attach': False}
        response = self.post_json('/nodes/%s/vendor_passthru/test' % node.uuid,
                                  info)
        mock_vendor.assert_called_once_with(
            mock.ANY, mock.ANY, node.uuid, 'test', 'POST', info, 'test-topic')
        self.assertEqual(expected_return_value, response.body)
        self.assertEqual(expected_status, response.status_code)

    def _test_vendor_passthru_ok_by_name(self, mock_vendor, return_value=None,
                                         is_async=True):
        expected_status = http_client.ACCEPTED if is_async else http_client.OK
        if return_value is None:
            expected_return_value = b''
        else:
            expected_return_value = json.dumps(return_value).encode('utf-8')

        node = obj_utils.create_test_node(self.context, name='node-109')
        info = {'foo': 'bar'}
        mock_vendor.return_value = {'return': return_value,
                                    'async': is_async,
                                    'attach': False}
        response = self.post_json('/nodes/%s/vendor_passthru/test' % node.name,
                                  info,
                                  headers={api_base.Version.string: "1.5"})
        mock_vendor.assert_called_once_with(
            mock.ANY, mock.ANY, node.uuid, 'test', 'POST', info, 'test-topic')
        self.assertEqual(expected_return_value, response.body)
        self.assertEqual(expected_status, response.status_code)

    @mock.patch.object(rpcapi.ConductorAPI, 'vendor_passthru', autospec=True)
    def test_vendor_passthru_async(self, mock_vendor):
        self._test_vendor_passthru_ok(mock_vendor)

    @mock.patch.object(rpcapi.ConductorAPI, 'vendor_passthru', autospec=True)
    def test_vendor_passthru_sync(self, mock_vendor):
        return_value = {'cat': 'meow'}
        self._test_vendor_passthru_ok(mock_vendor, return_value=return_value,
                                      is_async=False)

    @mock.patch.object(rpcapi.ConductorAPI, 'vendor_passthru', autospec=True)
    def test_vendor_passthru_put(self, mocked_vendor_passthru):
        node = obj_utils.create_test_node(self.context)
        return_value = {'return': None, 'async': True, 'attach': False}
        mocked_vendor_passthru.return_value = return_value
        response = self.put_json(
            '/nodes/%s/vendor_passthru/do_test' % node.uuid,
            {'test_key': 'test_value'})
        self.assertEqual(http_client.ACCEPTED, response.status_int)
        self.assertEqual(b'', response.body)

    @mock.patch.object(rpcapi.ConductorAPI, 'vendor_passthru', autospec=True)
    def test_vendor_passthru_by_name(self, mock_vendor):
        self._test_vendor_passthru_ok_by_name(mock_vendor)

    @mock.patch.object(rpcapi.ConductorAPI, 'vendor_passthru', autospec=True)
    def test_vendor_passthru_get(self, mocked_vendor_passthru):
        node = obj_utils.create_test_node(self.context)
        return_value = {'return': 'foo', 'async': False, 'attach': False}
        mocked_vendor_passthru.return_value = return_value
        response = self.get_json(
            '/nodes/%s/vendor_passthru/do_test' % node.uuid)
        self.assertEqual(return_value['return'], response)

    @mock.patch.object(rpcapi.ConductorAPI, 'vendor_passthru', autospec=True)
    def test_vendor_passthru_delete(self, mock_vendor_passthru):
        node = obj_utils.create_test_node(self.context)
        return_value = {'return': None, 'async': True, 'attach': False}
        mock_vendor_passthru.return_value = return_value
        response = self.delete(
            '/nodes/%s/vendor_passthru/do_test' % node.uuid)
        self.assertEqual(http_client.ACCEPTED, response.status_int)
        self.assertEqual(b'', response.body)

    def test_vendor_passthru_no_such_method(self):
        node = obj_utils.create_test_node(self.context)
        uuid = node.uuid
        info = {'foo': 'bar'}

        with mock.patch.object(rpcapi.ConductorAPI,
                               'vendor_passthru',
                               autospec=True) as mock_vendor:
            mock_vendor.side_effect = exception.UnsupportedDriverExtension(
                **{'driver': node.driver, 'node': uuid, 'extension': 'test'})
            response = self.post_json('/nodes/%s/vendor_passthru/test' % uuid,
                                      info, expect_errors=True)
            mock_vendor.assert_called_once_with(
                mock.ANY, mock.ANY, uuid, 'test', 'POST', info, 'test-topic')
            self.assertEqual(http_client.BAD_REQUEST, response.status_code)

    def test_vendor_passthru_without_method(self):
        node = obj_utils.create_test_node(self.context)
        response = self.post_json('/nodes/%s/vendor_passthru' % node.uuid,
                                  {'foo': 'bar'}, expect_errors=True)
        self.assertEqual('application/json', response.content_type, )
        self.assertEqual(http_client.BAD_REQUEST, response.status_code)
        self.assertTrue(response.json['error_message'])

    def test_post_ports_subresource_no_node_id(self):
        node = obj_utils.create_test_node(self.context)
        pdict = test_api_utils.port_post_data(node_id=None)
        pdict['node_uuid'] = node.uuid
        response = self.post_json('/nodes/ports', pdict,
                                  expect_errors=True)
        self.assertEqual(http_client.NOT_FOUND, response.status_int)

    def test_post_ports_subresource(self):
        node = obj_utils.create_test_node(self.context)
        pdict = test_api_utils.port_post_data(node_id=None)
        pdict['node_uuid'] = node.uuid
        response = self.post_json('/nodes/%s/ports' % node.uuid, pdict,
                                  expect_errors=True)
        self.assertEqual(http_client.FORBIDDEN, response.status_int)

    def test_post_portgroups_subresource(self):
        node = obj_utils.create_test_node(self.context)
        pgdict = test_api_utils.portgroup_post_data(node_id=None)
        pgdict['node_uuid'] = node.uuid
        response = self.post_json(
            '/nodes/%s/portgroups' % node.uuid, pgdict, expect_errors=True,
            headers={'X-OpenStack-Ironic-API-Version': '1.24'})
        self.assertEqual(http_client.FORBIDDEN, response.status_int)

    def test_post_volume_connectors_subresource_no_node_id(self):
        node = obj_utils.create_test_node(self.context)
        pdict = test_api_utils.volume_connector_post_data(node_id=None)
        pdict['node_uuid'] = node.uuid
        response = self.post_json(
            '/nodes/volume/connectors', pdict,
            expect_errors=True,
            headers={api_base.Version.string: str(api_v1.max_version())})
        self.assertEqual(http_client.NOT_FOUND, response.status_int)

    def test_post_volume_connectors_subresource(self):
        node = obj_utils.create_test_node(self.context)
        pdict = test_api_utils.volume_connector_post_data(node_id=None)
        pdict['node_uuid'] = node.uuid
        response = self.post_json(
            '/nodes/%s/volume/connectors' % node.uuid, pdict,
            expect_errors=True,
            headers={api_base.Version.string: str(api_v1.max_version())})
        self.assertEqual(http_client.FORBIDDEN, response.status_int)

    def test_post_volume_targets_subresource(self):
        node = obj_utils.create_test_node(self.context)
        pdict = test_api_utils.volume_target_post_data(node_id=None)
        pdict['node_uuid'] = node.uuid
        response = self.post_json(
            '/nodes/%s/volume/targets' % node.uuid, pdict,
            expect_errors=True,
            headers={api_base.Version.string: str(api_v1.max_version())})
        self.assertEqual(http_client.FORBIDDEN, response.status_int)

    def test_create_node_no_mandatory_field_driver(self):
        ndict = test_api_utils.post_get_test_node()
        del ndict['driver']
        response = self.post_json('/nodes', ndict, expect_errors=True)
        self.assertEqual(http_client.BAD_REQUEST, response.status_int)
        self.assertEqual('application/json', response.content_type)
        self.assertTrue(response.json['error_message'])

    def test_create_node_invalid_driver(self):
        ndict = test_api_utils.post_get_test_node()
        self.mock_gtf.side_effect = exception.NoValidHost('Fake Error')
        response = self.post_json('/nodes', ndict, expect_errors=True)
        self.assertEqual(http_client.BAD_REQUEST, response.status_int)
        self.assertEqual('application/json', response.content_type)
        self.assertTrue(response.json['error_message'])

    def test_create_node_no_chassis_uuid(self):
        ndict = test_api_utils.post_get_test_node()
        del ndict['chassis_uuid']
        response = self.post_json('/nodes', ndict)
        self.assertEqual('application/json', response.content_type)
        self.assertEqual(http_client.CREATED, response.status_int)
        # Check location header
        self.assertIsNotNone(response.location)
        expected_location = '/v1/nodes/%s' % ndict['uuid']
        self.assertEqual(urlparse.urlparse(response.location).path,
                         expected_location)

    @mock.patch.object(notification_utils, '_emit_api_notification',
                       autospec=True)
    def test_create_node_with_chassis_uuid(self, mock_notify):
        ndict = test_api_utils.post_get_test_node(
            chassis_uuid=self.chassis.uuid)
        response = self.post_json('/nodes', ndict)
        self.assertEqual('application/json', response.content_type)
        self.assertEqual(http_client.CREATED, response.status_int)
        result = self.get_json('/nodes/%s' % ndict['uuid'])
        self.assertEqual(ndict['chassis_uuid'], result['chassis_uuid'])
        # Check location header
        self.assertIsNotNone(response.location)
        expected_location = '/v1/nodes/%s' % ndict['uuid']
        self.assertEqual(urlparse.urlparse(response.location).path,
                         expected_location)
        mock_notify.assert_has_calls([mock.call(mock.ANY, mock.ANY, 'create',
                                      obj_fields.NotificationLevel.INFO,
                                      obj_fields.NotificationStatus.START,
                                      chassis_uuid=self.chassis.uuid),
                                      mock.call(mock.ANY, mock.ANY, 'create',
                                      obj_fields.NotificationLevel.INFO,
                                      obj_fields.NotificationStatus.END,
                                      chassis_uuid=self.chassis.uuid)])

    def test_create_node_chassis_uuid_not_found(self):
        ndict = test_api_utils.post_get_test_node(
            chassis_uuid='1a1a1a1a-2b2b-3c3c-4d4d-5e5e5e5e5e5e')
        response = self.post_json('/nodes', ndict, expect_errors=True)
        self.assertEqual('application/json', response.content_type)
        self.assertEqual(http_client.BAD_REQUEST, response.status_int)
        self.assertTrue(response.json['error_message'])

    def test_create_node_with_internal_field(self):
        ndict = test_api_utils.post_get_test_node()
        ndict['reservation'] = 'fake'
        response = self.post_json('/nodes', ndict, expect_errors=True)
        self.assertEqual('application/json', response.content_type)
        self.assertEqual(http_client.BAD_REQUEST, response.status_int)
        self.assertTrue(response.json['error_message'])

    @mock.patch.object(rpcapi.ConductorAPI, 'get_node_vendor_passthru_methods',
                       autospec=True)
    def test_vendor_passthru_methods(self, get_methods_mock):
        return_value = {'foo': 'bar'}
        get_methods_mock.return_value = return_value
        node = obj_utils.create_test_node(self.context)
        path = '/nodes/%s/vendor_passthru/methods' % node.uuid

        data = self.get_json(path)
        self.assertEqual(return_value, data)
        get_methods_mock.assert_called_once_with(mock.ANY, mock.ANY, node.uuid,
                                                 topic=mock.ANY)

        # Now let's test the cache: Reset the mock
        get_methods_mock.reset_mock()

        # Call it again
        data = self.get_json(path)
        self.assertEqual(return_value, data)
        # Assert RPC method wasn't called this time
        self.assertFalse(get_methods_mock.called)

    def test_create_node_network_interface(self):
        ndict = test_api_utils.post_get_test_node(
            network_interface='flat')
        response = self.post_json('/nodes', ndict,
                                  headers={api_base.Version.string:
                                           str(api_v1.max_version())})
        self.assertEqual(http_client.CREATED, response.status_int)
        result = self.get_json('/nodes/%s' % ndict['uuid'],
                               headers={api_base.Version.string:
                                        str(api_v1.max_version())})
        self.assertEqual('flat', result['network_interface'])

    def test_create_node_network_interface_old_api_version(self):
        ndict = test_api_utils.post_get_test_node(
            network_interface='flat')
        response = self.post_json('/nodes', ndict, expect_errors=True)
        self.assertEqual('application/json', response.content_type)
        self.assertEqual(http_client.NOT_ACCEPTABLE, response.status_int)

    def test_create_node_invalid_network_interface(self):
        ndict = test_api_utils.post_get_test_node(
            network_interface='foo')
        response = self.post_json('/nodes', ndict, expect_errors=True,
                                  headers={api_base.Version.string:
                                           str(api_v1.max_version())})
        self.assertEqual('application/json', response.content_type)
        self.assertEqual(http_client.BAD_REQUEST, response.status_int)

    def test_create_node_resource_class(self):
        ndict = test_api_utils.post_get_test_node(
            resource_class='foo')
        response = self.post_json('/nodes', ndict,
                                  headers={api_base.Version.string:
                                           str(api_v1.max_version())})
        self.assertEqual(http_client.CREATED, response.status_int)
        result = self.get_json('/nodes/%s' % ndict['uuid'],
                               headers={api_base.Version.string:
                                        str(api_v1.max_version())})
        self.assertEqual('foo', result['resource_class'])

    def test_create_node_resource_class_old_api_version(self):
        ndict = test_api_utils.post_get_test_node(
            resource_class='foo')
        response = self.post_json('/nodes', ndict, expect_errors=True)
        self.assertEqual('application/json', response.content_type)
        self.assertEqual(http_client.NOT_ACCEPTABLE, response.status_int)

    def test_create_node_storage_interface_old_api_version(self):
        headers = {api_base.Version.string: '1.32'}
        ndict = test_api_utils.post_get_test_node(storage_interface='cinder')
        response = self.post_json('/nodes', ndict, headers=headers,
                                  expect_errors=True)
        self.assertEqual('application/json', response.content_type)
        self.assertEqual(http_client.NOT_ACCEPTABLE, response.status_int)

    def test_create_node_invalid_storage_interface(self):
        ndict = test_api_utils.post_get_test_node(storage_interface='foo')
        response = self.post_json('/nodes', ndict, expect_errors=True,
                                  headers={api_base.Version.string:
                                           str(api_v1.max_version())})
        self.assertEqual('application/json', response.content_type)
        self.assertEqual(http_client.BAD_REQUEST, response.status_int)

    def test_create_node_invalid_bios_interface(self):
        ndict = test_api_utils.post_get_test_node(bios_interface='foo')
        response = self.post_json('/nodes', ndict, expect_errors=True,
                                  headers={api_base.Version.string:
                                           str(api_v1.max_version())})
        self.assertEqual('application/json', response.content_type)
        self.assertEqual(http_client.BAD_REQUEST, response.status_int)

    def test_create_node_automated_clean(self):
        ndict = test_api_utils.post_get_test_node(
            automated_clean=True)
        response = self.post_json('/nodes', ndict,
                                  headers={api_base.Version.string:
                                           str(api_v1.max_version())})
        self.assertEqual(http_client.CREATED, response.status_int)
        result = self.get_json('/nodes/%s' % ndict['uuid'],
                               headers={api_base.Version.string:
                                        str(api_v1.max_version())})
        self.assertEqual(True, result['automated_clean'])

    def test_create_node_automated_clean_old_api_version(self):
        headers = {api_base.Version.string: '1.32'}
        ndict = test_api_utils.post_get_test_node(automated_clean=True)
        response = self.post_json('/nodes', ndict, headers=headers,
                                  expect_errors=True)
        self.assertEqual('application/json', response.content_type)
        self.assertEqual(http_client.NOT_ACCEPTABLE, response.status_int)

    def test_create_node_protected_not_allowed(self):
        headers = {api_base.Version.string: '1.48'}
        ndict = test_api_utils.post_get_test_node()
        ndict['protected'] = True
        response = self.post_json('/nodes', ndict, headers=headers,
                                  expect_errors=True)
        self.assertEqual('application/json', response.content_type)
        self.assertEqual(http_client.BAD_REQUEST, response.status_int)

    def test_create_node_owner(self):
        ndict = test_api_utils.post_get_test_node(owner='cowsay')
        response = self.post_json('/nodes', ndict,
                                  headers={api_base.Version.string:
                                           str(api_v1.max_version()),
                                           'X-Project-Id': 'cowsay'})
        self.assertEqual(http_client.CREATED, response.status_int)
        result = self.get_json('/nodes/%s' % ndict['uuid'],
                               headers={api_base.Version.string:
                                        str(api_v1.max_version())})
        self.assertEqual('cowsay', result['owner'])

    def test_create_node_owner_system_scope(self):
        ndict = test_api_utils.post_get_test_node(owner='catsay')
        response = self.post_json('/nodes', ndict,
                                  headers={api_base.Version.string:
                                           str(api_v1.max_version()),
                                           'OpenStack-System-Scope': 'all',
                                           'X-Roles': 'admin'})
        self.assertEqual(http_client.CREATED, response.status_int)
        result = self.get_json('/nodes/%s' % ndict['uuid'],
                               headers={api_base.Version.string:
                                        str(api_v1.max_version())})
        self.assertEqual('catsay', result['owner'])

    def test_create_node_owner_recorded_project_scope(self):
        ndict = test_api_utils.post_get_test_node()
        response = self.post_json('/nodes', ndict,
                                  headers={api_base.Version.string:
                                           str(api_v1.max_version()),
                                           'X-Project-Id': 'ravensay'})
        self.assertEqual(http_client.CREATED, response.status_int)
        result = self.get_json('/nodes/%s' % ndict['uuid'],
                               headers={api_base.Version.string:
                                        str(api_v1.max_version())})
        self.assertEqual('ravensay', result['owner'])

    def test_create_node_owner_old_api_version(self):
        headers = {api_base.Version.string: '1.32'}
        ndict = test_api_utils.post_get_test_node(owner='bob')
        response = self.post_json('/nodes', ndict, headers=headers,
                                  expect_errors=True)
        self.assertEqual('application/json', response.content_type)
        self.assertEqual(http_client.NOT_ACCEPTABLE, response.status_int)

    def test_create_node_description(self):
        node = test_api_utils.post_get_test_node(description='useful stuff')
        response = self.post_json('/nodes', node,
                                  headers={api_base.Version.string:
                                           str(api_v1.max_version())})
        self.assertEqual(http_client.CREATED, response.status_int)
        result = self.get_json('/nodes/%s' % node['uuid'],
                               headers={api_base.Version.string:
                                        str(api_v1.max_version())})
        self.assertEqual('useful stuff', result['description'])

    def test_create_node_description_oversize(self):
        desc = '12345678' * 512 + 'last weed'
        node = test_api_utils.post_get_test_node(description=desc)
        response = self.post_json('/nodes', node,
                                  headers={api_base.Version.string:
                                           str(api_v1.max_version())},
                                  expect_errors=True)
        self.assertEqual('application/json', response.content_type)
        self.assertEqual(http_client.BAD_REQUEST, response.status_int)

    def test_create_node_lessee(self):
        ndict = test_api_utils.post_get_test_node(lessee='project')
        response = self.post_json('/nodes', ndict,
                                  headers={api_base.Version.string:
                                           str(api_v1.max_version())})
        self.assertEqual(http_client.CREATED, response.status_int)
        result = self.get_json('/nodes/%s' % ndict['uuid'],
                               headers={api_base.Version.string:
                                        str(api_v1.max_version())})
        self.assertEqual('project', result['lessee'])

    def test_create_node_lessee_old_api_version(self):
        headers = {api_base.Version.string: '1.64'}
        ndict = test_api_utils.post_get_test_node(lessee='project')
        response = self.post_json('/nodes', ndict, headers=headers,
                                  expect_errors=True)
        self.assertEqual('application/json', response.content_type)
        self.assertEqual(http_client.NOT_ACCEPTABLE, response.status_int)


class TestDelete(test_api_base.BaseApiTest):

    def setUp(self):
        super(TestDelete, self).setUp()
        p = mock.patch.object(rpcapi.ConductorAPI, 'get_topic_for',
                              autospec=True)
        self.mock_gtf = p.start()
        self.mock_gtf.return_value = 'test-topic'
        self.addCleanup(p.stop)

    @mock.patch.object(notification_utils, '_emit_api_notification',
                       autospec=True)
    @mock.patch.object(rpcapi.ConductorAPI, 'destroy_node', autospec=True)
    def test_delete_node(self, mock_dn, mock_notify):
        node = obj_utils.create_test_node(self.context)
        self.delete('/nodes/%s' % node.uuid)
        mock_dn.assert_called_once_with(mock.ANY, mock.ANY, node.uuid,
                                        'test-topic')
        mock_notify.assert_has_calls([mock.call(mock.ANY, mock.ANY, 'delete',
                                      obj_fields.NotificationLevel.INFO,
                                      obj_fields.NotificationStatus.START,
                                      chassis_uuid=None),
                                      mock.call(mock.ANY, mock.ANY, 'delete',
                                      obj_fields.NotificationLevel.INFO,
                                      obj_fields.NotificationStatus.END,
                                      chassis_uuid=None)])

    @mock.patch.object(rpcapi.ConductorAPI, 'destroy_node', autospec=True)
    def test_delete_node_by_name_unsupported(self, mock_dn):
        node = obj_utils.create_test_node(self.context, name='foo')
        self.delete('/nodes/%s' % node.name,
                    expect_errors=True)
        self.assertFalse(mock_dn.called)

    @mock.patch.object(rpcapi.ConductorAPI, 'destroy_node', autospec=True)
    def test_delete_node_by_name(self, mock_dn):
        node = obj_utils.create_test_node(self.context, name='foo.1')
        self.delete('/nodes/%s' % node.name,
                    headers={api_base.Version.string: "1.5"})
        mock_dn.assert_called_once_with(mock.ANY, mock.ANY, node.uuid,
                                        'test-topic')

    @mock.patch.object(rpcapi.ConductorAPI, 'destroy_node', autospec=True)
    def test_delete_node_by_name_with_json(self, mock_dn):
        node = obj_utils.create_test_node(self.context, name='foo')
        self.delete('/nodes/%s.json' % node.name,
                    headers={api_base.Version.string: "1.5"})
        mock_dn.assert_called_once_with(mock.ANY, mock.ANY, node.uuid,
                                        'test-topic')

    @mock.patch.object(objects.Node, 'get_by_uuid', autospec=True)
    def test_delete_node_not_found(self, mock_gbu):
        node = obj_utils.get_test_node(self.context)
        mock_gbu.side_effect = exception.NodeNotFound(node=node.uuid)

        response = self.delete('/nodes/%s' % node.uuid, expect_errors=True)
        self.assertEqual(http_client.NOT_FOUND, response.status_int)
        self.assertEqual('application/json', response.content_type)
        self.assertTrue(response.json['error_message'])
        mock_gbu.assert_called_once_with(mock.ANY, node.uuid)

    @mock.patch.object(objects.Node, 'get_by_name', autospec=True)
    def test_delete_node_not_found_by_name_unsupported(self, mock_gbn):
        node = obj_utils.get_test_node(self.context, name='foo')
        mock_gbn.side_effect = exception.NodeNotFound(node=node.name)

        response = self.delete('/nodes/%s' % node.name,
                               expect_errors=True)
        self.assertEqual(http_client.NOT_FOUND, response.status_int)
        self.assertFalse(mock_gbn.called)

    @mock.patch.object(objects.Node, 'get_by_name', autospec=True)
    def test_delete_node_not_found_by_name(self, mock_gbn):
        node = obj_utils.get_test_node(self.context, name='foo')
        mock_gbn.side_effect = exception.NodeNotFound(node=node.name)

        response = self.delete('/nodes/%s' % node.name,
                               headers={api_base.Version.string: "1.5"},
                               expect_errors=True)
        self.assertEqual(http_client.NOT_FOUND, response.status_int)
        self.assertEqual('application/json', response.content_type)
        self.assertTrue(response.json['error_message'])
        mock_gbn.assert_called_once_with(mock.ANY, node.name)

    def test_delete_ports_subresource_no_port_id(self):
        node = obj_utils.create_test_node(self.context)
        response = self.delete('/nodes/%s/ports' % node.uuid,
                               expect_errors=True)
        self.assertEqual(http_client.BAD_REQUEST, response.status_int)

    def test_delete_ports_subresource(self):
        node = obj_utils.create_test_node(self.context)
        port = obj_utils.create_test_port(self.context, node_id=node.id)
        response = self.delete(
            '/nodes/%(node_uuid)s/ports/%(port_uuid)s' %
            {'node_uuid': node.uuid, 'port_uuid': port.uuid},
            expect_errors=True)
        self.assertEqual(http_client.FORBIDDEN, response.status_int)

    def test_delete_portgroup_subresource(self):
        node = obj_utils.create_test_node(self.context)
        pg = obj_utils.create_test_portgroup(self.context, node_id=node.id)
        response = self.delete(
            '/nodes/%(node_uuid)s/portgroups/%(pg_uuid)s' %
            {'node_uuid': node.uuid, 'pg_uuid': pg.uuid},
            expect_errors=True,
            headers={'X-OpenStack-Ironic-API-Version': '1.24'})
        self.assertEqual(http_client.FORBIDDEN, response.status_int)

    def test_delete_volume_connectors_subresource_no_connector_id(self):
        node = obj_utils.create_test_node(self.context)
        response = self.delete(
            '/nodes/%s/volume/connectors' % node.uuid,
            expect_errors=True,
            headers={api_base.Version.string: str(api_v1.max_version())})
        self.assertEqual(http_client.BAD_REQUEST, response.status_int)

    def test_delete_volume_connectors_subresource(self):
        node = obj_utils.create_test_node(self.context)
        connector = obj_utils.create_test_volume_connector(self.context,
                                                           node_id=node.id)
        response = self.delete(
            '/nodes/%s/volume/connectors/%s' % (node.uuid, connector.uuid),
            expect_errors=True,
            headers={api_base.Version.string: str(api_v1.max_version())})
        self.assertEqual(http_client.FORBIDDEN, response.status_int)

    def test_delete_volume_targets_subresource(self):
        node = obj_utils.create_test_node(self.context)
        target = obj_utils.create_test_volume_target(self.context,
                                                     node_id=node.id)
        response = self.delete(
            '/nodes/%s/volume/targets/%s' % (node.uuid, target.uuid),
            expect_errors=True,
            headers={api_base.Version.string: str(api_v1.max_version())})
        self.assertEqual(http_client.FORBIDDEN, response.status_int)

    @mock.patch.object(notification_utils, '_emit_api_notification',
                       autospec=True)
    @mock.patch.object(rpcapi.ConductorAPI, 'destroy_node', autospec=True)
    def test_delete_associated(self, mock_dn, mock_notify):
        node = obj_utils.create_test_node(
            self.context,
            instance_uuid='aaaaaaaa-1111-bbbb-2222-cccccccccccc')
        mock_dn.side_effect = exception.NodeAssociated(
            node=node.uuid, instance=node.instance_uuid)

        response = self.delete('/nodes/%s' % node.uuid, expect_errors=True)
        self.assertEqual(http_client.CONFLICT, response.status_int)
        mock_dn.assert_called_once_with(mock.ANY, mock.ANY, node.uuid,
                                        'test-topic')
        mock_notify.assert_has_calls([mock.call(mock.ANY, mock.ANY, 'delete',
                                      obj_fields.NotificationLevel.INFO,
                                      obj_fields.NotificationStatus.START,
                                      chassis_uuid=None),
                                      mock.call(mock.ANY, mock.ANY, 'delete',
                                      obj_fields.NotificationLevel.ERROR,
                                      obj_fields.NotificationStatus.ERROR,
                                      chassis_uuid=None)])

    @mock.patch.object(objects.Node, 'get_by_uuid', autospec=True)
    @mock.patch.object(rpcapi.ConductorAPI, 'update_node', autospec=True)
    def test_delete_node_maintenance_mode(self, mock_update, mock_get):
        node = obj_utils.create_test_node(self.context, maintenance=True,
                                          maintenance_reason='blah')
        mock_get.return_value = node
        response = self.delete('/nodes/%s/maintenance' % node.uuid)
        self.assertEqual(http_client.ACCEPTED, response.status_int)
        self.assertEqual(b'', response.body)
        self.assertFalse(node.maintenance)
        self.assertIsNone(node.maintenance_reason)
        mock_get.assert_called_once_with(mock.ANY, node.uuid)
        mock_update.assert_called_once_with(mock.ANY, mock.ANY, node,
                                            topic='test-topic')

    @mock.patch.object(objects.Node, 'get_by_name', autospec=True)
    @mock.patch.object(rpcapi.ConductorAPI, 'update_node', autospec=True)
    def test_delete_node_maintenance_mode_by_name(self, mock_update,
                                                  mock_get):
        node = obj_utils.create_test_node(self.context, maintenance=True,
                                          maintenance_reason='blah',
                                          name='foo')
        mock_get.return_value = node
        response = self.delete('/nodes/%s/maintenance' % node.name,
                               headers={api_base.Version.string: "1.5"})
        self.assertEqual(http_client.ACCEPTED, response.status_int)
        self.assertEqual(b'', response.body)
        self.assertFalse(node.maintenance)
        self.assertIsNone(node.maintenance_reason)
        mock_get.assert_called_once_with(mock.ANY, node.name)
        mock_update.assert_called_once_with(mock.ANY, mock.ANY, node,
                                            topic='test-topic')


class TestPut(test_api_base.BaseApiTest):

    def setUp(self):
        super(TestPut, self).setUp()
        self.node = obj_utils.create_test_node(
            self.context,
            provision_state=states.AVAILABLE, name='node-39')
        p = mock.patch.object(rpcapi.ConductorAPI, 'get_topic_for',
                              autospec=True)
        self.mock_gtf = p.start()
        self.mock_gtf.return_value = 'test-topic'
        self.addCleanup(p.stop)
        p = mock.patch.object(rpcapi.ConductorAPI, 'change_node_power_state',
                              autospec=True)
        self.mock_cnps = p.start()
        self.addCleanup(p.stop)
        p = mock.patch.object(rpcapi.ConductorAPI, 'change_node_boot_mode',
                              autospec=True)
        self.mock_cnbm = p.start()
        self.addCleanup(p.stop)
        p = mock.patch.object(rpcapi.ConductorAPI, 'change_node_secure_boot',
                              autospec=True)
        self.mock_cnsb = p.start()
        self.addCleanup(p.stop)
        p = mock.patch.object(rpcapi.ConductorAPI, 'do_node_deploy',
                              autospec=True)
        self.mock_dnd = p.start()
        self.addCleanup(p.stop)
        p = mock.patch.object(rpcapi.ConductorAPI, 'do_node_tear_down',
                              autospec=True)
        self.mock_dntd = p.start()
        self.addCleanup(p.stop)
        p = mock.patch.object(rpcapi.ConductorAPI, 'inspect_hardware',
                              autospec=True)
        self.mock_dnih = p.start()
        self.addCleanup(p.stop)
        p = mock.patch.object(rpcapi.ConductorAPI, 'do_node_rescue',
                              autospec=True)
        self.mock_dnr = p.start()
        self.addCleanup(p.stop)
        p = mock.patch.object(rpcapi.ConductorAPI, 'do_node_unrescue',
                              autospec=True)
        self.mock_dnur = p.start()
        self.addCleanup(p.stop)

    def _test_power_state_success(self, target_state, timeout, api_version):
        if timeout is None:
            body = {'target': target_state}
        else:
            body = {'target': target_state, 'timeout': timeout}

        if api_version is None:
            response = self.put_json(
                '/nodes/%s/states/power' % self.node.uuid, body)
        else:
            response = self.put_json(
                '/nodes/%s/states/power' % self.node.uuid, body,
                headers={api_base.Version.string: api_version})

        self.assertEqual(http_client.ACCEPTED, response.status_code)
        self.assertEqual(b'', response.body)
        self.mock_cnps.assert_called_once_with(mock.ANY,
                                               mock.ANY,
                                               self.node.uuid,
                                               target_state,
                                               timeout=timeout,
                                               topic='test-topic')
        # Check location header
        self.assertIsNotNone(response.location)
        expected_location = '/v1/nodes/%s/states' % self.node.uuid
        self.assertEqual(urlparse.urlparse(response.location).path,
                         expected_location)

    def _test_power_state_failure(self, target_state, http_status_code,
                                  timeout, api_version):
        if timeout is None:
            body = {'target': target_state}
        else:
            body = {'target': target_state, 'timeout': timeout}

        if api_version is None:
            response = self.put_json(
                '/nodes/%s/states/power' % self.node.uuid, body,
                expect_errors=True)
        else:
            response = self.put_json(
                '/nodes/%s/states/power' % self.node.uuid, body,
                headers={api_base.Version.string: api_version},
                expect_errors=True)

        self.assertEqual('application/json', response.content_type)
        self.assertEqual(http_status_code, response.status_code)
        self.assertTrue(response.json['error_message'])

    def test_power_state_power_on_no_timeout_no_ver(self):
        self._test_power_state_success(states.POWER_ON, None, None)

    def test_power_state_power_on_no_timeout_valid_soft_ver(self):
        self._test_power_state_success(states.POWER_ON, None, "1.27")

    def test_power_state_power_on_no_timeout_invalid_soft_ver(self):
        self._test_power_state_success(states.POWER_ON, None, "1.26")

    def test_power_state_power_on_valid_timeout_no_ver(self):
        self._test_power_state_failure(
            states.POWER_ON, http_client.NOT_ACCEPTABLE, 2, None)

    def test_power_state_power_on_valid_timeout_valid_soft_ver(self):
        self._test_power_state_success(states.POWER_ON, 2, "1.27")

    def test_power_state_power_on_valid_timeout_invalid_soft_ver(self):
        self._test_power_state_failure(
            states.POWER_ON, http_client.NOT_ACCEPTABLE, 2, "1.26")

    def test_power_state_power_on_invalid_timeout_no_ver(self):
        self._test_power_state_failure(
            states.POWER_ON, http_client.BAD_REQUEST, 0, None)

    def test_power_state_power_on_invalid_timeout_valid_soft_ver(self):
        self._test_power_state_failure(
            states.POWER_ON, http_client.BAD_REQUEST, 0, "1.27")

    def test_power_state_power_on_invalid_timeout_invalid_soft_ver(self):
        self._test_power_state_failure(
            states.POWER_ON, http_client.BAD_REQUEST, 0, "1.26")

    def test_power_state_soft_power_off_no_timeout_no_ver(self):
        self._test_power_state_failure(
            states.SOFT_POWER_OFF, http_client.NOT_ACCEPTABLE, None, None)

    def test_power_state_soft_power_off_no_timeout_valid_soft_ver(self):
        self._test_power_state_success(states.SOFT_POWER_OFF, None, "1.27")

    def test_power_state_soft_power_off_no_timeout_invalid_soft_ver(self):
        self._test_power_state_failure(
            states.SOFT_POWER_OFF, http_client.NOT_ACCEPTABLE, None, "1.26")

    def test_power_state_soft_power_off_valid_timeout_no_ver(self):
        self._test_power_state_failure(
            states.SOFT_POWER_OFF, http_client.NOT_ACCEPTABLE, 2, None)

    def test_power_state_soft_power_off_valid_timeout_valid_soft_ver(self):
        self._test_power_state_success(states.SOFT_POWER_OFF, 2, "1.27")

    def test_power_state_soft_power_off_valid_timeout_invalid_soft_ver(self):
        self._test_power_state_failure(
            states.SOFT_POWER_OFF, http_client.NOT_ACCEPTABLE, 2, "1.26")

    def test_power_state_soft_power_off_invalid_timeout_no_ver(self):
        self._test_power_state_failure(
            states.SOFT_POWER_OFF, http_client.NOT_ACCEPTABLE, 0, None)

    def test_power_state_soft_power_off_invalid_timeout_valid_soft_ver(self):
        self._test_power_state_failure(
            states.SOFT_POWER_OFF, http_client.BAD_REQUEST, 0, "1.27")

    def test_power_state_soft_power_off_invalid_timeout_invalid_soft_ver(self):
        self._test_power_state_failure(
            states.SOFT_POWER_OFF, http_client.NOT_ACCEPTABLE, 0, "1.26")

    def test_power_state_by_name_unsupported(self):
        response = self.put_json('/nodes/%s/states/power' % self.node.name,
                                 {'target': states.POWER_ON},
                                 expect_errors=True)
        self.assertEqual(http_client.NOT_FOUND, response.status_code)

    def test_power_state_by_name(self):
        response = self.put_json('/nodes/%s/states/power' % self.node.name,
                                 {'target': states.POWER_ON},
                                 headers={api_base.Version.string: "1.5"})
        self.assertEqual(http_client.ACCEPTED, response.status_code)
        self.assertEqual(b'', response.body)
        self.mock_cnps.assert_called_once_with(mock.ANY,
                                               mock.ANY,
                                               self.node.uuid,
                                               states.POWER_ON,
                                               timeout=None,
                                               topic='test-topic')
        # Check location header
        self.assertIsNotNone(response.location)
        expected_location = '/v1/nodes/%s/states' % self.node.name
        self.assertEqual(urlparse.urlparse(response.location).path,
                         expected_location)

    def test_power_invalid_state_request(self):
        ret = self.put_json('/nodes/%s/states/power' % self.node.uuid,
                            {'target': 'not-supported'}, expect_errors=True)
        self.assertEqual(http_client.BAD_REQUEST, ret.status_code)

    def _test_boot_mode_success(self, target_state, api_version):

        body = {'target': target_state}

        if api_version is None:
            response = self.put_json(
                '/nodes/%s/states/boot_mode' % self.node.uuid, body)
        else:
            response = self.put_json(
                '/nodes/%s/states/boot_mode' % self.node.uuid, body,
                headers={api_base.Version.string: api_version})

        self.assertEqual(http_client.ACCEPTED, response.status_code)
        self.assertEqual(b'', response.body)
        self.mock_cnbm.assert_called_once_with(mock.ANY,
                                               mock.ANY,
                                               self.node.uuid,
                                               target_state,
                                               topic='test-topic')
        # Check location header
        self.assertIsNotNone(response.location)
        expected_location = '/v1/nodes/%s/states' % self.node.uuid
        self.assertEqual(urlparse.urlparse(response.location).path,
                         expected_location)

    def _test_boot_mode_failure(self, target_state, http_status_code,
                                api_version):

        body = {'target': target_state}

        if api_version is None:
            response = self.put_json(
                '/nodes/%s/states/boot_mode' % self.node.uuid, body,
                expect_errors=True)
        else:
            response = self.put_json(
                '/nodes/%s/states/boot_mode' % self.node.uuid, body,
                headers={api_base.Version.string: api_version},
                expect_errors=True)

        self.assertEqual(http_status_code, response.status_code)
        self.assertEqual('application/json', response.content_type)
        self.assertTrue(response.json['error_message'])

    def test_boot_mode_uefi_valid_soft_ver(self):
        self._test_boot_mode_success(boot_modes.UEFI, "1.76")

    def test_boot_mode_uefi_older_soft_ver(self):
        self._test_boot_mode_failure(
            boot_modes.UEFI, http_client.NOT_FOUND, "1.75")

    def test_boot_mode_bios_valid_soft_ver(self):
        self._test_boot_mode_success(boot_modes.LEGACY_BIOS, "1.76")

    def test_boot_mode_bios_older_soft_ver(self):
        self._test_boot_mode_failure(
            boot_modes.LEGACY_BIOS, http_client.NOT_FOUND, "1.75")

    def test_boot_mode_invalid_request(self):
        self._test_boot_mode_failure(
            'unsupported-efi', http_client.BAD_REQUEST, "1.76")

    def _test_secure_boot_success(self, target_state, api_version):

        body = {'target': target_state}

        if api_version is None:
            response = self.put_json(
                '/nodes/%s/states/secure_boot' % self.node.uuid, body)
        else:
            response = self.put_json(
                '/nodes/%s/states/secure_boot' % self.node.uuid, body,
                headers={api_base.Version.string: api_version})

        self.assertEqual(http_client.ACCEPTED, response.status_code)
        self.assertEqual(b'', response.body)
        self.mock_cnsb.assert_called_once_with(mock.ANY,
                                               mock.ANY,
                                               self.node.uuid,
                                               target_state,
                                               topic='test-topic')
        # Check location header
        self.assertIsNotNone(response.location)
        expected_location = '/v1/nodes/%s/states' % self.node.uuid
        self.assertEqual(urlparse.urlparse(response.location).path,
                         expected_location)

    def _test_secure_boot_failure(self, target_state, http_status_code,
                                  api_version):

        body = {'target': target_state}

        if api_version is None:
            response = self.put_json(
                '/nodes/%s/states/secure_boot' % self.node.uuid, body,
                expect_errors=True)
        else:
            response = self.put_json(
                '/nodes/%s/states/secure_boot' % self.node.uuid, body,
                headers={api_base.Version.string: api_version},
                expect_errors=True)

        self.assertEqual(http_status_code, response.status_code)
        self.assertEqual('application/json', response.content_type)
        self.assertTrue(response.json['error_message'])

    def test_secure_boot_on_valid_soft_ver(self):
        self._test_secure_boot_success(True, "1.76")

    def test_secure_boot_on_older_soft_ver(self):
        self._test_secure_boot_failure(
            True, http_client.NOT_FOUND, "1.75")

    def test_secure_boot_off_valid_soft_ver(self):
        self._test_secure_boot_success(False, "1.76")

    def test_secure_boot_off_older_soft_ver(self):
        self._test_secure_boot_failure(
            False, http_client.NOT_FOUND, "1.75")

    def test_secure_boot_off_valid_undocumented_request_zero(self):
        self._test_secure_boot_success(0, "1.76")

    def test_secure_boot_on_valid_undocumented_request_one(self):
        self._test_secure_boot_success(1, "1.76")

    def test_secure_boot_on_invalid_request_two(self):
        self._test_secure_boot_failure(2, http_client.BAD_REQUEST, "1.76")

    def test_secure_boot_invalid_request_nullstr(self):
        self._test_secure_boot_failure(
            '', http_client.BAD_REQUEST, "1.76")

    def test_secure_boot_invalid_request_boo(self):
        self._test_secure_boot_failure(
            'boo!', http_client.BAD_REQUEST, "1.76")

    def test_secure_boot_invalid_request_None(self):
        self._test_secure_boot_failure(
            None, http_client.BAD_REQUEST, "1.76")

    def test_power_change_when_being_cleaned(self):
        for state in (states.CLEANING, states.CLEANWAIT):
            self.node.provision_state = state
            self.node.save()
            ret = self.put_json('/nodes/%s/states/power' % self.node.uuid,
                                {'target': states.POWER_OFF},
                                expect_errors=True)
            self.assertEqual(http_client.BAD_REQUEST, ret.status_code)

    def test_provision_invalid_state_request(self):
        ret = self.put_json('/nodes/%s/states/provision' % self.node.uuid,
                            {'target': 'not-supported'}, expect_errors=True)
        self.assertEqual(http_client.BAD_REQUEST, ret.status_code)

    def test_provision_with_deploy(self):
        ret = self.put_json('/nodes/%s/states/provision' % self.node.uuid,
                            {'target': states.ACTIVE})
        self.assertEqual(http_client.ACCEPTED, ret.status_code)
        self.assertEqual(b'', ret.body)
        self.mock_dnd.assert_called_once_with(mock.ANY,
                                              context=mock.ANY,
                                              node_id=self.node.uuid,
                                              rebuild=False,
                                              configdrive=None,
                                              topic='test-topic',
                                              deploy_steps=None)
        # Check location header
        self.assertIsNotNone(ret.location)
        expected_location = '/v1/nodes/%s/states' % self.node.uuid
        self.assertEqual(urlparse.urlparse(ret.location).path,
                         expected_location)

    def test_provision_deploy(self):
        ret = self.put_json('/nodes/%s/states/provision' % self.node.uuid,
                            {'target': states.DEPLOY},
                            headers={api_base.Version.string: "1.73"})
        self.assertEqual(http_client.ACCEPTED, ret.status_code)
        self.assertEqual(b'', ret.body)
        self.mock_dnd.assert_called_once_with(mock.ANY,
                                              context=mock.ANY,
                                              node_id=self.node.uuid,
                                              rebuild=False,
                                              configdrive=None,
                                              topic='test-topic',
                                              deploy_steps=None)
        # Check location header
        self.assertIsNotNone(ret.location)
        expected_location = '/v1/nodes/%s/states' % self.node.uuid
        self.assertEqual(urlparse.urlparse(ret.location).path,
                         expected_location)

    def test_provision_by_name_unsupported(self):
        ret = self.put_json('/nodes/%s/states/provision' % self.node.name,
                            {'target': states.ACTIVE},
                            expect_errors=True)
        self.assertEqual(http_client.NOT_FOUND, ret.status_code)

    def test_provision_by_name(self):
        ret = self.put_json('/nodes/%s/states/provision' % self.node.name,
                            {'target': states.ACTIVE},
                            headers={api_base.Version.string: "1.5"})
        self.assertEqual(http_client.ACCEPTED, ret.status_code)
        self.assertEqual(b'', ret.body)

        self.mock_dnd.assert_called_once_with(mock.ANY,
                                              context=mock.ANY,
                                              node_id=self.node.uuid,
                                              rebuild=False,
                                              configdrive=None,
                                              topic='test-topic',
                                              deploy_steps=None)

    def test_provision_with_deploy_configdrive(self):
        FAKE_CD = """
w7FJYV8ywqx+wqnCpwPCoXHDisO6HMO2w4nDsBBJccOvXsKUMsO9OcOPCQLCnMKoPSFLwp
DDhj7Ck8KqwprDpcKWw6XChsOMw5lSEcKUZcO0PUJiWcK4wq0owr4ye8Ozw67ClzXDmsO7
UxvCpjnCkFQgw73Ch8Kaw5HCicKlXMOvUnDDvg5uwoFkwqDCl8KAEWwCbUQvw7I5JcKUw7
VbKl3Di8O4LMKuwrHChMOBw5plaVJKci04w7fCgcOgVhkwwoLCgilxwqTCpDNCGzdNw5N6
wpgAw6jDn8ODLBBlMGcawrEZwr3DiVPDtMKTwpcxwrpBwrrDtcOEw5YTw7MMwqnCsMKqwp
PCkMK1wpTDssKfwrDCscOsEEDDo8OAw5DCqsKKGBRqwqPDqx7Cg8KkDcOkwoIuwo/CgcK0
ZcKNf3N7wqIYQcKgQDnCq8KFw6DCvMOwWAHChMO3w5xWb8O3wq7Dn8K4eXgWw742woUqw5
/DvcK+ScKcX8KzwprCuD3DgcOsC8Oqwp0CwqB8TsOIHsKVwozCv8O+w4LCmE9GCMORw63D
icOQw4ZFasOzw4Uvw7NSw6Qbw77DkBgkwo4COcOzOWLClRNQXcOHwojCrsOdHMKIw6nDuM
ORHMKeXMO8fcK0By7CiMKwHSXCoEQgfQhWwpMdSsO8LgHCjh87DQc= """
        ret = self.put_json('/nodes/%s/states/provision' % self.node.uuid,
                            {'target': states.ACTIVE,
                             'configdrive': FAKE_CD})
        self.assertEqual(http_client.ACCEPTED, ret.status_code)
        self.assertEqual(b'', ret.body)
        self.mock_dnd.assert_called_once_with(mock.ANY,
                                              context=mock.ANY,
                                              node_id=self.node.uuid,
                                              rebuild=False,
                                              configdrive=FAKE_CD,
                                              topic='test-topic',
                                              deploy_steps=None)
        # Check location header
        self.assertIsNotNone(ret.location)
        expected_location = '/v1/nodes/%s/states' % self.node.uuid
        self.assertEqual(urlparse.urlparse(ret.location).path,
                         expected_location)

    def test_provision_with_deploy_configdrive_url(self):
        ret = self.put_json('/nodes/%s/states/provision' % self.node.uuid,
                            {'target': states.ACTIVE,
                             'configdrive': 'http://example.com'})
        self.assertEqual(http_client.ACCEPTED, ret.status_code)
        self.assertEqual(b'', ret.body)
        self.mock_dnd.assert_called_once_with(mock.ANY,
                                              context=mock.ANY,
                                              node_id=self.node.uuid,
                                              rebuild=False,
                                              configdrive='http://example.com',
                                              topic='test-topic',
                                              deploy_steps=None)
        # Check location header
        self.assertIsNotNone(ret.location)
        expected_location = '/v1/nodes/%s/states' % self.node.uuid
        self.assertEqual(urlparse.urlparse(ret.location).path,
                         expected_location)

    def test_provision_with_deploy_configdrive_as_dict(self):
        ret = self.put_json('/nodes/%s/states/provision' % self.node.uuid,
                            {'target': states.ACTIVE,
                             'configdrive': {'user_data': 'foo'}},
                            headers={api_base.Version.string: '1.56'})
        self.assertEqual(http_client.ACCEPTED, ret.status_code)
        self.assertEqual(b'', ret.body)
        self.mock_dnd.assert_called_once_with(mock.ANY,
                                              context=mock.ANY,
                                              node_id=self.node.uuid,
                                              rebuild=False,
                                              configdrive={'user_data': 'foo'},
                                              topic='test-topic',
                                              deploy_steps=None)

    def test_provision_with_deploy_configdrive_as_dict_all_fields(self):
        fake_cd = {'user_data': {'serialize': 'me'},
                   'meta_data': {'hostname': 'example.com'},
                   'network_data': {'links': []},
                   'vendor_data': {'foo': 'bar'}}
        ret = self.put_json('/nodes/%s/states/provision' % self.node.uuid,
                            {'target': states.ACTIVE,
                             'configdrive': fake_cd},
                            headers={api_base.Version.string: '1.60'})
        self.assertEqual(http_client.ACCEPTED, ret.status_code)
        self.assertEqual(b'', ret.body)
        self.mock_dnd.assert_called_once_with(mock.ANY,
                                              context=mock.ANY,
                                              node_id=self.node.uuid,
                                              rebuild=False,
                                              configdrive=fake_cd,
                                              topic='test-topic',
                                              deploy_steps=None)

    def test_provision_with_deploy_configdrive_invalid_type(self):
        ret = self.put_json('/nodes/%s/states/provision' % self.node.uuid,
                            {'target': states.ACTIVE,
                             'configdrive': ["aabb"]},
                            headers={api_base.Version.string: '1.60'},
                            expect_errors=True)
        self.assertEqual(http_client.BAD_REQUEST, ret.status_code)

    def test_provision_with_deploy_configdrive_not_base64(self):
        ret = self.put_json('/nodes/%s/states/provision' % self.node.uuid,
                            {'target': states.ACTIVE,
                             # Simulate invalid JSON provided to CLI
                             'configdrive': '{"meta_data": '},
                            headers={api_base.Version.string: '1.60'},
                            expect_errors=True)
        self.assertEqual(http_client.BAD_REQUEST, ret.status_code)

    @mock.patch.object(api_utils, 'check_allow_deploy_steps', autospec=True)
    def test_provision_with_deploy_deploy_steps(self, mock_check):
        deploy_steps = [{'interface': 'bios',
                         'step': 'factory_reset',
                         'priority': 95,
                         'args': {}}]
        ret = self.put_json('/nodes/%s/states/provision' % self.node.uuid,
                            {'target': states.ACTIVE,
                             'deploy_steps': deploy_steps})
        self.assertEqual(http_client.ACCEPTED, ret.status_code)
        self.assertEqual(b'', ret.body)
        self.mock_dnd.assert_called_once_with(mock.ANY,
                                              context=mock.ANY,
                                              node_id=self.node.uuid,
                                              rebuild=False,
                                              configdrive=None,
                                              topic='test-topic',
                                              deploy_steps=deploy_steps)
        # Check location header
        self.assertIsNotNone(ret.location)
        expected_location = '/v1/nodes/%s/states' % self.node.uuid
        self.assertEqual(urlparse.urlparse(ret.location).path,
                         expected_location)

    def test_provision_with_deploy_deploy_steps_fail(self):
        # Mandatory 'priority' missing in the step
        deploy_steps = [{'interface': 'bios',
                         'step': 'factory_reset'}]
        ret = self.put_json('/nodes/%s/states/provision' % self.node.uuid,
                            {'target': states.ACTIVE,
                             'deploy_steps': deploy_steps},
                            expect_errors=True)
        self.assertEqual(http_client.NOT_ACCEPTABLE, ret.status_code)

    def test_provision_with_rebuild(self):
        node = self.node
        node.provision_state = states.ACTIVE
        node.target_provision_state = states.NOSTATE
        node.save()
        ret = self.put_json('/nodes/%s/states/provision' % self.node.uuid,
                            {'target': states.REBUILD})
        self.assertEqual(http_client.ACCEPTED, ret.status_code)
        self.assertEqual(b'', ret.body)
        self.mock_dnd.assert_called_once_with(mock.ANY,
                                              context=mock.ANY,
                                              node_id=self.node.uuid,
                                              rebuild=True,
                                              configdrive=None,
                                              topic='test-topic',
                                              deploy_steps=None)
        # Check location header
        self.assertIsNotNone(ret.location)
        expected_location = '/v1/nodes/%s/states' % self.node.uuid
        self.assertEqual(urlparse.urlparse(ret.location).path,
                         expected_location)

    def test_provision_with_rebuild_unsupported_configdrive(self):
        node = self.node
        node.provision_state = states.ACTIVE
        node.target_provision_state = states.NOSTATE
        node.save()
        ret = self.put_json('/nodes/%s/states/provision' % self.node.uuid,
                            {'target': states.REBUILD, 'configdrive': 'foo'},
                            expect_errors=True)
        self.assertEqual(http_client.BAD_REQUEST, ret.status_code)

    def test_provision_with_rebuild_configdrive(self):
        node = self.node
        node.provision_state = states.ACTIVE
        node.target_provision_state = states.NOSTATE
        node.save()
        ret = self.put_json('/nodes/%s/states/provision' % self.node.uuid,
                            {'target': states.REBUILD, 'configdrive': 'foo'},
                            headers={api_base.Version.string: '1.35'})
        self.assertEqual(http_client.ACCEPTED, ret.status_code)
        self.assertEqual(b'', ret.body)
        self.mock_dnd.assert_called_once_with(mock.ANY,
                                              context=mock.ANY,
                                              node_id=self.node.uuid,
                                              rebuild=True,
                                              configdrive='foo',
                                              topic='test-topic',
                                              deploy_steps=None)
        # Check location header
        self.assertIsNotNone(ret.location)
        expected_location = '/v1/nodes/%s/states' % self.node.uuid
        self.assertEqual(urlparse.urlparse(ret.location).path,
                         expected_location)

    def test_provision_with_configdrive_not_active(self):
        ret = self.put_json('/nodes/%s/states/provision' % self.node.uuid,
                            {'target': states.DELETED, 'configdrive': 'foo'},
                            expect_errors=True)
        self.assertEqual(http_client.BAD_REQUEST, ret.status_code)

    @mock.patch.object(api_utils, 'check_allow_deploy_steps', autospec=True)
    def test_provision_with_rebuild_deploy_steps(self, mock_check):
        node = self.node
        node.provision_state = states.ACTIVE
        node.target_provision_state = states.NOSTATE
        node.save()
        deploy_steps = [{'interface': 'bios',
                         'step': 'factory_reset',
                         'priority': 95,
                         'args': {}}]
        ret = self.put_json('/nodes/%s/states/provision' % self.node.uuid,
                            {'target': states.REBUILD,
                             'deploy_steps': deploy_steps})
        self.assertEqual(http_client.ACCEPTED, ret.status_code)
        self.assertEqual(b'', ret.body)
        self.mock_dnd.assert_called_once_with(mock.ANY,
                                              context=mock.ANY,
                                              node_id=self.node.uuid,
                                              rebuild=True,
                                              configdrive=None,
                                              topic='test-topic',
                                              deploy_steps=deploy_steps)
        # Check location header
        self.assertIsNotNone(ret.location)
        expected_location = '/v1/nodes/%s/states' % self.node.uuid
        self.assertEqual(urlparse.urlparse(ret.location).path,
                         expected_location)

    def test_provision_with_tear_down(self):
        node = self.node
        node.provision_state = states.ACTIVE
        node.target_provision_state = states.NOSTATE
        node.save()
        ret = self.put_json('/nodes/%s/states/provision' % node.uuid,
                            {'target': states.DELETED})
        self.assertEqual(http_client.ACCEPTED, ret.status_code)
        self.assertEqual(b'', ret.body)
        self.mock_dntd.assert_called_once_with(
            mock.ANY, mock.ANY, node.uuid, 'test-topic')
        # Check location header
        self.assertIsNotNone(ret.location)
        expected_location = '/v1/nodes/%s/states' % node.uuid
        self.assertEqual(urlparse.urlparse(ret.location).path,
                         expected_location)

    def test_provision_with_tear_down_undeploy(self):
        node = self.node
        node.provision_state = states.ACTIVE
        node.target_provision_state = states.NOSTATE
        node.save()
        ret = self.put_json('/nodes/%s/states/provision' % node.uuid,
                            {'target': states.UNDEPLOY})
        self.assertEqual(http_client.ACCEPTED, ret.status_code)
        self.assertEqual(b'', ret.body)
        self.mock_dntd.assert_called_once_with(
            mock.ANY, mock.ANY, node.uuid, 'test-topic')
        # Check location header
        self.assertIsNotNone(ret.location)
        expected_location = '/v1/nodes/%s/states' % node.uuid
        self.assertEqual(urlparse.urlparse(ret.location).path,
                         expected_location)

    def test_provision_already_in_progress(self):
        node = self.node
        node.provision_state = states.DEPLOYING
        node.target_provision_state = states.ACTIVE
        node.reservation = 'fake-host'
        node.save()
        ret = self.put_json('/nodes/%s/states/provision' % node.uuid,
                            {'target': states.ACTIVE},
                            expect_errors=True)
        self.assertEqual(http_client.CONFLICT, ret.status_code)  # Conflict
        self.assertFalse(self.mock_dnd.called)

    def test_provision_locked_with_correct_state(self):
        node = self.node
        node.provision_state = states.AVAILABLE
        node.target_provision_state = states.NOSTATE
        node.reservation = 'fake-host'
        node.save()
        self.mock_dnd.side_effect = exception.NodeLocked(node='', host='')
        ret = self.put_json('/nodes/%s/states/provision' % node.uuid,
                            {'target': states.ACTIVE},
                            expect_errors=True)
        self.assertEqual(http_client.CONFLICT, ret.status_code)  # Conflict
        self.assertTrue(self.mock_dnd.called)

    def test_provision_with_tear_down_in_progress_deploywait(self):
        node = self.node
        node.provision_state = states.DEPLOYWAIT
        node.target_provision_state = states.ACTIVE
        node.save()
        ret = self.put_json('/nodes/%s/states/provision' % node.uuid,
                            {'target': states.DELETED})
        self.assertEqual(http_client.ACCEPTED, ret.status_code)
        self.assertEqual(b'', ret.body)
        self.mock_dntd.assert_called_once_with(
            mock.ANY, mock.ANY, node.uuid, 'test-topic')
        # Check location header
        self.assertIsNotNone(ret.location)
        expected_location = '/v1/nodes/%s/states' % node.uuid
        self.assertEqual(urlparse.urlparse(ret.location).path,
                         expected_location)

    # NOTE(tenbrae): this test asserts API functionality which is not part of
    # the new-ironic-state-machine in Kilo. It is retained for backwards
    # compatibility with Juno.
    # TODO(tenbrae): add a deprecation-warning to the REST result
    # and check for it here.
    def test_provision_with_deploy_after_deployfail(self):
        node = self.node
        node.provision_state = states.DEPLOYFAIL
        node.target_provision_state = states.ACTIVE
        node.save()
        ret = self.put_json('/nodes/%s/states/provision' % node.uuid,
                            {'target': states.ACTIVE})
        self.assertEqual(http_client.ACCEPTED, ret.status_code)
        self.assertEqual(b'', ret.body)
        self.mock_dnd.assert_called_once_with(mock.ANY,
                                              context=mock.ANY,
                                              node_id=self.node.uuid,
                                              rebuild=False,
                                              configdrive=None,
                                              topic='test-topic',
                                              deploy_steps=None)
        # Check location header
        self.assertIsNotNone(ret.location)
        expected_location = '/v1/nodes/%s/states' % node.uuid
        self.assertEqual(expected_location,
                         urlparse.urlparse(ret.location).path)

    def test_provision_already_in_state(self):
        self.node.provision_state = states.ACTIVE
        self.node.save()
        ret = self.put_json('/nodes/%s/states/provision' % self.node.uuid,
                            {'target': states.ACTIVE},
                            expect_errors=True)
        self.assertEqual(http_client.BAD_REQUEST, ret.status_code)

    @mock.patch.object(rpcapi.ConductorAPI, 'do_provisioning_action',
                       autospec=True)
    def test_provide_from_manage(self, mock_dpa):
        self.node.provision_state = states.MANAGEABLE
        self.node.save()

        ret = self.put_json('/nodes/%s/states/provision' % self.node.uuid,
                            {'target': states.VERBS['provide']},
                            headers={api_base.Version.string: "1.4"})
        self.assertEqual(http_client.ACCEPTED, ret.status_code)
        self.assertEqual(b'', ret.body)
        mock_dpa.assert_called_once_with(mock.ANY, mock.ANY, self.node.uuid,
                                         states.VERBS['provide'],
                                         'test-topic')

    def test_rescue_raises_error_before_1_38(self):
        """Test that a lower API client cannot use the rescue verb"""
        ret = self.put_json('/nodes/%s/states/provision' % self.node.uuid,
                            {'target': states.VERBS['rescue'],
                             'rescue_password': 'password'},
                            headers={api_base.Version.string: "1.37"},
                            expect_errors=True)
        self.assertEqual(http_client.NOT_ACCEPTABLE, ret.status_code)

    def test_unrescue_raises_error_before_1_38(self):
        """Test that a lower API client cannot use the unrescue verb"""
        ret = self.put_json('/nodes/%s/states/provision' % self.node.uuid,
                            {'target': states.VERBS['unrescue']},
                            headers={api_base.Version.string: "1.37"},
                            expect_errors=True)
        self.assertEqual(http_client.NOT_ACCEPTABLE, ret.status_code)

    def test_provision_unexpected_rescue_password(self):
        self.node.provision_state = states.AVAILABLE
        self.node.save()
        ret = self.put_json('/nodes/%s/states/provision' % self.node.uuid,
                            {'target': states.ACTIVE,
                             'rescue_password': 'password'},
                            headers={api_base.Version.string: "1.38"},
                            expect_errors=True)
        self.assertEqual(http_client.BAD_REQUEST, ret.status_code)
        self.assertIn('\\"rescue_password\\" is only valid',
                      ret.json['error_message'])
        self.assertFalse(self.mock_dnr.called)

    def test_provision_rescue_no_password(self):
        self.node.provision_state = states.ACTIVE
        self.node.save()
        ret = self.put_json('/nodes/%s/states/provision' % self.node.uuid,
                            {'target': states.VERBS['rescue']},
                            headers={api_base.Version.string: "1.38"},
                            expect_errors=True)
        self.assertEqual(http_client.BAD_REQUEST, ret.status_code)
        self.assertIn('A non-empty \\"rescue_password\\" is required',
                      ret.json['error_message'])
        self.assertFalse(self.mock_dnr.called)

    def test_provision_rescue_empty_password(self):
        self.node.provision_state = states.ACTIVE
        self.node.save()
        ret = self.put_json('/nodes/%s/states/provision' % self.node.uuid,
                            {'target': states.VERBS['rescue'],
                             'rescue_password': '      '},
                            headers={api_base.Version.string: "1.38"},
                            expect_errors=True)
        self.assertEqual(http_client.BAD_REQUEST, ret.status_code)
        self.assertIn('A non-empty \\"rescue_password\\" is required',
                      ret.json['error_message'])
        self.assertFalse(self.mock_dnr.called)

    def _test_provision_rescue_in_allowed_state(self, prov_state):
        node = self.node
        node.provision_state = prov_state
        node.reservation = 'fake-host'
        node.save()
        ret = self.put_json('/nodes/%s/states/provision' % node.uuid,
                            {'target': states.VERBS['rescue'],
                             'rescue_password': 'password'},
                            headers={api_base.Version.string: "1.38"})
        self.assertEqual(http_client.ACCEPTED, ret.status_code)
        self.assertEqual(b'', ret.body)
        self.mock_dnr.assert_called_once_with(
            mock.ANY, mock.ANY, self.node.uuid, 'password', 'test-topic')
        self.mock_dnr.reset_mock()

    def test_provision_rescue_in_allowed_states(self):
        allowed_states = [states.ACTIVE, states.RESCUE,
                          states.RESCUEFAIL, states.UNRESCUEFAIL]
        for state in allowed_states:
            self._test_provision_rescue_in_allowed_state(state)

    def _test_provision_rescue_in_disallowed_state(self, prov_state):
        node = self.node
        node.provision_state = prov_state
        node.reservation = 'fake-host'
        node.save()
        ret = self.put_json('/nodes/%s/states/provision' % node.uuid,
                            {'target': states.VERBS['rescue'],
                             'rescue_password': 'password'},
                            headers={api_base.Version.string: "1.38"},
                            expect_errors=True)
        self.assertEqual(http_client.CONFLICT, ret.status_code)
        self.assertFalse(self.mock_dnr.called)

    def test_provision_rescue_in_disallowed_states(self):
        disallowed_states = [states.DELETING, states.RESCUEWAIT,
                             states.RESCUING, states.UNRESCUING]
        for state in disallowed_states:
            self._test_provision_rescue_in_disallowed_state(state)

    def _test_provision_unrescue_in_allowed_state(self, prov_state):
        node = self.node
        node.provision_state = prov_state
        node.reservation = 'fake-host'
        node.save()
        ret = self.put_json('/nodes/%s/states/provision' % node.uuid,
                            {'target': states.VERBS['unrescue']},
                            headers={api_base.Version.string: "1.38"})
        self.assertEqual(http_client.ACCEPTED, ret.status_code)
        self.assertEqual(b'', ret.body)
        self.mock_dnur.assert_called_once_with(
            mock.ANY, mock.ANY, self.node.uuid, 'test-topic')
        self.mock_dnur.reset_mock()

    def test_provision_unrescue_in_allowed_states(self):
        allowed_states = [states.RESCUE, states.RESCUEFAIL,
                          states.UNRESCUEFAIL]
        for state in allowed_states:
            self._test_provision_unrescue_in_allowed_state(state)

    def _test_provision_unrescue_in_disallowed_state(self, prov_state):
        node = self.node
        node.provision_state = prov_state
        node.reservation = 'fake-host'
        node.save()
        ret = self.put_json('/nodes/%s/states/provision' % node.uuid,
                            {'target': states.VERBS['unrescue']},
                            headers={api_base.Version.string: "1.38"},
                            expect_errors=True)
        self.assertEqual(http_client.CONFLICT, ret.status_code)
        self.assertFalse(self.mock_dnur.called)

    def test_provision_unrescue_in_disallowed_states(self):
        disallowed_states = [states.ACTIVE, states.DELETING,
                             states.RESCUEWAIT, states.RESCUING,
                             states.UNRESCUING]
        for state in disallowed_states:
            self._test_provision_unrescue_in_disallowed_state(state)

    def test_inspect_already_in_progress(self):
        node = self.node
        node.provision_state = states.INSPECTING
        node.target_provision_state = states.MANAGEABLE
        node.reservation = 'fake-host'
        node.save()
        ret = self.put_json('/nodes/%s/states/provision' % node.uuid,
                            {'target': states.MANAGEABLE},
                            expect_errors=True)
        self.assertEqual(http_client.CONFLICT, ret.status_code)  # Conflict

    def test_inspect_validation_failed_status_code(self):
        self.mock_dnih.side_effect = exception.InvalidParameterValue(
            err='Failed to validate inspection or power info.')
        node = self.node
        node.provision_state = states.MANAGEABLE
        node.reservation = 'fake-host'
        node.save()
        ret = self.put_json('/nodes/%s/states/provision' % node.uuid,
                            {'target': 'inspect'},
                            headers={api_base.Version.string: "1.6"},
                            expect_errors=True)
        self.assertEqual(http_client.BAD_REQUEST, ret.status_code)

    def test_inspect_validation_failed_missing_parameter_value(self):
        self.mock_dnih.side_effect = exception.MissingParameterValue(
            err='Failed to validate inspection or power info.')
        node = self.node
        node.provision_state = states.MANAGEABLE
        node.reservation = 'fake-host'
        node.save()
        ret = self.put_json('/nodes/%s/states/provision' % node.uuid,
                            {'target': 'inspect'},
                            headers={api_base.Version.string: "1.6"},
                            expect_errors=True)
        self.assertEqual(http_client.BAD_REQUEST, ret.status_code)

    @mock.patch.object(rpcapi.ConductorAPI, 'do_provisioning_action',
                       autospec=True)
    def test_manage_from_available(self, mock_dpa):
        self.node.provision_state = states.AVAILABLE
        self.node.save()

        ret = self.put_json('/nodes/%s/states/provision' % self.node.uuid,
                            {'target': states.VERBS['manage']},
                            headers={api_base.Version.string: "1.4"})
        self.assertEqual(http_client.ACCEPTED, ret.status_code)
        self.assertEqual(b'', ret.body)
        mock_dpa.assert_called_once_with(mock.ANY, mock.ANY, self.node.uuid,
                                         states.VERBS['manage'],
                                         'test-topic')

    @mock.patch.object(rpcapi.ConductorAPI, 'do_provisioning_action',
                       autospec=True)
    def test_bad_requests_in_managed_state(self, mock_dpa):
        self.node.provision_state = states.MANAGEABLE
        self.node.save()

        for state in [states.ACTIVE, states.REBUILD, states.DELETED]:
            ret = self.put_json('/nodes/%s/states/provision' % self.node.uuid,
                                {'target': states.ACTIVE},
                                expect_errors=True)
            self.assertEqual(http_client.BAD_REQUEST, ret.status_code)
        self.assertEqual(0, mock_dpa.call_count)

    @mock.patch.object(rpcapi.ConductorAPI, 'do_provisioning_action',
                       autospec=True)
    def test_abort_cleanwait(self, mock_dpa):
        self.node.provision_state = states.CLEANWAIT
        self.node.save()

        ret = self.put_json('/nodes/%s/states/provision' % self.node.uuid,
                            {'target': states.VERBS['abort']},
                            headers={api_base.Version.string: "1.13"})
        self.assertEqual(http_client.ACCEPTED, ret.status_code)
        self.assertEqual(b'', ret.body)
        mock_dpa.assert_called_once_with(mock.ANY, mock.ANY, self.node.uuid,
                                         states.VERBS['abort'],
                                         'test-topic')

    def test_abort_invalid_state(self):
        # "abort" is only valid for nodes in CLEANWAIT
        self.node.provision_state = states.CLEANING
        self.node.save()
        ret = self.put_json('/nodes/%s/states/provision' % self.node.uuid,
                            {'target': states.VERBS['abort']},
                            headers={api_base.Version.string: "1.13"},
                            expect_errors=True)
        self.assertEqual(http_client.BAD_REQUEST, ret.status_code)

    def test_provision_with_cleansteps_not_clean(self):
        self.node.provision_state = states.MANAGEABLE
        self.node.save()
        ret = self.put_json('/nodes/%s/states/provision' % self.node.uuid,
                            {'target': states.VERBS['provide'],
                             'clean_steps': 'foo'},
                            headers={api_base.Version.string: "1.4"},
                            expect_errors=True)
        self.assertEqual(http_client.BAD_REQUEST, ret.status_code)

    def test_clean_no_cleansteps(self):
        self.node.provision_state = states.MANAGEABLE
        self.node.save()
        ret = self.put_json('/nodes/%s/states/provision' % self.node.uuid,
                            {'target': states.VERBS['clean']},
                            headers={api_base.Version.string: "1.15"},
                            expect_errors=True)
        self.assertEqual(http_client.BAD_REQUEST, ret.status_code)

    @mock.patch.object(rpcapi.ConductorAPI, 'do_node_clean', autospec=True)
    @mock.patch.object(api_node, '_check_clean_steps', autospec=True)
    def test_clean_check_steps_fail(self, mock_check, mock_rpcapi):
        self.node.provision_state = states.MANAGEABLE
        self.node.save()
        mock_check.side_effect = exception.InvalidParameterValue('bad')
        clean_steps = [{"step": "upgrade_firmware", "interface": "deploy"}]
        ret = self.put_json('/nodes/%s/states/provision' % self.node.uuid,
                            {'target': states.VERBS['clean'],
                             'clean_steps': clean_steps},
                            headers={api_base.Version.string: "1.15"},
                            expect_errors=True)
        self.assertEqual(http_client.BAD_REQUEST, ret.status_code)
        mock_check.assert_called_once_with(clean_steps)
        self.assertFalse(mock_rpcapi.called)

    @mock.patch.object(rpcapi.ConductorAPI, 'do_node_clean', autospec=True)
    @mock.patch.object(api_node, '_check_clean_steps', autospec=True)
    def test_clean(self, mock_check, mock_rpcapi):
        self.node.provision_state = states.MANAGEABLE
        self.node.save()
        clean_steps = [{"step": "upgrade_firmware", "interface": "deploy"}]
        ret = self.put_json('/nodes/%s/states/provision' % self.node.uuid,
                            {'target': states.VERBS['clean'],
                             'clean_steps': clean_steps},
                            headers={api_base.Version.string: "1.15"})
        self.assertEqual(http_client.ACCEPTED, ret.status_code)
        self.assertEqual(b'', ret.body)
        mock_check.assert_called_once_with(clean_steps)
        mock_rpcapi.assert_called_once_with(mock.ANY, mock.ANY, self.node.uuid,
                                            clean_steps, None,
                                            topic='test-topic')

    @mock.patch.object(rpcapi.ConductorAPI, 'do_node_clean', autospec=True)
    @mock.patch.object(api_node, '_check_clean_steps', autospec=True)
    def test_clean_disable_ramdisk(self, mock_check, mock_rpcapi):
        self.node.provision_state = states.MANAGEABLE
        self.node.save()
        clean_steps = [{"step": "upgrade_firmware", "interface": "deploy"}]
        ret = self.put_json('/nodes/%s/states/provision' % self.node.uuid,
                            {'target': states.VERBS['clean'],
                             'clean_steps': clean_steps,
                             'disable_ramdisk': True},
                            headers={api_base.Version.string: "1.70"})
        self.assertEqual(http_client.ACCEPTED, ret.status_code)
        self.assertEqual(b'', ret.body)
        mock_check.assert_called_once_with(clean_steps)
        mock_rpcapi.assert_called_once_with(mock.ANY, mock.ANY,
                                            self.node.uuid,
                                            clean_steps, True,
                                            topic='test-topic')

    @mock.patch.object(rpcapi.ConductorAPI, 'do_node_clean', autospec=True)
    @mock.patch.object(api_node, '_check_clean_steps', autospec=True)
    def test_clean_disable_ramdisk_old_api(self, mock_check, mock_rpcapi):
        self.node.provision_state = states.MANAGEABLE
        self.node.save()
        clean_steps = [{"step": "upgrade_firmware", "interface": "deploy"}]
        ret = self.put_json('/nodes/%s/states/provision' % self.node.uuid,
                            {'target': states.VERBS['clean'],
                             'clean_steps': clean_steps,
                             'disable_ramdisk': True},
                            headers={api_base.Version.string: "1.69"},
                            expect_errors=True)
        self.assertEqual(http_client.NOT_ACCEPTABLE, ret.status_code)

    def test_adopt_raises_error_before_1_17(self):
        """Test that a lower API client cannot use the adopt verb"""
        ret = self.put_json('/nodes/%s/states/provision' % self.node.uuid,
                            {'target': states.VERBS['adopt']},
                            headers={api_base.Version.string: "1.16"},
                            expect_errors=True)
        self.assertEqual(http_client.NOT_ACCEPTABLE, ret.status_code)

    @mock.patch.object(rpcapi.ConductorAPI, 'do_provisioning_action',
                       autospec=True)
    def test_adopt_from_manage(self, mock_dpa):
        """Test that a node can be adopted from the manageable state"""
        self.node.provision_state = states.MANAGEABLE
        self.node.save()

        ret = self.put_json('/nodes/%s/states/provision' % self.node.uuid,
                            {'target': states.VERBS['adopt']},
                            headers={api_base.Version.string: "1.17"})
        self.assertEqual(http_client.ACCEPTED, ret.status_code)
        self.assertEqual(b'', ret.body)
        mock_dpa.assert_called_once_with(mock.ANY, mock.ANY, self.node.uuid,
                                         states.VERBS['adopt'],
                                         'test-topic')

    @mock.patch.object(rpcapi.ConductorAPI, 'do_provisioning_action',
                       autospec=True)
    def test_adopt_from_adoptfail(self, mock_dpa):
        """Test that a node in ADOPTFAIL can be adopted"""
        self.node.provision_state = states.ADOPTFAIL
        self.node.save()

        ret = self.put_json('/nodes/%s/states/provision' % self.node.uuid,
                            {'target': states.VERBS['adopt']},
                            headers={api_base.Version.string: "1.17"})
        self.assertEqual(http_client.ACCEPTED, ret.status_code)
        self.assertEqual(b'', ret.body)
        mock_dpa.assert_called_once_with(mock.ANY, mock.ANY, self.node.uuid,
                                         states.VERBS['adopt'],
                                         'test-topic')

    @mock.patch.object(rpcapi.ConductorAPI, 'do_provisioning_action',
                       autospec=True)
    def test_adopt_from_active_fails(self, mock_dpa):
        """Test that an ACTIVE node cannot be adopted"""
        self.node.provision_state = states.ACTIVE
        self.node.save()

        ret = self.put_json('/nodes/%s/states/provision' % self.node.uuid,
                            {'target': states.VERBS['adopt']},
                            headers={api_base.Version.string: "1.17"},
                            expect_errors=True)
        self.assertEqual(http_client.BAD_REQUEST, ret.status_code)
        self.assertEqual(0, mock_dpa.call_count)

    @mock.patch.object(rpcapi.ConductorAPI, 'do_provisioning_action',
                       autospec=True)
    def test_manage_from_adoptfail(self, mock_dpa):
        """Test that a node can be sent to MANAGEABLE from ADOPTFAIL"""
        self.node.provision_state = states.ADOPTFAIL
        self.node.save()

        ret = self.put_json('/nodes/%s/states/provision' % self.node.uuid,
                            {'target': states.VERBS['manage']},
                            headers={api_base.Version.string: "1.17"})
        self.assertEqual(http_client.ACCEPTED, ret.status_code)
        self.assertEqual(b'', ret.body)
        mock_dpa.assert_called_once_with(mock.ANY, mock.ANY, self.node.uuid,
                                         states.VERBS['manage'],
                                         'test-topic')

    @mock.patch.object(rpcapi.ConductorAPI, 'do_provisioning_action',
                       autospec=True)
    def test_bad_requests_in_adopting_state(self, mock_dpa):
        """Test that a node in ADOPTING fails with invalid requests

        Verify that an API request fails if the ACTIVE, REBUILD, or DELETED
        state is requested by an API client when the node is in ADOPTING
        state.
        """
        self.node.provision_state = states.ADOPTING
        self.node.save()

        for state in [states.ACTIVE, states.REBUILD, states.DELETED]:
            ret = self.put_json('/nodes/%s/states/provision' % self.node.uuid,
                                {'target': state},
                                expect_errors=True)
            self.assertEqual(http_client.BAD_REQUEST, ret.status_code)
        self.assertEqual(0, mock_dpa.call_count)

    @mock.patch.object(rpcapi.ConductorAPI, 'do_provisioning_action',
                       autospec=True)
    def test_bad_requests_in_adoption_failed_state(self, mock_dpa):
        """Test that a node in ADOPTFAIL fails with invalid requests

        Verify that an API request fails if the ACTIVE, REBUILD, or DELETED
        state is requested by an API client when the node is in ADOPTFAIL
        state.
        """
        self.node.provision_state = states.ADOPTFAIL
        self.node.save()

        for state in [states.ACTIVE, states.REBUILD, states.DELETED]:
            ret = self.put_json('/nodes/%s/states/provision' % self.node.uuid,
                                {'target': state},
                                expect_errors=True)
            self.assertEqual(http_client.BAD_REQUEST, ret.status_code)
        self.assertEqual(0, mock_dpa.call_count)

    @mock.patch.object(rpcapi.ConductorAPI, 'do_provisioning_action',
                       autospec=True)
    def test_unhold_cleanhold(self, mock_dpa):
        self.node.provision_state = states.CLEANHOLD
        self.node.save()

        ret = self.put_json('/nodes/%s/states/provision' % self.node.uuid,
                            {'target': states.VERBS['unhold']},
                            headers={api_base.Version.string: "1.85"})
        self.assertEqual(http_client.ACCEPTED, ret.status_code)
        self.assertEqual(b'', ret.body)
        mock_dpa.assert_called_once_with(mock.ANY, mock.ANY, self.node.uuid,
                                         states.VERBS['unhold'],
                                         'test-topic')

    @mock.patch.object(rpcapi.ConductorAPI, 'do_provisioning_action',
                       autospec=True)
    def test_abort_cleanhold(self, mock_dpa):
        self.node.provision_state = states.CLEANHOLD
        self.node.save()

        ret = self.put_json('/nodes/%s/states/provision' % self.node.uuid,
                            {'target': states.VERBS['abort']},
                            headers={api_base.Version.string: "1.85"})
        self.assertEqual(http_client.ACCEPTED, ret.status_code)
        self.assertEqual(b'', ret.body)
        mock_dpa.assert_called_once_with(mock.ANY, mock.ANY, self.node.uuid,
                                         states.VERBS['abort'],
                                         'test-topic')

    @mock.patch.object(rpcapi.ConductorAPI, 'do_provisioning_action',
                       autospec=True)
    def test_unhold_deployhold(self, mock_dpa):
        self.node.provision_state = states.DEPLOYHOLD
        self.node.save()

        ret = self.put_json('/nodes/%s/states/provision' % self.node.uuid,
                            {'target': states.VERBS['unhold']},
                            headers={api_base.Version.string: "1.85"})
        self.assertEqual(http_client.ACCEPTED, ret.status_code)
        self.assertEqual(b'', ret.body)
        mock_dpa.assert_called_once_with(mock.ANY, mock.ANY, self.node.uuid,
                                         states.VERBS['unhold'],
                                         'test-topic')

    @mock.patch.object(rpcapi.ConductorAPI, 'do_provisioning_action',
                       autospec=True)
    def test_abort_deployhold(self, mock_dpa):
        self.node.provision_state = states.DEPLOYHOLD
        self.node.save()

        ret = self.put_json('/nodes/%s/states/provision' % self.node.uuid,
                            {'target': states.VERBS['abort']},
                            headers={api_base.Version.string: "1.85"})
        self.assertEqual(http_client.ACCEPTED, ret.status_code)
        self.assertEqual(b'', ret.body)
        mock_dpa.assert_called_once_with(mock.ANY, mock.ANY, self.node.uuid,
                                         states.VERBS['abort'],
                                         'test-topic')

    @mock.patch.object(rpcapi.ConductorAPI, 'do_provisioning_action',
                       autospec=True)
    def test_unhold_cleanhold_not_allowed(self, mock_dpa):
        self.node.provision_state = states.CLEANHOLD
        self.node.save()

        ret = self.put_json('/nodes/%s/states/provision' % self.node.uuid,
                            {'target': states.VERBS['unhold']},
                            headers={api_base.Version.string: "1.84"},
                            expect_errors=True)
        self.assertEqual(http_client.NOT_ACCEPTABLE, ret.status_code)
        mock_dpa.assert_not_called()

    @mock.patch.object(rpcapi.ConductorAPI, 'do_provisioning_action',
                       autospec=True)
    def test_unhold_deployhold_not_allowed(self, mock_dpa):
        self.node.provision_state = states.DEPLOYHOLD
        self.node.save()

        ret = self.put_json('/nodes/%s/states/provision' % self.node.uuid,
                            {'target': states.VERBS['unhold']},
                            headers={api_base.Version.string: "1.84"},
                            expect_errors=True)
        self.assertEqual(http_client.NOT_ACCEPTABLE, ret.status_code)
        mock_dpa.assert_not_called()

    @mock.patch.object(rpcapi.ConductorAPI, 'do_provisioning_action',
                       autospec=True)
    def test_unhold_servicehold(self, mock_dpa):
        self.node.provision_state = states.SERVICEHOLD
        self.node.save()

        ret = self.put_json('/nodes/%s/states/provision' % self.node.uuid,
                            {'target': states.VERBS['unhold']},
                            headers={api_base.Version.string: "1.86"})
        self.assertEqual(http_client.ACCEPTED, ret.status_code)
        self.assertEqual(b'', ret.body)
        mock_dpa.assert_called_once_with(mock.ANY, mock.ANY, self.node.uuid,
                                         states.VERBS['unhold'],
                                         'test-topic')

    @mock.patch.object(rpcapi.ConductorAPI, 'do_node_service',
                       autospec=True)
    def test_service(self, mock_dns):
        self.node.provision_state = states.SERVICEHOLD
        self.node.save()

        ret = self.put_json('/nodes/%s/states/provision' % self.node.uuid,
                            {'target': states.VERBS['service'],
                             'service_steps': [{
                                 'interface': 'deploy',
                                 'step': 'meow'}]},
                            headers={api_base.Version.string: "1.87"})
        self.assertEqual(http_client.ACCEPTED, ret.status_code)
        self.assertEqual(b'', ret.body)
        mock_dns.assert_called_once_with(
            mock.ANY, mock.ANY, self.node.uuid,
            [{'interface': 'deploy', 'step': 'meow'}],
            None, topic='test-topic')

    @mock.patch.object(rpcapi.ConductorAPI, 'do_node_service',
                       autospec=True)
    def test_service_args_required(self, mock_dns):
        self.node.provision_state = states.SERVICEHOLD
        self.node.save()

        ret = self.put_json('/nodes/%s/states/provision' % self.node.uuid,
                            {'target': states.VERBS['service']},
                            headers={api_base.Version.string: "1.87"},
                            expect_errors=True)
        self.assertEqual(http_client.BAD_REQUEST, ret.status_code)
        self.assertIn('error_message', ret.json)
        mock_dns.assert_not_called()

    def test_set_console_mode_enabled(self):
        with mock.patch.object(rpcapi.ConductorAPI,
                               'set_console_mode',
                               autospec=True) as mock_scm:
            ret = self.put_json('/nodes/%s/states/console' % self.node.uuid,
                                {'enabled': "true"})
            self.assertEqual(http_client.ACCEPTED, ret.status_code)
            self.assertEqual(b'', ret.body)
            mock_scm.assert_called_once_with(mock.ANY, mock.ANY,
                                             self.node.uuid,
                                             True, 'test-topic')
            # Check location header
            self.assertIsNotNone(ret.location)
            expected_location = '/v1/nodes/%s/states/console' % self.node.uuid
            self.assertEqual(urlparse.urlparse(ret.location).path,
                             expected_location)

    @mock.patch.object(rpcapi.ConductorAPI, 'set_console_mode', autospec=True)
    def test_set_console_by_name_unsupported(self, mock_scm):
        ret = self.put_json('/nodes/%s/states/console' % self.node.name,
                            {'enabled': "true"},
                            expect_errors=True)
        self.assertEqual(http_client.NOT_FOUND, ret.status_code)

    @mock.patch.object(rpcapi.ConductorAPI, 'set_console_mode', autospec=True)
    def test_set_console_by_name(self, mock_scm):
        ret = self.put_json('/nodes/%s/states/console' % self.node.name,
                            {'enabled': "true"},
                            headers={api_base.Version.string: "1.5"})
        self.assertEqual(http_client.ACCEPTED, ret.status_code)
        self.assertEqual(b'', ret.body)
        mock_scm.assert_called_once_with(mock.ANY, mock.ANY, self.node.uuid,
                                         True, 'test-topic')

    def test_set_console_mode_disabled(self):
        with mock.patch.object(rpcapi.ConductorAPI,
                               'set_console_mode',
                               autospec=True) as mock_scm:
            ret = self.put_json('/nodes/%s/states/console' % self.node.uuid,
                                {'enabled': "false"})
            self.assertEqual(http_client.ACCEPTED, ret.status_code)
            self.assertEqual(b'', ret.body)
            mock_scm.assert_called_once_with(mock.ANY, mock.ANY,
                                             self.node.uuid,
                                             False, 'test-topic')
            # Check location header
            self.assertIsNotNone(ret.location)
            expected_location = '/v1/nodes/%s/states/console' % self.node.uuid
            self.assertEqual(urlparse.urlparse(ret.location).path,
                             expected_location)

    def test_set_console_mode_bad_request(self):
        with mock.patch.object(rpcapi.ConductorAPI,
                               'set_console_mode',
                               autospec=True) as mock_scm:
            ret = self.put_json('/nodes/%s/states/console' % self.node.uuid,
                                {'enabled': "invalid-value"},
                                expect_errors=True)
            self.assertEqual(http_client.BAD_REQUEST, ret.status_code)
            # assert set_console_mode wasn't called
            assert not mock_scm.called

    def test_set_console_mode_bad_request_missing_parameter(self):
        with mock.patch.object(rpcapi.ConductorAPI,
                               'set_console_mode',
                               autospec=True) as mock_scm:
            ret = self.put_json('/nodes/%s/states/console' % self.node.uuid,
                                {}, expect_errors=True)
            self.assertEqual(http_client.BAD_REQUEST, ret.status_code)
            # assert set_console_mode wasn't called
            assert not mock_scm.called

    def test_set_console_mode_console_not_supported(self):
        with mock.patch.object(rpcapi.ConductorAPI,
                               'set_console_mode',
                               autospec=True) as mock_scm:
            mock_scm.side_effect = exception.UnsupportedDriverExtension(
                extension='console', driver='test-driver')
            ret = self.put_json('/nodes/%s/states/console' % self.node.uuid,
                                {'enabled': "true"}, expect_errors=True)
            self.assertEqual(http_client.BAD_REQUEST, ret.status_code)
            mock_scm.assert_called_once_with(mock.ANY, mock.ANY,
                                             self.node.uuid,
                                             True, 'test-topic')

    def test_provision_node_in_maintenance_fail(self):
        self.node.maintenance = True
        self.node.save()

        ret = self.put_json('/nodes/%s/states/provision' % self.node.uuid,
                            {'target': states.ACTIVE},
                            expect_errors=True)
        self.assertEqual(http_client.BAD_REQUEST, ret.status_code)
        self.assertTrue(ret.json['error_message'])

    @mock.patch.object(rpcapi.ConductorAPI, 'set_target_raid_config',
                       autospec=True)
    def test_put_raid(self, set_raid_config_mock):
        raid_config = {'logical_disks': [{'size_gb': 100, 'raid_level': 1}]}
        ret = self.put_json(
            '/nodes/%s/states/raid' % self.node.uuid, raid_config,
            headers={api_base.Version.string: "1.12"})
        self.assertEqual(http_client.NO_CONTENT, ret.status_code)
        self.assertEqual(b'', ret.body)
        set_raid_config_mock.assert_called_once_with(
            mock.ANY, mock.ANY, self.node.uuid, raid_config, topic=mock.ANY)

    @mock.patch.object(rpcapi.ConductorAPI, 'set_target_raid_config',
                       autospec=True)
    def test_put_raid_older_version(self, set_raid_config_mock):
        raid_config = {'logical_disks': [{'size_gb': 100, 'raid_level': 1}]}
        ret = self.put_json(
            '/nodes/%s/states/raid' % self.node.uuid, raid_config,
            headers={api_base.Version.string: "1.5"},
            expect_errors=True)
        self.assertEqual(http_client.NOT_ACCEPTABLE, ret.status_code)
        self.assertFalse(set_raid_config_mock.called)

    @mock.patch.object(rpcapi.ConductorAPI, 'set_target_raid_config',
                       autospec=True)
    def test_put_raid_iface_not_supported(self, set_raid_config_mock):
        raid_config = {'logical_disks': [{'size_gb': 100, 'raid_level': 1}]}
        set_raid_config_mock.side_effect = (
            exception.UnsupportedDriverExtension(extension='raid',
                                                 driver='fake-hardware'))
        ret = self.put_json(
            '/nodes/%s/states/raid' % self.node.uuid, raid_config,
            headers={api_base.Version.string: "1.12"},
            expect_errors=True)
        self.assertEqual(http_client.NOT_FOUND, ret.status_code)
        self.assertTrue(ret.json['error_message'])
        set_raid_config_mock.assert_called_once_with(
            mock.ANY, mock.ANY, self.node.uuid, raid_config, topic=mock.ANY)

    @mock.patch.object(rpcapi.ConductorAPI, 'set_target_raid_config',
                       autospec=True)
    def test_put_raid_invalid_parameter_value(self, set_raid_config_mock):
        raid_config = {'logical_disks': [{'size_gb': 100, 'raid_level': 1}]}
        set_raid_config_mock.side_effect = exception.InvalidParameterValue(
            'foo')
        ret = self.put_json(
            '/nodes/%s/states/raid' % self.node.uuid, raid_config,
            headers={api_base.Version.string: "1.12"},
            expect_errors=True)
        self.assertEqual(http_client.BAD_REQUEST, ret.status_code)
        self.assertTrue(ret.json['error_message'])
        set_raid_config_mock.assert_called_once_with(
            mock.ANY, mock.ANY, self.node.uuid, raid_config, topic=mock.ANY)

    @mock.patch.object(rpcapi.ConductorAPI, 'set_boot_device', autospec=True)
    def test_set_boot_device(self, mock_sbd):
        device = boot_devices.PXE
        ret = self.put_json('/nodes/%s/management/boot_device'
                            % self.node.uuid, {'boot_device': device})
        self.assertEqual(http_client.NO_CONTENT, ret.status_code)
        self.assertEqual(b'', ret.body)
        mock_sbd.assert_called_once_with(mock.ANY, mock.ANY, self.node.uuid,
                                         device, persistent=False,
                                         topic='test-topic')

    @mock.patch.object(rpcapi.ConductorAPI, 'set_boot_device', autospec=True)
    def test_set_boot_device_by_name(self, mock_sbd):
        device = boot_devices.PXE
        ret = self.put_json('/nodes/%s/management/boot_device'
                            % self.node.name, {'boot_device': device},
                            headers={api_base.Version.string: "1.5"})
        self.assertEqual(http_client.NO_CONTENT, ret.status_code)
        self.assertEqual(b'', ret.body)
        mock_sbd.assert_called_once_with(mock.ANY, mock.ANY, self.node.uuid,
                                         device, persistent=False,
                                         topic='test-topic')

    @mock.patch.object(rpcapi.ConductorAPI, 'set_boot_device', autospec=True)
    def test_set_boot_device_not_supported(self, mock_sbd):
        mock_sbd.side_effect = exception.UnsupportedDriverExtension(
            extension='management', driver='test-driver')
        device = boot_devices.PXE
        ret = self.put_json('/nodes/%s/management/boot_device'
                            % self.node.uuid, {'boot_device': device},
                            expect_errors=True)
        self.assertEqual(http_client.BAD_REQUEST, ret.status_code)
        self.assertTrue(ret.json['error_message'])
        mock_sbd.assert_called_once_with(mock.ANY, mock.ANY, self.node.uuid,
                                         device, persistent=False,
                                         topic='test-topic')

    @mock.patch.object(rpcapi.ConductorAPI, 'set_boot_device', autospec=True)
    def test_set_boot_device_persistent(self, mock_sbd):
        device = boot_devices.PXE
        ret = self.put_json('/nodes/%s/management/boot_device?persistent=True'
                            % self.node.uuid, {'boot_device': device})
        self.assertEqual(http_client.NO_CONTENT, ret.status_code)
        self.assertEqual(b'', ret.body)
        mock_sbd.assert_called_once_with(mock.ANY, mock.ANY, self.node.uuid,
                                         device, persistent=True,
                                         topic='test-topic')

    @mock.patch.object(rpcapi.ConductorAPI, 'set_boot_device', autospec=True)
    def test_set_boot_device_persistent_invalid_value(self, mock_sbd):
        device = boot_devices.PXE
        ret = self.put_json('/nodes/%s/management/boot_device?persistent=blah'
                            % self.node.uuid, {'boot_device': device},
                            expect_errors=True)
        self.assertEqual('application/json', ret.content_type)
        self.assertEqual(http_client.BAD_REQUEST, ret.status_code)

    @mock.patch.object(rpcapi.ConductorAPI, 'inject_nmi', autospec=True)
    def test_inject_nmi(self, mock_inject_nmi):
        ret = self.put_json('/nodes/%s/management/inject_nmi'
                            % self.node.uuid, {},
                            headers={api_base.Version.string: "1.29"})
        self.assertEqual(http_client.NO_CONTENT, ret.status_code)
        self.assertEqual(b'', ret.body)
        mock_inject_nmi.assert_called_once_with(mock.ANY, mock.ANY,
                                                self.node.uuid,
                                                topic='test-topic')

    @mock.patch.object(rpcapi.ConductorAPI, 'inject_nmi', autospec=True)
    def test_inject_nmi_not_allowed(self, mock_inject_nmi):
        ret = self.put_json('/nodes/%s/management/inject_nmi'
                            % self.node.uuid, {},
                            headers={api_base.Version.string: "1.28"},
                            expect_errors=True)
        self.assertEqual(http_client.NOT_FOUND, ret.status_code)
        self.assertTrue(ret.json['error_message'])
        self.assertFalse(mock_inject_nmi.called)

    @mock.patch.object(rpcapi.ConductorAPI, 'inject_nmi', autospec=True)
    def test_inject_nmi_not_supported(self, mock_inject_nmi):
        mock_inject_nmi.side_effect = exception.UnsupportedDriverExtension(
            extension='management', driver='test-driver')
        ret = self.put_json('/nodes/%s/management/inject_nmi'
                            % self.node.uuid, {},
                            headers={api_base.Version.string: "1.29"},
                            expect_errors=True)
        self.assertEqual(http_client.BAD_REQUEST, ret.status_code)
        self.assertTrue(ret.json['error_message'])
        mock_inject_nmi.assert_called_once_with(mock.ANY, mock.ANY,
                                                self.node.uuid,
                                                topic='test-topic')

    def _test_set_node_maintenance_mode(self, mock_update, mock_get, reason,
                                        node_ident, is_by_name=False):
        request_body = {}
        if reason:
            request_body['reason'] = reason

        self.node.maintenance = False
        mock_get.return_value = self.node
        if is_by_name:
            headers = {api_base.Version.string: "1.5"}
        else:
            headers = {}
        ret = self.put_json('/nodes/%s/maintenance' % node_ident,
                            request_body, headers=headers)
        self.assertEqual(http_client.ACCEPTED, ret.status_code)
        self.assertEqual(b'', ret.body)
        self.assertTrue(self.node.maintenance)
        self.assertEqual(reason, self.node.maintenance_reason)
        mock_get.assert_called_once_with(mock.ANY, node_ident)
        mock_update.assert_called_once_with(mock.ANY, mock.ANY, self.node,
                                            topic='test-topic')

    @mock.patch.object(notification_utils, '_emit_api_notification',
                       autospec=True)
    @mock.patch.object(objects.Node, 'get_by_uuid', autospec=True)
    @mock.patch.object(rpcapi.ConductorAPI, 'update_node', autospec=True)
    def test_set_node_maintenance_mode(self, mock_update, mock_get,
                                       mock_notify):
        self._test_set_node_maintenance_mode(mock_update, mock_get,
                                             'fake_reason', self.node.uuid)
        mock_notify.assert_has_calls([mock.call(mock.ANY, mock.ANY,
                                                'maintenance_set',
                                     obj_fields.NotificationLevel.INFO,
                                     obj_fields.NotificationStatus.START),
                                     mock.call(mock.ANY, mock.ANY,
                                               'maintenance_set',
                                     obj_fields.NotificationLevel.INFO,
                                     obj_fields.NotificationStatus.END)])

    @mock.patch.object(objects.Node, 'get_by_uuid', autospec=True)
    @mock.patch.object(rpcapi.ConductorAPI, 'update_node', autospec=True)
    def test_set_node_maintenance_mode_no_reason(self, mock_update, mock_get):
        self._test_set_node_maintenance_mode(mock_update, mock_get, None,
                                             self.node.uuid)

    @mock.patch.object(objects.Node, 'get_by_name', autospec=True)
    @mock.patch.object(rpcapi.ConductorAPI, 'update_node', autospec=True)
    def test_set_node_maintenance_mode_by_name(self, mock_update, mock_get):
        self._test_set_node_maintenance_mode(mock_update, mock_get,
                                             'fake_reason', self.node.name,
                                             is_by_name=True)

    @mock.patch.object(objects.Node, 'get_by_name', autospec=True)
    @mock.patch.object(rpcapi.ConductorAPI, 'update_node', autospec=True)
    def test_set_node_maintenance_mode_no_reason_by_name(self, mock_update,
                                                         mock_get):
        self._test_set_node_maintenance_mode(mock_update, mock_get, None,
                                             self.node.name, is_by_name=True)

    @mock.patch.object(notification_utils, '_emit_api_notification',
                       autospec=True)
    @mock.patch.object(objects.Node, 'get_by_uuid', autospec=True)
    @mock.patch.object(rpcapi.ConductorAPI, 'update_node', autospec=True)
    def test_set_node_maintenance_mode_error(self, mock_update, mock_get,
                                             mock_notify):
        mock_get.return_value = self.node
        mock_update.side_effect = Exception()
        self.put_json('/nodes/%s/maintenance' % self.node.uuid,
                      {'reason': 'fake'}, expect_errors=True)
        mock_notify.assert_has_calls([mock.call(mock.ANY, mock.ANY,
                                                'maintenance_set',
                                     obj_fields.NotificationLevel.INFO,
                                     obj_fields.NotificationStatus.START),
                                     mock.call(mock.ANY, mock.ANY,
                                               'maintenance_set',
                                     obj_fields.NotificationLevel.ERROR,
                                     obj_fields.NotificationStatus.ERROR)])

    def test_inspect_abort_raises_before_1_41(self):
        self.node.provision_state = states.INSPECTWAIT
        self.node.save()
        ret = self.put_json('/nodes/%s/states/provision' % self.node.uuid,
                            {'target': states.VERBS['abort']},
                            headers={api_base.Version.string: "1.40"},
                            expect_errors=True)
        self.assertEqual(http_client.NOT_ACCEPTABLE, ret.status_code)

    @mock.patch.object(rpcapi.ConductorAPI, 'do_provisioning_action',
                       autospec=True)
    def test_inspect_abort_accepted_after_1_41(self, mock_provision):
        self.node.provision_state = states.INSPECTWAIT
        self.node.save()
        ret = self.put_json('/nodes/%s/states/provision' % self.node.uuid,
                            {'target': states.VERBS['abort']},
                            headers={api_base.Version.string: "1.41"})
        self.assertEqual(http_client.ACCEPTED, ret.status_code)

    @mock.patch.object(rpcapi.ConductorAPI, 'set_indicator_state',
                       autospec=True)
    def test_set_indicator_state(self, mock_sis):
        component = components.SYSTEM
        indicator_id = 'led'
        indicator_name = indicator_id + '@' + component
        state = indicator_states.ON
        ret = self.put_json(
            '/nodes/%s/management/indicators'
            '/%s' % (self.node.uuid, indicator_name),
            {'state': state})
        self.assertEqual(http_client.NO_CONTENT, ret.status_code)
        self.assertEqual(b'', ret.body)
        mock_sis.assert_called_once_with(
            mock.ANY, mock.ANY, self.node.uuid, component, indicator_id, state,
            topic='test-topic')

    @mock.patch.object(rpcapi.ConductorAPI, 'set_indicator_state',
                       autospec=True)
    def test_set_indicator_state_versioning(self, mock_sis):
        component = components.SYSTEM
        indicator_id = 'led'
        indicator_name = indicator_id + '@' + component
        state = indicator_states.ON
        ret = self.put_json(
            '/nodes/%s/management/indicators'
            '/%s' % (self.node.uuid, indicator_name),
            {'state': state}, headers={api_base.Version.string: "1.63"})

        self.assertEqual(http_client.NO_CONTENT, ret.status_code)
        self.assertEqual(b'', ret.body)
        mock_sis.assert_called_once_with(
            mock.ANY, mock.ANY, self.node.uuid, component, indicator_id, state,
            topic='test-topic')

    @mock.patch.object(rpcapi.ConductorAPI, 'set_indicator_state',
                       autospec=True)
    def test_set_indicator_state_not_supported(self, mock_sis):
        mock_sis.side_effect = exception.UnsupportedDriverExtension(
            extension='management', driver='test-driver')
        component = components.SYSTEM
        indicator_id = 'led'
        indicator_name = indicator_id + '@' + component
        state = indicator_states.ON
        ret = self.put_json(
            '/nodes/%s/management/indicators'
            '/%s' % (self.node.uuid, indicator_name),
            {'state': state}, expect_errors=True)
        self.assertEqual(http_client.BAD_REQUEST, ret.status_code)
        self.assertTrue(ret.json['error_message'])
        mock_sis.assert_called_once_with(
            mock.ANY, mock.ANY, self.node.uuid, component, indicator_id, state,
            topic='test-topic')

    @mock.patch.object(rpcapi.ConductorAPI, 'set_indicator_state',
                       autospec=True)
    def test_set_indicator_state_qs(self, mock_sis):
        component = components.SYSTEM
        indicator_id = 'led'
        indicator_name = indicator_id + '@' + component
        state = indicator_states.ON
        ret = self.put_json(
            '/nodes/%s/management/indicators/%s?'
            'state=%s' % (self.node.uuid, indicator_name, state), {})
        self.assertEqual(http_client.NO_CONTENT, ret.status_code)
        self.assertEqual(b'', ret.body)
        mock_sis.assert_called_once_with(
            mock.ANY, mock.ANY, self.node.uuid, component, indicator_id, state,
            topic='test-topic')

    @mock.patch.object(rpcapi.ConductorAPI, 'set_indicator_state',
                       autospec=True)
    def test_set_indicator_state_invalid_value(self, mock_sis):
        mock_sis.side_effect = exception.InvalidParameterValue('error')
        component = components.SYSTEM
        indicator_id = 'led'
        indicator_name = indicator_id + '@' + component
        ret = self.put_json(
            '/nodes/%s/management/indicators/%s?'
            'state=glow' % (self.node.uuid, indicator_name), {},
            expect_errors=True)
        self.assertEqual('application/json', ret.content_type)
        self.assertEqual(http_client.BAD_REQUEST, ret.status_code)


class TestCheckCleanSteps(db_base.DbTestCase):
    def test__check_clean_steps_not_list(self):
        clean_steps = {"step": "upgrade_firmware", "interface": "deploy"}
        self.assertRaisesRegex(exception.InvalidParameterValue,
                               "not of type 'array'",
                               api_node._check_clean_steps, clean_steps)

    def test__check_clean_steps_step_not_dict(self):
        clean_steps = ['clean step']
        self.assertRaisesRegex(exception.InvalidParameterValue,
                               "not of type 'object'",
                               api_node._check_clean_steps, clean_steps)

    def test__check_clean_steps_step_key_invalid(self):
        clean_steps = [{"step": "upgrade_firmware", "interface": "deploy",
                        "unknown": "upgrade_firmware"}]
        self.assertRaisesRegex(exception.InvalidParameterValue,
                               'unexpected',
                               api_node._check_clean_steps, clean_steps)

    def test__check_clean_steps_step_missing_interface(self):
        clean_steps = [{"step": "upgrade_firmware"}]
        self.assertRaisesRegex(exception.InvalidParameterValue,
                               'interface',
                               api_node._check_clean_steps, clean_steps)

    def test__check_clean_steps_step_missing_step_key(self):
        clean_steps = [{"interface": "deploy"}]
        self.assertRaisesRegex(exception.InvalidParameterValue,
                               'step',
                               api_node._check_clean_steps, clean_steps)

    def test__check_clean_steps_step_missing_step_value(self):
        clean_steps = [{"step": None, "interface": "deploy"}]
        self.assertRaisesRegex(exception.InvalidParameterValue,
                               "not of type 'string'",
                               api_node._check_clean_steps, clean_steps)

    def test__check_clean_steps_step_min_length_step_value(self):
        clean_steps = [{"step": "", "interface": "deploy"}]
        self.assertRaisesRegex(exception.InvalidParameterValue,
                               'is too short',
                               api_node._check_clean_steps, clean_steps)

    def test__check_clean_steps_step_interface_value_invalid(self):
        clean_steps = [{"step": "upgrade_firmware", "interface": "not"}]
        self.assertRaisesRegex(exception.InvalidParameterValue,
                               'is not one of',
                               api_node._check_clean_steps, clean_steps)

    def test__check_clean_steps_step_args_value_invalid(self):
        clean_steps = [{"step": "upgrade_firmware", "interface": "deploy",
                        "args": "invalid args"}]
        self.assertRaisesRegex(exception.InvalidParameterValue,
                               'args',
                               api_node._check_clean_steps, clean_steps)

    def test__check_clean_steps_valid(self):
        clean_steps = [{"step": "upgrade_firmware", "interface": "deploy"}]
        api_node._check_clean_steps(clean_steps)

        step1 = {"step": "upgrade_firmware", "interface": "deploy",
                 "args": {"arg1": "value1", "arg2": "value2"}}
        # NOTE(TheJulia): _check_service_steps and _check_deploy_steps
        # both route back to _check_steps which is what backs _check
        # clean steps. It is needful duplication for cases, but it doesn't
        # make a ton of sense to copy/paste everything over and over unless
        # there is a specific case. In any case, do the needful here.
        api_node._check_clean_steps([step1])

        step2 = {"step": "configure raid", "interface": "raid",
                 "args": {}}
        api_node._check_clean_steps([step1, step2])

        api_node._check_service_steps([step1])
        api_node._check_service_steps([step1, step2])
        # Schema differences exist, cleaning doesn't have a schema for
        # priority when validated.

        step1['priority'] = 10
        step2['priority'] = 12
        api_node._check_deploy_steps([step1])

        api_node._check_deploy_steps([step1, step2])

    @mock.patch.object(api_utils, 'check_node_policy_and_retrieve',
                       autospec=True)
    def test__check_clean_steps_child_node(self, mock_policy):
        node = obj_utils.create_test_node(
            self.context,
            provision_state=states.AVAILABLE,
            name='node-1337')
        obj_utils.create_test_node(
            self.context,
            provision_state=states.AVAILABLE,
            name='node-n071337',
            parent_node=node.uuid,
            uuid=uuidutils.generate_uuid())
        clean_steps = [{"step": "upgrade_firmware", "interface": "deploy",
                        "execute_on_child_nodes": True}]
        api_node._check_clean_steps(clean_steps)
        mock_policy.assert_not_called()

    @mock.patch.object(api_utils, 'check_node_policy_and_retrieve',
                       autospec=True)
    def test__check_clean_steps_child_node_list(self, mock_policy):
        node = obj_utils.create_test_node(
            self.context,
            provision_state=states.AVAILABLE,
            name='node-1337')
        child_node_1 = obj_utils.create_test_node(
            self.context,
            provision_state=states.AVAILABLE,
            name='node-n071337',
            parent_node=node.uuid,
            uuid=uuidutils.generate_uuid())
        child_node_2 = obj_utils.create_test_node(
            self.context,
            provision_state=states.AVAILABLE,
            name='node-1337-card1',
            parent_node=node.uuid,
            uuid=uuidutils.generate_uuid())

        clean_steps = [{"step": "upgrade_firmware", "interface": "deploy",
                        "execute_on_child_nodes": True,
                        "limit_child_node_execution": [child_node_1.uuid,
                                                       child_node_2.uuid]}]
        api_node._check_clean_steps(clean_steps)
        mock_policy.assert_has_calls([
            mock.call('baremetal:node:set_provision_state',
                      child_node_1.uuid),
            mock.call('baremetal:node:set_provision_state',
                      child_node_2.uuid)])

    @mock.patch.object(api_node, '_check_steps', autospec=True)
    def test_check__check_steps_wrappers(self, check_mock):
        api_node._check_clean_steps({})
        self.assertEqual(1, check_mock.call_count)
        api_node._check_deploy_steps({})
        self.assertEqual(2, check_mock.call_count)
        api_node._check_service_steps({})
        self.assertEqual(3, check_mock.call_count)


class TestAttachDetachVif(test_api_base.BaseApiTest):

    def setUp(self):
        super(TestAttachDetachVif, self).setUp()
        self.vif_version = "1.28"
        self.node = obj_utils.create_test_node(
            self.context,
            provision_state=states.AVAILABLE, name='node-39')
        p = mock.patch.object(rpcapi.ConductorAPI, 'get_topic_for',
                              autospec=True)
        self.mock_gtf = p.start()
        self.mock_gtf.return_value = 'test-topic'
        self.addCleanup(p.stop)

    @mock.patch.object(objects.Node, 'get_by_uuid', autospec=True)
    def test_vif_subcontroller_old_version(self, mock_get):
        mock_get.return_value = self.node
        ret = self.get_json('/nodes/%s/vifs' % self.node.uuid,
                            headers={api_base.Version.string: "1.26"},
                            expect_errors=True)
        self.assertEqual(http_client.NOT_FOUND, ret.status_code)

    @mock.patch.object(objects.Node, 'get_by_uuid', autospec=True)
    @mock.patch.object(rpcapi.ConductorAPI, 'vif_list', autospec=True)
    def test_vif_list(self, mock_list, mock_get):
        mock_get.return_value = self.node
        mock_list.return_value = []
        self.get_json('/nodes/%s/vifs' % self.node.uuid,
                      headers={api_base.Version.string:
                               self.vif_version})

        mock_get.assert_called_once_with(mock.ANY, self.node.uuid)
        mock_list.assert_called_once_with(mock.ANY, mock.ANY, self.node.uuid,
                                          topic='test-topic')

    @mock.patch.object(objects.Node, 'get_by_uuid', autospec=True)
    @mock.patch.object(rpcapi.ConductorAPI, 'vif_attach', autospec=True)
    def test_vif_attach(self, mock_attach, mock_get):
        vif_id = uuidutils.generate_uuid()
        request_body = {
            'id': vif_id
        }

        mock_get.return_value = self.node

        ret = self.post_json('/nodes/%s/vifs' % self.node.uuid,
                             request_body,
                             headers={api_base.Version.string:
                                      self.vif_version})

        self.assertEqual(http_client.NO_CONTENT, ret.status_code)
        mock_get.assert_called_once_with(mock.ANY, self.node.uuid)
        mock_attach.assert_called_once_with(mock.ANY, mock.ANY, self.node.uuid,
                                            vif_info=request_body,
                                            topic='test-topic')

    @mock.patch.object(objects.Node, 'get_by_name', autospec=True)
    @mock.patch.object(rpcapi.ConductorAPI, 'vif_attach', autospec=True)
    def test_vif_attach_by_node_name(self, mock_attach, mock_get):
        vif_id = uuidutils.generate_uuid()
        request_body = {
            'id': vif_id
        }

        mock_get.return_value = self.node

        ret = self.post_json('/nodes/%s/vifs' % self.node.name,
                             request_body,
                             headers={api_base.Version.string:
                                      self.vif_version})

        self.assertEqual(http_client.NO_CONTENT, ret.status_code)
        mock_get.assert_called_once_with(mock.ANY, self.node.name)
        mock_attach.assert_called_once_with(mock.ANY, mock.ANY, self.node.uuid,
                                            vif_info=request_body,
                                            topic='test-topic')

    @mock.patch.object(rpcapi.ConductorAPI, 'vif_attach', autospec=True)
    def test_vif_attach_node_not_found(self, mock_attach):
        vif_id = uuidutils.generate_uuid()
        request_body = {
            'id': vif_id
        }

        ret = self.post_json('/nodes/doesntexist/vifs',
                             request_body, expect_errors=True,
                             headers={api_base.Version.string:
                                      self.vif_version})

        self.assertEqual(http_client.NOT_FOUND, ret.status_code)
        self.assertTrue(ret.json['error_message'])
        self.assertFalse(mock_attach.called)

    @mock.patch.object(objects.Node, 'get_by_name', autospec=True)
    @mock.patch.object(rpcapi.ConductorAPI, 'vif_attach', autospec=True)
    def test_vif_attach_conductor_unavailable(self, mock_attach, mock_get):
        vif_id = uuidutils.generate_uuid()
        request_body = {
            'id': vif_id
        }
        mock_get.return_value = self.node
        self.mock_gtf.side_effect = exception.NoValidHost('boom')
        ret = self.post_json('/nodes/%s/vifs' % self.node.name,
                             request_body, expect_errors=True,
                             headers={api_base.Version.string:
                                      self.vif_version})

        self.assertEqual(http_client.BAD_REQUEST, ret.status_code)
        self.assertTrue(ret.json['error_message'])
        self.assertFalse(mock_attach.called)

    @mock.patch.object(objects.Node, 'get_by_uuid', autospec=True)
    @mock.patch.object(rpcapi.ConductorAPI, 'vif_attach', autospec=True)
    def test_vif_attach_no_vif_id(self, mock_attach, mock_get):
        vif_id = uuidutils.generate_uuid()
        request_body = {
            'bad_id': vif_id
        }

        mock_get.return_value = self.node

        ret = self.post_json('/nodes/%s/vifs' % self.node.uuid,
                             request_body, expect_errors=True,
                             headers={api_base.Version.string:
                                      self.vif_version})

        self.assertEqual(http_client.BAD_REQUEST, ret.status_code)
        self.assertTrue(ret.json['error_message'])

    @mock.patch.object(objects.Node, 'get_by_uuid', autospec=True)
    @mock.patch.object(rpcapi.ConductorAPI, 'vif_attach', autospec=True)
    def test_vif_attach_invalid_vif_id(self, mock_attach, mock_get):
        request_body = {
            'id': "invalid%id^"
        }

        mock_get.return_value = self.node

        ret = self.post_json('/nodes/%s/vifs' % self.node.uuid,
                             request_body, expect_errors=True,
                             headers={api_base.Version.string:
                                      self.vif_version})

        self.assertEqual(http_client.BAD_REQUEST, ret.status_code)
        self.assertTrue(ret.json['error_message'])

    @mock.patch.object(objects.Node, 'get_by_uuid', autospec=True)
    @mock.patch.object(rpcapi.ConductorAPI, 'vif_attach', autospec=True)
    def test_vif_attach_node_locked(self, mock_attach, mock_get):
        vif_id = uuidutils.generate_uuid()
        request_body = {
            'id': vif_id
        }

        mock_get.return_value = self.node
        mock_attach.side_effect = exception.NodeLocked(node='', host='')

        ret = self.post_json('/nodes/%s/vifs' % self.node.uuid,
                             request_body, expect_errors=True,
                             headers={api_base.Version.string:
                                      self.vif_version})

        self.assertEqual(http_client.CONFLICT, ret.status_code)
        self.assertTrue(ret.json['error_message'])

    @mock.patch.object(objects.Node, 'get_by_uuid', autospec=True)
    @mock.patch.object(rpcapi.ConductorAPI, 'vif_attach', autospec=True)
    def test_vif_attach_port_uuid_and_portgroup_uuid(self, mock_attach,
                                                     mock_get):
        vif_id = uuidutils.generate_uuid()
        request_body = {
            'id': vif_id,
            'port_uuid': 'port-uuid',
            'portgroup_uuid': 'portgroup-uuid'
        }

        mock_get.return_value = self.node

        ret = self.post_json('/nodes/%s/vifs' % self.node.uuid,
                             request_body, expect_errors=True,
                             headers={api_base.Version.string:
                                      "1.67"})

        self.assertEqual(http_client.BAD_REQUEST, ret.status_int)
        self.assertTrue(ret.json['error_message'])

    @mock.patch.object(objects.Node, 'get_by_uuid', autospec=True)
    @mock.patch.object(rpcapi.ConductorAPI, 'vif_attach', autospec=True)
    def test_vif_attach_port_uuid_and_portgroup_uuid_old(self, mock_attach,
                                                         mock_get):
        vif_id = uuidutils.generate_uuid()
        request_body = {
            'id': vif_id,
            'port_uuid': 'port-uuid',
            'portgroup_uuid': 'portgroup-uuid'
        }

        mock_get.return_value = self.node

        ret = self.post_json('/nodes/%s/vifs' % self.node.uuid,
                             request_body,
                             headers={api_base.Version.string:
                                      self.vif_version})

        self.assertEqual(http_client.NO_CONTENT, ret.status_code)
        mock_get.assert_called_once_with(mock.ANY, self.node.uuid)
        mock_attach.assert_called_once_with(mock.ANY, mock.ANY, self.node.uuid,
                                            vif_info=request_body,
                                            topic='test-topic')

    @mock.patch.object(objects.Node, 'get_by_uuid', autospec=True)
    @mock.patch.object(rpcapi.ConductorAPI, 'vif_detach', autospec=True)
    def test_vif_detach(self, mock_detach, mock_get):
        vif_id = uuidutils.generate_uuid()

        mock_get.return_value = self.node

        ret = self.delete('/nodes/%s/vifs/%s' % (self.node.uuid, vif_id),
                          headers={api_base.Version.string:
                                   self.vif_version})

        self.assertEqual(http_client.NO_CONTENT, ret.status_code)
        mock_get.assert_called_once_with(mock.ANY, self.node.uuid)
        mock_detach.assert_called_once_with(mock.ANY, mock.ANY, self.node.uuid,
                                            vif_id=vif_id,
                                            topic='test-topic')

    @mock.patch.object(objects.Node, 'get_by_name', autospec=True)
    @mock.patch.object(rpcapi.ConductorAPI, 'vif_detach', autospec=True)
    def test_vif_detach_by_node_name(self, mock_detach, mock_get):
        vif_id = uuidutils.generate_uuid()

        mock_get.return_value = self.node

        ret = self.delete('/nodes/%s/vifs/%s' % (self.node.name, vif_id),
                          headers={api_base.Version.string: self.vif_version})

        self.assertEqual(http_client.NO_CONTENT, ret.status_code)
        mock_get.assert_called_once_with(mock.ANY, self.node.name)
        mock_detach.assert_called_once_with(mock.ANY, mock.ANY, self.node.uuid,
                                            vif_id=vif_id,
                                            topic='test-topic')

    @mock.patch.object(rpcapi.ConductorAPI, 'vif_detach', autospec=True)
    def test_vif_detach_node_not_found(self, mock_detach):
        vif_id = uuidutils.generate_uuid()

        ret = self.delete('/nodes/doesntexist/vifs/%s' % vif_id,
                          headers={api_base.Version.string: self.vif_version},
                          expect_errors=True)

        self.assertEqual(http_client.NOT_FOUND, ret.status_code)
        self.assertTrue(ret.json['error_message'])
        self.assertFalse(mock_detach.called)

    @mock.patch.object(objects.Node, 'get_by_uuid', autospec=True)
    @mock.patch.object(rpcapi.ConductorAPI, 'vif_detach', autospec=True)
    def test_vif_detach_node_locked(self, mock_detach, mock_get):
        vif_id = uuidutils.generate_uuid()

        mock_get.return_value = self.node
        mock_detach.side_effect = exception.NodeLocked(node='', host='')

        ret = self.delete('/nodes/%s/vifs/%s' % (self.node.uuid, vif_id),
                          headers={api_base.Version.string: self.vif_version},
                          expect_errors=True)

        self.assertEqual(http_client.CONFLICT, ret.status_code)
        self.assertTrue(ret.json['error_message'])


class TestBIOS(test_api_base.BaseApiTest):

    def setUp(self):
        super(TestBIOS, self).setUp()
        self.version = "1.74"
        self.node = obj_utils.create_test_node(
            self.context, id=1)
        self.bios = obj_utils.create_test_bios_setting(self.context,
                                                       node_id=self.node.id)

    def test_get_all_bios(self):
        ret = self.get_json('/nodes/%s/bios' % self.node.uuid,
                            headers={api_base.Version.string: self.version})

        expected_json = [
            {'created_at': ret['bios'][0]['created_at'],
             'updated_at': ret['bios'][0]['updated_at'],
             'links': [
                {'href': 'http://localhost/v1/nodes/%s/bios/virtualization'
                 % self.node.uuid, 'rel': 'self'},
                {'href': 'http://localhost/nodes/%s/bios/virtualization'
                 % self.node.uuid, 'rel': 'bookmark'}],
             'name': 'virtualization', 'value': 'on'}]
        self.assertEqual({'bios': expected_json}, ret)

    def test_get_all_bios_fails_with_bad_version(self):
        ret = self.get_json('/nodes/%s/bios' % self.node.uuid,
                            headers={api_base.Version.string: "1.39"},
                            expect_errors=True)
        self.assertEqual(http_client.NOT_FOUND, ret.status_code)

    def test_get_one_bios(self):
        ret = self.get_json('/nodes/%s/bios/virtualization' % self.node.uuid,
                            headers={api_base.Version.string: self.version})

        expected_json = {
            'virtualization': {
                'allowable_values': ['on', 'off'],
                'attribute_type': 'Enumeration',
                'created_at': ret['virtualization']['created_at'],
                'links': [
                    {'href': 'http://localhost/v1/nodes/%s/bios/virtualization'
                     % self.node.uuid, u'rel': u'self'},
                    {'href': 'http://localhost/nodes/%s/bios/virtualization'
                     % self.node.uuid, u'rel': u'bookmark'}],
                'lower_bound': None,
                'min_length': None,
                'max_length': None,
                'name': 'virtualization',
                'read_only': False,
                'reset_required': True,
                'unique': False,
                'updated_at': None,
                'upper_bound': None,
                'value': 'on'}}

        self.assertEqual(expected_json, ret)

    def test_get_one_bios_no_registry(self):
        ret = self.get_json('/nodes/%s/bios/virtualization' % self.node.uuid,
                            headers={api_base.Version.string: "1.73"})

        expected_json = {
            'virtualization': {
                'created_at': ret['virtualization']['created_at'],
                'updated_at': ret['virtualization']['updated_at'],
                'links': [
                    {'href': 'http://localhost/v1/nodes/%s/bios/virtualization'
                     % self.node.uuid, 'rel': 'self'},
                    {'href': 'http://localhost/nodes/%s/bios/virtualization'
                     % self.node.uuid, 'rel': 'bookmark'}],
                'name': 'virtualization', 'value': 'on'}}
        self.assertEqual(expected_json, ret)

    def test_get_one_bios_fails_with_bad_version(self):
        ret = self.get_json('/nodes/%s/bios/virtualization' % self.node.uuid,
                            headers={api_base.Version.string: "1.39"},
                            expect_errors=True)
        self.assertEqual(http_client.NOT_FOUND, ret.status_code)

    def test_get_one_bios_fails_if_not_found(self):
        ret = self.get_json('/nodes/%s/bios/fake_setting' % self.node.uuid,
                            headers={api_base.Version.string: self.version},
                            expect_errors=True)
        self.assertEqual(http_client.NOT_FOUND, ret.status_code)
        self.assertIn("fake_setting", ret.json['error_message'])
        self.assertNotIn(self.node.id, ret.json['error_message'])

    def test_get_all_bios_with_detail(self):
        ret = self.get_json('/nodes/%s/bios?detail=True' % self.node.uuid,
                            headers={api_base.Version.string: self.version})

        expected_json = [
            {'allowable_values': ['on', 'off'],
             'attribute_type': 'Enumeration',
             'created_at': ret['bios'][0]['created_at'],
             'links': [
                 {'href': 'http://localhost/v1/nodes/%s/bios/virtualization'
                  % self.node.uuid, 'rel': 'self'},
                 {'href': 'http://localhost/nodes/%s/bios/virtualization'
                  % self.node.uuid, 'rel': 'bookmark'}],
             'lower_bound': None,
             'max_length': None,
             'min_length': None,
             'name': 'virtualization',
             'read_only': False,
             'reset_required': True,
             'unique': False,
             'updated_at': None,
             'upper_bound': None,
             'value': 'on'}]

        self.assertEqual({'bios': expected_json}, ret)

    def test_get_all_bios_detail_false(self):
        ret = self.get_json('/nodes/%s/bios?detail=False' % self.node.uuid,
                            headers={api_base.Version.string: self.version})

        expected_json = [
            {'created_at': ret['bios'][0]['created_at'],
             'updated_at': ret['bios'][0]['updated_at'],
             'links': [
                {'href': 'http://localhost/v1/nodes/%s/bios/virtualization'
                 % self.node.uuid, 'rel': 'self'},
                {'href': 'http://localhost/nodes/%s/bios/virtualization'
                 % self.node.uuid, 'rel': 'bookmark'}],
                'name': 'virtualization', 'value': 'on'}]
        self.assertEqual({'bios': expected_json}, ret)

    def test_get_all_bios_detail_old_version(self):
        ret = self.get_json('/nodes/%s/bios?detail=True' % self.node.uuid,
                            headers={api_base.Version.string: "1.73"},
                            expect_errors=True)

        self.assertEqual(http_client.BAD_REQUEST, ret.status_int)

    def test_get_bios_fields_old_version(self):
        ret = self.get_json('/nodes/%s/bios?fields=name,read_only'
                            % self.node.uuid,
                            headers={api_base.Version.string: "1.73"},
                            expect_errors=True)

        self.assertEqual(http_client.BAD_REQUEST, ret.status_int)

    def test_get_bios_detail_and_fields(self):
        ret = self.get_json('/nodes/%s/bios?detail=True?fields=name,read_only'
                            % self.node.uuid,
                            headers={api_base.Version.string: "1.74"},
                            expect_errors=True)

        self.assertEqual(http_client.BAD_REQUEST, ret.status_int)

    def test_get_bios_fields(self):
        ret = self.get_json('/nodes/%s/bios?fields=name,read_only'
                            % self.node.uuid,
                            headers={api_base.Version.string: self.version})

        expected_json = [
            {'created_at': ret['bios'][0]['created_at'],
             'links': [
                 {'href': 'http://localhost/v1/nodes/%s/bios/virtualization'
                  % self.node.uuid, 'rel': 'self'},
                 {'href': 'http://localhost/nodes/%s/bios/virtualization'
                  % self.node.uuid, 'rel': 'bookmark'}],
             'name': 'virtualization',
             'read_only': False,
             'updated_at': None}]

        self.assertEqual({'bios': expected_json}, ret)


class TestTraits(test_api_base.BaseApiTest):

    def setUp(self):
        super(TestTraits, self).setUp()
        self.version = "1.37"
        self.node = obj_utils.create_test_node(
            self.context,
            provision_state=states.AVAILABLE, name='node-39')
        self.traits = ['CUSTOM_1', 'CUSTOM_2']
        self._add_traits(self.node, self.traits)
        self.node.obj_reset_changes()
        p = mock.patch.object(rpcapi.ConductorAPI, 'get_topic_for',
                              autospec=True)
        self.mock_gtf = p.start()
        self.mock_gtf.return_value = 'test-topic'
        self.addCleanup(p.stop)

    def _add_traits(self, node, traits):
        if traits:
            node.traits = objects.TraitList.create(
                self.context, node.id, traits)

    def test_get_all_traits(self):
        ret = self.get_json('/nodes/%s/traits' % self.node.uuid,
                            headers={api_base.Version.string: self.version})
        self.assertEqual({'traits': self.traits}, ret)

    def test_get_all_traits_fails_with_node_not_found(self):
        ret = self.get_json('/nodes/badname/traits',
                            headers={api_base.Version.string: self.version},
                            expect_errors=True)
        self.assertEqual(http_client.NOT_FOUND, ret.status_code)

    def test_get_all_traits_fails_with_bad_version(self):
        ret = self.get_json('/nodes/%s/traits' % self.node.uuid,
                            headers={api_base.Version.string: "1.36"},
                            expect_errors=True)
        self.assertEqual(http_client.NOT_ACCEPTABLE, ret.status_code)

    @mock.patch.object(rpcapi.ConductorAPI, 'add_node_traits', autospec=True)
    @mock.patch.object(notification_utils, '_emit_api_notification',
                       autospec=True)
    def test_set_all_traits(self, mock_notify, mock_add):
        traits = ['CUSTOM_3']
        request_body = {'traits': traits}
        ret = self.put_json('/nodes/%s/traits' % self.node.name,
                            request_body,
                            headers={api_base.Version.string: self.version})
        self.assertEqual(http_client.NO_CONTENT, ret.status_code)
        mock_add.assert_called_once_with(mock.ANY, mock.ANY, self.node.id,
                                         traits, replace=True,
                                         topic='test-topic')
        mock_notify.assert_has_calls(
            [mock.call(mock.ANY, mock.ANY, 'update',
                       obj_fields.NotificationLevel.INFO,
                       obj_fields.NotificationStatus.START, chassis_uuid=None),
             mock.call(mock.ANY, mock.ANY, 'update',
                       obj_fields.NotificationLevel.INFO,
                       obj_fields.NotificationStatus.END,
                       chassis_uuid=None)])
        notify_args = mock_notify.call_args_list
        self.assertEqual(traits, notify_args[0][0][1].traits.get_trait_names())
        self.assertEqual(traits, notify_args[1][0][1].traits.get_trait_names())
        self.assertIsNone(ret.location)

    @mock.patch.object(rpcapi.ConductorAPI, 'add_node_traits', autospec=True)
    @mock.patch.object(notification_utils, '_emit_api_notification',
                       autospec=True)
    def test_set_all_traits_with_chassis(self, mock_notify, mock_add):
        traits = ['CUSTOM_3']
        chassis = obj_utils.create_test_chassis(self.context)
        self.node.chassis_id = chassis.id
        self.node.save()
        request_body = {'traits': traits}
        ret = self.put_json('/nodes/%s/traits' % self.node.name,
                            request_body,
                            headers={api_base.Version.string: self.version})
        self.assertEqual(http_client.NO_CONTENT, ret.status_code)
        mock_add.assert_called_once_with(mock.ANY, mock.ANY, self.node.id,
                                         traits, replace=True,
                                         topic='test-topic')
        mock_notify.assert_has_calls(
            [mock.call(mock.ANY, mock.ANY, 'update',
                       obj_fields.NotificationLevel.INFO,
                       obj_fields.NotificationStatus.START,
                       chassis_uuid=chassis.uuid),
             mock.call(mock.ANY, mock.ANY, 'update',
                       obj_fields.NotificationLevel.INFO,
                       obj_fields.NotificationStatus.END,
                       chassis_uuid=chassis.uuid)])
        notify_args = mock_notify.call_args_list
        self.assertEqual(traits, notify_args[0][0][1].traits.get_trait_names())
        self.assertEqual(traits, notify_args[1][0][1].traits.get_trait_names())
        self.assertIsNone(ret.location)

    @mock.patch.object(rpcapi.ConductorAPI, 'add_node_traits', autospec=True)
    @mock.patch.object(notification_utils, '_emit_api_notification',
                       autospec=True)
    def test_set_all_traits_empty(self, mock_notify, mock_add):
        request_body = {'traits': []}
        ret = self.put_json('/nodes/%s/traits' % self.node.name,
                            request_body,
                            headers={api_base.Version.string: self.version})
        self.assertEqual(http_client.NO_CONTENT, ret.status_code)
        mock_add.assert_called_once_with(mock.ANY, mock.ANY, self.node.id,
                                         [], replace=True,
                                         topic='test-topic')
        mock_notify.assert_has_calls(
            [mock.call(mock.ANY, mock.ANY, 'update',
                       obj_fields.NotificationLevel.INFO,
                       obj_fields.NotificationStatus.START, chassis_uuid=None),
             mock.call(mock.ANY, mock.ANY, 'update',
                       obj_fields.NotificationLevel.INFO,
                       obj_fields.NotificationStatus.END,
                       chassis_uuid=None)])
        notify_args = mock_notify.call_args_list
        self.assertEqual([], notify_args[0][0][1].traits.get_trait_names())
        self.assertEqual([], notify_args[1][0][1].traits.get_trait_names())
        self.assertIsNone(ret.location)

    @mock.patch.object(rpcapi.ConductorAPI, 'add_node_traits', autospec=True)
    @mock.patch.object(notification_utils, '_emit_api_notification',
                       autospec=True)
    def test_set_all_traits_rejects_bad_trait(self, mock_notify, mock_add):
        request_body = {'traits': ['CUSTOM_3', 'BAD_TRAIT']}
        ret = self.put_json('/nodes/%s/traits' % self.node.name,
                            request_body,
                            headers={api_base.Version.string: self.version},
                            expect_errors=True)
        self.assertEqual(http_client.BAD_REQUEST, ret.status_code)
        self.assertFalse(mock_add.called)
        self.assertFalse(mock_notify.called)

    @mock.patch.object(rpcapi.ConductorAPI, 'add_node_traits', autospec=True)
    @mock.patch.object(notification_utils, '_emit_api_notification',
                       autospec=True)
    def test_set_all_traits_rejects_too_long_trait(self, mock_notify,
                                                   mock_add):
        # Maximum length is 255.
        long_trait = 'CUSTOM_' + 'T' * 249
        request_body = {'traits': ['CUSTOM_3', long_trait]}
        ret = self.put_json('/nodes/%s/traits' % self.node.name,
                            request_body,
                            headers={api_base.Version.string: self.version},
                            expect_errors=True)
        self.assertEqual(http_client.BAD_REQUEST, ret.status_code)
        self.assertFalse(mock_add.called)
        self.assertFalse(mock_notify.called)

    @mock.patch.object(rpcapi.ConductorAPI, 'add_node_traits', autospec=True)
    @mock.patch.object(notification_utils, '_emit_api_notification',
                       autospec=True)
    def test_set_all_traits_rejects_no_body(self, mock_notify, mock_add):
        ret = self.put_json('/nodes/%s/traits' % self.node.name, {},
                            headers={api_base.Version.string: self.version},
                            expect_errors=True)
        self.assertEqual(http_client.BAD_REQUEST, ret.status_code)
        self.assertFalse(mock_add.called)
        self.assertFalse(mock_notify.called)

    def test_set_all_traits_fails_with_bad_version(self):
        request_body = {'traits': []}
        ret = self.put_json('/nodes/%s/traits' % self.node.uuid, request_body,
                            headers={api_base.Version.string: "1.36"},
                            expect_errors=True)
        self.assertEqual(http_client.METHOD_NOT_ALLOWED, ret.status_code)

    @mock.patch.object(rpcapi.ConductorAPI, 'add_node_traits', autospec=True)
    @mock.patch.object(notification_utils, '_emit_api_notification',
                       autospec=True)
    def test_add_single_trait(self, mock_notify, mock_add):
        ret = self.put_json('/nodes/%s/traits/CUSTOM_3' % self.node.name, {},
                            headers={api_base.Version.string: self.version})
        self.assertEqual(http_client.NO_CONTENT, ret.status_code)
        mock_add.assert_called_once_with(mock.ANY, mock.ANY, self.node.id,
                                         ['CUSTOM_3'], replace=False,
                                         topic='test-topic')
        mock_notify.assert_has_calls(
            [mock.call(mock.ANY, mock.ANY, 'update',
                       obj_fields.NotificationLevel.INFO,
                       obj_fields.NotificationStatus.START, chassis_uuid=None),
             mock.call(mock.ANY, mock.ANY, 'update',
                       obj_fields.NotificationLevel.INFO,
                       obj_fields.NotificationStatus.END,
                       chassis_uuid=None)])
        traits = self.traits + ['CUSTOM_3']
        notify_args = mock_notify.call_args_list
        self.assertEqual(traits, notify_args[0][0][1].traits.get_trait_names())
        self.assertEqual(traits, notify_args[1][0][1].traits.get_trait_names())
        # Check location header.
        self.assertIsNotNone(ret.location)
        expected_location = '/v1/nodes/%s/traits/CUSTOM_3' % self.node.name
        self.assertEqual(expected_location,
                         urlparse.urlparse(ret.location).path)

    @mock.patch.object(rpcapi.ConductorAPI, 'add_node_traits', autospec=True)
    @mock.patch.object(notification_utils, '_emit_api_notification',
                       autospec=True)
    def test_no_add_single_trait_via_body(self, mock_notify, mock_add):
        request_body = {'trait': 'CUSTOM_3'}
        ret = self.put_json('/nodes/%s/traits' % self.node.name,
                            request_body,
                            headers={api_base.Version.string: self.version},
                            expect_errors=True)
        self.assertEqual(http_client.BAD_REQUEST, ret.status_code)
        self.assertFalse(mock_add.called)
        self.assertFalse(mock_notify.called)

    @mock.patch.object(rpcapi.ConductorAPI, 'add_node_traits', autospec=True)
    @mock.patch.object(notification_utils, '_emit_api_notification',
                       autospec=True)
    def test_no_add_single_trait_via_body_2(self, mock_notify, mock_add):
        request_body = {'traits': ['CUSTOM_3']}
        ret = self.put_json('/nodes/%s/traits/CUSTOM_3' % self.node.name,
                            request_body,
                            headers={api_base.Version.string: self.version},
                            expect_errors=True)
        self.assertEqual(http_client.BAD_REQUEST, ret.status_code)
        self.assertFalse(mock_add.called)
        self.assertFalse(mock_notify.called)

    @mock.patch.object(rpcapi.ConductorAPI, 'add_node_traits', autospec=True)
    @mock.patch.object(notification_utils, '_emit_api_notification',
                       autospec=True)
    def test_add_single_trait_rejects_bad_trait(self, mock_notify, mock_add):
        ret = self.put_json('/nodes/%s/traits/bad_trait' % self.node.name, {},
                            headers={api_base.Version.string: self.version},
                            expect_errors=True)
        self.assertEqual(http_client.BAD_REQUEST, ret.status_code)
        self.assertFalse(mock_add.called)
        self.assertFalse(mock_notify.called)

    @mock.patch.object(rpcapi.ConductorAPI, 'add_node_traits', autospec=True)
    @mock.patch.object(notification_utils, '_emit_api_notification',
                       autospec=True)
    def test_add_single_trait_rejects_too_long_trait(self, mock_notify,
                                                     mock_add):
        # Maximum length is 255.
        long_trait = 'CUSTOM_' + 'T' * 249
        ret = self.put_json('/nodes/%s/traits/%s' % (
                            self.node.name, long_trait), {},
                            headers={api_base.Version.string: self.version},
                            expect_errors=True)
        self.assertEqual(http_client.BAD_REQUEST, ret.status_code)
        self.assertFalse(mock_add.called)
        self.assertFalse(mock_notify.called)

    @mock.patch.object(rpcapi.ConductorAPI, 'add_node_traits', autospec=True)
    @mock.patch.object(notification_utils, '_emit_api_notification',
                       autospec=True)
    def test_add_single_trait_fails_max_trait_limit(self, mock_notify,
                                                    mock_add):
        mock_add.side_effect = exception.InvalidParameterValue(
            err='too many traits')
        ret = self.put_json('/nodes/%s/traits/CUSTOM_3' % self.node.name, {},
                            headers={api_base.Version.string: self.version},
                            expect_errors=True)
        self.assertEqual(http_client.BAD_REQUEST, ret.status_code)
        mock_add.assert_called_once_with(mock.ANY, mock.ANY, self.node.id,
                                         ['CUSTOM_3'], replace=False,
                                         topic='test-topic')
        mock_notify.assert_has_calls(
            [mock.call(mock.ANY, mock.ANY, 'update',
                       obj_fields.NotificationLevel.INFO,
                       obj_fields.NotificationStatus.START, chassis_uuid=None),
             mock.call(mock.ANY, mock.ANY, 'update',
                       obj_fields.NotificationLevel.ERROR,
                       obj_fields.NotificationStatus.ERROR,
                       chassis_uuid=None)])
        traits = self.traits + ['CUSTOM_3']
        notify_args = mock_notify.call_args_list
        self.assertEqual(traits, notify_args[0][0][1].traits.get_trait_names())
        self.assertEqual(traits, notify_args[1][0][1].traits.get_trait_names())

    @mock.patch.object(rpcapi.ConductorAPI, 'add_node_traits', autospec=True)
    @mock.patch.object(notification_utils, '_emit_api_notification',
                       autospec=True)
    def test_add_single_trait_fails_if_node_locked(self, mock_notify,
                                                   mock_add):
        mock_add.side_effect = exception.NodeLocked(
            node=self.node.uuid, host='host1')
        ret = self.put_json('/nodes/%s/traits/CUSTOM_3' % self.node.name, {},
                            headers={api_base.Version.string: self.version},
                            expect_errors=True)
        self.assertEqual(http_client.CONFLICT, ret.status_code)
        mock_add.assert_called_once_with(mock.ANY, mock.ANY, self.node.id,
                                         ['CUSTOM_3'], replace=False,
                                         topic='test-topic')
        mock_notify.assert_has_calls(
            [mock.call(mock.ANY, mock.ANY, 'update',
                       obj_fields.NotificationLevel.INFO,
                       obj_fields.NotificationStatus.START, chassis_uuid=None),
             mock.call(mock.ANY, mock.ANY, 'update',
                       obj_fields.NotificationLevel.ERROR,
                       obj_fields.NotificationStatus.ERROR,
                       chassis_uuid=None)])
        traits = self.traits + ['CUSTOM_3']
        notify_args = mock_notify.call_args_list
        self.assertEqual(traits, notify_args[0][0][1].traits.get_trait_names())
        self.assertEqual(traits, notify_args[1][0][1].traits.get_trait_names())

    @mock.patch.object(rpcapi.ConductorAPI, 'add_node_traits', autospec=True)
    @mock.patch.object(notification_utils, '_emit_api_notification',
                       autospec=True)
    def test_add_single_trait_fails_if_node_not_found(self, mock_notify,
                                                      mock_add):
        mock_add.side_effect = exception.NodeNotFound(node=self.node.uuid)
        ret = self.put_json('/nodes/%s/traits/CUSTOM_3' % self.node.name, {},
                            headers={api_base.Version.string: self.version},
                            expect_errors=True)
        self.assertEqual(http_client.NOT_FOUND, ret.status_code)
        mock_add.assert_called_once_with(mock.ANY, mock.ANY, self.node.id,
                                         ['CUSTOM_3'], replace=False,
                                         topic='test-topic')
        mock_notify.assert_has_calls(
            [mock.call(mock.ANY, mock.ANY, 'update',
                       obj_fields.NotificationLevel.INFO,
                       obj_fields.NotificationStatus.START, chassis_uuid=None),
             mock.call(mock.ANY, mock.ANY, 'update',
                       obj_fields.NotificationLevel.ERROR,
                       obj_fields.NotificationStatus.ERROR,
                       chassis_uuid=None)])
        traits = self.traits + ['CUSTOM_3']
        notify_args = mock_notify.call_args_list
        self.assertEqual(traits, notify_args[0][0][1].traits.get_trait_names())
        self.assertEqual(traits, notify_args[1][0][1].traits.get_trait_names())

    def test_add_single_trait_fails_with_bad_version(self):
        ret = self.put_json('/nodes/%s/traits/CUSTOM_TRAIT1' % self.node.uuid,
                            {}, headers={api_base.Version.string: "1.36"},
                            expect_errors=True)
        self.assertEqual(http_client.METHOD_NOT_ALLOWED, ret.status_code)

    @mock.patch.object(rpcapi.ConductorAPI, 'remove_node_traits',
                       autospec=True)
    @mock.patch.object(notification_utils, '_emit_api_notification',
                       autospec=True)
    def test_delete_all_traits(self, mock_notify, mock_remove):
        ret = self.delete('/nodes/%s/traits' % self.node.name,
                          headers={api_base.Version.string: self.version})
        self.assertEqual(http_client.NO_CONTENT, ret.status_code)
        mock_remove.assert_called_once_with(mock.ANY, mock.ANY, self.node.id,
                                            None, topic='test-topic')
        mock_notify.assert_has_calls(
            [mock.call(mock.ANY, mock.ANY, 'update',
                       obj_fields.NotificationLevel.INFO,
                       obj_fields.NotificationStatus.START, chassis_uuid=None),
             mock.call(mock.ANY, mock.ANY, 'update',
                       obj_fields.NotificationLevel.INFO,
                       obj_fields.NotificationStatus.END,
                       chassis_uuid=None)])
        notify_args = mock_notify.call_args_list
        self.assertEqual([], notify_args[0][0][1].traits.get_trait_names())
        self.assertEqual([], notify_args[1][0][1].traits.get_trait_names())

    @mock.patch.object(rpcapi.ConductorAPI, 'remove_node_traits',
                       autospec=True)
    @mock.patch.object(notification_utils, '_emit_api_notification',
                       autospec=True)
    def test_delete_all_traits_with_chassis(self, mock_notify, mock_remove):
        chassis = obj_utils.create_test_chassis(self.context)
        self.node.chassis_id = chassis.id
        self.node.save()
        ret = self.delete('/nodes/%s/traits' % self.node.name,
                          headers={api_base.Version.string: self.version})
        self.assertEqual(http_client.NO_CONTENT, ret.status_code)
        mock_remove.assert_called_once_with(mock.ANY, mock.ANY, self.node.id,
                                            None, topic='test-topic')
        mock_notify.assert_has_calls(
            [mock.call(mock.ANY, mock.ANY, 'update',
                       obj_fields.NotificationLevel.INFO,
                       obj_fields.NotificationStatus.START,
                       chassis_uuid=chassis.uuid),
             mock.call(mock.ANY, mock.ANY, 'update',
                       obj_fields.NotificationLevel.INFO,
                       obj_fields.NotificationStatus.END,
                       chassis_uuid=chassis.uuid)])
        notify_args = mock_notify.call_args_list
        self.assertEqual([], notify_args[0][0][1].traits.get_trait_names())
        self.assertEqual([], notify_args[1][0][1].traits.get_trait_names())

    def test_delete_all_traits_fails_with_bad_version(self):
        ret = self.delete('/nodes/%s/traits' % self.node.uuid,
                          headers={api_base.Version.string: "1.36"},
                          expect_errors=True)
        self.assertEqual(http_client.NOT_FOUND, ret.status_code)

    @mock.patch.object(rpcapi.ConductorAPI, 'remove_node_traits',
                       autospec=True)
    @mock.patch.object(notification_utils, '_emit_api_notification',
                       autospec=True)
    def test_delete_trait(self, mock_notify, mock_remove):
        ret = self.delete('/nodes/%s/traits/CUSTOM_1' % self.node.name,
                          headers={api_base.Version.string: self.version})
        self.assertEqual(http_client.NO_CONTENT, ret.status_code)
        mock_remove.assert_called_once_with(mock.ANY, mock.ANY, self.node.id,
                                            ['CUSTOM_1'], topic='test-topic')
        mock_notify.assert_has_calls(
            [mock.call(mock.ANY, mock.ANY, 'update',
                       obj_fields.NotificationLevel.INFO,
                       obj_fields.NotificationStatus.START, chassis_uuid=None),
             mock.call(mock.ANY, mock.ANY, 'update',
                       obj_fields.NotificationLevel.INFO,
                       obj_fields.NotificationStatus.END,
                       chassis_uuid=None)])
        traits = ['CUSTOM_2']
        notify_args = mock_notify.call_args_list
        self.assertEqual(traits, notify_args[0][0][1].traits.get_trait_names())
        self.assertEqual(traits, notify_args[1][0][1].traits.get_trait_names())

    @mock.patch.object(rpcapi.ConductorAPI, 'remove_node_traits',
                       autospec=True)
    @mock.patch.object(notification_utils, '_emit_api_notification',
                       autospec=True)
    def test_delete_trait_fails_if_node_locked(self, mock_notify, mock_remove):
        mock_remove.side_effect = exception.NodeLocked(
            node=self.node.uuid, host='host1')
        ret = self.delete('/nodes/%s/traits/CUSTOM_1' % self.node.name,
                          headers={api_base.Version.string: self.version},
                          expect_errors=True)
        self.assertEqual(http_client.CONFLICT, ret.status_code)
        mock_remove.assert_called_once_with(mock.ANY, mock.ANY, self.node.id,
                                            ['CUSTOM_1'], topic='test-topic')
        mock_notify.assert_has_calls(
            [mock.call(mock.ANY, mock.ANY, 'update',
                       obj_fields.NotificationLevel.INFO,
                       obj_fields.NotificationStatus.START, chassis_uuid=None),
             mock.call(mock.ANY, mock.ANY, 'update',
                       obj_fields.NotificationLevel.ERROR,
                       obj_fields.NotificationStatus.ERROR,
                       chassis_uuid=None)])
        traits = ['CUSTOM_2']
        notify_args = mock_notify.call_args_list
        self.assertEqual(traits, notify_args[0][0][1].traits.get_trait_names())
        self.assertEqual(traits, notify_args[1][0][1].traits.get_trait_names())

    @mock.patch.object(rpcapi.ConductorAPI, 'remove_node_traits',
                       autospec=True)
    @mock.patch.object(notification_utils, '_emit_api_notification',
                       autospec=True)
    def test_delete_trait_fails_if_node_not_found(self, mock_notify,
                                                  mock_remove):
        mock_remove.side_effect = exception.NodeNotFound(node=self.node.uuid)
        ret = self.delete('/nodes/%s/traits/CUSTOM_1' % self.node.name,
                          headers={api_base.Version.string: self.version},
                          expect_errors=True)
        self.assertEqual(http_client.NOT_FOUND, ret.status_code)
        mock_remove.assert_called_once_with(mock.ANY, mock.ANY, self.node.id,
                                            ['CUSTOM_1'], topic='test-topic')
        mock_notify.assert_has_calls(
            [mock.call(mock.ANY, mock.ANY, 'update',
                       obj_fields.NotificationLevel.INFO,
                       obj_fields.NotificationStatus.START, chassis_uuid=None),
             mock.call(mock.ANY, mock.ANY, 'update',
                       obj_fields.NotificationLevel.ERROR,
                       obj_fields.NotificationStatus.ERROR,
                       chassis_uuid=None)])
        traits = ['CUSTOM_2']
        notify_args = mock_notify.call_args_list
        self.assertEqual(traits, notify_args[0][0][1].traits.get_trait_names())
        self.assertEqual(traits, notify_args[1][0][1].traits.get_trait_names())

    @mock.patch.object(rpcapi.ConductorAPI, 'remove_node_traits',
                       autospec=True)
    @mock.patch.object(notification_utils, '_emit_api_notification',
                       autospec=True)
    def test_delete_trait_fails_if_trait_not_found(self, mock_notify,
                                                   mock_remove):
        mock_remove.side_effect = exception.NodeTraitNotFound(
            node_id=self.node.id, trait='CUSTOM_12')
        ret = self.delete('/nodes/%s/traits/CUSTOM_12' % self.node.name,
                          headers={api_base.Version.string: self.version},
                          expect_errors=True)
        self.assertEqual(http_client.NOT_FOUND, ret.status_code)
        self.assertIn(self.node.uuid, ret.json['error_message'])
        self.assertNotIn(self.node.id, ret.json['error_message'])
        mock_remove.assert_called_once_with(mock.ANY, mock.ANY, self.node.id,
                                            ['CUSTOM_12'], topic='test-topic')
        mock_notify.assert_has_calls(
            [mock.call(mock.ANY, mock.ANY, 'update',
                       obj_fields.NotificationLevel.INFO,
                       obj_fields.NotificationStatus.START, chassis_uuid=None),
             mock.call(mock.ANY, mock.ANY, 'update',
                       obj_fields.NotificationLevel.ERROR,
                       obj_fields.NotificationStatus.ERROR,
                       chassis_uuid=None)])
        notify_args = mock_notify.call_args_list
        self.assertEqual(self.traits,
                         notify_args[0][0][1].traits.get_trait_names())
        self.assertEqual(self.traits,
                         notify_args[1][0][1].traits.get_trait_names())

    def test_delete_trait_fails_with_bad_version(self):
        ret = self.delete('/nodes/%s/traits/CUSTOM_TRAIT1' % self.node.uuid,
                          headers={api_base.Version.string: "1.36"},
                          expect_errors=True)
        self.assertEqual(http_client.NOT_FOUND, ret.status_code)


class TestNodeHistory(test_api_base.BaseApiTest):

    def setUp(self):
        super(TestNodeHistory, self).setUp()
        self.version = "1.78"
        self.node = obj_utils.create_test_node(
            self.context,
            provision_state=states.AVAILABLE, name='node-54')
        self.node.save()
        self.node.obj_reset_changes()

    def _add_history_entries(self):
        self.event1 = objects.NodeHistory(node_id=self.node.id, event='meow',
                                          conductor='cat-tree1',
                                          user='peaches')
        self.event1.create()
        self.event2 = objects.NodeHistory(node_id=self.node.id, event='purr',
                                          conductor='cat-tree2',
                                          user='sage')
        self.event2.create()
        self.event3 = objects.NodeHistory(node_id=self.node.id,
                                          event='g' + 'rrrr' * 64 + '!',
                                          conductor='cat-tree3',
                                          user='bella')
        self.event3.create()

    def test_get_all_history(self):
        ret = self.get_json('/nodes/%s/history' % self.node.uuid,
                            headers={api_base.Version.string: self.version})
        self.assertEqual({'history': []}, ret)

    def test_get_all_old_version(self):
        ret = self.get_json('/nodes/%s/history' % self.node.uuid,
                            headers={api_base.Version.string: "1.77"},
                            expect_errors=True)
        self.assertEqual(http_client.NOT_FOUND, ret.status_code)

    def test_get_all_history_returns_entries(self):
        self._add_history_entries()
        ret = self.get_json('/nodes/%s/history' % self.node.uuid,
                            headers={api_base.Version.string: self.version})
        self.assertIn('history', ret)
        entries = ret['history']
        self.assertEqual(3, len(entries))
        self.assertEqual('meow', entries[0]['event'])
        self.assertEqual('purr', entries[1]['event'])
        self.assertIn('grr', entries[2]['event'])
        self.assertNotIn('r!', entries[2]['event'])
        self.assertIn('...', entries[2]['event'])
        for entry in [0, 1, 2]:
            for field in ['conductor', 'user']:
                self.assertNotIn(field, entries[entry])
            self.assertIn('severity', entries[entry])

    def test_get_all_history_returns_detail(self):
        self._add_history_entries()
        ret = self.get_json('/nodes/%s/history?detail=true' % self.node.uuid,
                            headers={api_base.Version.string: self.version})
        self.assertIn('history', ret)
        entries = ret['history']
        self.assertEqual(3, len(entries))
        self.assertEqual('meow', entries[0]['event'])
        self.assertEqual('purr', entries[1]['event'])
        self.assertIn('grr', entries[2]['event'])
        self.assertIn('r!', entries[2]['event'])
        for entry in [0, 1, 2]:
            for field in ['conductor', 'user', 'severity', 'event_type']:
                self.assertIn(field, entries[entry])

    def test_get_history_item(self):
        self._add_history_entries()
        record = self.get_json('/nodes/%s/history/%s' % (self.node.uuid,
                                                         self.event1.uuid),
                               headers={api_base.Version.string: self.version})
        self.assertEqual(8, len(record))
        expected_keys = ['created_at', 'links', 'event',
                         'event_type', 'severity', 'user', 'uuid']
        for key in expected_keys:
            self.assertIn(key, record)
        self.assertNotIn('updated_at', record)
        self.assertEqual('cat-tree1', record['conductor'])
        self.assertEqual('meow', record['event'])
        self.assertEqual('peaches', record['user'])
        self.assertEqual(self.event1.uuid, record['uuid'])

    def test_get_history_item_not_found(self):
        self._add_history_entries()
        ret = self.get_json('/nodes/%s/history/52949728-59fc-'
                            '4651-84c8-b0a16b469372' % self.node.uuid,
                            headers={api_base.Version.string: self.version},
                            expect_errors=True)
        self.assertEqual(http_client.NOT_FOUND, ret.status_code)

    def test_get_history_item_old_version(self):
        ret = self.get_json('/nodes/%s/history/1234' % self.node.uuid,
                            headers={api_base.Version.string: "1.77"},
                            expect_errors=True)
        self.assertEqual(http_client.NOT_FOUND, ret.status_code)

    def test_get_all_pagination(self):
        self._add_history_entries()
        # First request, initial request with a limit of 1.
        ret = self.get_json('/nodes/%s/history?limit=1' % self.node.uuid,
                            headers={api_base.Version.string: self.version})
        self.assertIn('history', ret)
        entries = ret['history']
        self.assertEqual(1, len(entries))
        result_uuid = entries[0]['uuid']
        self.assertEqual(self.event1.uuid, result_uuid)
        self.assertIn('next', ret)
        self.assertIn('nodes/%s/history' % self.node.uuid, ret['next'])
        self.assertIn('limit=1', ret['next'])
        self.assertIn('marker=%s' % result_uuid, ret['next'])
        # Second request
        ret = self.get_json('/nodes/%s/history?limit=1&marker=%s' %
                            (self.node.uuid, result_uuid),
                            headers={api_base.Version.string: self.version})
        self.assertIn('history', ret)
        entries = ret['history']
        self.assertEqual(1, len(entries))
        result_uuid = entries[0]['uuid']
        self.assertEqual(self.event2.uuid, result_uuid)
        self.assertIn('nodes/%s/history' % self.node.uuid, ret['next'])
        self.assertIn('limit=1', ret['next'])
        self.assertIn('marker=%s' % result_uuid, ret['next'])
        # Third request
        ret = self.get_json('/nodes/%s/history?limit=1&marker=%s' %
                            (self.node.uuid, result_uuid),
                            headers={api_base.Version.string: self.version})
        self.assertIn('history', ret)
        entries = ret['history']
        self.assertEqual(1, len(entries))
        result_uuid = entries[0]['uuid']
        self.assertEqual(self.event3.uuid, result_uuid)
        self.assertIn('nodes/%s/history' % self.node.uuid, ret['next'])
        self.assertIn('limit=1', ret['next'])
        self.assertIn('marker=%s' % result_uuid, ret['next'])


class TestNodeInventory(test_api_base.BaseApiTest):
    fake_inventory_data = {'cpu': {'count': 1,
                                   'model_name': 'qemu64',
                                   'architecture': 'x86_64'}}
    fake_plugin_data = {"disks": [{"name": "/dev/vda"}]}

    def setUp(self):
        super(TestNodeInventory, self).setUp()
        self.version = "1.81"
        self.node = obj_utils.create_test_node(
            self.context,
            provision_state=states.AVAILABLE, name='node-81')
        CONF.set_override('data_backend', 'database', group='inventory')

    @mock.patch.object(inspect_utils, 'get_inspection_data', autospec=True)
    def test_get_old_version(self, mock_get):
        ret = self.get_json('/nodes/%s/inventory' % self.node.uuid,
                            headers={api_base.Version.string: "1.80"},
                            expect_errors=True)
        self.assertEqual(http_client.NOT_FOUND, ret.status_code)
        mock_get.assert_not_called()

    def test_get_inventory_no_inventory(self):
        ret = self.get_json('/nodes/%s/inventory' % self.node.uuid,
                            headers={api_base.Version.string: self.version},
                            expect_errors=True)
        self.assertEqual(http_client.NOT_FOUND, ret.status_code)

    def test_get_inventory(self):
        obj_utils.create_test_inventory(
            self.context, self.node,
            inventory_data=self.fake_inventory_data,
            plugin_data=self.fake_plugin_data)
        ret = self.get_json('/nodes/%s/inventory' % self.node.uuid,
                            headers={api_base.Version.string: self.version})
        self.assertEqual({'inventory': self.fake_inventory_data,
                          'plugin_data': self.fake_plugin_data}, ret)


class TestNodeShardGets(test_api_base.BaseApiTest):
    def setUp(self):
        super(TestNodeShardGets, self).setUp()
        p = mock.patch.object(rpcapi.ConductorAPI, 'get_topic_for',
                              autospec=True)
        self.mock_gtf = p.start()
        self.mock_gtf.return_value = 'test-topic'
        self.addCleanup(p.stop)
        self.mock_get_conductor_for = self.useFixture(
            fixtures.MockPatchObject(rpcapi.ConductorAPI, 'get_conductor_for',
                                     autospec=True)).mock
        self.mock_get_conductor_for.return_value = 'fake.conductor'
        self.node = obj_utils.create_test_node(self.context, shard='foo')
        self.headers = {api_base.Version.string: '1.82'}

    def test_get_node_shard_field(self):
        result = self.get_json(
            '/nodes/%s' % self.node.uuid, headers=self.headers)
        self.assertEqual('foo', result['shard'])

    def test_get_node_shard_field_fails_wrong_version(self):
        headers = {api_base.Version.string: '1.80'}
        result = self.get_json('/nodes/%s' % self.node.uuid, headers=headers)
        self.assertNotIn('shard', result)

    def test_filtering_by_shard(self):
        result = self.get_json(
            '/nodes?shard=foo', fields='shard', headers=self.headers)
        self.assertEqual(1, len(result['nodes']))
        self.assertEqual('foo', result['nodes'][0]['shard'])

    def test_filtering_by_shard_fails_wrong_version(self):
        headers = {api_base.Version.string: '1.80'}

        result = self.get_json('/nodes?shard=foo',
                               expect_errors=True, headers=headers)
        self.assertEqual(http_client.NOT_ACCEPTABLE, result.status_code)

    def test_filtering_by_single_shard_detail(self):
        result = self.get_json('/nodes/detail?shard=foo', headers=self.headers)
        self.assertEqual(1, len(result['nodes']))
        self.assertEqual('foo', result['nodes'][0]['shard'])

    def test_filtering_by_multi_shard_detail(self):
        obj_utils.create_test_node(
            self.context, uuid=uuid.uuid4(), shard='bar')
        result = self.get_json(
            '/nodes?shard=foo,bar', headers=self.headers)
        self.assertEqual(2, len(result['nodes']))

    def test_filtering_by_shard_detail_fails_wrong_version(self):
        headers = {api_base.Version.string: '1.80'}

        result = self.get_json('/nodes/detail?shard=foo',
                               expect_errors=True, headers=headers)
        self.assertEqual(http_client.NOT_ACCEPTABLE, result.status_code)

    def test_filtering_by_sharded(self):
        obj_utils.create_test_node(self.context, uuid=uuid.uuid4())
        obj_utils.create_test_node(self.context, uuid=uuid.uuid4())
        # We now have one node in shard foo (setUp) and two unsharded.
        result_true = self.get_json(
            '/nodes?sharded=true', headers=self.headers)
        result_false = self.get_json(
            '/nodes?sharded=false', headers=self.headers)
        self.assertEqual(1, len(result_true['nodes']))
        self.assertEqual(2, len(result_false['nodes']))


@mock.patch.object(rpcapi.ConductorAPI, 'create_node',
                   lambda _api, _ctx, node, _topic: _create_node_locally(node))
class TestNodeShardPost(test_api_base.BaseApiTest):
    def setUp(self):
        super(TestNodeShardPost, self).setUp()
        p = mock.patch.object(rpcapi.ConductorAPI, 'get_topic_for',
                              autospec=True)
        self.mock_gtf = p.start()
        self.mock_gtf.return_value = 'test-topic'
        self.addCleanup(p.stop)
        self.chassis = obj_utils.create_test_chassis(self.context)

    def test_create_node_with_shard(self):
        shard = 'foo'
        ndict = test_api_utils.post_get_test_node(shard=shard)
        headers = {api_base.Version.string: '1.82'}
        response = self.post_json('/nodes', ndict, headers=headers)
        self.assertEqual(http_client.CREATED, response.status_int)

        result = self.get_json('/nodes/%s' % ndict['uuid'], headers=headers)
        self.assertEqual(ndict['uuid'], result['uuid'])
        self.assertEqual(shard, result['shard'])

    def test_create_node_with_shard_fail_wrong_version(self):
        headers = {api_base.Version.string: '1.80'}
        shard = 'foo'
        ndict = test_api_utils.post_get_test_node(shard=shard)
        response = self.post_json(
            '/nodes', ndict, expect_errors=True, headers=headers)
        self.assertEqual(http_client.NOT_ACCEPTABLE, response.status_int)


class TestNodeShardPatch(test_api_base.BaseApiTest):
    def setUp(self):
        super(TestNodeShardPatch, self).setUp()
        self.node = obj_utils.create_test_node(self.context, name='node-57.1')
        p = mock.patch.object(rpcapi.ConductorAPI, 'get_topic_for',
                              autospec=True)
        self.mock_gtf = p.start()
        self.mock_gtf.return_value = 'test-topic'
        self.addCleanup(p.stop)
        p = mock.patch.object(rpcapi.ConductorAPI, 'update_node',
                              autospec=True)
        self.mock_update_node = p.start()
        self.addCleanup(p.stop)

    def test_node_add_shard(self):
        self.mock_update_node.return_value = self.node
        (self
         .mock_update_node
         .return_value
         .updated_at) = "2013-12-03T06:20:41.184720+00:00"
        headers = {api_base.Version.string: '1.82'}
        shard = 'shard1'
        body = [{
            'path': '/shard',
            'value': shard,
            'op': 'add',
        }]

        response = self.patch_json(
            '/nodes/%s' % self.node.uuid, body, headers=headers)
        self.assertEqual(http_client.OK, response.status_code)
        self.mock_update_node.assert_called_once()

    def test_node_add_shard_fail_wrong_version(self):
        self.mock_update_node.return_value = self.node
        (self
         .mock_update_node
         .return_value
         .updated_at) = "2013-12-03T06:20:41.184720+00:00"
        headers = {api_base.Version.string: '1.80'}
        shard = 'shard1'
        body = [{
            'path': '/shard',
            'value': shard,
            'op': 'add',
        }]

        response = self.patch_json('/nodes/%s' % self.node.uuid,
                                   body, expect_errors=True, headers=headers)
        self.mock_update_node.assert_not_called()
        self.assertEqual(http_client.NOT_ACCEPTABLE, response.status_code)


class TestNodeChildrenTestCase(test_api_base.BaseApiTest):
    def setUp(self):
        super(TestNodeChildrenTestCase, self).setUp()
        self.node = obj_utils.create_test_node(self.context, name='din')
        self.child_node = obj_utils.create_test_node(
            self.context,
            uuid=uuidutils.generate_uuid(),
            name='not-yoda',
            parent_node=self.node.uuid)
        self.headers = {api_base.Version.string: '1.83'}

    def test_list_nodes(self):
        response = self.get_json(
            '/nodes/', headers=self.headers)
        self.assertEqual(1, len(response['nodes']))
        self.assertEqual('din', response['nodes'][0]['name'])

    def test_get_child_node(self):
        response = self.get_json(
            '/nodes/%s/children' % self.node.uuid, headers=self.headers)
        self.assertEqual(1, len(response['children']))
        self.assertEqual(self.child_node.uuid, response['children'][0])

    def test_list_nodes_with_include_children(self):
        response = self.get_json(
            '/nodes/?include_children=True', headers=self.headers)
        self.assertEqual(2, len(response['nodes']))

    def test_list_nodes_ignores_parent_if_include_children_indicated(self):
        response = self.get_json(
            '/nodes/?include_children=True&parent_node=111',
            headers=self.headers)
        self.assertEqual(2, len(response['nodes']))

    @mock.patch.object(api_utils, 'check_list_policy', autospec=True)
    def test_list_nodes_cannot_see_children_if_not_owned(self, mock_policy):
        project_id = uuidutils.generate_uuid()
        mock_policy.return_value = project_id
        self.node['owner'] = project_id
        self.node.save()
        response = self.get_json(
            '/nodes/?parent_node={}'.format(project_id),
            headers=self.headers)
        self.assertEqual(0, len(response['nodes']))

    @mock.patch.object(api_utils, 'check_list_policy', autospec=True)
    def test_list_nodes_with_children_only_parent(self, mock_policy):
        project_id = uuidutils.generate_uuid()
        headers = self.headers.copy()
        mock_policy.return_value = project_id
        self.node['lessee'] = project_id
        self.node.save()
        response = self.get_json(
            '/nodes/?include_children=True&'
            'fields=uuid,lessee,name,parent_node',
            headers=headers)
        self.assertEqual(1, len(response['nodes']))
        self.assertEqual(self.node.uuid, response['nodes'][0]['uuid'])

    def test_list_nodes_lists_empty_for_specific_parent(self):
        node = obj_utils.create_test_node(
            self.context,
            uuid=uuidutils.generate_uuid(),
            name='kryze',
            parent_node=self.node.uuid)
        response = self.get_json(
            '/nodes/?parent_node={}'.format(node.uuid), headers=self.headers)
        self.assertEqual(0, len(response['nodes']))

    def test_list_node_children_by_single_node(self):
        obj_utils.create_test_node(
            self.context,
            uuid=uuidutils.generate_uuid(),
            name='kryze',
            parent_node=self.node.uuid)
        response = self.get_json(
            '/nodes/{}/children'.format(self.node.uuid), headers=self.headers)
        self.assertEqual(2, len(response['children']))


@mock.patch.object(rpcapi.ConductorAPI, 'create_node',
                   lambda _api, _ctx, node, _topic: _create_node_locally(node))
class TestNodeParentNodePost(test_api_base.BaseApiTest):
    def setUp(self):
        super(TestNodeParentNodePost, self).setUp()
        self.node = obj_utils.create_test_node(self.context, name='din')

        p = mock.patch.object(rpcapi.ConductorAPI, 'get_topic_for',
                              autospec=True)
        self.mock_gtf = p.start()
        self.mock_gtf.return_value = 'test-topic'
        self.addCleanup(p.stop)
        self.chassis = obj_utils.create_test_chassis(self.context)

    def test_create_node_with_parent_node(self):
        ndict = test_api_utils.post_get_test_node(
            uuid=uuidutils.generate_uuid())
        ndict['parent_node'] = self.node.uuid
        headers = {api_base.Version.string: '1.83'}
        response = self.post_json('/nodes', ndict, headers=headers)
        self.assertEqual(http_client.CREATED, response.status_int)

        result = self.get_json('/nodes/{}'.format(ndict['uuid']),
                               headers=headers)
        self.assertEqual(ndict['uuid'], result['uuid'])
        self.assertEqual(self.node.uuid, result['parent_node'])

    def test_create_node_with_parent_node_fail_wrong_version(self):
        headers = {api_base.Version.string: '1.82'}
        ndict = test_api_utils.post_get_test_node(
            uuid=uuidutils.generate_uuid())
        ndict['parent_node'] = self.node.uuid
        response = self.post_json(
            '/nodes', ndict, expect_errors=True, headers=headers)
        self.assertEqual(http_client.NOT_ACCEPTABLE, response.status_int)

    def test_create_node_with_named_parent_node_succeeds(self):
        ndict = test_api_utils.post_get_test_node(
            uuid=uuidutils.generate_uuid())
        ndict['parent_node'] = 'din'
        headers = {api_base.Version.string: '1.83'}
        response = self.post_json('/nodes', ndict, expect_errors=True,
                                  headers=headers)
        self.assertEqual(http_client.CREATED, response.status_int)
        self.assertEqual(self.node.uuid, response.json['parent_node'])


class TestNodeParentNodePatch(test_api_base.BaseApiTest):
    def setUp(self):
        super(TestNodeParentNodePatch, self).setUp()
        self.node = obj_utils.create_test_node(
            self.context,
            name='djarin')
        self.child_node = obj_utils.create_test_node(
            self.context,
            name='the_child',
            uuid=uuidutils.generate_uuid())

        p = mock.patch.object(rpcapi.ConductorAPI, 'get_topic_for',
                              autospec=True)
        self.mock_gtf = p.start()
        self.mock_gtf.return_value = 'test-topic'
        self.addCleanup(p.stop)
        p = mock.patch.object(rpcapi.ConductorAPI, 'update_node',
                              autospec=True)
        self.mock_update_node = p.start()
        self.addCleanup(p.stop)

    def test_node_add_parent(self):
        self.mock_update_node.return_value = self.node
        (self
         .mock_update_node
         .return_value
         .updated_at) = "2013-12-03T06:20:41.184720+00:00"
        headers = {api_base.Version.string: '1.83'}
        body = [{
            'path': '/parent_node',
            'value': self.node.uuid,
            'op': 'add',
        }]

        response = self.patch_json(
            '/nodes/%s' % self.child_node.uuid, body, headers=headers)
        self.assertEqual(http_client.OK, response.status_code)
        self.mock_update_node.assert_called_once()

    def test_node_add_parent_node_fail_wrong_version(self):
        headers = {api_base.Version.string: '1.82'}
        body = [{
            'path': '/parent_node',
            'value': self.node.uuid,
            'op': 'add',
        }]

        response = self.patch_json('/nodes/%s' % self.child_node.uuid,
                                   body, expect_errors=True, headers=headers)
        self.mock_update_node.assert_not_called()
        self.assertEqual(http_client.NOT_ACCEPTABLE, response.status_code)

    def test_node_add_parent_node_not_uuid(self):
        headers = {api_base.Version.string: '1.83'}
        body = [{
            'path': '/parent_node',
            'value': 'djarin',
            'op': 'add',
        }]

        self.patch_json('/nodes/%s' % self.child_node.uuid,
                        body, expect_errors=True, headers=headers)
        self.mock_update_node.assert_called_once()
        test_node = self.mock_update_node.call_args.args[2]
        self.assertEqual(self.node.uuid, test_node.parent_node)

    def test_node_remove_parent(self):
        self.mock_update_node.return_value = self.node
        (self
         .mock_update_node
         .return_value
         .updated_at) = "2013-12-03T06:20:41.184720+00:00"
        headers = {api_base.Version.string: '1.83'}
        body = [{
            'path': '/parent_node',
            'op': 'remove',
        }]

        response = self.patch_json(
            '/nodes/%s' % self.child_node.uuid, body, headers=headers)
        self.assertEqual(http_client.OK, response.status_code)
        self.mock_update_node.assert_called_once()


class TestNodeFirmwareComponent(test_api_base.BaseApiTest):

    def setUp(self):
        super(TestNodeFirmwareComponent, self).setUp()
        self.version = "1.86"
        self.node = obj_utils.create_test_node(
            self.context, id=1)

        self.fw_cmp = obj_utils.create_test_firmware_component(
            self.context, node_id=self.node.id)
        self.fw_cmp2 = obj_utils.create_test_firmware_component(
            self.context, node_id=self.node.id, component='BIOS')

    def test_get_all_firmware_components(self):
        ret = self.get_json('/nodes/%s/firmware' % self.node.uuid,
                            headers={api_base.Version.string: self.version})
        expected_components = [
            {'created_at': ret['firmware'][0]['created_at'],
             'updated_at': ret['firmware'][0]['updated_at'],
             'component': 'BIOS',
             'initial_version': 'v1.0.0', 'current_version': 'v1.0.0',
             'last_version_flashed': None},
            {'created_at': ret['firmware'][1]['created_at'],
             'updated_at': ret['firmware'][1]['updated_at'],
             'component': 'bmc',
             'initial_version': 'v1.0.0', 'current_version': 'v1.0.0',
             'last_version_flashed': None}]
        self.assertEqual({'firmware': expected_components}, ret)

    def test_wrong_version_get_all_firmware_components_old_version(self):
        ret = self.get_json('/nodes/%s/firmware' % self.node.uuid,
                            headers={api_base.Version.string: "1.81"},
                            expect_errors=True)

        self.assertEqual(http_client.NOT_FOUND, ret.status_int)


@mock.patch.object(rpcapi.ConductorAPI, 'get_topic_for',
                   lambda *_: 'test-topic')
class TestNodeVmedia(test_api_base.BaseApiTest):

    def setUp(self):
        super().setUp()
        self.version = "1.89"
        self.node = obj_utils.create_test_node(self.context)

    @mock.patch.object(rpcapi.ConductorAPI, 'attach_virtual_media',
                       autospec=True)
    def test_attach(self, mock_attach):
        vmedia = {'device_type': boot_devices.CDROM,
                  'image_url': 'https://image',
                  'image_download_source': 'http'}
        ret = self.post_json('/nodes/%s/vmedia' % self.node.uuid, vmedia,
                             headers={api_base.Version.string: self.version})
        self.assertEqual(http_client.NO_CONTENT, ret.status_int)
        mock_attach.assert_called_once_with(
            mock.ANY, mock.ANY, self.node.uuid,
            device_type=boot_devices.CDROM, image_url='https://image',
            image_download_source='http', topic='test-topic')

    @mock.patch.object(rpcapi.ConductorAPI, 'attach_virtual_media',
                       autospec=True)
    def test_attach_required_only(self, mock_attach):
        vmedia = {'device_type': boot_devices.CDROM,
                  'image_url': 'http://image'}
        ret = self.post_json('/nodes/%s/vmedia' % self.node.uuid, vmedia,
                             headers={api_base.Version.string: self.version})
        self.assertEqual(http_client.NO_CONTENT, ret.status_int)
        mock_attach.assert_called_once_with(
            mock.ANY, mock.ANY, self.node.uuid,
            device_type=boot_devices.CDROM, image_url='http://image',
            image_download_source='local', topic='test-topic')

    def test_attach_missing_device_type(self):
        vmedia = {'image_url': 'http://image'}
        ret = self.post_json('/nodes/%s/vmedia' % self.node.uuid, vmedia,
                             headers={api_base.Version.string: self.version},
                             expect_errors=True)
        self.assertEqual(http_client.BAD_REQUEST, ret.status_int)
        self.assertIn(b"device_type", ret.body)

    def test_attach_invalid_device_type(self):
        vmedia = {'device_type': 'cat',
                  'image_url': 'http://image'}
        ret = self.post_json('/nodes/%s/vmedia' % self.node.uuid, vmedia,
                             headers={api_base.Version.string: self.version},
                             expect_errors=True)
        self.assertEqual(http_client.BAD_REQUEST, ret.status_int)
        self.assertIn(b"cat", ret.body)

    def test_attach_missing_image_url(self):
        vmedia = {'device_type': boot_devices.CDROM}
        ret = self.post_json('/nodes/%s/vmedia' % self.node.uuid, vmedia,
                             headers={api_base.Version.string: self.version},
                             expect_errors=True)
        self.assertEqual(http_client.BAD_REQUEST, ret.status_int)
        self.assertIn(b"image_url", ret.body)

    def test_attach_invalid_image_url(self):
        vmedia = {'device_type': boot_devices.CDROM,
                  'image_url': 'abcd'}
        ret = self.post_json('/nodes/%s/vmedia' % self.node.uuid, vmedia,
                             headers={api_base.Version.string: self.version},
                             expect_errors=True)
        self.assertEqual(http_client.BAD_REQUEST, ret.status_int)
        self.assertIn(b"URL scheme", ret.body)

    def test_attach_wrong_version(self):
        vmedia = {'device_type': boot_devices.CDROM,
                  'image_url': 'http://image'}
        ret = self.post_json('/nodes/%s/vmedia' % self.node.uuid, vmedia,
                             headers={api_base.Version.string: "1.87"},
                             expect_errors=True)
        self.assertEqual(http_client.NOT_FOUND, ret.status_int)

    @mock.patch.object(rpcapi.ConductorAPI, 'detach_virtual_media',
                       autospec=True)
    def test_detach_everything(self, mock_detach):
        ret = self.delete('/nodes/%s/vmedia' % self.node.uuid,
                          headers={api_base.Version.string: self.version})
        self.assertEqual(http_client.NO_CONTENT, ret.status_int)
        mock_detach.assert_called_once_with(
            mock.ANY, mock.ANY, self.node.uuid,
            device_types=None, topic='test-topic')

    @mock.patch.object(rpcapi.ConductorAPI, 'detach_virtual_media',
                       autospec=True)
    def test_detach_specific_via_url(self, mock_detach):
        ret = self.delete('/nodes/%s/vmedia/%s'
                          % (self.node.uuid, boot_devices.CDROM),
                          headers={api_base.Version.string: self.version})
        self.assertEqual(http_client.NO_CONTENT, ret.status_int)
        mock_detach.assert_called_once_with(
            mock.ANY, mock.ANY, self.node.uuid,
            device_types=[boot_devices.CDROM], topic='test-topic')

    @mock.patch.object(rpcapi.ConductorAPI, 'detach_virtual_media',
                       autospec=True)
    def test_detach_specific_via_argument(self, mock_detach):
        ret = self.delete('/nodes/%s/vmedia?device_types=%s'
                          % (self.node.uuid, boot_devices.CDROM),
                          headers={api_base.Version.string: self.version})
        self.assertEqual(http_client.NO_CONTENT, ret.status_int)
        mock_detach.assert_called_once_with(
            mock.ANY, mock.ANY, self.node.uuid,
            device_types=[boot_devices.CDROM], topic='test-topic')

    def test_detach_wrong_device_types(self):
        ret = self.delete('/nodes/%s/vmedia?device_types=cdrom,cat'
                          % self.node.uuid,
                          headers={api_base.Version.string: self.version},
                          expect_errors=True)
        self.assertEqual(http_client.BAD_REQUEST, ret.status_int)
        self.assertIn(b"cat", ret.body)

    def test_detach_wrong_version(self):
        ret = self.delete('/nodes/%s/vmedia' % self.node.uuid,
                          headers={api_base.Version.string: "1.87"},
                          expect_errors=True)
        self.assertEqual(http_client.NOT_FOUND, ret.status_int)
