ironic/ironic/tests/unit/api/controllers/v1/test_volume_target.py
Ruby Loo feac8cfb78 Pin API version during rolling upgrade
During a rolling upgrade, when the new services are pinned to the
old release, the API version will also be pinned to the old release.
This will prevent users from accessing new features that may not quite
work.

The .sample was updated to reflect the change to the help string for
the [DEFAULT]/pin_release_version configuration option. The update also
pulled in changes for other options, from other (non-ironic) libraries.

Change-Id: I38a0f106b589945fb62071f3dfe5bff43c6fee93
Partial-Bug: #1708549
2017-12-04 10:17:30 -05:00

928 lines
44 KiB
Python

# -*- encoding: utf-8 -*-
#
# 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 /volume targets/ methods.
"""
import datetime
import mock
from oslo_config import cfg
from oslo_utils import timeutils
from oslo_utils import uuidutils
import six
from six.moves import http_client
from six.moves.urllib import parse as urlparse
from wsme import types as wtypes
from ironic.api.controllers import base as api_base
from ironic.api.controllers import v1 as api_v1
from ironic.api.controllers.v1 import notification_utils
from ironic.api.controllers.v1 import utils as api_utils
from ironic.api.controllers.v1 import volume_target as api_volume_target
from ironic.common import exception
from ironic.conductor import rpcapi
from ironic import objects
from ironic.objects import fields as obj_fields
from ironic.tests import base
from ironic.tests.unit.api import base as test_api_base
from ironic.tests.unit.api import utils as apiutils
from ironic.tests.unit.db import utils as dbutils
from ironic.tests.unit.objects import utils as obj_utils
def post_get_test_volume_target(**kw):
target = apiutils.volume_target_post_data(**kw)
node = dbutils.get_test_node()
target['node_uuid'] = kw.get('node_uuid', node['uuid'])
return target
class TestVolumeTargetObject(base.TestCase):
def test_volume_target_init(self):
target_dict = apiutils.volume_target_post_data(node_id=None)
del target_dict['extra']
target = api_volume_target.VolumeTarget(**target_dict)
self.assertEqual(wtypes.Unset, target.extra)
class TestListVolumeTargets(test_api_base.BaseApiTest):
headers = {api_base.Version.string: str(api_v1.max_version())}
def setUp(self):
super(TestListVolumeTargets, self).setUp()
self.node = obj_utils.create_test_node(self.context)
def test_empty(self):
data = self.get_json('/volume/targets', headers=self.headers)
self.assertEqual([], data['targets'])
def test_one(self):
target = obj_utils.create_test_volume_target(
self.context, node_id=self.node.id)
data = self.get_json('/volume/targets', headers=self.headers)
self.assertEqual(target.uuid, data['targets'][0]["uuid"])
self.assertNotIn('extra', data['targets'][0])
# never expose the node_id
self.assertNotIn('node_id', data['targets'][0])
def test_one_invalid_api_version(self):
obj_utils.create_test_volume_target(
self.context, node_id=self.node.id)
response = self.get_json(
'/volume/targets',
headers={api_base.Version.string: str(api_v1.min_version())},
expect_errors=True)
self.assertEqual(http_client.NOT_FOUND, response.status_int)
def test_get_one(self):
target = obj_utils.create_test_volume_target(
self.context, node_id=self.node.id)
data = self.get_json('/volume/targets/%s' % target.uuid,
headers=self.headers)
self.assertEqual(target.uuid, data['uuid'])
self.assertIn('extra', data)
self.assertIn('node_uuid', data)
# never expose the node_id
self.assertNotIn('node_id', data)
def test_get_one_invalid_api_version(self):
target = obj_utils.create_test_volume_target(self.context,
node_id=self.node.id)
response = self.get_json(
'/volume/targets/%s' % target.uuid,
headers={api_base.Version.string: str(api_v1.min_version())},
expect_errors=True)
self.assertEqual(http_client.NOT_FOUND, response.status_int)
def test_get_one_custom_fields(self):
target = obj_utils.create_test_volume_target(
self.context, node_id=self.node.id)
fields = 'boot_index,extra'
data = self.get_json(
'/volume/targets/%s?fields=%s' % (target.uuid, fields),
headers=self.headers)
# We always append "links"
self.assertItemsEqual(['boot_index', 'extra', 'links'], data)
def test_get_collection_custom_fields(self):
fields = 'uuid,extra'
for i in range(3):
obj_utils.create_test_volume_target(
self.context, node_id=self.node.id,
uuid=uuidutils.generate_uuid(), boot_index=i)
data = self.get_json(
'/volume/targets?fields=%s' % fields,
headers=self.headers)
self.assertEqual(3, len(data['targets']))
for target in data['targets']:
# We always append "links"
self.assertItemsEqual(['uuid', 'extra', 'links'], target)
def test_get_custom_fields_invalid_fields(self):
target = obj_utils.create_test_volume_target(
self.context, node_id=self.node.id)
fields = 'uuid,spongebob'
response = self.get_json(
'/volume/targets/%s?fields=%s' % (target.uuid, fields),
headers=self.headers, 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_detail(self):
target = obj_utils.create_test_volume_target(
self.context, node_id=self.node.id)
data = self.get_json('/volume/targets?detail=True',
headers=self.headers)
self.assertEqual(target.uuid, data['targets'][0]["uuid"])
self.assertIn('extra', data['targets'][0])
self.assertIn('node_uuid', data['targets'][0])
# never expose the node_id
self.assertNotIn('node_id', data['targets'][0])
def test_detail_false(self):
target = obj_utils.create_test_volume_target(
self.context, node_id=self.node.id)
data = self.get_json('/volume/targets?detail=False',
headers=self.headers)
self.assertEqual(target.uuid, data['targets'][0]["uuid"])
self.assertNotIn('extra', data['targets'][0])
# never expose the node_id
self.assertNotIn('node_id', data['targets'][0])
def test_detail_invalid_api_version(self):
obj_utils.create_test_volume_target(self.context,
node_id=self.node.id)
response = self.get_json(
'/volume/targets?detail=True',
headers={api_base.Version.string: str(api_v1.min_version())},
expect_errors=True)
self.assertEqual(http_client.NOT_FOUND, response.status_int)
def test_detail_sepecified_by_path(self):
obj_utils.create_test_volume_target(self.context,
node_id=self.node.id)
response = self.get_json(
'/volume/targets/detail', headers=self.headers,
expect_errors=True)
self.assertEqual(http_client.BAD_REQUEST, response.status_int)
def test_detail_against_single(self):
target = obj_utils.create_test_volume_target(
self.context, node_id=self.node.id)
response = self.get_json('/volume/targets/%s?detail=True'
% target.uuid,
headers=self.headers,
expect_errors=True)
self.assertEqual(http_client.BAD_REQUEST, response.status_int)
def test_detail_and_fields(self):
target = obj_utils.create_test_volume_target(
self.context, node_id=self.node.id)
fields = 'boot_index,extra'
response = self.get_json('/volume/targets/%s?detail=True&fields=%s'
% (target.uuid, fields),
headers=self.headers,
expect_errors=True)
self.assertEqual(http_client.BAD_REQUEST, response.status_int)
def test_many(self):
targets = []
for id_ in range(5):
target = obj_utils.create_test_volume_target(
self.context, node_id=self.node.id,
uuid=uuidutils.generate_uuid(), boot_index=id_)
targets.append(target.uuid)
data = self.get_json('/volume/targets', headers=self.headers)
self.assertEqual(len(targets), len(data['targets']))
uuids = [n['uuid'] for n in data['targets']]
six.assertCountEqual(self, targets, uuids)
def test_links(self):
uuid = uuidutils.generate_uuid()
obj_utils.create_test_volume_target(self.context,
uuid=uuid,
node_id=self.node.id)
data = self.get_json('/volume/targets/%s' % uuid,
headers=self.headers)
self.assertIn('links', data.keys())
self.assertEqual(2, len(data['links']))
self.assertIn(uuid, data['links'][0]['href'])
for l in data['links']:
bookmark = l['rel'] == 'bookmark'
self.assertTrue(self.validate_link(l['href'], bookmark=bookmark,
headers=self.headers))
def test_collection_links(self):
targets = []
for id_ in range(5):
target = obj_utils.create_test_volume_target(
self.context, node_id=self.node.id,
uuid=uuidutils.generate_uuid(), boot_index=id_)
targets.append(target.uuid)
data = self.get_json('/volume/targets/?limit=3', headers=self.headers)
self.assertEqual(3, len(data['targets']))
next_marker = data['targets'][-1]['uuid']
self.assertIn(next_marker, data['next'])
self.assertIn('volume/targets', data['next'])
def test_collection_links_default_limit(self):
cfg.CONF.set_override('max_limit', 3, 'api')
targets = []
for id_ in range(5):
target = obj_utils.create_test_volume_target(
self.context, node_id=self.node.id,
uuid=uuidutils.generate_uuid(), boot_index=id_)
targets.append(target.uuid)
data = self.get_json('/volume/targets', headers=self.headers)
self.assertEqual(3, len(data['targets']))
next_marker = data['targets'][-1]['uuid']
self.assertIn(next_marker, data['next'])
self.assertIn('volume/targets', data['next'])
def test_collection_links_detail(self):
targets = []
for id_ in range(5):
target = obj_utils.create_test_volume_target(
self.context, node_id=self.node.id,
uuid=uuidutils.generate_uuid(), boot_index=id_)
targets.append(target.uuid)
data = self.get_json('/volume/targets?detail=True&limit=3',
headers=self.headers)
self.assertEqual(3, len(data['targets']))
next_marker = data['targets'][-1]['uuid']
self.assertIn(next_marker, data['next'])
self.assertIn('volume/targets', data['next'])
self.assertIn('detail=True', data['next'])
def test_sort_key(self):
targets = []
for id_ in range(3):
target = obj_utils.create_test_volume_target(
self.context, node_id=self.node.id,
uuid=uuidutils.generate_uuid(), boot_index=id_)
targets.append(target.uuid)
data = self.get_json('/volume/targets?sort_key=uuid',
headers=self.headers)
uuids = [n['uuid'] for n in data['targets']]
self.assertEqual(sorted(targets), uuids)
def test_sort_key_invalid(self):
invalid_keys_list = ['foo', 'extra', 'properties']
for invalid_key in invalid_keys_list:
response = self.get_json('/volume/targets?sort_key=%s'
% invalid_key,
headers=self.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'])
@mock.patch.object(api_utils, 'get_rpc_node')
def test_get_all_by_node_name_ok(self, mock_get_rpc_node):
# GET /v1/volume/targets specifying node_name - success
mock_get_rpc_node.return_value = self.node
for i in range(5):
if i < 3:
node_id = self.node.id
else:
node_id = 100000 + i
obj_utils.create_test_volume_target(
self.context, node_id=node_id,
uuid=uuidutils.generate_uuid(), boot_index=i)
data = self.get_json("/volume/targets?node=%s" % 'test-node',
headers=self.headers)
self.assertEqual(3, len(data['targets']))
@mock.patch.object(api_utils, 'get_rpc_node')
def test_detail_by_node_name_ok(self, mock_get_rpc_node):
# GET /v1/volume/targets/?detail=True specifying node_name - success
mock_get_rpc_node.return_value = self.node
target = obj_utils.create_test_volume_target(
self.context, node_id=self.node.id)
data = self.get_json('/volume/targets?detail=True&node=%s' %
'test-node',
headers=self.headers)
self.assertEqual(target.uuid, data['targets'][0]['uuid'])
self.assertEqual(self.node.uuid, data['targets'][0]['node_uuid'])
@mock.patch.object(rpcapi.ConductorAPI, 'update_volume_target')
class TestPatch(test_api_base.BaseApiTest):
headers = {api_base.Version.string: str(api_v1.max_version())}
def setUp(self):
super(TestPatch, self).setUp()
self.node = obj_utils.create_test_node(self.context)
self.target = obj_utils.create_test_volume_target(
self.context, node_id=self.node.id)
p = mock.patch.object(rpcapi.ConductorAPI, 'get_topic_for')
self.mock_gtf = p.start()
self.mock_gtf.return_value = 'test-topic'
self.addCleanup(p.stop)
@mock.patch.object(notification_utils, '_emit_api_notification')
def test_update_byid(self, mock_notify, mock_upd):
extra = {'foo': 'bar'}
mock_upd.return_value = self.target
mock_upd.return_value.extra = extra
response = self.patch_json('/volume/targets/%s'
% self.target.uuid,
[{'path': '/extra/foo',
'value': 'bar',
'op': 'add'}],
headers=self.headers)
self.assertEqual('application/json', response.content_type)
self.assertEqual(http_client.OK, response.status_code)
self.assertEqual(extra, response.json['extra'])
kargs = mock_upd.call_args[0][1]
self.assertEqual(extra, kargs.extra)
mock_notify.assert_has_calls([mock.call(mock.ANY, mock.ANY, 'update',
obj_fields.NotificationLevel.INFO,
obj_fields.NotificationStatus.START,
node_uuid=self.node.uuid),
mock.call(mock.ANY, mock.ANY, 'update',
obj_fields.NotificationLevel.INFO,
obj_fields.NotificationStatus.END,
node_uuid=self.node.uuid)])
def test_update_byid_invalid_api_version(self, mock_upd):
headers = {api_base.Version.string: str(api_v1.min_version())}
response = self.patch_json('/volume/targets/%s'
% self.target.uuid,
[{'path': '/extra/foo',
'value': 'bar',
'op': 'add'}],
headers=headers,
expect_errors=True)
self.assertEqual(http_client.NOT_FOUND, response.status_int)
def test_update_not_found(self, mock_upd):
uuid = uuidutils.generate_uuid()
response = self.patch_json('/volume/targets/%s' % uuid,
[{'path': '/extra/foo',
'value': 'bar',
'op': 'add'}],
headers=self.headers,
expect_errors=True)
self.assertEqual('application/json', response.content_type)
self.assertEqual(http_client.NOT_FOUND, response.status_int)
self.assertTrue(response.json['error_message'])
self.assertFalse(mock_upd.called)
def test_replace_singular(self, mock_upd):
boot_index = 100
mock_upd.return_value = self.target
mock_upd.return_value.boot_index = boot_index
response = self.patch_json('/volume/targets/%s' % self.target.uuid,
[{'path': '/boot_index',
'value': boot_index,
'op': 'replace'}],
headers=self.headers)
self.assertEqual('application/json', response.content_type)
self.assertEqual(http_client.OK, response.status_code)
self.assertEqual(boot_index, response.json['boot_index'])
self.assertTrue(mock_upd.called)
kargs = mock_upd.call_args[0][1]
self.assertEqual(boot_index, kargs.boot_index)
@mock.patch.object(notification_utils, '_emit_api_notification')
def test_replace_boot_index_already_exist(self, mock_notify, mock_upd):
boot_index = 100
mock_upd.side_effect = \
exception.VolumeTargetBootIndexAlreadyExists(boot_index=boot_index)
response = self.patch_json('/volume/targets/%s'
% self.target.uuid,
[{'path': '/boot_index',
'value': boot_index,
'op': 'replace'}],
expect_errors=True, headers=self.headers)
self.assertEqual('application/json', response.content_type)
self.assertEqual(http_client.CONFLICT, response.status_code)
self.assertTrue(response.json['error_message'])
self.assertTrue(mock_upd.called)
kargs = mock_upd.call_args[0][1]
self.assertEqual(boot_index, kargs.boot_index)
mock_notify.assert_has_calls([mock.call(mock.ANY, mock.ANY, 'update',
obj_fields.NotificationLevel.INFO,
obj_fields.NotificationStatus.START,
node_uuid=self.node.uuid),
mock.call(mock.ANY, mock.ANY, 'update',
obj_fields.NotificationLevel.ERROR,
obj_fields.NotificationStatus.ERROR,
node_uuid=self.node.uuid)])
def test_replace_invalid_power_state(self, mock_upd):
mock_upd.side_effect = \
exception.InvalidStateRequested(
action='volume target update', node=self.node.uuid,
state='power on')
response = self.patch_json('/volume/targets/%s'
% self.target.uuid,
[{'path': '/boot_index',
'value': 0,
'op': 'replace'}],
expect_errors=True, headers=self.headers)
self.assertEqual('application/json', response.content_type)
self.assertEqual(http_client.BAD_REQUEST, response.status_code)
self.assertTrue(response.json['error_message'])
self.assertTrue(mock_upd.called)
kargs = mock_upd.call_args[0][1]
self.assertEqual(0, kargs.boot_index)
def test_replace_node_uuid(self, mock_upd):
mock_upd.return_value = self.target
response = self.patch_json('/volume/targets/%s'
% self.target.uuid,
[{'path': '/node_uuid',
'value': self.node.uuid,
'op': 'replace'}],
headers=self.headers)
self.assertEqual('application/json', response.content_type)
self.assertEqual(http_client.OK, response.status_code)
def test_replace_node_uuid_inalid_type(self, mock_upd):
response = self.patch_json('/volume/targets/%s'
% self.target.uuid,
[{'path': '/node_uuid',
'value': 123,
'op': 'replace'}],
expect_errors=True, headers=self.headers)
self.assertEqual('application/json', response.content_type)
self.assertEqual(http_client.BAD_REQUEST, response.status_code)
self.assertIn(b'Expected a UUID for node_uuid, but received 123.',
response.body)
self.assertFalse(mock_upd.called)
def test_add_node_uuid(self, mock_upd):
mock_upd.return_value = self.target
response = self.patch_json('/volume/targets/%s'
% self.target.uuid,
[{'path': '/node_uuid',
'value': self.node.uuid,
'op': 'add'}],
headers=self.headers)
self.assertEqual('application/json', response.content_type)
self.assertEqual(http_client.OK, response.status_code)
def test_add_node_uuid_invalid_type(self, mock_upd):
response = self.patch_json('/volume/targets/%s'
% self.target.uuid,
[{'path': '/node_uuid',
'value': 123,
'op': 'add'}],
expect_errors=True, headers=self.headers)
self.assertEqual('application/json', response.content_type)
self.assertEqual(http_client.BAD_REQUEST, response.status_code)
self.assertIn(b'Expected a UUID for node_uuid, but received 123.',
response.body)
self.assertFalse(mock_upd.called)
def test_add_node_id(self, mock_upd):
response = self.patch_json('/volume/targets/%s'
% self.target.uuid,
[{'path': '/node_id',
'value': '1',
'op': 'add'}],
headers=self.headers,
expect_errors=True)
self.assertEqual('application/json', response.content_type)
self.assertEqual(http_client.BAD_REQUEST, response.status_code)
self.assertFalse(mock_upd.called)
def test_replace_node_id(self, mock_upd):
response = self.patch_json('/volume/targets/%s'
% self.target.uuid,
[{'path': '/node_id',
'value': '1',
'op': 'replace'}],
headers=self.headers,
expect_errors=True)
self.assertEqual('application/json', response.content_type)
self.assertEqual(http_client.BAD_REQUEST, response.status_code)
self.assertFalse(mock_upd.called)
def test_remove_node_id(self, mock_upd):
response = self.patch_json('/volume/targets/%s'
% self.target.uuid,
[{'path': '/node_id',
'op': 'remove'}],
headers=self.headers,
expect_errors=True)
self.assertEqual('application/json', response.content_type)
self.assertEqual(http_client.BAD_REQUEST, response.status_code)
self.assertFalse(mock_upd.called)
def test_replace_non_existent_node_uuid(self, mock_upd):
node_uuid = '12506333-a81c-4d59-9987-889ed5f8687b'
response = self.patch_json('/volume/targets/%s'
% self.target.uuid,
[{'path': '/node_uuid',
'value': node_uuid,
'op': 'replace'}],
headers=self.headers,
expect_errors=True)
self.assertEqual('application/json', response.content_type)
self.assertEqual(http_client.BAD_REQUEST, response.status_code)
self.assertIn(node_uuid, response.json['error_message'])
self.assertFalse(mock_upd.called)
def test_replace_multi(self, mock_upd):
extra = {"foo1": "bar1", "foo2": "bar2", "foo3": "bar3"}
self.target.extra = extra
self.target.save()
# mutate extra so we replace all of them
extra = dict((k, extra[k] + 'x') for k in extra.keys())
patch = []
for k in extra.keys():
patch.append({'path': '/extra/%s' % k,
'value': extra[k],
'op': 'replace'})
mock_upd.return_value = self.target
mock_upd.return_value.extra = extra
response = self.patch_json('/volume/targets/%s'
% self.target.uuid,
patch,
headers=self.headers)
self.assertEqual('application/json', response.content_type)
self.assertEqual(http_client.OK, response.status_code)
self.assertEqual(extra, response.json['extra'])
kargs = mock_upd.call_args[0][1]
self.assertEqual(extra, kargs.extra)
def test_remove_multi(self, mock_upd):
extra = {"foo1": "bar1", "foo2": "bar2", "foo3": "bar3"}
self.target.extra = extra
self.target.save()
# Remove one item from the collection.
extra.pop('foo1')
mock_upd.return_value = self.target
mock_upd.return_value.extra = extra
response = self.patch_json('/volume/targets/%s'
% self.target.uuid,
[{'path': '/extra/foo1',
'op': 'remove'}],
headers=self.headers)
self.assertEqual('application/json', response.content_type)
self.assertEqual(http_client.OK, response.status_code)
self.assertEqual(extra, response.json['extra'])
kargs = mock_upd.call_args[0][1]
self.assertEqual(extra, kargs.extra)
# Remove the collection.
extra = {}
mock_upd.return_value.extra = extra
response = self.patch_json('/volume/targets/%s'
% self.target.uuid,
[{'path': '/extra', 'op': 'remove'}],
headers=self.headers)
self.assertEqual('application/json', response.content_type)
self.assertEqual(http_client.OK, response.status_code)
self.assertEqual({}, response.json['extra'])
kargs = mock_upd.call_args[0][1]
self.assertEqual(extra, kargs.extra)
# Assert nothing else was changed.
self.assertEqual(self.target.uuid, response.json['uuid'])
self.assertEqual(self.target.volume_type,
response.json['volume_type'])
self.assertEqual(self.target.boot_index, response.json['boot_index'])
def test_remove_non_existent_property_fail(self, mock_upd):
response = self.patch_json('/volume/targets/%s'
% self.target.uuid,
[{'path': '/extra/non-existent',
'op': 'remove'}],
headers=self.headers,
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'])
self.assertFalse(mock_upd.called)
def test_remove_mandatory_field(self, mock_upd):
response = self.patch_json('/volume/targets/%s'
% self.target.uuid,
[{'path': '/boot_index',
'op': 'remove'}],
headers=self.headers,
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'])
self.assertFalse(mock_upd.called)
def test_add_root(self, mock_upd):
boot_index = 100
mock_upd.return_value = self.target
mock_upd.return_value.boot_index = boot_index
response = self.patch_json('/volume/targets/%s'
% self.target.uuid,
[{'path': '/boot_index',
'value': boot_index,
'op': 'add'}],
headers=self.headers)
self.assertEqual('application/json', response.content_type)
self.assertEqual(http_client.OK, response.status_code)
self.assertEqual(boot_index, response.json['boot_index'])
self.assertTrue(mock_upd.called)
kargs = mock_upd.call_args[0][1]
self.assertEqual(boot_index, kargs.boot_index)
def test_add_root_non_existent(self, mock_upd):
response = self.patch_json('/volume/targets/%s'
% self.target.uuid,
[{'path': '/foo',
'value': 'bar',
'op': 'add'}],
headers=self.headers,
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'])
self.assertFalse(mock_upd.called)
def test_add_multi(self, mock_upd):
extra = {"foo1": "bar1", "foo2": "bar2", "foo3": "bar3"}
patch = []
for k in extra.keys():
patch.append({'path': '/extra/%s' % k,
'value': extra[k],
'op': 'add'})
mock_upd.return_value = self.target
mock_upd.return_value.extra = extra
response = self.patch_json('/volume/targets/%s'
% self.target.uuid,
patch,
headers=self.headers)
self.assertEqual('application/json', response.content_type)
self.assertEqual(http_client.OK, response.status_code)
self.assertEqual(extra, response.json['extra'])
kargs = mock_upd.call_args[0][1]
self.assertEqual(extra, kargs.extra)
def test_remove_uuid(self, mock_upd):
response = self.patch_json('/volume/targets/%s'
% self.target.uuid,
[{'path': '/uuid',
'op': 'remove'}],
headers=self.headers,
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'])
self.assertFalse(mock_upd.called)
class TestPost(test_api_base.BaseApiTest):
headers = {api_base.Version.string: str(api_v1.max_version())}
def setUp(self):
super(TestPost, self).setUp()
self.node = obj_utils.create_test_node(self.context)
@mock.patch.object(notification_utils, '_emit_api_notification')
@mock.patch.object(timeutils, 'utcnow')
def test_create_volume_target(self, mock_utcnow, mock_notify):
pdict = post_get_test_volume_target()
test_time = datetime.datetime(2000, 1, 1, 0, 0)
mock_utcnow.return_value = test_time
response = self.post_json('/volume/targets', pdict,
headers=self.headers)
self.assertEqual(http_client.CREATED, response.status_int)
result = self.get_json('/volume/targets/%s' % pdict['uuid'],
headers=self.headers)
self.assertEqual(pdict['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/volume/targets/%s' % pdict['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,
node_uuid=self.node.uuid),
mock.call(mock.ANY, mock.ANY, 'create',
obj_fields.NotificationLevel.INFO,
obj_fields.NotificationStatus.END,
node_uuid=self.node.uuid)])
def test_create_volume_target_invalid_api_version(self):
pdict = post_get_test_volume_target()
response = self.post_json(
'/volume/targets', pdict,
headers={api_base.Version.string: str(api_v1.min_version())},
expect_errors=True)
self.assertEqual(http_client.NOT_FOUND, response.status_int)
def test_create_volume_target_doesnt_contain_id(self):
with mock.patch.object(
self.dbapi, 'create_volume_target',
wraps=self.dbapi.create_volume_target) as cp_mock:
pdict = post_get_test_volume_target(extra={'foo': 123})
self.post_json('/volume/targets', pdict,
headers=self.headers)
result = self.get_json('/volume/targets/%s' % pdict['uuid'],
headers=self.headers)
self.assertEqual(pdict['extra'], result['extra'])
cp_mock.assert_called_once_with(mock.ANY)
# Check that 'id' is not in first arg of positional args.
self.assertNotIn('id', cp_mock.call_args[0][0])
@mock.patch.object(notification_utils.LOG, 'exception', autospec=True)
@mock.patch.object(notification_utils.LOG, 'warning', autospec=True)
def test_create_volume_target_generate_uuid(self, mock_warning,
mock_exception):
pdict = post_get_test_volume_target()
del pdict['uuid']
response = self.post_json('/volume/targets', pdict,
headers=self.headers)
result = self.get_json('/volume/targets/%s' % response.json['uuid'],
headers=self.headers)
self.assertEqual(pdict['boot_index'], result['boot_index'])
self.assertTrue(uuidutils.is_uuid_like(result['uuid']))
self.assertFalse(mock_warning.called)
self.assertFalse(mock_exception.called)
@mock.patch.object(notification_utils, '_emit_api_notification')
@mock.patch.object(objects.VolumeTarget, 'create')
def test_create_volume_target_error(self, mock_create, mock_notify):
mock_create.side_effect = Exception()
tdict = post_get_test_volume_target()
self.post_json('/volume/targets', tdict, headers=self.headers,
expect_errors=True)
mock_notify.assert_has_calls([mock.call(mock.ANY, mock.ANY, 'create',
obj_fields.NotificationLevel.INFO,
obj_fields.NotificationStatus.START,
node_uuid=self.node.uuid),
mock.call(mock.ANY, mock.ANY, 'create',
obj_fields.NotificationLevel.ERROR,
obj_fields.NotificationStatus.ERROR,
node_uuid=self.node.uuid)])
def test_create_volume_target_valid_extra(self):
pdict = post_get_test_volume_target(
extra={'str': 'foo', 'int': 123, 'float': 0.1, 'bool': True,
'list': [1, 2], 'none': None, 'dict': {'cat': 'meow'}})
self.post_json('/volume/targets', pdict, headers=self.headers)
result = self.get_json('/volume/targets/%s' % pdict['uuid'],
headers=self.headers)
self.assertEqual(pdict['extra'], result['extra'])
def test_create_volume_target_no_mandatory_field_type(self):
pdict = post_get_test_volume_target()
del pdict['volume_type']
response = self.post_json('/volume/targets', pdict,
headers=self.headers,
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_volume_target_no_mandatory_field_value(self):
pdict = post_get_test_volume_target()
del pdict['boot_index']
response = self.post_json('/volume/targets', pdict,
headers=self.headers,
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_volume_target_no_mandatory_field_node_uuid(self):
pdict = post_get_test_volume_target()
del pdict['node_uuid']
response = self.post_json('/volume/targets', pdict,
headers=self.headers,
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_volume_target_invalid_node_uuid_format(self):
pdict = post_get_test_volume_target(node_uuid=123)
response = self.post_json('/volume/targets', pdict,
headers=self.headers,
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'])
self.assertIn(b'Expected a UUID but received 123.', response.body)
def test_node_uuid_to_node_id_mapping(self):
pdict = post_get_test_volume_target(node_uuid=self.node['uuid'])
self.post_json('/volume/targets', pdict, headers=self.headers)
# GET doesn't return the node_id it's an internal value
target = self.dbapi.get_volume_target_by_uuid(pdict['uuid'])
self.assertEqual(self.node['id'], target.node_id)
def test_create_volume_target_node_uuid_not_found(self):
pdict = post_get_test_volume_target(
node_uuid='1a1a1a1a-2b2b-3c3c-4d4d-5e5e5e5e5e5e')
response = self.post_json('/volume/targets', pdict,
headers=self.headers,
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, 'destroy_volume_target')
class TestDelete(test_api_base.BaseApiTest):
headers = {api_base.Version.string: str(api_v1.max_version())}
def setUp(self):
super(TestDelete, self).setUp()
self.node = obj_utils.create_test_node(self.context)
self.target = obj_utils.create_test_volume_target(
self.context, node_id=self.node.id)
gtf = mock.patch.object(rpcapi.ConductorAPI, 'get_topic_for')
self.mock_gtf = gtf.start()
self.mock_gtf.return_value = 'test-topic'
self.addCleanup(gtf.stop)
@mock.patch.object(notification_utils, '_emit_api_notification')
def test_delete_volume_target_byid(self, mock_notify, mock_dvc):
self.delete('/volume/targets/%s' % self.target.uuid,
headers=self.headers,
expect_errors=True)
self.assertTrue(mock_dvc.called)
mock_notify.assert_has_calls([mock.call(mock.ANY, mock.ANY, 'delete',
obj_fields.NotificationLevel.INFO,
obj_fields.NotificationStatus.START,
node_uuid=self.node.uuid),
mock.call(mock.ANY, mock.ANY, 'delete',
obj_fields.NotificationLevel.INFO,
obj_fields.NotificationStatus.END,
node_uuid=self.node.uuid)])
def test_delete_volume_target_byid_invalid_api_version(self, mock_dvc):
headers = {api_base.Version.string: str(api_v1.min_version())}
response = self.delete('/volume/targets/%s' % self.target.uuid,
headers=headers,
expect_errors=True)
self.assertEqual(http_client.NOT_FOUND, response.status_int)
@mock.patch.object(notification_utils, '_emit_api_notification')
def test_delete_volume_target_node_locked(self, mock_notify, mock_dvc):
self.node.reserve(self.context, 'fake', self.node.uuid)
mock_dvc.side_effect = exception.NodeLocked(node='fake-node',
host='fake-host')
ret = self.delete('/volume/targets/%s' % self.target.uuid,
headers=self.headers,
expect_errors=True)
self.assertEqual(http_client.CONFLICT, ret.status_code)
self.assertTrue(ret.json['error_message'])
self.assertTrue(mock_dvc.called)
mock_notify.assert_has_calls([mock.call(mock.ANY, mock.ANY, 'delete',
obj_fields.NotificationLevel.INFO,
obj_fields.NotificationStatus.START,
node_uuid=self.node.uuid),
mock.call(mock.ANY, mock.ANY, 'delete',
obj_fields.NotificationLevel.ERROR,
obj_fields.NotificationStatus.ERROR,
node_uuid=self.node.uuid)])
def test_delete_volume_target_invalid_power_state(self, mock_dvc):
mock_dvc.side_effect = exception.InvalidStateRequested(
action='volume target deletion', node=self.node.uuid,
state='power on')
ret = self.delete('/volume/targets/%s' % self.target.uuid,
headers=self.headers,
expect_errors=True)
self.assertEqual(http_client.BAD_REQUEST, ret.status_code)
self.assertTrue(ret.json['error_message'])
self.assertTrue(mock_dvc.called)