bb2d0e5a84
Now all OpenStack resource classes inherit CloudResource that provides getRegion method and regionName property. This allows to assign resources to different regions. getRegion() returns CloudRegion instance that resource or it parent belong to. CloudRegion has the similar interface to Environment class and is the correct way to get HeatStack instance associated with the regoin, default network configuration, security group manager and agent listener instances. Environment acts as the default region so backward compatibility is not broken. However new applications should not use environment to set security group rules but rather a region(s) of their instance(s) in order to work correctly when their instances were configured to use region other then the default. Change-Id: I4dbf40c65042e9a354f3bfadfcd63a63e6e3e418
343 lines
11 KiB
Python
343 lines
11 KiB
Python
# Copyright (c) 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 copy
|
|
import datetime
|
|
import os
|
|
import uuid
|
|
|
|
import eventlet.event
|
|
from oslo_config import cfg
|
|
from oslo_log import log as logging
|
|
from oslo_serialization import base64
|
|
import six
|
|
from yaql import specs
|
|
|
|
import murano.common.exceptions as exceptions
|
|
from murano.common.messaging import message
|
|
from murano.dsl import dsl
|
|
import murano.engine.system.common as common
|
|
|
|
LOG = logging.getLogger(__name__)
|
|
CONF = cfg.CONF
|
|
|
|
|
|
class AgentException(Exception):
|
|
pass
|
|
|
|
|
|
@dsl.name('io.murano.system.Agent')
|
|
class Agent(object):
|
|
def __init__(self, host):
|
|
self._enabled = False
|
|
if CONF.engine.disable_murano_agent:
|
|
LOG.debug('Use of murano-agent is disallowed '
|
|
'by the server configuration')
|
|
return
|
|
|
|
self._host = host
|
|
self._enabled = True
|
|
env = host.find_owner('io.murano.Environment')
|
|
self._queue = str('e%s-h%s' % (env.id, host.id)).lower()
|
|
|
|
@property
|
|
def enabled(self):
|
|
return self._enabled
|
|
|
|
def prepare(self):
|
|
# (sjmc7) - turn this into a no-op if agents are disabled
|
|
if CONF.engine.disable_murano_agent:
|
|
LOG.debug('Use of murano-agent is disallowed '
|
|
'by the server configuration')
|
|
return
|
|
|
|
region = dsl.MuranoObjectInterface.create(self._host().getRegion())
|
|
with common.create_rmq_client(region) as client:
|
|
client.declare(self._queue, enable_ha=True, ttl=86400000)
|
|
|
|
def queue_name(self):
|
|
return self._queue
|
|
|
|
def _check_enabled(self):
|
|
if CONF.engine.disable_murano_agent:
|
|
raise exceptions.PolicyViolationException(
|
|
'Use of murano-agent is disallowed '
|
|
'by the server configuration')
|
|
|
|
def _prepare_message(self, template, msg_id):
|
|
msg = message.Message()
|
|
msg.body = template
|
|
msg.id = msg_id
|
|
return msg
|
|
|
|
def _send(self, template, wait_results, timeout):
|
|
"""Send a message over the MQ interface."""
|
|
region = self._host().getRegion()
|
|
msg_id = template.get('ID', uuid.uuid4().hex)
|
|
if wait_results:
|
|
event = eventlet.event.Event()
|
|
listener = region['agentListener']
|
|
listener().subscribe(msg_id, event)
|
|
|
|
msg = self._prepare_message(template, msg_id)
|
|
with common.create_rmq_client(region) as client:
|
|
client.send(message=msg, key=self._queue)
|
|
|
|
if wait_results:
|
|
try:
|
|
with eventlet.Timeout(timeout):
|
|
result = event.wait()
|
|
|
|
except eventlet.Timeout:
|
|
listener().unsubscribe(msg_id)
|
|
raise exceptions.TimeoutException(
|
|
'The murano-agent did not respond '
|
|
'within {0} seconds'.format(timeout))
|
|
|
|
if not result:
|
|
return None
|
|
|
|
if result.get('FormatVersion', '1.0.0').startswith('1.'):
|
|
return self._process_v1_result(result)
|
|
|
|
else:
|
|
return self._process_v2_result(result)
|
|
|
|
else:
|
|
return None
|
|
|
|
@specs.parameter(
|
|
'resources', dsl.MuranoObjectParameter('io.murano.system.Resources'))
|
|
def call(self, template, resources, timeout=None):
|
|
if timeout is None:
|
|
timeout = CONF.engine.agent_timeout
|
|
self._check_enabled()
|
|
plan = self.build_execution_plan(template, resources())
|
|
return self._send(plan, True, timeout)
|
|
|
|
@specs.parameter(
|
|
'resources', dsl.MuranoObjectParameter('io.murano.system.Resources'))
|
|
def send(self, template, resources):
|
|
self._check_enabled()
|
|
plan = self.build_execution_plan(template, resources())
|
|
return self._send(plan, False, 0)
|
|
|
|
def call_raw(self, plan, timeout=None):
|
|
if timeout is None:
|
|
timeout = CONF.engine.agent_timeout
|
|
self._check_enabled()
|
|
return self._send(plan, True, timeout)
|
|
|
|
def send_raw(self, plan):
|
|
self._check_enabled()
|
|
return self._send(plan, False, 0)
|
|
|
|
def is_ready(self, timeout=100):
|
|
try:
|
|
self.wait_ready(timeout)
|
|
except exceptions.TimeoutException:
|
|
return False
|
|
else:
|
|
return True
|
|
|
|
def wait_ready(self, timeout=100):
|
|
self._check_enabled()
|
|
template = {'Body': 'return', 'FormatVersion': '2.0.0', 'Scripts': {}}
|
|
self.call(template, False, timeout)
|
|
|
|
def _process_v1_result(self, result):
|
|
if result['IsException']:
|
|
raise AgentException(dict(self._get_exception_info(
|
|
result.get('Result', [])), source='execution_plan'))
|
|
else:
|
|
results = result.get('Result', [])
|
|
if not result:
|
|
return None
|
|
value = results[-1]
|
|
if value['IsException']:
|
|
raise AgentException(dict(self._get_exception_info(
|
|
value.get('Result', [])), source='command'))
|
|
else:
|
|
return value.get('Result')
|
|
|
|
def _process_v2_result(self, result):
|
|
error_code = result.get('ErrorCode', 0)
|
|
if not error_code:
|
|
return result.get('Body')
|
|
else:
|
|
body = result.get('Body') or {}
|
|
err = {
|
|
'message': body.get('Message'),
|
|
'details': body.get('AdditionalInfo'),
|
|
'errorCode': error_code,
|
|
'time': result.get('Time')
|
|
}
|
|
for attr in ('Message', 'AdditionalInfo'):
|
|
if attr in body:
|
|
del body[attr]
|
|
err['extra'] = body if body else None
|
|
raise AgentException(err)
|
|
|
|
def _get_array_item(self, array, index):
|
|
return array[index] if len(array) > index else None
|
|
|
|
def _get_exception_info(self, data):
|
|
data = data or []
|
|
return {
|
|
'type': self._get_array_item(data, 0),
|
|
'message': self._get_array_item(data, 1),
|
|
'command': self._get_array_item(data, 2),
|
|
'details': self._get_array_item(data, 3),
|
|
'timestamp': datetime.datetime.now().isoformat()
|
|
}
|
|
|
|
def build_execution_plan(self, template, resources):
|
|
template = copy.deepcopy(template)
|
|
if not isinstance(template, dict):
|
|
raise ValueError('Incorrect execution plan ')
|
|
format_version = template.get('FormatVersion')
|
|
if not format_version or format_version.startswith('1.'):
|
|
return self._build_v1_execution_plan(template, resources)
|
|
else:
|
|
return self._build_v2_execution_plan(template, resources)
|
|
|
|
def _build_v1_execution_plan(self, template, resources):
|
|
scripts_folder = 'scripts'
|
|
script_files = template.get('Scripts', [])
|
|
scripts = []
|
|
for script in script_files:
|
|
script_path = os.path.join(scripts_folder, script)
|
|
script_path = base64.encode_as_text(script_path) + "\n"
|
|
scripts.append(resources.string(script_path, binary=True))
|
|
template['Scripts'] = scripts
|
|
return template
|
|
|
|
def _build_v2_execution_plan(self, template, resources):
|
|
scripts_folder = 'scripts'
|
|
plan_id = uuid.uuid4().hex
|
|
template['ID'] = plan_id
|
|
if 'Action' not in template:
|
|
template['Action'] = 'Execute'
|
|
if 'Files' not in template:
|
|
template['Files'] = {}
|
|
|
|
files = {}
|
|
for file_id, file_descr in template['Files'].items():
|
|
files[file_descr['Name']] = file_id
|
|
|
|
for name, script in template.get('Scripts', {}).items():
|
|
if 'EntryPoint' not in script:
|
|
raise ValueError('No entry point in script ' + name)
|
|
|
|
if 'Application' == script['Type']:
|
|
if script['EntryPoint'] not in files:
|
|
script['EntryPoint'] = self._place_file(
|
|
scripts_folder, script['EntryPoint'],
|
|
template, resources, files)
|
|
else:
|
|
script['EntryPoint'] = files[script['EntryPoint']]
|
|
if 'Files' in script:
|
|
for i, file in enumerate(script['Files']):
|
|
if self._get_name(file) not in files:
|
|
script['Files'][i] = self._place_file(
|
|
scripts_folder, file, template, resources, files)
|
|
else:
|
|
script['Files'][i] = files[file]
|
|
return template
|
|
|
|
def _is_url(self, file):
|
|
file = self._get_url(file)
|
|
parts = six.moves.urllib.parse.urlsplit(file)
|
|
if not parts.scheme or not parts.netloc:
|
|
return False
|
|
else:
|
|
return True
|
|
|
|
def _get_url(self, file):
|
|
if isinstance(file, dict):
|
|
return list(file.values())[0]
|
|
else:
|
|
return file
|
|
|
|
def _get_name(self, file):
|
|
if isinstance(file, dict):
|
|
name = list(file.keys())[0]
|
|
else:
|
|
name = file
|
|
|
|
if self._is_url(name):
|
|
name = name[name.rindex('/') + 1:len(name)]
|
|
elif name.startswith('<') and name.endswith('>'):
|
|
name = name[1: -1]
|
|
return name
|
|
|
|
def _get_file_value(self, file):
|
|
if isinstance(file, dict):
|
|
file = list(file.values())[0]
|
|
return file
|
|
|
|
def _get_body(self, file, resources, folder):
|
|
use_base64 = self._is_base64(file)
|
|
if use_base64:
|
|
path = os.path.join(folder, file[1: -1])
|
|
body = resources.string(path, binary=True)
|
|
body = base64.encode_as_text(body) + "\n"
|
|
else:
|
|
path = os.path.join(folder, file)
|
|
body = resources.string(path)
|
|
return body
|
|
|
|
def _is_base64(self, file):
|
|
return file.startswith('<') and file.endswith('>')
|
|
|
|
def _get_body_type(self, file):
|
|
return 'Base64' if self._is_base64(file) else 'Text'
|
|
|
|
def _place_file(self, folder, file, template, resources, files):
|
|
file_value = self._get_file_value(file)
|
|
name = self._get_name(file)
|
|
file_id = uuid.uuid4().hex
|
|
|
|
if self._is_url(file_value):
|
|
template['Files'][file_id] = self._get_file_des_downloadable(file)
|
|
files[name] = file_id
|
|
|
|
else:
|
|
template['Files'][file_id] = self._get_file_description(
|
|
file, resources, folder)
|
|
files[name] = file_id
|
|
return file_id
|
|
|
|
def _get_file_des_downloadable(self, file):
|
|
name = self._get_name(file)
|
|
file = self._get_file_value(file)
|
|
return {
|
|
'Name': str(name),
|
|
'URL': file,
|
|
'Type': 'Downloadable'
|
|
}
|
|
|
|
def _get_file_description(self, file, resources, folder):
|
|
name = self._get_name(file)
|
|
file_value = self._get_file_value(file)
|
|
|
|
body_type = self._get_body_type(file_value)
|
|
body = self._get_body(file_value, resources, folder)
|
|
return {
|
|
'Name': name,
|
|
'BodyType': body_type,
|
|
'Body': body
|
|
}
|