fuel-web/nailgun/nailgun/test/unit/test_task.py

678 lines
25 KiB
Python

# -*- coding: utf-8 -*-
# Copyright 2013 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
from oslo_serialization import jsonutils
import yaml
from nailgun import consts
from nailgun.db.sqlalchemy.models import Task
from nailgun.errors import errors
from nailgun.extensions.volume_manager.manager import VolumeManager
from nailgun import objects
from nailgun.task import task
from nailgun.test.base import BaseTestCase
from nailgun.utils import reverse
class TestClusterDeletionTask(BaseTestCase):
def create_cluster_and_execute_deletion_task(
self, attributes=None, os=consts.RELEASE_OS.centos):
self.env.create(
cluster_kwargs={
'editable_attributes': attributes,
},
release_kwargs={
'operating_system': os,
'version': '2025-7.0',
},
)
self.fake_task = Task(name=consts.TASK_NAMES.cluster_deletion,
cluster=self.env.clusters[0])
task.ClusterDeletionTask.execute(self.fake_task)
@mock.patch('nailgun.task.task.DeletionTask', autospec=True)
@mock.patch.object(task.DeleteIBPImagesTask, 'execute')
def test_target_images_deletion_skipped_empty_attributes(
self, mock_img_task, mock_del):
self.create_cluster_and_execute_deletion_task({})
self.assertTrue(mock_del.execute.called)
self.assertFalse(mock_img_task.called)
@mock.patch('nailgun.task.task.DeletionTask', autospec=True)
@mock.patch.object(task.DeleteIBPImagesTask, 'execute')
def test_target_images_deletion_skipped_os_centos(
self, mock_img_task, mock_del):
attributes = {'provision': {
'method': consts.PROVISION_METHODS.image,
}}
self.create_cluster_and_execute_deletion_task(attributes)
self.assertTrue(mock_del.execute.called)
self.assertFalse(mock_img_task.called)
@mock.patch('nailgun.task.task.DeletionTask', autospec=True)
@mock.patch.object(task.DeleteIBPImagesTask, 'execute')
def test_target_images_deletion_skipped_os_ubuntu_cobbler(
self, mock_img_task, mock_del):
os = consts.RELEASE_OS.ubuntu
attributes = {'provision': {
'method': consts.PROVISION_METHODS.cobbler,
}}
self.create_cluster_and_execute_deletion_task(attributes, os)
self.assertTrue(mock_del.execute.called)
self.assertFalse(mock_img_task.called)
@mock.patch('nailgun.task.task.DeletionTask', autospec=True)
@mock.patch.object(task.DeleteIBPImagesTask, 'execute')
def test_target_images_deletion_executed(self, mock_img_task, mock_del):
os = consts.RELEASE_OS.ubuntu
attributes = {'provision': {
'method': consts.PROVISION_METHODS.image,
}}
self.create_cluster_and_execute_deletion_task(attributes, os)
self.assertTrue(mock_del.execute.called)
self.assertTrue(mock_img_task.called)
fake_attrs = objects.Attributes.merged_attrs_values(
self.fake_task.cluster.attributes)
mock_img_task.assert_called_once_with(
mock.ANY, fake_attrs['provision']['image_data'])
class TestDeleteIBPImagesTask(BaseTestCase):
@mock.patch('nailgun.task.task.settings')
@mock.patch('nailgun.task.task.make_astute_message')
def test_message(self, mock_astute, mock_settings):
mock_settings.PROVISIONING_IMAGES_PATH = '/fake/path'
mock_settings.REMOVE_IMAGES_TIMEOUT = 'fake_timeout'
task_mock = mock.Mock()
task_mock.cluster.id = '123'
task_mock.uuid = 'fake_uuid'
fake_image_data = {'/': {'uri': 'http://a.b/fake.img'},
'/boot': {'uri': 'http://c.d/fake-boot.img'}}
task.DeleteIBPImagesTask.message(task_mock, fake_image_data)
rpc_message = mock_astute.call_args[0][3]
rm_cmd = rpc_message['tasks'][0]['parameters'].pop('cmd')
mock_astute.assert_called_once_with(
mock.ANY, 'execute_tasks', 'remove_images_resp', mock.ANY)
self.assertEqual(rpc_message, {
'tasks': [{
'type': 'shell',
'uids': [consts.MASTER_ROLE],
'parameters': {
'retries': 3,
'cwd': '/',
'timeout': 'fake_timeout',
'interval': 1}}]})
self.assertTrue(rm_cmd.startswith('rm -f'))
self.assertIn('/fake/path/fake-boot.img', rm_cmd)
self.assertIn('/fake/path/fake.img', rm_cmd)
class TestHelperUpdateClusterStatus(BaseTestCase):
def setUp(self):
super(TestHelperUpdateClusterStatus, self).setUp()
self.env.create(
nodes_kwargs=[
{'roles': ['controller']},
{'roles': ['compute', 'virt']},
{'roles': ['cinder']}])
def node_should_be_error_with_type(self, node, error_type):
self.assertEqual(node.status, 'error')
self.assertEqual(node.error_type, error_type)
self.assertEqual(node.progress, 0)
def nodes_should_not_be_error(self, nodes):
for node in nodes:
self.assertEqual(node.status, 'discover')
@property
def cluster(self):
return self.env.clusters[0]
def test_update_nodes_to_error_if_deployment_task_failed(self):
self.cluster.nodes[0].status = 'deploying'
self.cluster.nodes[0].progress = 12
deployment_task = Task(name='deployment', cluster=self.cluster,
status='error')
self.db.add(deployment_task)
self.db.commit()
objects.Task._update_cluster_data(deployment_task)
self.db.flush()
self.assertEqual(self.cluster.status, 'error')
self.node_should_be_error_with_type(self.cluster.nodes[0], 'deploy')
self.nodes_should_not_be_error(self.cluster.nodes[1:])
def test_update_cluster_to_error_if_deploy_task_failed(self):
deploy_task = Task(name='deploy', cluster=self.cluster, status='error')
self.db.add(deploy_task)
self.db.commit()
objects.Task._update_cluster_data(deploy_task)
self.db.flush()
self.assertEqual(self.cluster.status, 'error')
def test_update_nodes_to_error_if_provision_task_failed(self):
self.cluster.nodes[0].status = 'provisioning'
self.cluster.nodes[0].progress = 12
provision_task = Task(name='provision', cluster=self.cluster,
status='error')
self.db.add(provision_task)
self.db.commit()
objects.Task._update_cluster_data(provision_task)
self.db.flush()
self.assertEqual(self.cluster.status, 'error')
self.node_should_be_error_with_type(self.cluster.nodes[0], 'provision')
self.nodes_should_not_be_error(self.cluster.nodes[1:])
def test_update_cluster_to_operational(self):
deploy_task = Task(name='deploy', cluster=self.cluster, status='ready')
self.db.add(deploy_task)
self.db.commit()
objects.Task._update_cluster_data(deploy_task)
self.db.flush()
self.assertEqual(self.cluster.status, 'operational')
def test_update_if_parent_task_is_ready_all_nodes_should_be_ready(self):
for node in self.cluster.nodes:
node.status = 'ready'
node.progress = 100
self.cluster.nodes[0].status = 'deploying'
self.cluster.nodes[0].progress = 24
deploy_task = Task(name='deploy', cluster=self.cluster, status='ready')
self.db.add(deploy_task)
self.db.commit()
objects.Task._update_cluster_data(deploy_task)
self.db.flush()
self.assertEqual(self.cluster.status, 'operational')
for node in self.cluster.nodes:
self.assertEqual(node.status, 'ready')
self.assertEqual(node.progress, 100)
def test_update_cluster_status_if_task_was_already_in_error_status(self):
for node in self.cluster.nodes:
node.status = 'provisioning'
node.progress = 12
provision_task = Task(name='provision', cluster=self.cluster,
status='error')
self.db.add(provision_task)
self.db.commit()
data = {'status': 'error', 'progress': 100}
objects.Task.update(provision_task, data)
self.db.flush()
self.assertEqual(self.cluster.status, 'error')
self.assertEqual(provision_task.status, 'error')
for node in self.cluster.nodes:
self.assertEqual(node.status, 'error')
self.assertEqual(node.progress, 0)
def test_do_not_set_cluster_to_error_if_validation_failed(self):
for task_name in ['check_before_deployment', 'check_networks']:
supertask = Task(
name='deploy',
cluster=self.cluster,
status='error')
check_task = Task(
name=task_name,
cluster=self.cluster,
status='error')
supertask.subtasks.append(check_task)
self.db.add(check_task)
self.db.commit()
objects.Task._update_cluster_data(supertask)
self.db.flush()
self.assertEqual(self.cluster.status, 'new')
class TestCheckBeforeDeploymentTask(BaseTestCase):
def setUp(self):
super(TestCheckBeforeDeploymentTask, self).setUp()
self.env.create(
release_kwargs={'version': '1111-8.0'},
cluster_kwargs={
'net_provider': 'neutron',
'net_segment_type': 'gre'
},
nodes_kwargs=[{'roles': ['controller']}])
self.env.create_node()
self.node = self.env.nodes[0]
self.cluster = self.env.clusters[0]
self.task = Task(cluster_id=self.env.clusters[0].id)
self.env.db.add(self.task)
self.env.db.commit()
def set_node_status(self, status):
self.node.status = status
self.env.db.commit()
self.assertEqual(self.node.status, status)
def set_node_error_type(self, error_type):
self.node.error_type = error_type
self.env.db.commit()
self.assertEqual(self.node.error_type, error_type)
def is_checking_required(self):
return task.CheckBeforeDeploymentTask._is_disk_checking_required(
self.node)
def test_is_disk_checking_required(self):
self.set_node_status('ready')
self.assertFalse(self.is_checking_required())
self.set_node_status('deploying')
self.assertFalse(self.is_checking_required())
self.set_node_status('discover')
self.assertTrue(self.is_checking_required())
self.set_node_status('provisioned')
self.assertFalse(self.is_checking_required())
def test_is_disk_checking_required_in_case_of_error(self):
self.set_node_status('error')
self.set_node_error_type('provision')
self.assertTrue(self.is_checking_required())
self.set_node_error_type('deploy')
self.assertFalse(self.is_checking_required())
def test_check_volumes_and_disks_do_not_run_if_node_ready(self):
self.set_node_status('ready')
with mock.patch.object(
VolumeManager,
'check_disk_space_for_deployment') as check_mock:
task.CheckBeforeDeploymentTask._check_disks(self.task)
self.assertFalse(check_mock.called)
with mock.patch.object(
VolumeManager,
'check_volume_sizes_for_deployment') as check_mock:
task.CheckBeforeDeploymentTask._check_volumes(self.task)
self.assertFalse(check_mock.called)
def test_check_volumes_and_disks_run_if_node_not_ready(self):
self.set_node_status('discover')
with mock.patch.object(
VolumeManager,
'check_disk_space_for_deployment') as check_mock:
task.CheckBeforeDeploymentTask._check_disks(self.task)
self.assertEqual(check_mock.call_count, 1)
with mock.patch.object(
VolumeManager,
'check_volume_sizes_for_deployment') as check_mock:
task.CheckBeforeDeploymentTask._check_volumes(self.task)
self.assertEqual(check_mock.call_count, 1)
def test_check_nodes_online_raises_exception(self):
self.node.online = False
self.env.db.commit()
self.assertRaises(
errors.NodeOffline,
task.CheckBeforeDeploymentTask._check_nodes_are_online,
self.task)
def test_check_nodes_online_do_not_raise_exception_node_to_deletion(self):
self.node.online = False
self.node.pending_deletion = True
self.env.db.commit()
task.CheckBeforeDeploymentTask._check_nodes_are_online(self.task)
def test_check_controllers_count_operational_cluster(self):
self.cluster.status = consts.CLUSTER_STATUSES.operational
# remove old controller and add new one
self.node.pending_deletion = True
new_controller = self.env.create_node()
new_controller.pendint_addition = True
self.assertRaises(
errors.NotEnoughControllers,
task.CheckBeforeDeploymentTask._check_controllers_count,
self.task)
def test_check_controllers_count_new_cluster(self):
self.cluster.status = consts.CLUSTER_STATUSES.new
# check there's not exceptions with one controller
self.assertNotRaises(
errors.NotEnoughControllers,
task.CheckBeforeDeploymentTask._check_controllers_count,
self.task)
# check there's exception with one non-controller node
self.node.roles = ['compute']
self.env.db.flush()
self.assertRaises(
errors.NotEnoughControllers,
task.CheckBeforeDeploymentTask._check_controllers_count,
self.task)
def find_net_by_name(self, nets, name):
for net in nets['networks']:
if net['name'] == name:
return net
def test_network_template_validation(self):
net_template = self.env.read_fixtures(['network_template'])[0]
objects.Cluster.set_network_template(
self.cluster,
net_template
)
self.assertNotRaises(
errors.NetworkTemplateMissingRoles,
task.CheckBeforeDeploymentTask._validate_network_template,
self.task)
ceph_node = self.env.create_node(roles=['ceph-osd'],
cluster_id=self.cluster.id)
self.assertRaises(
errors.NetworkTemplateMissingRoles,
task.CheckBeforeDeploymentTask._validate_network_template,
self.task)
objects.Node.delete(ceph_node)
del (net_template['adv_net_template']['default']
['network_scheme']['common']['roles']['murano/api'])
objects.Cluster.set_network_template(
self.cluster,
net_template
)
self.assertRaisesRegexp(
errors.NetworkTemplateMissingNetRoles,
"Network roles murano/api are missing",
task.CheckBeforeDeploymentTask._validate_network_template,
self.task)
def test_missing_network_group_with_template(self):
net_template = self.env.read_fixtures(['network_template'])[0]
objects.Cluster.set_network_template(
self.cluster,
net_template
)
public = [n for n in self.cluster.network_groups
if n.name == consts.NETWORKS.public][0]
self.env._delete_network_group(public.id)
self.assertRaisesRegexp(
errors.NetworkTemplateMissingNetworkGroup,
"The following network groups are missing: public",
task.CheckBeforeDeploymentTask._validate_network_template,
self.task)
def test_missing_network_group_with_template_multi_ng(self):
net_template = self.env.read_fixtures(['network_template'])[0]
resp = self.env.create_node_group(name='group-custom-1',
cluster_id=self.cluster.id)
del self.cluster.nodes[0]
ng = objects.NodeGroup.get_by_uid(resp.json_body['id'])
self.env.create_nodes_w_interfaces_count(
1, 5,
roles=['controller'],
cluster_id=self.cluster.id,
group_id=ng.id
)
objects.Cluster.set_network_template(
self.cluster,
net_template
)
public = [n for n in ng.networks
if n.name == consts.NETWORKS.public][0]
self.env._delete_network_group(public.id)
self.assertRaisesRegexp(
errors.NetworkTemplateMissingNetworkGroup,
("The following network groups are missing: public "
".* group-custom-1"),
task.CheckBeforeDeploymentTask._validate_network_template,
self.task)
def test_check_public_networks(self):
cluster = self.env.clusters[0]
self.env.create_nodes(
2, api=True, roles=['controller'], cluster_id=cluster.id)
self.env.create_nodes(
2, api=True, roles=['compute'], cluster_id=cluster.id)
# we have 3 controllers now
self.assertEqual(
sum('controller' in n.all_roles for n in self.env.nodes),
3
)
attrs = cluster.attributes.editable
self.assertEqual(
attrs['public_network_assignment']['assign_to_all_nodes']['value'],
False
)
self.assertFalse(
objects.Cluster.should_assign_public_to_all_nodes(cluster))
resp = self.env.neutron_networks_get(cluster.id)
nets = resp.json_body
# not enough IPs for 3 nodes and 2 VIPs
self.find_net_by_name(nets, 'public')['ip_ranges'] = \
[["172.16.0.2", "172.16.0.5"]]
resp = self.env.neutron_networks_put(cluster.id, nets)
self.assertEqual(resp.status_code, 200)
self.assertRaises(
errors.NetworkCheckError,
task.CheckBeforeDeploymentTask._check_public_network,
self.task)
# enough IPs for 3 nodes and 2 VIPs
self.find_net_by_name(nets, 'public')['ip_ranges'] = \
[["172.16.0.2", "172.16.0.6"]]
resp = self.env.neutron_networks_put(cluster.id, nets)
self.assertEqual(resp.status_code, 200)
self.assertNotRaises(
errors.NetworkCheckError,
task.CheckBeforeDeploymentTask._check_public_network,
self.task)
attrs['public_network_assignment']['assign_to_all_nodes']['value'] = \
True
resp = self.app.patch(
reverse(
'ClusterAttributesHandler',
kwargs={'cluster_id': cluster.id}),
params=jsonutils.dumps({'editable': attrs}),
headers=self.default_headers
)
self.assertEqual(200, resp.status_code)
self.assertTrue(
objects.Cluster.should_assign_public_to_all_nodes(cluster))
self.assertRaises(
errors.NetworkCheckError,
task.CheckBeforeDeploymentTask._check_public_network,
self.task)
def test_check_deployment_graph_with_correct_data(self):
correct_yaml_tasks = """
- id: test-controller
type: group
role: [test-controller]
requires: [primary-controller]
required_for: [deploy_end]
parameters:
strategy:
type: parallel
amount: 2
"""
tasks = yaml.load(correct_yaml_tasks)
deployment_tasks = objects.Cluster.get_deployment_tasks(self.cluster)
deployment_tasks.extend(tasks)
objects.Cluster.update(
self.cluster,
{'deployment_tasks': deployment_tasks})
task.CheckBeforeDeploymentTask.\
_check_deployment_graph_for_correctness(
self.task)
def test_check_deployment_graph_with_incorrect_dependencies_data(self):
incorrect_dependencies_yaml_tasks = """
- id: test-controller
type: group
role: [primary-controller]
required_for: [non_existing_stage]
parameters:
strategy:
type: one_by_one
"""
tasks = yaml.load(incorrect_dependencies_yaml_tasks)
deployment_tasks = objects.Cluster.get_deployment_tasks(self.cluster)
deployment_tasks.extend(tasks)
objects.Cluster.update(
self.cluster,
{'deployment_tasks': deployment_tasks})
with self.assertRaisesRegexp(
errors.InvalidData,
"Tasks 'non_existing_stage' can't be in requires|required_for|"
"groups|tasks for \['test-controller'\] because they don't "
"exist in the graph"):
task.CheckBeforeDeploymentTask.\
_check_deployment_graph_for_correctness(
self.task)
def test_check_deployment_graph_with_cycling_dependencies_data(self):
incorrect_cycle_yaml_tasks = """
- id: test-controller-1
type: role
requires: [test-controller-2]
- id: test-controller-2
type: role
requires: [test-controller-1]
"""
tasks = yaml.load(incorrect_cycle_yaml_tasks)
deployment_tasks = objects.Cluster.get_deployment_tasks(self.cluster)
deployment_tasks.extend(tasks)
objects.Cluster.update(
self.cluster,
{'deployment_tasks': deployment_tasks})
with self.assertRaisesRegexp(
errors.InvalidData,
"Tasks can not be processed because it contains cycles in it"):
task.CheckBeforeDeploymentTask.\
_check_deployment_graph_for_correctness(
self.task)
class TestDeployTask(BaseTestCase):
def create_deploy_tasks(self):
self.env.create()
cluster = self.env.clusters[0]
deploy_task = Task(name=consts.TASK_NAMES.deploy,
cluster_id=cluster.id,
status=consts.TASK_STATUSES.pending)
self.db.add(deploy_task)
self.db.flush()
provision_task = Task(name=consts.TASK_NAMES.provision,
status=consts.TASK_STATUSES.pending,
parent_id=deploy_task.id, cluster_id=cluster.id)
self.db.add(provision_task)
deployment_task = Task(name=consts.TASK_NAMES.deployment,
status=consts.TASK_STATUSES.pending,
parent_id=deploy_task.id, cluster_id=cluster.id)
self.db.add(deployment_task)
self.db.flush()
return deploy_task, provision_task, deployment_task
def test_running_status_bubble_for_deploy_task(self):
deploy_task, provision_task, deployment_task = \
self.create_deploy_tasks()
objects.Task.update(provision_task,
{'status': consts.TASK_STATUSES.running})
# Only deploy and provision tasks are running now
self.assertEqual(consts.TASK_STATUSES.running, deploy_task.status)
self.assertEqual(consts.TASK_STATUSES.running, provision_task.status)
self.assertEqual(consts.TASK_STATUSES.pending, deployment_task.status)
def test_error_status_bubble_for_deploy_task(self):
deploy_task, provision_task, deployment_task = \
self.create_deploy_tasks()
objects.Task.update(provision_task,
{'status': consts.TASK_STATUSES.error})
# All tasks have error status
self.assertEqual(consts.TASK_STATUSES.error, deploy_task.status)
self.assertEqual(consts.TASK_STATUSES.error, provision_task.status)
self.assertEqual(consts.TASK_STATUSES.error, deployment_task.status)
def test_ready_status_bubble_for_deploy_task(self):
deploy_task, provision_task, deployment_task = \
self.create_deploy_tasks()
objects.Task.update(provision_task,
{'status': consts.TASK_STATUSES.ready})
# Not all child bugs in ready state
self.assertEqual(consts.TASK_STATUSES.running, deploy_task.status)
self.assertEqual(consts.TASK_STATUSES.ready, provision_task.status)
self.assertEqual(consts.TASK_STATUSES.pending, deployment_task.status)
# All child bugs in ready state
objects.Task.update(deployment_task,
{'status': consts.TASK_STATUSES.ready})
self.assertEqual(consts.TASK_STATUSES.ready, deploy_task.status)
self.assertEqual(consts.TASK_STATUSES.ready, provision_task.status)
self.assertEqual(consts.TASK_STATUSES.ready, deployment_task.status)