fuel-web/nailgun/nailgun/test/unit/test_openstack_config_handl...

510 lines
20 KiB
Python

# Copyright 2015 Mirantis, Inc.
#
# 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.
import mock
import six.moves.urllib.parse as urlparse
from oslo_serialization import jsonutils
from nailgun import consts
from nailgun.objects.serializers.openstack_config import \
OpenstackConfigSerializer
from nailgun.test.base import BaseIntegrationTest
from nailgun.utils import reverse
class TestOpenstackConfigHandlers(BaseIntegrationTest):
def setUp(self):
super(TestOpenstackConfigHandlers, self).setUp()
release_kwargs = {
'version': 'liberty-9.0',
'operating_system': consts.RELEASE_OS.ubuntu,
}
cluster_kwargs = {'net_provider': 'neutron'}
self.env.create_cluster(api=False,
status=consts.CLUSTER_STATUSES.operational,
release_kwargs=release_kwargs,
cluster_kwargs=cluster_kwargs)
self.env.create_cluster(api=False,
status=consts.CLUSTER_STATUSES.operational,
release_kwargs=release_kwargs,
cluster_kwargs=cluster_kwargs)
self.clusters = self.env.clusters
self.nodes = self.env.create_nodes(
3, cluster_id=self.clusters[0].id,
roles=["compute"],
status=consts.NODE_STATUSES.ready)
self.env.create_openstack_config(
cluster_id=self.clusters[0].id, configuration={})
self.env.create_openstack_config(
cluster_id=self.clusters[0].id, node_id=self.nodes[1].id,
configuration={
'nova_config': 'value_inactive'
})
self.env.create_openstack_config(
cluster_id=self.clusters[0].id, node_id=self.nodes[1].id,
configuration={
'nova_config': 'value_1_1'
})
self.env.create_openstack_config(
cluster_id=self.clusters[0].id, node_id=self.nodes[2].id,
configuration={
'nova_config': 'value_2_1'
})
self.configs = self.env.openstack_configs
def create_running_deployment_task(self):
return self.env.create_task(
cluster_id=self.clusters[0].id,
name=consts.TASK_NAMES.deployment,
status=consts.TASK_STATUSES.running
).id
def check_fail_deploy_running(self, deploy_task_id, resp):
self.assertEqual(resp.status_code, 403)
self.assertEqual("Cannot perform the action because there are "
"running deployment tasks '{0}'"
"".format(deploy_task_id), resp.json_body['message'])
def test_openstack_config_upload_new(self):
data = {
'cluster_id': self.clusters[0].id,
'node_id': self.nodes[0].id,
'configuration': {}
}
resp = self.app.post(
reverse('OpenstackConfigCollectionHandler'),
jsonutils.dumps(data),
headers=self.default_headers)
self.assertEqual(resp.status_code, 201)
config = resp.json_body[0]
self.assertEqual(config['cluster_id'], self.clusters[0].id)
self.assertEqual(config['node_id'], self.nodes[0].id)
def test_openstack_config_upload_new_multinode(self):
data = {
'cluster_id': self.clusters[0].id,
'node_ids': [self.nodes[0].id, self.nodes[1].id],
'configuration': {}
}
resp = self.app.post(
reverse('OpenstackConfigCollectionHandler'),
jsonutils.dumps(data),
headers=self.default_headers)
self.assertEqual(resp.status_code, 201)
configs = resp.json_body
self.assertEqual(len(configs), 2)
self.assertEqual(configs[0]['node_id'], self.nodes[0].id)
self.assertEqual(configs[1]['node_id'], self.nodes[1].id)
def test_openstack_config_upload_override(self):
data = {
'cluster_id': self.clusters[0].id,
'node_id': self.nodes[1].id,
'configuration': {
'nova_config': 'value_1_2'
}
}
resp = self.app.post(
reverse('OpenstackConfigCollectionHandler'),
jsonutils.dumps(data),
headers=self.default_headers)
self.assertEqual(resp.status_code, 201)
config = resp.json_body[0]
self.assertEqual(config['cluster_id'], self.clusters[0].id)
self.assertEqual(config['node_id'], self.nodes[1].id)
resp = self.app.get(
reverse('OpenstackConfigHandler',
{'obj_id': self.configs[1].id}),
headers=self.default_headers)
self.assertEqual(resp.status_code, 200)
self.assertEqual(resp.json_body['is_active'], False)
def test_openstack_config_upload_override_multinode(self):
data = {
'cluster_id': self.clusters[0].id,
'node_ids': [self.nodes[1].id, self.nodes[2].id],
'configuration': {
'nova_config': 'overridden_value'
}
}
resp = self.app.post(
reverse('OpenstackConfigCollectionHandler'),
jsonutils.dumps(data),
headers=self.default_headers)
self.assertEqual(resp.status_code, 201)
configs = resp.json_body
self.assertEqual(configs[0]['node_id'], self.nodes[1].id)
self.assertEqual(configs[1]['node_id'], self.nodes[2].id)
resp = self.app.get(
reverse('OpenstackConfigHandler',
{'obj_id': self.configs[1].id}),
headers=self.default_headers)
self.assertEqual(resp.status_code, 200)
self.assertEqual(resp.json_body['is_active'], False)
resp = self.app.get(
reverse('OpenstackConfigHandler',
{'obj_id': self.configs[2].id}),
headers=self.default_headers)
self.assertEqual(resp.status_code, 200)
self.assertEqual(resp.json_body['is_active'], False)
def test_openstack_config_upload_fail(self):
data = {
'cluster_id': self.clusters[1].id,
'node_id': self.nodes[1].id,
'configuration': {}
}
resp = self.app.post(
reverse('OpenstackConfigCollectionHandler'),
jsonutils.dumps(data),
headers=self.default_headers, expect_errors=True)
self.assertEqual(resp.status_code, 400)
self.assertEqual(
resp.json_body['message'],
"Nodes '{0}' are not assigned to cluster '{1}'".format(
self.nodes[1].id, self.clusters[1].id))
def test_openstack_config_upload_fail_not_supported_config(self):
"""Test for uploading an update for not supported OpenStack config"""
data = {
'cluster_id': self.clusters[0].id,
'node_id': self.nodes[0].id,
'configuration': {
'not_supported_config': {}
}
}
resp = self.app.post(
reverse('OpenstackConfigCollectionHandler'),
jsonutils.dumps(data),
headers=self.default_headers,
expect_errors=True)
self.assertEqual(resp.status_code, 400)
self.assertRegexpMatches(
resp.json_body['message'],
r"Configurations '\w+' can not be updated")
def test_openstack_config_upload_fail_deploy_running(self):
deploy_task_id = self.create_running_deployment_task()
data = {
'cluster_id': self.clusters[0].id,
'configuration': {}
}
resp = self.app.post(
reverse('OpenstackConfigCollectionHandler'),
jsonutils.dumps(data),
headers=self.default_headers,
expect_errors=True)
self.check_fail_deploy_running(deploy_task_id, resp)
def test_openstack_config_list(self):
# List all configurations for cluster
url = self._make_filter_url(cluster_id=self.clusters[0].id)
resp = self.app.get(url, headers=self.default_headers)
self.assertEqual(resp.status_code, 200)
self.assertEqual(len(resp.json_body), 3)
# List all configurations for specific node
url = self._make_filter_url(
cluster_id=self.clusters[0].id, node_id=self.nodes[1].id)
resp = self.app.get(url, headers=self.default_headers)
self.assertEqual(resp.status_code, 200)
self.assertEqual(len(resp.json_body), 1)
# List all inactive configurations for cluster
url = self._make_filter_url(
cluster_id=self.clusters[0].id, is_active=0)
resp = self.app.get(url, headers=self.default_headers)
self.assertEqual(resp.status_code, 200)
self.assertEqual(len(resp.json_body), 1)
self.assertFalse(resp.json_body[0]['is_active'])
# Check there is no configurations for second cluster
url = self._make_filter_url(cluster_id=self.clusters[1].id)
resp = self.app.get(url, headers=self.default_headers)
self.assertEqual(len(resp.json_body), 0)
def test_openstack_config_list_fail(self):
url = self._make_filter_url(
cluster_id=self.clusters[0].id, node_id=self.nodes[0].id,
node_role='controller')
resp = self.app.get(url, headers=self.default_headers,
expect_errors=True)
self.assertEqual(resp.status_code, 400)
self.assertRegexpMatches(
resp.json_body['message'],
r"Parameter '\w+' conflicts with '\w+(, \w+)*'")
def test_openstack_config_list_invalid_params(self):
for param in ['cluster_id', 'node_id', 'is_active']:
url = self._make_filter_url(**{param: 'invalidvalue'})
resp = self.app.get(url, headers=self.default_headers,
expect_errors=True)
self.assertEqual(resp.status_code, 400)
self.assertEqual(
resp.json_body['message'],
"Invalid '{0}' value: 'invalidvalue'".format(param))
def test_openstack_config_get(self):
resp = self.app.get(
reverse('OpenstackConfigHandler',
{'obj_id': self.configs[0].id}),
headers=self.default_headers)
self.assertEqual(resp.status_code, 200)
config = resp.json_body
self.assertDictContainsSubset({
'cluster_id': self.configs[0].cluster_id,
'node_id': self.configs[0].node_id,
'node_role': self.configs[0].node_role,
}, config)
self.assertEqual(sorted(config.keys()),
sorted(OpenstackConfigSerializer.fields))
def test_openstack_config_put(self):
resp = self.app.put(
reverse('OpenstackConfigHandler',
{'obj_id': self.configs[0].id}),
expect_errors=True)
self.assertEqual(resp.status_code, 405)
@mock.patch('objects.Cluster.get_deployment_tasks')
def execute_update_open_stack_config(
self, tasks_mock, expect_errors=False, **kwargs):
tasks_mock.return_value = [{
'id': 'upload_configuration',
'type': 'upload_file',
'version': '2.0.0',
'role': '*',
'parameters': {
'timeout': 180,
},
'refresh_on': ['*']
}]
data = {'cluster_id': self.clusters[0].id}
data.update(kwargs)
resp = self.app.put(
reverse('OpenstackConfigExecuteHandler'),
jsonutils.dumps(data),
headers=self.default_headers,
expect_errors=expect_errors
)
return resp
@mock.patch('nailgun.task.task.rpc.cast')
def test_openstack_config_execute_with_granular_deploy(self, mock_rpc):
self.env.disable_task_deploy(self.clusters[0])
resp = self.execute_update_open_stack_config()
self.assertEqual(resp.status_code, 202)
message = mock_rpc.call_args_list[0][0][1]
self.assertEqual('execute_tasks', message['method'])
self.assertEqual('update_config_resp', message['respond_to'])
# there is no task deduplication in granular deployment
# and some of tasks can be included
# to result list more than 1 times
self.assertItemsEqual(
((n.uid, 'upload_file') for n in self.clusters[0].nodes),
{(t['uids'][0], t['type']) for t in message['args']['tasks']}
)
node_1_upload_config = (
t['parameters']['data'] for t in message['args']['tasks']
if self.nodes[1].uid in t['uids']
)
self.assertItemsEqual(
[
'configuration: {}\n',
'configuration: {nova_config: value_1_1}\n'
],
node_1_upload_config
)
@mock.patch('nailgun.task.task.rpc.cast')
def test_openstack_config_execute_with_task_deploy(self, mock_rpc):
resp = self.execute_update_open_stack_config()
self.assertEqual(resp.status_code, 202)
message = mock_rpc.call_args_list[0][0][1]
self.assertEqual('task_deploy', message['method'])
self.assertEqual('update_config_resp', message['respond_to'])
tasks_graph = message['args']['tasks_graph']
tasks_directory = message['args']['tasks_directory']
nodes = [n.uid for n in self.clusters[0].nodes]
nodes.append(None)
self.assertItemsEqual(nodes, tasks_graph)
self.assertEqual(
'upload_file',
tasks_graph[nodes[0]][0]['type']
)
node_1_upload_config = (
tasks_directory[t['id']]['parameters']['data']
for t in tasks_graph[nodes[1]]
)
self.assertItemsEqual(
[
'configuration: {}\n',
'configuration: {nova_config: value_1_1}\n'
],
node_1_upload_config
)
@mock.patch('objects.OpenstackConfigCollection.find_configs_for_nodes')
def test_openstack_config_successfully_exit_if_no_changes(self, m_conf):
m_conf.return_value = []
resp = self.execute_update_open_stack_config()
self.assertEqual(200, resp.status_code)
self.assertEqual(consts.TASK_STATUSES.ready, resp.json_body['status'])
@mock.patch('nailgun.task.task.rpc.cast')
def test_openstack_config_execute_force(self, _):
# Turn node 2 into provisioned state
self.env.nodes[2].status = consts.NODE_STATUSES.provisioned
# need to persistent state in database because handler will revert
# all changes on error.
self.db.commit()
# Try to update OpenStack configuration for cluster
resp = self.execute_update_open_stack_config(expect_errors=True)
# Request shouldn't pass a validation
self.assertEqual(resp.status_code, 400)
self.assertEqual("Nodes '{0}' are not in status 'ready' and "
"can not be updated directly."
"".format(self.env.nodes[2].uid),
resp.json_body['message'])
# Try to update OpenStack configuration for cluster with 'force' key
resp = self.execute_update_open_stack_config(force=True)
# Update OpenStack configuration executed successfully
self.assertEqual(resp.status_code, 202)
def test_openstack_config_execute_fail_cluster_not_operational(self):
self.clusters[0].status = consts.CLUSTER_STATUSES.error
self.db.flush()
resp = self.execute_update_open_stack_config(expect_errors=True)
self.assertEqual(resp.status_code, 400)
self.assertEqual(resp.json_body['message'],
"Cluster should be in the status 'operational'")
def test_openstack_config_execute_fail_deploy_running(self):
deploy_task_id = self.create_running_deployment_task()
resp = self.execute_update_open_stack_config(expect_errors=True)
self.check_fail_deploy_running(deploy_task_id, resp)
def test_openstack_config_execute_fail_no_ready_nodes(self):
# Turn node 0 into provisioned state
self.env.nodes[0].status = consts.NODE_STATUSES.provisioned
self.env.nodes[1].status = consts.NODE_STATUSES.provisioned
self.env.nodes[2].status = consts.NODE_STATUSES.provisioned
self.db.flush()
# Try to update configuration for node 0
resp = self.execute_update_open_stack_config(expect_errors=True)
# Request shouldn't pass a validation
self.assertEqual(resp.status_code, 400)
self.assertEqual("No nodes in status 'ready'",
resp.json_body['message'])
def test_openstack_config_execute_fail_not_existed_cluster(self):
# Try to update not existed cluster
resp = self.execute_update_open_stack_config(
expect_errors=True, cluster_id=-1
)
# Request shouldn't pass a validation
self.assertEqual(resp.status_code, 404)
self.assertEqual("Object 'Cluster' with UID=-1 is not found in DB",
resp.json_body['message'])
def test_openstack_config_delete(self):
obj_id = self.configs[0].id
resp = self.app.delete(
reverse('OpenstackConfigHandler',
{'obj_id': obj_id}),
expect_errors=True)
self.assertEqual(resp.status_code, 204)
resp = self.app.get(
reverse('OpenstackConfigHandler',
{'obj_id': obj_id}),
headers=self.default_headers)
self.assertEqual(resp.status_code, 200)
self.assertEqual(resp.json_body['is_active'], False)
# Try delete already deleted object
resp = self.app.delete(
reverse('OpenstackConfigHandler',
{'obj_id': obj_id}),
headers=self.default_headers, expect_errors=True)
self.assertEqual(resp.status_code, 400)
self.assertEqual(
resp.json_body['message'],
"Configuration '{0}' has been already disabled.".format(obj_id))
def test_openstack_config_delete_fail_deploy_running(self):
deploy_task_id = self.create_running_deployment_task()
resp = self.app.delete(
reverse('OpenstackConfigHandler',
{'obj_id': self.configs[0].id}),
expect_errors=True)
self.check_fail_deploy_running(deploy_task_id, resp)
@mock.patch('objects.Cluster.get_deployment_tasks')
@mock.patch('nailgun.task.task.rpc.cast')
def test_execute_update_open_stack_config_w_custom_graph(
self, mock_rpc, tasks_mock):
data = {'cluster_id': self.clusters[0].id}
resp = self.app.put(
reverse('OpenstackConfigExecuteHandler') +
'?graph_type=custom-graph',
jsonutils.dumps(data),
headers=self.default_headers,
expect_errors=False
)
self.assertEqual(202, resp.status_code)
self.assertIn('custom-graph', tasks_mock.call_args[0])
@classmethod
def _make_filter_url(cls, **kwargs):
return '{0}?{1}'.format(
reverse('OpenstackConfigCollectionHandler'),
urlparse.urlencode(kwargs))
@mock.patch('nailgun.task.task.rpc.cast', mock.MagicMock())
@mock.patch("nailgun.objects.Release.is_lcm_supported")
@mock.patch("nailgun.task.task.ClusterTransaction.message")
def test_openstack_config_call_apply_changes_if_lcm(
self, message_mock, lcm_supported_mock):
lcm_supported_mock.return_value = True
message_mock.return_value = {
'method': 'task_deploy', 'args': {
'tasks_graph': {'master': []}
}
}
self.execute_update_open_stack_config()
self.assertEqual(1, message_mock.call_count)