class CloudifyClient(object): @specs.parameter('app', dsl.MuranoObjectParameter('io.murano.Application')) def __init__(self, app): cloudify_manager = self.CONF.cloudify_manager self._client = cloudify_rest_client.CloudifyClient(cloudify_manager) self._blueprint_id = '{0}-{1}'.format(app.type.name, app.type.version) self._deployment_id = app.id self._application_package = app.package @specs.parameter('entry_point', yaqltypes.String()) def publish_blueprint(self, entry_point): global archive_upload_lock if self._check_blueprint_exists(): return path = self._application_package.get_resource(entry_point) with archive_upload_lock: try: self._client.blueprints.upload( path, self._blueprint_id) except cloudify_exceptions.CloudifyClientError as e: if e.status_code != 409: raise def _check_blueprint_exists(self): try: self._client.blueprints.get(self._blueprint_id) return True except cloudify_exceptions.CloudifyClientError as e: if e.status_code == 404: return False raise @specs.parameter('parameters', dict) def create_deployment(self, parameters=None): self._client.deployments.create( self._blueprint_id, self._deployment_id, parameters) def delete_deployment(self): self._client.deployments.delete(self._deployment_id) def wait_deployment_ready(self): while True: executions = self._client.executions.list(self._deployment_id) if any(t.status in ('pending', 'started') for t in executions): time.sleep(3) else: deployment = self._client.deployments.get(self._deployment_id) return deployment.outputs @specs.parameter('name', yaqltypes.String()) @specs.parameter('parameters', dict) def execute_workflow(self, name, parameters=None): self._client.executions.start(self._deployment_id, name, parameters) @classmethod def init_plugin(cls): cls.CONF = cfg.init_config(CONF)
def _create_instance_mpl_stub(murano_method): def payload(__context, __receiver, *args, **kwargs): return murano_method.invoke(__receiver, args, kwargs, __context, True) fd = _create_basic_mpl_stub(murano_method, 1, payload, False) receiver_type = dsl.MuranoObjectParameter(weakref.proxy( murano_method.declaring_type), decorate=False) fd.set_parameter(specs.ParameterDefinition('__receiver', receiver_type, 1)) return fd
def get_function_definition(func, murano_method, original_name): cls = murano_method.declaring_type.extension_class def param_type_func(name): return None if not cls else _infer_parameter_type(name, cls.__name__) body = func if (cls is None or helpers.inspect_is_method(cls, original_name) or helpers.inspect_is_classmethod(cls, original_name)): body = helpers.function(func) fd = specs.get_function_definition(body, convention=CONVENTION, parameter_type_func=param_type_func) fd.is_method = True fd.is_function = False if not cls or helpers.inspect_is_method(cls, original_name): fd.set_parameter(0, dsl.MuranoObjectParameter( murano_method.declaring_type), overwrite=True) if cls and helpers.inspect_is_classmethod(cls, original_name): _remove_first_parameter(fd) body = func name = getattr(func, '__murano_name', None) if name: fd.name = name fd.insert_parameter(specs.ParameterDefinition('?1', yaqltypes.Context(), 0)) is_static = cls and (helpers.inspect_is_static(cls, original_name) or helpers.inspect_is_classmethod(cls, original_name)) if is_static: fd.insert_parameter( specs.ParameterDefinition('?2', yaqltypes.PythonType(object), 1)) def payload(__context, __self, *args, **kwargs): with helpers.contextual(__context): __context[constants.CTX_NAMES_SCOPE] = \ murano_method.declaring_type return body(__self.extension, *args, **kwargs) def static_payload(__context, __receiver, *args, **kwargs): with helpers.contextual(__context): __context[constants.CTX_NAMES_SCOPE] = \ murano_method.declaring_type return body(*args, **kwargs) if is_static: fd.payload = static_payload else: fd.payload = payload fd.meta[constants.META_MURANO_METHOD] = murano_method return fd
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._environment = host.find_owner('io.murano.Environment') self._enabled = True self._queue = str('e%s-h%s' % (self._environment.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 with common.create_rmq_client() 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.""" msg_id = template.get('ID', uuid.uuid4().hex) if wait_results: event = eventlet.event.Event() listener = self._environment['agentListener'] listener().subscribe(msg_id, event) msg = self._prepare_message(template, msg_id) with common.create_rmq_client() 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) scripts.append(resources.string(script_path).encode('base64')) 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 and file.startswith('<') and file.endswith('>'): file = file[1:-1] body = resources.string(os.path.join(folder, file)) if use_base64: body = body.encode('base64') 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}
@specs.yaql_property(dsl_types.MuranoProperty) @specs.name('usage') def property_usage(murano_property): return murano_property.usage @specs.yaql_property(dsl_types.MuranoProperty) @specs.name('declaring_type') def property_owner(murano_property): return murano_property.declaring_type @specs.name('get_value') @specs.parameter('property_', dsl_types.MuranoProperty) @specs.parameter('object_', dsl.MuranoObjectParameter(nullable=True, decorate=False)) @specs.method def property_get_value(context, property_, object_): if object_ is None: return helpers.get_executor().get_static_property( property_.declaring_type, name=property_.name, context=context) return object_.cast(property_.declaring_type).get_property( name=property_.name, context=context) @specs.name('set_value') @specs.parameter('property_', dsl_types.MuranoProperty) @specs.parameter('object_', dsl.MuranoObjectParameter(nullable=True, decorate=False)) @specs.method def property_set_value(context, property_, object_, value):
import eventlet from yaql.language import expressions from yaql.language import specs from yaql.language import utils from yaql.language import yaqltypes from murano.dsl import constants from murano.dsl import dsl from murano.dsl import dsl_types from murano.dsl import helpers from murano.dsl import reflection from murano.dsl import serializer @specs.parameter('object_', dsl.MuranoObjectParameter()) def id_(object_): return object_.id @specs.parameter('object_', dsl.MuranoObjectParameter()) @specs.parameter('type__', dsl.MuranoTypeParameter()) @specs.parameter('version_spec', yaqltypes.String(True)) def cast(context, object_, type__, version_spec=None): return helpers.cast(object_, type__.type.name, version_spec or helpers.get_type(context)) @specs.parameter('__type_name', dsl.MuranoTypeParameter()) @specs.parameter('__extra', utils.MappingType) @specs.parameter('__owner',
return constext @specs.parameter('kwargs', yaqltypes.Lambda(with_context=True)) def with_original(context, **kwargs): new_context = context.create_child_context() original_context = context[constants.CTX_ORIGINAL_CONTEXT] for k, v in six.iteritems(kwargs): new_context['$' + k] = v(original_context) return new_context @specs.parameter('target', yaqltypes.AnyOf(dsl.MuranoTypeParameter(), dsl.MuranoObjectParameter())) @specs.parameter('target_method', yaqltypes.String()) @specs.parameter('mock_object', dsl.MuranoObjectParameter()) @specs.parameter('mock_name', yaqltypes.String()) def inject_method_with_str(context, target, target_method, mock_object, mock_name): ctx_manager = helpers.get_executor(context).context_manager current_class = helpers.get_type(context) mock_func = current_class.find_single_method(mock_name) original_class = target.type original_function = original_class.find_single_method(target_method) result_fd = original_function.instance_stub.clone() def payload_adapter(__context, __sender, *args, **kwargs):
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') self._host = host self._enabled = not CONF.engine.disable_murano_agent env = host.find_owner('io.murano.Environment') self._queue = str('e%s-h%s' % (env.id, host.id)).lower() self._signing_key = None if CONF.engine.signing_key: key_path = os.path.expanduser(CONF.engine.signing_key) if not os.path.exists(key_path): LOG.warn("Key file %s does not exist. " "Message signing is disabled") else: with open(key_path, "rb") as key_file: key_data = key_file.read() self._signing_key = serialization.load_pem_private_key( key_data, password=None, backend=default_backend()) self._last_stamp = 0 self._initialized = False @property def enabled(self): return self._enabled @specs.parameter('line_prefix', specs.yaqltypes.String()) def signing_key(self, line_prefix=''): if not self._signing_key: return "" key = self._signing_key.public_key().public_bytes( serialization.Encoding.PEM, serialization.PublicFormat.PKCS1) return line_prefix + line_prefix.join(key.splitlines(True)) def _initialize(self): if self._initialized: return # (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') else: region = dsl.MuranoObjectInterface.create(self._host().getRegion()) with common.create_rmq_client(region) as client: client.declare(self._queue, enable_ha=True, ttl=86400000) self._initialized = True 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.""" self._initialize() 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, signing_func=self._sign) 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_raw(template, timeout) def _sign(self, msg): if not self._signing_key: return None return self._signing_key.sign((self._queue + msg).encode('utf-8'), padding.PKCS1v15(), hashes.SHA256()) 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 _generate_stamp(self): stamp = int(time.time() * 10000) if stamp <= self._last_stamp: stamp = self._last_stamp + 1 self._last_stamp = stamp return stamp 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) scripts.append( base64.encode_as_text(resources.string(script_path, binary=True), encoding='latin1')) template['Scripts'] = scripts template['Stamp'] = self._generate_stamp() return template def _build_v2_execution_plan(self, template, resources): scripts_folder = 'scripts' plan_id = uuid.uuid4().hex template['ID'] = plan_id template['Stamp'] = self._generate_stamp() 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 = 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}
return constext @specs.parameter('kwargs', yaqltypes.Lambda(with_context=True)) def with_original(context, **kwargs): new_context = context.create_child_context() original_context = context[constants.CTX_ORIGINAL_CONTEXT] for k, v in kwargs.items(): new_context['$' + k] = v(original_context) return new_context @specs.parameter( 'target', yaqltypes.AnyOf(dsl.MuranoTypeParameter(), dsl.MuranoObjectParameter())) @specs.parameter('target_method', yaqltypes.String()) @specs.parameter('mock_object', dsl.MuranoObjectParameter()) @specs.parameter('mock_name', yaqltypes.String()) def inject_method_with_str(context, target, target_method, mock_object, mock_name): ctx_manager = helpers.get_executor().context_manager current_class = helpers.get_type(context) mock_func = current_class.find_single_method(mock_name) original_class = target.type original_function = original_class.find_single_method(target_method) result_fd = original_function.instance_stub.clone() def payload_adapter(__context, __sender, *args, **kwargs):
class GarbageCollector(object): @staticmethod @specs.parameter('publisher', dsl.MuranoObjectParameter(decorate=False)) @specs.parameter('subscriber', dsl.MuranoObjectParameter(decorate=False)) @specs.parameter('handler', yaqltypes.String(nullable=True)) def subscribe_destruction(publisher, subscriber, handler=None): publisher_this = publisher.real_this subscriber_this = subscriber.real_this if handler: subscriber.type.find_single_method(handler) dependency = GarbageCollector._find_dependency( publisher_this, subscriber_this, handler) if not dependency: dependency = {'subscriber': helpers.weak_ref(subscriber_this), 'handler': handler} publisher_this.destruction_dependencies.append(dependency) @staticmethod @specs.parameter('publisher', dsl.MuranoObjectParameter(decorate=False)) @specs.parameter('subscriber', dsl.MuranoObjectParameter(decorate=False)) @specs.parameter('handler', yaqltypes.String(nullable=True)) def unsubscribe_destruction(publisher, subscriber, handler=None): publisher_this = publisher.real_this subscriber_this = subscriber.real_this if handler: subscriber.type.find_single_method(handler) dds = publisher_this.destruction_dependencies dependency = GarbageCollector._find_dependency( publisher_this, subscriber_this, handler) if dependency: dds.remove(dependency) @staticmethod def _find_dependency(publisher, subscriber, handler): dds = publisher.destruction_dependencies for dd in dds: if dd['handler'] != handler: continue d_subscriber = dd['subscriber'] if d_subscriber: d_subscriber = d_subscriber() if d_subscriber == subscriber: return dd @staticmethod def collect(): helpers.get_executor().object_store.cleanup() @staticmethod @specs.parameter('object_', dsl.MuranoObjectParameter(decorate=False)) def is_doomed(object_): return helpers.get_object_store().is_doomed(object_) @staticmethod @specs.parameter('object_', dsl.MuranoObjectParameter(decorate=False)) def is_destroyed(object_): return object_.destroyed