Added 'destroy' method that is called on deleted instances

Added ability to modify/remove data from structures (like Heat
templates) via jsonpatch and thus added ability to clean up Heat
resources that was obtained by deleted instances

Closes bug: #1296624

Change-Id: I4db226a5ab00ff363f8b5d44a5d690df942622e8
This commit is contained in:
Stan Lagun 2014-04-08 14:08:56 +04:00
parent d000b7b1f8
commit ac6a0dedec
42 changed files with 618 additions and 67 deletions

View File

@ -134,8 +134,8 @@ function init_murano() {
recreate_database murano utf8
$MURANO_BIN_DIR/murano-manage --config-file $MURANO_CONF_FILE db-sync
$MURANO_BIN_DIR/murano-manage --config-file $MURANO_CONF_FILE import-package $MURANO_DIR/meta/packages/core
$MURANO_BIN_DIR/murano-manage --config-file $MURANO_CONF_FILE import-package $MURANO_DIR/meta/packages/activedirectory
$MURANO_BIN_DIR/murano-manage --config-file $MURANO_CONF_FILE import-package $MURANO_DIR/meta/io.murano
$MURANO_BIN_DIR/murano-manage --config-file $MURANO_CONF_FILE import-package $MURANO_DIR/meta/io.murano.windows.ActiveDirectory
}

View File

@ -1,8 +1,7 @@
Namespaces:
=: io.murano.services.windows.activeDirectory
=: io.murano.windows.activeDirectory
std: io.murano
sys: io.murano.system
win: io.murano.services.windows
Name: ActiveDirectory
@ -29,5 +28,9 @@ Properties:
Workflow:
deploy:
Body:
- $.primaryController.deploy()
- $.secondaryControllers.pselect($.deploy())
- $.primaryController.deploy()
- $.secondaryControllers.pselect($.deploy())
- $.reportDeployed()
destroy:
- $.reportDestroyed()

View File

@ -1,8 +1,8 @@
Namespaces:
=: io.murano.services.windows.activeDirectory
=: io.murano.windows.activeDirectory
std: io.murano
sys: io.murano.system
win: io.murano.services.windows
win: io.murano.windows
Name: Controller

View File

@ -0,0 +1,20 @@
Namespaces:
=: io.murano.windows
ad: io.murano.windows.activeDirectory
Name: DomainHost
Extends: Host
Properties:
domain:
Contract: $.class(ad:ActiveDirectory).notNull()
Workflow:
deploy:
Arguments:
Body:
- $.super($.deploy())
#- $.joinDomain($.domain)
# Workaround against broken ResourceManager:
- $.super($.joinDomain($this.domain))

View File

@ -0,0 +1,49 @@
Namespaces:
=: io.murano.windows
ad: io.murano.windows.activeDirectory
res: io.murano.resources
sys: io.murano.system
Name: Host
Extends: res:Instance
Properties:
adminAccountName:
Contract: $.string().notNull()
Default: Administrator
adminPassword:
Contract: $.string().notNull()
Workflow:
initialize:
Body:
- $.super($.initialize())
deploy:
Body:
- $.super($.deploy())
- $resources: new(sys:Resources)
- $template: $resources.json('SetPassword.template').bind(dict(
adminPassword => $.adminPassword
))
- $.agent.send($template, $resources)
joinDomain:
Arguments:
- domain:
Contract: $.class(ad:ActiveDirectory).notNull()
Body:
- $resources: new(sys:Resources)
- $template: $resources.json('JoinDomain.template').bind(dict(
domain => $domain.name,
domainUser => $domain.adminAccountName,
domainPassword => $domain.adminPassword,
ouPath => '',
dnsIp => $domain.primaryController.dnsIp
))
- $.agent.call($template, $resources)

View File

