text: #    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')
  
          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.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)
  