murano/murano/tests/unit/engine/system/test_agent.py

726 lines
26 KiB
Python

# Copyright (c) 2015 Telefonica I+D
# Copyright (c) 2016 AT&T Corp
#
# 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 copy
import datetime
import json
import os
import tempfile
import mock
from oslo_serialization import base64
import yaml as yamllib
from murano.common import exceptions
from murano.dsl import dsl
from murano.dsl import murano_object
from murano.dsl import murano_type
from murano.dsl import object_store
from murano.engine.system import agent
from murano.engine.system import resource_manager
from murano.tests.unit import base
class TestAgent(base.MuranoTestCase):
def setUp(self):
super(TestAgent, self).setUp()
if hasattr(yamllib, 'CSafeLoader'):
self.yaml_loader = yamllib.CSafeLoader
else:
self.yaml_loader = yamllib.SafeLoader
self.override_config('disable_murano_agent', False, group='engine')
self.override_config('signing_key', False, group='engine')
mock_host = mock.MagicMock()
mock_host.id = '1234'
mock_host.find_owner = lambda *args, **kwargs: mock_host
mock_host().getRegion.return_value = mock.MagicMock(
__class__=dsl.MuranoObjectInterface)
self.rabbit_mq_settings = {
'agentRabbitMq': {'login': 'test_login',
'password': 'test_password',
'host': 'test_host',
'port': 123,
'virtual_host': 'test_virtual_host'}
}
mock_host().getRegion()().getConfig.return_value =\
self.rabbit_mq_settings
self.agent = agent.Agent(mock_host)
self.resources = mock.Mock(spec=resource_manager.ResourceManager)
self.resources.string.return_value = 'text'
self.addCleanup(mock.patch.stopall)
def _read(self, path):
execution_plan_dir = os.path.abspath(
os.path.join(__file__, '../execution_plans/')
)
with open(execution_plan_dir + "/" + path) as file:
return file.read()
@mock.patch('murano.common.messaging.mqclient.kombu')
def test_send_creates_queue(self, mock_kombu):
self.agent.send_raw({})
# Verify that MQClient was instantiated, by checking whether
# kombu.Connection was called.
mock_kombu.Connection.assert_called_with(
'amqp://{login}:{password}@{host}:{port}/{virtual_host}'.format(
**self.rabbit_mq_settings['agentRabbitMq']
), ssl=None)
# Verify that client.declare() was called by checking whether kombu
# functions were called.
self.assertEqual(1, mock_kombu.Exchange.call_count)
self.assertEqual(1, mock_kombu.Queue.call_count)
@mock.patch('murano.engine.system.agent.LOG')
def test_send_with_murano_agent_disabled(self, mock_log):
self.override_config('disable_murano_agent', True, group='engine')
self.assertRaises(exceptions.PolicyViolationException,
self.agent.send_raw, {})
@mock.patch('murano.engine.system.agent.Agent._sign')
@mock.patch('murano.common.messaging.mqclient.kombu')
def test_send(self, mock_kombu, mock_sign):
template = yamllib.load(
self._read('template_with_files.template'),
Loader=self.yaml_loader)
self.agent._queue = 'test_queue'
mock_sign.return_value = 'SIGNATURE'
plan = self.agent.build_execution_plan(template, self.resources)
with mock.patch.object(self.agent, 'build_execution_plan',
return_value=plan):
self.agent.send(template, self.resources)
self.assertEqual(1, mock_kombu.Producer.call_count)
mock_kombu.Producer().publish.assert_called_once_with(
exchange='',
routing_key='test_queue',
body=json.dumps(plan),
message_id=plan['ID'],
headers={'signature': 'SIGNATURE'}
)
@mock.patch('murano.engine.system.agent.eventlet.event.Event')
@mock.patch('murano.common.messaging.mqclient.kombu')
def test_is_ready(self, mock_kombu, mock_event):
v2_result = yamllib.load(
self._read('application.template'),
Loader=self.yaml_loader)
mock_event().wait.side_effect = None
mock_event().wait.return_value = v2_result
self.assertTrue(self.agent.is_ready(1))
mock_event().wait.side_effect = agent.eventlet.Timeout
self.assertFalse(self.agent.is_ready(1))
@mock.patch('murano.engine.system.agent.Agent._sign')
@mock.patch('murano.engine.system.agent.eventlet.event.Event')
@mock.patch('murano.common.messaging.mqclient.kombu')
def test_call_with_v1_result(self, mock_kombu, mock_event, mock_sign):
template = yamllib.load(
self._read('template_with_files.template'),
Loader=self.yaml_loader)
test_v1_result = {
'FormatVersion': '1.0.0',
'IsException': False,
'Result': [
{
'IsException': False,
'Result': 'test_result'
}
]
}
mock_event().wait.side_effect = None
mock_event().wait.return_value = test_v1_result
mock_sign.return_value = 'SIGNATURE'
self.agent._queue = 'test_queue'
plan = self.agent.build_execution_plan(template, self.resources)
mock.patch.object(
self.agent, 'build_execution_plan', return_value=plan).start()
result = self.agent.call(template, self.resources, None)
self.assertIsNotNone(result)
self.assertEqual('test_result', result)
self.assertEqual(1, mock_kombu.Producer.call_count)
mock_kombu.Producer().publish.assert_called_once_with(
exchange='',
routing_key='test_queue',
body=json.dumps(plan),
message_id=plan['ID'],
headers={'signature': 'SIGNATURE'}
)
@mock.patch('murano.engine.system.agent.Agent._sign')
@mock.patch('murano.engine.system.agent.eventlet.event.Event')
@mock.patch('murano.common.messaging.mqclient.kombu')
def test_call_with_v2_result(self, mock_kombu, mock_event, mock_sign):
template = yamllib.load(
self._read('template_with_files.template'),
Loader=self.yaml_loader)
v2_result = yamllib.load(
self._read('application.template'),
Loader=self.yaml_loader)
mock_event().wait.side_effect = None
mock_event().wait.return_value = v2_result
mock_sign.return_value = 'SIGNATURE'
self.agent._queue = 'test_queue'
plan = self.agent.build_execution_plan(template, self.resources)
mock.patch.object(
self.agent, 'build_execution_plan', return_value=plan).start()
result = self.agent.call(template, self.resources, None)
self.assertIsNotNone(result)
self.assertEqual(v2_result['Body'], result)
self.assertEqual(1, mock_kombu.Producer.call_count)
mock_kombu.Producer().publish.assert_called_once_with(
exchange='',
routing_key='test_queue',
body=json.dumps(plan),
message_id=plan['ID'],
headers={'signature': 'SIGNATURE'}
)
@mock.patch('murano.engine.system.agent.eventlet.event.Event')
@mock.patch('murano.common.messaging.mqclient.kombu')
def test_call_with_no_result(self, mock_kombu, mock_event):
template = yamllib.load(
self._read('template_with_files.template'),
Loader=self.yaml_loader)
mock_event().wait.side_effect = None
mock_event().wait.return_value = None
result = self.agent.call(template, self.resources, None)
self.assertIsNone(result)
@mock.patch('murano.engine.system.agent.eventlet.event.Event')
@mock.patch('murano.common.messaging.mqclient.kombu')
def test_call_except_timeout(self, mock_kombu, mock_event):
self.override_config('agent_timeout', 1, group='engine')
mock_event().wait.side_effect = agent.eventlet.Timeout
template = yamllib.load(
self._read('template_with_files.template'),
Loader=self.yaml_loader)
expected_error_msg = 'The murano-agent did not respond within 1 '\
'seconds'
with self.assertRaisesRegex(exceptions.TimeoutException,
expected_error_msg):
self.agent.call(template, self.resources, None)
@mock.patch('murano.engine.system.agent.datetime')
def test_process_v1_result_with_error_code(self, mock_datetime):
now = datetime.datetime.now().isoformat()
mock_datetime.datetime.now().isoformat.return_value = now
v1_result = {
'IsException': True,
'Result': [
'Error Type',
'Error Message',
'Error Command',
'Error Details'
]
}
expected_error = {
'source': 'execution_plan',
'command': 'Error Command',
'details': 'Error Details',
'message': 'Error Message',
'type': 'Error Type',
'timestamp': now
}
self.assertTrue(self._are_exceptions_equal(
agent.AgentException, expected_error,
self.agent._process_v1_result, v1_result))
v1_result = {
'IsException': False,
'Result': [
'Error Type',
'Error Message',
'Error Command',
'Error Details',
{
'IsException': True,
'Result': [
'Nested Error Type',
'Nested Error Message',
'Nested Error Command',
'Nested Error Details'
]
}
]
}
expected_error = {
'source': 'command',
'command': 'Nested Error Command',
'details': 'Nested Error Details',
'message': 'Nested Error Message',
'type': 'Nested Error Type',
'timestamp': now
}
self.assertTrue(self._are_exceptions_equal(
agent.AgentException, expected_error,
self.agent._process_v1_result, v1_result))
def test_process_v2_result_with_error_code(self):
v2_result = {
'Body': {
'Message': 'Test Error Message',
'AdditionalInfo': 'Test Additional Info',
'ExtraAttr': 'Test extra data'
},
'FormatVersion': '2.0.0',
'Name': 'TestApp',
'ErrorCode': 123,
'Time': 'Right now'
}
expected_error = {
'errorCode': 123,
'message': 'Test Error Message',
'details': 'Test Additional Info',
'time': 'Right now',
'extra': {'ExtraAttr': 'Test extra data'}
}
self.assertTrue(self._are_exceptions_equal(
agent.AgentException, expected_error,
self.agent._process_v2_result, v2_result))
def _are_exceptions_equal(self, exception, expected_error, function,
result):
"""Checks whether expected and returned dict from exception are equal.
Because casting dicts to strings changes the ordering of the keys,
manual comparison of the result and expected result is performed.
"""
try:
# deepcopy must be performed because _process_v1_result
# deletes attrs from the original dict passed in.
self.assertRaises(exception, function, copy.deepcopy(result))
function(result)
except exception as e:
e_string = str(e).replace("'", "\"").replace('None', 'null')
e_dict = json.loads(e_string)
self.assertEqual(sorted(expected_error.keys()),
sorted(e_dict.keys()))
for key, val in expected_error.items():
self.assertEqual(val, e_dict[key])
except Exception:
return False
return True
class TestExecutionPlan(base.MuranoTestCase):
def setUp(self):
super(TestExecutionPlan, self).setUp()
if hasattr(yamllib, 'CSafeLoader'):
self.yaml_loader = yamllib.CSafeLoader
else:
self.yaml_loader = yamllib.SafeLoader
self.override_config('signing_key', False, group='engine')
self.mock_murano_class = mock.Mock(spec=murano_type.MuranoClass)
self.mock_murano_class.name = 'io.murano.system.Agent'
self.mock_murano_class.declared_parents = []
self.mock_object_store = mock.Mock(spec=object_store.ObjectStore)
object_interface = mock.Mock(spec=murano_object.MuranoObject)
object_interface.id = '1234'
object_interface.find_owner = lambda *args, **kwargs: object_interface
self.agent = agent.Agent(object_interface)
self.resources = mock.Mock(spec=resource_manager.ResourceManager)
self.resources.string.return_value = 'text'
self.uuids = ['ID1', 'ID2', 'ID3', 'ID4']
self.mock_uuid = self._stub_uuid(self.uuids)
time_mock = mock.patch('time.time').start()
time_mock.return_value = 2
self.addCleanup(mock.patch.stopall)
def _read(self, path):
execution_plan_dir = os.path.abspath(
os.path.join(__file__, '../execution_plans/')
)
with open(execution_plan_dir + "/" + path) as file:
return file.read()
def test_execution_plan_v2_application_type(self):
template = yamllib.load(
self._read('application.template'),
Loader=self.yaml_loader)
template = self.agent.build_execution_plan(template, self.resources)
self.assertEqual(self._get_application(), template)
def test_execution_plan_v2_chef_type(self):
template = yamllib.load(
self._read('chef.template'),
Loader=self.yaml_loader)
template = self.agent.build_execution_plan(template, self.resources)
self.assertEqual(self._get_chef(), template)
def test_execution_plan_v2_telnet_application(self):
template = yamllib.load(
self._read('DeployTelnet.template'),
Loader=self.yaml_loader)
template = self.agent.build_execution_plan(template, self.resources)
self.assertEqual(self._get_telnet_application(), template)
def test_execution_plan_v2_tomcat_application(self):
template = yamllib.load(
self._read('DeployTomcat.template'),
Loader=self.yaml_loader)
template = self.agent.build_execution_plan(template, self.resources)
def test_execution_plan_v2_app_without_files(self):
template = yamllib.load(
self._read('application_without_files.template'),
Loader=self.yaml_loader)
template = self.agent.build_execution_plan(template, self.resources)
self.assertEqual(self._get_app_without_files(), template)
def test_execution_plan_v2_app_with_file_in_template(self):
template = yamllib.load(
self._read('template_with_files.template'),
Loader=self.yaml_loader)
template = self.agent.build_execution_plan(template, self.resources)
self.assertEqual(self._get_app_with_files_in_template(), template)
def _get_application(self):
return {
'Action': 'Execute',
'Body': 'return deploy(args.appName).stdout\n',
'Files': {
self.uuids[1]: {
'Body': 'text',
'BodyType': 'Text',
'Name': 'deployTomcat.sh'
},
self.uuids[2]: {
'Body': 'dGV4dA==\n',
'BodyType': 'Base64',
'Name': 'installer'
},
self.uuids[3]: {
'Body': 'dGV4dA==\n',
'BodyType': 'Base64',
'Name': 'common.sh'
}
},
'FormatVersion': '2.0.0',
'ID': self.uuids[0],
'Stamp': 20000,
'Name': 'Deploy Tomcat',
'Parameters': {
'appName': '$appName'
},
'Scripts': {
'deploy': {
'EntryPoint': self.uuids[1],
'Files': [
self.uuids[2],
self.uuids[3]
],
'Options': {
'captureStderr': True,
'captureStdout': True
},
'Type': 'Application',
'Version': '1.0.0'
}
},
'Version': '1.0.0'
}
def _get_app_with_files_in_template(self):
return {
'Action': 'Execute',
'Body': 'return deploy(args.appName).stdout\n',
'Files': {
self.uuids[1]: {
'Body': 'text',
'BodyType': 'Text',
'Name': 'deployTomcat.sh'
},
'updateScript': {
'Body': 'text',
'BodyType': 'Text',
'Name': 'updateScript'
},
},
'FormatVersion': '2.0.0',
'ID': self.uuids[0],
'Stamp': 20000,
'Name': 'Deploy Tomcat',
'Parameters': {
'appName': '$appName'
},
'Scripts': {
'deploy': {
'EntryPoint': self.uuids[1],
'Files': [
'updateScript'
],
'Options': {
'captureStderr': True,
'captureStdout': True
},
'Type': 'Application',
'Version': '1.0.0'
}
},
'Version': '1.0.0'
}
def _get_app_without_files(self):
return {
'Action': 'Execute',
'Body': 'return deploy(args.appName).stdout\n',
'Files': {
self.uuids[1]: {
'Body': 'text',
'BodyType': 'Text',
'Name': 'deployTomcat.sh'
},
},
'FormatVersion': '2.0.0',
'ID': self.uuids[0],
'Stamp': 20000,
'Name': 'Deploy Tomcat',
'Parameters': {
'appName': '$appName'
},
'Scripts': {
'deploy': {
'EntryPoint': self.uuids[1],
'Files': [],
'Options': {
'captureStderr': True,
'captureStdout': True
},
'Type': 'Application',
'Version': '1.0.0'
}
},
'Version': '1.0.0'
}
def _get_chef(self):
return {
'Action': 'Execute',
'Body': 'return deploy(args.appName).stdout\n',
'Files': {
self.uuids[1]: {
'Name': 'tomcat.git',
'Type': 'Downloadable',
'URL': 'https://github.com/tomcat.git'
},
self.uuids[2]: {
'Name': 'java',
'Type': 'Downloadable',
'URL': 'https://github.com/java.git'
},
},
'FormatVersion': '2.0.0',
'ID': self.uuids[0],
'Stamp': 20000,
'Name': 'Deploy Chef',
'Parameters': {
'appName': '$appName'
},
'Scripts': {
'deploy': {
'EntryPoint': 'cookbook/recipe',
'Files': [
self.uuids[1],
self.uuids[2]
],
'Options': {
'captureStderr': True,
'captureStdout': True
},
'Type': 'Chef',
'Version': '1.0.0'
}
},
'Version': '1.0.0'
}
def _get_telnet_application(self):
return {
'Action': 'Execute',
'Body': 'return deploy(args.appName).stdout\n',
'Files': {
self.uuids[1]: {
'Body': 'text',
'BodyType': 'Text',
'Name': 'deployTelnet.sh'
},
self.uuids[2]: {
'Body': 'text',
'BodyType': 'Text',
'Name': 'installer.sh'
},
self.uuids[3]: {
'Body': 'text',
'BodyType': 'Text',
'Name': 'common.sh'
}
},
'FormatVersion': '2.0.0',
'ID': self.uuids[0],
'Stamp': 20000,
'Name': 'Deploy Telnet',
'Parameters': {
'appName': '$appName'
},
'Scripts': {
'deploy': {
'EntryPoint': self.uuids[1],
'Files': [
self.uuids[2],
self.uuids[3]
],
'Options': {
'captureStderr': True,
'captureStdout': True
},
'Type': 'Application',
'Version': '1.0.0'
}
},
'Version': '1.0.0'
}
def _stub_uuid(self, values=None):
class FakeUUID(object):
def __init__(self, v):
self.hex = v
if values is None:
values = []
mock_uuid4 = mock.patch('uuid.uuid4').start()
mock_uuid4.side_effect = [FakeUUID(v) for v in values]
return mock_uuid4
@mock.patch('murano.engine.system.resource_manager.ResourceManager'
'._get_package')
def test_file_line_endings(self, _get_package):
class FakeResources(object):
"""Class with only string() method from ResourceManager class"""
@staticmethod
def string(name, owner=None, binary=False):
return resource_manager.ResourceManager.string(
receiver=None, name=name, owner=owner, binary=binary)
# make path equal to provided name inside resources.string()
package = mock.Mock()
package.get_resource.side_effect = lambda m: m
_get_package.return_value = package
text = b"First line\nSecond line\rThird line\r\nFourth line"
modified_text = u"First line\nSecond line\nThird line\nFourth line"
encoded_text = base64.encode_as_text(text) + "\n"
resources = FakeResources()
with tempfile.NamedTemporaryFile() as script_file:
script_file.write(text)
script_file.file.flush()
os.fsync(script_file.file.fileno())
# check that data has been written correctly
script_file.seek(0)
file_data = script_file.read()
self.assertEqual(text, file_data)
# check resources.string() output
# text file
result = resources.string(script_file.name)
self.assertEqual(modified_text, result)
# binary file
result = resources.string(script_file.name, binary=True)
self.assertEqual(text, result)
# check _get_body() output
filename = os.path.basename(script_file.name)
folder = os.path.dirname(script_file.name)
# text file
body = self.agent._get_body(filename, resources, folder)
self.assertEqual(modified_text, body)
# binary file
filename = '<{0}>'.format(filename)
body = self.agent._get_body(filename, resources, folder)
self.assertEqual(encoded_text, body)
def test_queue_name(self):
self.agent._queue = 'test_queue'
self.assertEqual(self.agent.queue_name(), self.agent._queue)
def test_prepare_message(self):
template = {'test'}
msg_id = 12345
msg = self.agent._prepare_message(template, msg_id)
self.assertEqual(msg.id, msg_id)
self.assertEqual(msg._body, template)
def test_execution_plan_v1(self):
template = yamllib.load(
self._read('application.template'),
Loader=self.yaml_loader)
rtn_template = self.agent._build_v1_execution_plan(template,
self.resources)
self.assertEqual(template, rtn_template)
def test_get_array_item(self):
array = [1, 2, 3]
index = 2
self.assertEqual(array[2], self.agent._get_array_item(array, index))
index = 3
self.assertIsNone(self.agent._get_array_item(array, index))
def test_execution_plan_error(self):
template = None
self.assertRaises(ValueError,
self.agent.build_execution_plan,
template, self.resources)