@ -1,8 +1,7 @@
Namespaces:
=: io.murano.services.windows.activeDirectory
=: io.murano.windows.activeDirectory
std: io.murano
sys: io.murano.system
win: io.murano.services.windows
Name: PrimaryController
@ -22,7 +21,6 @@ Workflow:
deploy:
Arguments:
Body:
- $.debugPrint('Deploying Primary Controller for domain {0}'.format($this.domain.name))
- $.super($.deploy())
- $resources: new(io.murano.system.Resources)
- $template: $resources.json('CreatePrimaryDC.template').bind(dict(

View File

@ -1,5 +1,5 @@
Namespaces:
=: io.murano.services.windows.activeDirectory
=: io.murano.windows.activeDirectory
std: io.murano
sys: io.murano.system
@ -15,7 +15,6 @@ Workflow:
deploy:
Body:
- $.debugPrint('Deploying Secondary Controller for domain {0}'.format($this.domain.name))
- $.super($.deploy())
- $.host.joinDomain($.domain)
- $resources: new(sys:Resources)

View File

@ -0,0 +1,25 @@
{
"Scripts": [
"ImportCoreFunctions.ps1",
"Join-Domain.ps1"
],
"Commands": [
{
"Name": "Set-NetworkAdapterConfiguration",
"Arguments": {
"FirstAvailable": true,
"DNSServer": "$dnsIp"
}
},
{
"Name": "Join-Domain",
"Arguments": {
"Username": "$domainUser",
"Password": "$domainPassword",
"DomainName": "$domain",
"OUPath": "$ouPath"
}
}
],
"RebootOnCompletion": 1
}

View File

@ -0,0 +1,17 @@
{
"Scripts": [
"ImportCoreFunctions.ps1",
"Set-LocalUserPassword.ps1"
],
"Commands": [
{
"Name": "Set-LocalUserPassword",
"Arguments": {
"UserName": "Administrator",
"Password": "$adminPassword",
"Force": true
}
}
],
"RebootOnCompletion": 0
}

View File

@ -0,0 +1,67 @@
trap {
&$TrapHandler
}
Function Join-Domain {
<#
.SYNOPSIS
Executes "Join domain" action.
Requires 'CoreFunctions' module
#>
param (
[String] $DomainName = '',
[String] $UserName = '',
[String] $Password = '',
[String] $OUPath = '',
[Switch] $AllowRestart
)
begin {
Show-InvocationInfo $MyInvocation
}
end {
Show-InvocationInfo $MyInvocation -End
}
process {
trap {
&$TrapHandler
}
if ($UserName -eq '') {
$UserName = 'Administrator'
}
$Credential = New-Credential -UserName "$DomainName\$UserName" -Password $Password
if (Test-ComputerName -DomainName $DomainName -ErrorAction 'SilentlyContinue') {
Write-LogWarning "Computer already joined to domain '$DomainName'"
}
else {
Write-Log "Joining computer to domain '$DomainName' ..."
if ($OUPath -eq '') {
Add-Computer -DomainName $DomainName -Credential $Credential -Force
}
else {
Add-Computer -DomainName $DomainName -Credential $Credential -OUPath $OUPath -Force
}
$null = Exec 'ipconfig' @('/registerdns') -RedirectStreams
Write-Log "Waiting 30 seconds to restart ..."
Start-Sleep -Seconds 30
<#
if ($AllowRestart) {
Write-Log "Restarting computer ..."
Restart-Computer -Force
}
else {
Write-Log "Please restart the computer now."
}
#>
}
}
}

View File

@ -0,0 +1,37 @@
trap {
&$TrapHandler
}
Function Set-LocalUserPassword {
param (
[String] $UserName,
[String] $Password,
[Switch] $Force
)
begin {
Show-InvocationInfo $MyInvocation
}
end {
Show-InvocationInfo $MyInvocation -End
}
process {
trap {
&$TrapHandler
}
if ((Get-WmiObject Win32_UserAccount -Filter "LocalAccount = 'True' AND Name='$UserName'") -eq $null) {
throw "Unable to find local user account '$UserName'"
}
if ($Force) {
Write-Log "Changing password for user '$UserName' to '*****'" # :)
$null = ([ADSI] "WinNT://./$UserName").SetPassword($Password)
}
else {
Write-LogWarning "You are trying to change password for user '$UserName'. To do this please run the command again with -Force parameter."
}
}
}

View File

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

View File

@ -0,0 +1,27 @@
Format: 1.0
Type: Application
FullName: io.murano.windows.ActiveDirectory
Name: Active Directory
Description: |
A domain service hosted in Windows environment by using Active Directory Role.
May be clustered by combining a number of secondary domain controllers with one primary
Author: 'murano.io'
Tags: [Windows, Domain, demo, win2012, microsoft]
Classes:
io.murano.windows.Host: Host.yaml
io.murano.windows.DomainHost: DomainHost.yaml
io.murano.windows.activeDirectory.ActiveDirectory: ActiveDirectory.yaml
io.murano.windows.activeDirectory.Controller: Controller.yaml
io.murano.windows.activeDirectory.PrimaryController: PrimaryController.yaml
io.murano.windows.activeDirectory.SecondaryController: SecondaryController.yaml
# UI: ui.yaml # default to ui.yaml, will use default if skipped
Logo: logo2.png # defaults to logo.png, will use default if skipped

View File

@ -0,0 +1,81 @@
Namespaces:
=: io.murano.resources
sys: io.murano.system
Name: Instance
Properties:
name:
Contract: $.string().notNull()
flavor:
Contract: $.string().notNull()
image:
Contract: $.string().notNull()
agent:
Contract: $.class(sys:Agent)
Type: Runtime
Workflow:
initialize:
Body:
- $.environment: $.find(Environment).require()
- $.agent: new(sys:Agent, host => $)
- $.resources: new(sys:Resources)
deploy:
Body:
- $userData: $.prepareUserData()
- $template:
Resources:
$.name:
Type: 'AWS::EC2::Instance'
Properties:
InstanceType: $.flavor
ImageId: $.image
UserData: $userData
- $.environment.stack.updateTemplate($template)
- $.environment.stack.push()
- $.environment.instanceNotifier.trackApplication($this)
destroy:
Body:
- $template: $.environment.stack.current()
- $patchBlock:
op: remove,
path: format('/Resources/{0}', $.name)
- $template: patch($template, $patchBlock)
- $.environment.stack.setTemplate($template)
- $.environment.stack.push()
- $.environment.instanceNotifier.untrackApplication($this)
prepareUserData:
Body:
- If: !yaql "'w' in toLower($.image)"
Then:
- $configFile: $.resources.string('Agent-v1.template')
- $initScript: $.resources.string('windows-init.ps1')
Else:
- $configFile: $.resources.string('Agent-v2.template')
- $initScript: $.resources.string('linux-init.sh')
- $configReplacements:
"%RABBITMQ_HOST%": config(rabbitmq, host)
"%RABBITMQ_PORT%": config(rabbitmq, port)
"%RABBITMQ_USER%": config(rabbitmq, login)
"%RABBITMQ_PASSWORD%": config(rabbitmq, password)
"%RABBITMQ_VHOST%": config(rabbitmq, virtual_host)
"%RABBITMQ_SSL%": str(config(rabbitmq, ssl)).toLower()
"%RABBITMQ_INPUT_QUEUE%": $.agent.queueName()
"%RESULT_QUEUE%": $.environment.agentListener.queueName()
- $scriptReplacements:
"%AGENT_CONFIG_BASE64%": base64encode($configFile.replace($configReplacements))
"%INTERNAL_HOSTNAME%": $.name
"%MURANO_SERVER_ADDRESS%": coalesce(config(file_server), config(rabbitmq, host))
"%CA_ROOT_CERT_BASE64%": ""
- Return: $initScript.replace($scriptReplacements)

View File

@ -0,0 +1,36 @@
<?xml version="1.0" encoding="UTF-8" ?>
<configuration>
<configSections>
<section name="nlog" type="NLog.Config.ConfigSectionHandler, NLog"/>
</configSections>
<startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5" />
</startup>
<nlog xmlns="http://www.nlog-project.org/schemas/NLog.xsd"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<targets>
<target name="file" xsi:type="File" fileName="${basedir}/log.txt"
layout="${date} ${level}: &lt;${logger:shortName=true}&gt; ${message} ${exception:format=tostring}"/>
</targets>
<rules>
<logger name="*" minlevel="Debug" writeTo="file" />
</rules>
</nlog>
<appSettings>
<add key="rabbitmq.host" value="%RABBITMQ_HOST%"/>
<add key="rabbitmq.port" value="%RABBITMQ_PORT%"/>
<add key="rabbitmq.user" value="%RABBITMQ_USER%"/>
<add key="rabbitmq.password" value="%RABBITMQ_PASSWORD%"/>
<add key="rabbitmq.vhost" value="%RABBITMQ_VHOST%"/>
<add key="rabbitmq.inputQueue" value="%RABBITMQ_INPUT_QUEUE%"/>
<add key="rabbitmq.resultExchange" value=""/>
<add key="rabbitmq.resultRoutingKey" value="%RESULT_QUEUE%"/>
<add key="rabbitmq.durableMessages" value="true"/>
<add key="rabbitmq.ssl" value="%RABBITMQ_SSL%"/>
<add key="rabbitmq.allowInvalidCA" value="true"/>
<add key="rabbitmq.sslServerName" value=""/>
</appSettings>
</configuration>

View File

@ -0,0 +1,35 @@
[DEFAULT]
debug=True
verbose=True
log_file = /var/log/murano-agnet.log
storage=/var/murano/plans
[rabbitmq]
# Input queue name
input_queue = %RABBITMQ_INPUT_QUEUE%
# Output routing key (usually queue name)
result_routing_key = %RESULT_QUEUE%
# Connection parameters to RabbitMQ service
# Hostname or IP address where RabbitMQ is located.
host = %RABBITMQ_HOST%
# RabbitMQ port (5672 is a default)
port = %RABBITMQ_PORT%
# Use SSL for RabbitMQ connections (True or False)
ssl = %RABBITMQ_SSL%
# Path to SSL CA certificate or empty to allow self signed server certificate
ca_certs =
# RabbitMQ credentials. Fresh RabbitMQ installation has "guest" account with "guest" password.
login = %RABBITMQ_USER%
password = %RABBITMQ_PASSWORD%
# RabbitMQ virtual host (vhost). Fresh RabbitMQ installation has "/" vhost preconfigured.
virtual_host = %RABBITMQ_VHOST%

View File

@ -0,0 +1,11 @@
#!/bin/sh
service murano-agent stop
AgentConfigBase64='%AGENT_CONFIG_BASE64%'
mkdir /etc/murano
echo $AgentConfigBase64 | base64 -d > /etc/murano/agent.conf
chmod 664 /etc/murano/agent.conf
service murano-agent start

View File

@ -0,0 +1,68 @@
#ps1
$WindowsAgentConfigBase64 = '%AGENT_CONFIG_BASE64%'
$WindowsAgentConfigFile = "C:\Murano\Agent\WindowsAgent.exe.config"
$WindowsAgentLogFile = "C:\Murano\Agent\log.txt"
$NewComputerName = '%INTERNAL_HOSTNAME%'
$MuranoFileShare = '\\%MURANO_SERVER_ADDRESS%\share'
$CaRootCertBase64 = "%CA_ROOT_CERT_BASE64%"
$CaRootCertFile = "C:\Murano\ca.cert"
$RestartRequired = $false
Import-Module CoreFunctions
Initialize-Logger 'CloudBase-Init' 'C:\Murano\PowerShell.log'
$ErrorActionPreference = 'Stop'
trap {
Write-LogError '<exception>'
Write-LogError $_ -EntireObject
Write-LogError '</exception>'
exit 1
}
Write-Log "Importing CA certificate ..."
if ($CaRootCertBase64 -eq '') {
Write-Log "Importing CA certificate ... skipped"
}
else {
ConvertFrom-Base64String -Base64String $CaRootCertBase64 -Path $CaRootCertFile
$cert = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2 $CaRootCertFile
$store = New-Object System.Security.Cryptography.X509Certificates.X509Store("AuthRoot","LocalMachine")
$store.Open("MaxAllowed")
$store.Add($cert)
$store.Close()
Write-Log "Importing CA certificate ... done"
}
Write-Log "Updating Murano Windows Agent."
Stop-Service "Murano Agent"
Backup-File $WindowsAgentConfigFile
Remove-Item $WindowsAgentConfigFile -Force -ErrorAction 'SilentlyContinue'
Remove-Item $WindowsAgentLogFile -Force -ErrorAction 'SilentlyContinue'
ConvertFrom-Base64String -Base64String $WindowsAgentConfigBase64 -Path $WindowsAgentConfigFile
Exec sc.exe 'config','"Murano Agent"','start=','delayed-auto'
Write-Log "Service has been updated."
Write-Log "Adding environment variable 'MuranoFileShare' = '$MuranoFileShare' ..."
[Environment]::SetEnvironmentVariable('MuranoFileShare', $MuranoFileShare, [EnvironmentVariableTarget]::Machine)
Write-Log "Environment variable added."
Write-Log "Renaming computer to '$NewComputerName' ..."
$null = Rename-Computer -NewName $NewComputerName -Force
Write-Log "New name assigned, restart required."
$RestartRequired = $true
Write-Log 'All done!'
if ( $RestartRequired ) {
Write-Log "Restarting computer ..."
Restart-Computer -Force
}
else {
Start-Service 'Murano Agent'
}

View File

@ -0,0 +1,21 @@
Format: 1.0
Type: Library
FullName: io.murano
Name: Core library
Description: |
Core MuranoPL library
Author: 'murano.io'
Tags: [MuranoPL]
Classes:
io.murano.Object: Object.yaml
io.murano.Environment: Environment.yaml
io.murano.Application: Application.yaml
io.murano.resources.Instance: resources/Instance.yaml

View File

@ -1,16 +0,0 @@
Format: 1.0
Type: Application
FullName: io.murano.windows.activeDirectory.ActiveDirectory
Name: Active Directory
Description: |
A domain service hosted in Windows environment by using Active Directory Role.
May be clustered by combining a number of secondary domain controllers with one primary
Author: 'murano.io'
Tags: [Windows, Domain, demo, win2012, microsoft]
Classes:
io.murano.windows.activeDirectory.ActiveDirectory: ad.yaml
io.murano.windows.activeDirectory.Controller: controller.yaml
io.murano.windows.activeDirectory.PrimaryController: primary_controller.yaml
io.murano.windows.activeDirectory.SecondaryController: secondary_controller.yaml
# UI: ui.yaml # default to ui.yaml, will use default if skipped
Logo: logo2.png # defaults to logo.png, will use default if skipped

View File

@ -1,12 +0,0 @@
Format: 1.0
Type: Library
FullName: io.murano.core
Name: Core library
Description: |
Core MuranoPL library
Author: 'murano.io'
Tags: [MuranoPL]
Classes:
io.murano.Object: object.yaml
io.murano.Environment: environment.yaml
io.murano.Application: application.yaml

View File

@ -92,3 +92,7 @@ def main():
CONF.command.func()
except Exception as e:
sys.exit("ERROR: %s" % e)
if __name__ == '__main__':
main()

View File

@ -52,7 +52,8 @@ class TaskProcessingEndpoint(object):
exc = executor.MuranoDslExecutor(cl, env)
obj = exc.load(task['model'])
obj.type.invoke('deploy', exc, obj, {})
if obj is not None:
obj.type.invoke('deploy', exc, obj, {})
s_res = results_serializer.serialize(obj, exc)
rpc.api().process_result(s_res)

View File

@ -122,13 +122,15 @@ class EnvironmentServices(object):
#preparing data for removal from conductor
env = environment.description
env['services'] = {}
env['deleted'] = True
env['Objects'] = None
#Set X-Auth-Token for conductor
env['token'] = token
data = {
'model': env,
'token': token,
'tenant_id': environment.tenant_id
}
rpc.engine().handle_task(env)
rpc.engine().handle_task(data)
with unit.begin():
unit.delete(environment)

View File

@ -12,6 +12,7 @@
# License for the specific language governing permissions and limitations
# under the License.
import collections
import functools
import inspect
import types
@ -216,5 +217,49 @@ class MuranoDslExecutor(object):
if not isinstance(data, types.DictionaryType):
raise TypeError()
self._attribute_store.load(data.get('Attributes') or [])
return self._object_store.load(data.get('Objects') or {},
None, self._root_context)
result = self._object_store.load(data.get('Objects') or {},
None, self._root_context)
self.cleanup(data)
return result
def cleanup(self, data):
objects_copy = data.get('ObjectsCopy')
if not objects_copy:
return
gc_object_store = object_store.ObjectStore(self._class_loader)
gc_object_store.load(objects_copy, None, self._root_context)
objects_to_clean = []
for object_id in self._list_potential_object_ids(objects_copy):
if gc_object_store.has(object_id) \
and not self._object_store.has(object_id):
obj = gc_object_store.get(object_id)
objects_to_clean.append(obj)
if objects_to_clean:
backup = self._object_store
try:
self._object_store = gc_object_store
for obj in objects_to_clean:
methods = obj.type.find_method('destroy')
for cls, method in methods:
try:
cls.invoke(method, self, obj, {})
except Exception:
pass
finally:
self._object_store = backup
def _list_potential_object_ids(self, data):
if isinstance(data, types.DictionaryType):
for val in data.values():
for res in self._list_potential_object_ids(val):
yield res
sys_dict = data.get('?')
if isinstance(sys_dict, types.DictionaryType) \
and sys_dict.get('id') \
and sys_dict.get('type'):
yield sys_dict['id']
elif isinstance(data, collections.Iterable) and not isinstance(
data, types.StringTypes):
for val in data:
for res in self._list_potential_object_ids(val):
yield res

View File

@ -39,12 +39,17 @@ class ObjectStore(object):
return self._parent_store.get(object_id)
return None
def has(self, object_id):
return object_id in self._store
def put(self, murano_object):
self._store[murano_object.object_id] = murano_object
def load(self, value, parent, context, defaults=None):
#tmp_store = ObjectStore(self._class_loader, self)
if value is None:
return None
if '?' not in value or 'type' not in value['?']:
raise ValueError()
system_key = value['?']

View File

@ -23,13 +23,19 @@ class ObjRef(object):
def serialize(root_object, executor):
serialized_objects = set()
tree = _pass1_serialize(root_object, None, serialized_objects)
_pass2_serialize(tree, serialized_objects)
if root_object is None:
tree = None
attributes = []
else:
serialized_objects = set()
tree = _pass1_serialize(root_object, None, serialized_objects)
_pass2_serialize(tree, serialized_objects)
attributes = executor.attribute_store.serialize(serialized_objects)
return {
'Objects': tree,
'Attributes': executor.attribute_store.serialize(serialized_objects)
'ObjectsCopy': tree,
'Attributes': attributes
}

View File

@ -166,21 +166,25 @@ class HeatStack(murano_object.MuranoObject):
current_status = self._get_status()
if current_status == 'NOT_FOUND':
self._heat_client.stacks.create(
stack_name=self._name,
parameters=self._parameters,
template=self._template,
disable_rollback=False)
if self._template.get('Resources'):
self._heat_client.stacks.create(
stack_name=self._name,
parameters=self._parameters,
template=self._template,
disable_rollback=False)
self._wait_state(
lambda status: status == 'CREATE_COMPLETE')
self._wait_state(
lambda status: status == 'CREATE_COMPLETE')
else:
self._heat_client.stacks.update(
stack_id=self._name,
parameters=self._parameters,
template=self._template)
self._wait_state(
lambda status: status == 'UPDATE_COMPLETE')
if self._template.get('Resources'):
self._heat_client.stacks.update(
stack_id=self._name,
parameters=self._parameters,
template=self._template)
self._wait_state(
lambda status: status == 'UPDATE_COMPLETE')
else:
self.delete()
self._applied = True
@ -191,3 +195,5 @@ class HeatStack(murano_object.MuranoObject):
stack_id=self._name)
self._wait_state(
lambda status: status in ('DELETE_COMPLETE', 'NOT_FOUND'))
self._template = {}
self._applied = True

View File

@ -17,6 +17,8 @@ import base64
import re
import types
import jsonpatch
import jsonpointer
import yaql.context
import muranoapi.common.config as cfg
@ -188,6 +190,18 @@ def _pselect(collection, composer):
return helpers.parallel_select(collection(), composer)
def _patch(obj, patch):
obj = obj()
patch = patch()
if not isinstance(patch, types.ListType):
patch = [patch]
patch = jsonpatch.JsonPatch(patch)
try:
return patch.apply(obj)
except jsonpointer.JsonPointerException:
return obj
def register(context):
context.register_function(
lambda json, mappings: _transform_json(json(), mappings()), 'bind')
@ -213,3 +227,4 @@ def register(context):
context.register_function(_substr, 'substr')
context.register_function(_str, 'str')
context.register_function(_int, 'int')
context.register_function(_patch, 'patch')

View File

@ -124,7 +124,7 @@ class OpenstackException(Exception):
if _FATAL_EXCEPTION_FORMAT_ERRORS:
raise
else:
# at least get the core message out if something happened
# at least get the io.murano message out if something happened
self._error_string = self.msg_fmt
def __str__(self):

View File

@ -18,6 +18,7 @@ iso8601>=0.1.8
six>=1.5.2
netaddr>=0.7.6
PyYAML>=3.1.0
jsonpatch>=1.1
# Note you will need gcc buildtools installed and must
# have installed libxml headers for lxml to be successfully