MuranoPL Reference¶
To develop applications, murano project refers to Murano Programming Language (MuranoPL). It is represented by easily readable YAML and YAQL languages. The sections below describe these languages.
YAML¶
YAML is an easily readable data serialization format that is a superset of JSON. Unlike JSON, YAML is designed to be read and written by humans and relies on visual indentation to denote nesting of data structures. This is similar to how Python uses indentation for block structures instead of curly brackets in most C-like languages. Also YAML may contain more data types as compared to JSON. See http://yaml.org/ for a detailed description of YAML.
MuranoPL is designed to be representable in YAML so that MuranoPL code could remain readable and structured. Usually MuranoPL files are YAML encoded documents. But MuranoPL engine itself does not deal directly with YAML documents, and it is up to the hosting application to locate and deserialize the definitions of particular classes. This gives the hosting application the ability to control where those definitions can be found (a file system, a database, a remote repository, etc.) and possibly use some other serialization formats instead of YAML.
MuranoPL engine relies on a host deserialization code when detecting YAQL expressions in a source definition. It provides them as instances of the YaqlExpression class rather than plain strings. Usually, YAQL expressions can be distinguished by the presence of $ (the dollar sign) and operators, but in YAML, a developer can always state the type by using YAML tags explicitly. For example:
1 2 3 4 5 | Some text - a string
$.something() - a YAQL expression
"$.something()" - a string because quotes are used
!!str $ - a string because a YAML tag is used
!yaql "text" - a YAQL expression because a YAML tag is used
|
YAQL¶
YAQL (Yet Another Query Language) is a query language that was also designed as a part of the murano project. MuranoPL makes an extensive use of YAQL. A description of YAQL can be found here.
Simply speaking, YAQL is the language for expression evaluation.
The following examples are all valid YAQL expressions:
2 + 2, foo() > bar(), true != false
.
The interesting thing in YAQL is that it has no built in list of functions. Everything YAQL can access is customizable. YAQL cannot call any function that was not explicitly registered to be accessible by YAQL. The same is true for operators. So the result of the expression 2 * foo(3, 4) completely depends on explicitly provided implementations of “foo” and “operator_*”.
YAQL uses a dollar sign ($) to access external variables, which are also
explicitly provided by the host application, and function arguments.
$variable
is a syntax to get a value of the variable “$variable”,
$1, $2, etc. are the names for function arguments. “$” is a name for current object:
data on which an expression is evaluated, or a name of a single argument. Thus,
“$” in the beginning of an expression and “$” in the middle of it can refer
to different things.
By default, YAQL has a lot of functions that can be registered in a YAQL
context. This is very similar to how SQL works but uses more Python-like
syntax. For example: $.where($.myObj.myScalar > 5
,
$.myObj.myArray.len() > 0
, and $.myObj.myArray.any($ = 4)).select($.myObj.myArray[0])
can be executed on $ = array
of objects,
and result in another array that is a filtration and projection of a source data.
Note
There is no assignment operator in YAQL, and =
means
comparison, the same what ==
means in Python.
As YAQL has no access to underlying operating system resources and is fully controllable by the host, it is secure to execute YAQL expressions without establishing a trust to the executed code. Also, because functions are not predefined, different methods can be accessible in different context. So, YAQL expressions that are used to specify property contracts are not necessarily valid in workflow definitions.
Common class structure¶
Here is a common template for class declarations. Note, that it is in the YAML format.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | Name: class name
Namespaces: namespaces specification
Extends: [list of parent classes]
Properties: properties declaration
Methods:
methodName:
Arguments:
- list
- of
- arguments
Body:
- list
- of
- instructions
|
Thus MuranoPL class is a YAML dictionary with predefined key names, all keys except
for Name
are optional and can be omitted (but must be valid if specified).
Class name¶
Class names are alphanumeric names of the classes. Traditionally, all class names begin with an upper-case letter symbol and are written in PascalCasing.
In MuranoPL all class names are unique. At the same time, MuranoPL
supports namespaces. So, in different namespaces you can have classes
with the same name. You can specify a namespace explicitly, like
ns:MyName. If you omit the namespace specification, MyName
is
expanded using the default namespace =:
. Therefore, MyName
equals =:MyName
if =
is a valid namespace.
Namespaces¶
Namespaces declaration specifies prefixes that can be used in the class body to make long class names shorter.
Namespaces:
=: io.murano.services.windows
srv: io.murano.services
std: io.murano
In the example above, the srv: Something
class name is automatically
translated to io.murano.services.Something
.
=
means the current namespace, so that MyClass
means
io.murano.services.windows.MyClass
.
If the class name contains the period (.) in its name, then it is assumed
to be already fully namespace qualified and is not expanded.
Thus ns.Myclass
remains as is.
Note
To make class names globally unique, we recommend specifying a developer’s domain name as a part of the namespace.
Extends¶
MuranoPL supports multiple inheritance. If present, the Extends
section
shows base classes that are extended. If the list consists of a single entry,
then you can write it as a scalar string instead of an array. If you
do not specify any parents or omit the key, then the class extends
io.murano.Object
. Thus, io.murano.Object
is the root class
for all class hierarchies.
Properties¶
Properties are class attributes that together with methods create public class interface. Usually, but not always, properties are the values, and reference other objects that have to be entered in an environment designer prior to a workflow invocation.
Properties have the following declaration format:
propertyName:
Contract: property contract
Usage: property usage
Default: property default
Contract¶
Contract is a YAQL expression that says what type of the value is expected for
the property as well as additional constraints imposed on a property. Using
contracts you can define what value can be assigned to a property or argument.
In case of invalid input data it may be automatically transformed to confirm
to the contract. For example, if bool value is expected and user passes any
not null value it will be converted to True
. If converting is impossible
exception ContractViolationException
will be raised.
The following contracts are available:
Operation | Definition |
---|---|
$.int()
|
an integer value (may be null). String values consisting of digits are converted to integers
|
$.int().notNull()
|
a mandatory integer
|
$.string()
$.string().notNull()
|
a string. If the value is not a string, it is converted to a string
|
$.bool()
$.bool().notNull()
|
bools are true and false.
0 is converted to false, other integers to true |
$.class(ns:ClassName)
$.class(ns:ClassName).notNull()
|
value must be a reference to an instance of specified class name
|
$.class(ns:ClassName, ns:DefaultClassName)
|
create instance of the
ns:DefaultClassName class if no instance provided |
$.class(ns:Name).check($.p = 12)
|
the value must be of the
ns:Name type and have the p property equal to 12 |
$.class(ns:Name).owned()
|
a current object must be direct or indirect owner of the value
|
$.class(ns:Name).notOwned()
|
the value must be owned by any object except current one
|
[$.int()]
[$.int().notNull()]
|
an array of integers. Similar to other types.
|
[$.int().check($ > 0)]
|
an array of the positive integers (thus not null)
|
[$.int(), $.string()]
|
an array that has at least two elements, first is int and others are strings
|
[$.int(), 2]
[$.int(), 2, 5]
|
an array of ints with at least 2 items
an array of ints with at least 2 items, and maximum of 5 items
|
{ A: $.int(), B: [$.string()] }
|
the dictionary with the
A key of the int type and B - an array of strings |
$
[]
{}
|
any scalar or data structure as is
any array
any dictionary
|
{ $.string().notNull(): $.int().notNull() }
|
dictionary string -> int
|
A: StringMap
$.string().notNull(): $
|
the dictionary with the
A key that must be equal to StringMap , and other keys beany scalar or data structure
|
$.check($ in $this.myStaticMethod())
|
the value must be equal to one of a member of a list returned by static method of the class
|
$.check($this.myStaticMethod($))
|
the static method of the class must return true for the value
|
In the example above property port
must be int value greater than 0 and
less than 65536; scope
must be a string value and one of ‘public’, ‘cloud’,
‘host’ or ‘internal’, and protocol
must be a string value and either
‘TCP’ or ‘UDP’. When user passes some values to these properties it will be checked
that values confirm to the contracts.
Namespaces:
=: io.murano.apps.docker
std: io.murano
Name: ApplicationPort
Properties:
port:
Contract: $.int().notNull().check($ > 0 and $ < 65536)
scope:
Contract: $.string().notNull().check($ in list(public, cloud, host, internal))
Default: private
protocol:
Contract: $.string().notNull().check($ in list(TCP, UDP))
Default: TCP
Methods:
getRepresentation:
Body:
Return:
port: $.port
scope: $.scope
protocol: $.protocol
Property usage¶
Usage states the purpose of the property. This implies who and how can access it. The following usages are available:
Value
|
Explanation
|
---|---|
In
|
Input property. Values of such properties are obtained from a user
and cannot be modified in MuranoPL workflows. This is the default
value for the Usage key.
|
Out
|
A value is obtained from executing MuranoPL workflow and cannot be
modified by a user.
|
InOut
|
A value can be modified both by user and by workflow.
|
Const
|
The same as
In but once workflow is executed a property cannot be
changed neither by a user nor by a workflow. |
Runtime
|
A property is visible only from within workflows. It is neither read
from input nor serialized to a workflow output.
|
Static
|
Property is defined on a class rather than on an instance.
See Static methods and properties for details.
|
Config
|
A property allows to have per-class configuration. A value is obtained
from the config file rather than from the object model. These config
files are stored in a special folder that is configured in the
[engine] section of the Murano config file under the
class_configs key. |
The usage attribute is optional and can be omitted (which implies In
).
If the workflow tries to write to a property that is not declared with one of the types above, it is considered to be private and accessible only to that class (and not serialized to output and thus would be lost upon the next deployment). An attempt to read the property that was not initialized results in an exception.
Default¶
Default is a value that is used if the property value is not mentioned in the input object model, but not when it is set to null. Default, if specified, must conform to a declared property contract. If Default is not specified, then null is the default.
For properties that are references to other classes, Default can modify a default value of the referenced objects. For example:
p:
Contract: $.class(MyClass)
Default: {a: 12}
This overrides default for the a
property of MyClass
for instance
of MyClass
that is created for this property.
Workflow¶
Workflows are the methods that describe how the entities that are represented by MuranoPL classes are deployed.
In a typical scenario, the root object in an input data model is of
the io.murano.Environment
type, and has the deploy
method.
This method invocation causes a series of infrastructure activities
(typically, a Heat stack modification) and the deployment scripts
execution initiated by VM agents commands. The role of the workflow
is to map data from the input object model, or a result of previously
executed actions, to the parameters of these activities and to
initiate these activities in a correct order.
Methods¶
Methods have input parameters, and can return a value to a caller. Methods are defined in the Workflow section of the class using the following template:
methodName:
Scope: Public
Arguments:
- list
- of
- arguments
Body:
- list
- of
- instructions
Public is an optional parameter that specifies methods to be executed by direct triggering after deployment.
Method arguments¶
Arguments are optional too, and are declared using the same syntax as class properties. Same as properties, arguments also have contracts and optional defaults.
Unlike class properties Arguments may have a different set of Usages:
Value
|
Explanation
|
---|---|
Standard
|
Regular method argument. Holds a single value based on its contract.
This is the default value for the Usage key.
|
VarArgs
|
A variable length argument. Method body sees it as a list of values,
each matching a contract of the argument.
|
KwArgs
|
A keywrod-based argument, Method body sees it as a dict of values,
with keys being valid keyword strings and values matching a contract
of the argument.
|
Arguments example:
scaleRc:
Arguments:
- rcName:
Contract: $.string().notNull()
- newSize:
Contract: $.int().notNull()
- rest:
Contract: $.int()
Usage: VarArgs
- others:
Contract: $.int()
Usage: KwArgs
Method body¶
The Method body is an array of instructions that get executed sequentially. There are 3 types of instructions that can be found in a workflow body:
- Expressions,
- Assignments,
- Block constructs.
Method usage¶
Usage states the purpose of the method. This implies who and how can access it. The following usages are available:
Value
|
Explanation
|
---|---|
Runtime
|
Normal instance method.
|
Static
|
Static method that does not require class instance.
See Static methods and properties for details.
|
Extension
|
Extension static method that extends some other type.
See Extension methods for details.
|
Action
|
Method can be invoked from outside (using Murano API).
This option is deprecated for the package format versions > 1.3 in
favor of
Scope: Public and occasionally will be no longer
supported.
See Murano actions for details. |
The Usage
attribute is optional and can be omitted (which implies
Runtime
).
Method scope¶
The Scope
attribute declares method visibility. It can have two possible
values:
- Session - regular method that is accessible from anywhere in the current execution session. This is the default if the attribute is omitted;
- Public - accessible anywhere, both within the session and from outside through the API call.
The Scope
attribute is optional and can be omitted (which implies
Session
).
Expressions¶
Expressions are YAQL expressions that are executed for their side effect.
All accessible object methods can be called in the expression using
the $obj.methodName(arguments)
syntax.
Expression | Explanation |
---|---|
$.methodName()
$this.methodName()
|
invoke method ‘methodName’ on this (self) object
|
$.property.methodName()
$this.property.methodName()
|
invocation of method on object that is in
property |
$.method(1, 2, 3)
|
methods can have arguments
|
$.method(1, 2, thirdParameter => 3)
|
named parameters also supported
|
list($.foo().bar($this.property), $p)
|
complex expressions can be constructed
|
Assignment¶
Assignments are single key dictionaries with a YAQL expression as a key and arbitrary structure as a value. Such a construct is evaluated as an assignment.
Assignment | Explanation |
---|---|
$x: value
|
assigns
value to the local variable $x |
$.x: value
$this.x: value
|
assign
value to the object’s property |
$.x: $.y
|
copies the value of the property
y to the property x |
$x: [$a, $b]
|
sets
$x to the array of two values: $a and $b |
$x:
SomeKey:
NestedKey: $variable
|
structures of any level of complexity can be evaluated
|
$.x[0]: value
|
assigns
value to the first array entry of the x property |
$.x: $.x.append(value)
|
appends
value to the array in the x property |
$.x: $.x.insert(1, value)
|
inserts
value into position 1 of the array in the x property |
$x: list($a, $b).delete(0)
|
sets
$x to the list without the item at index 0 |
$.x.key.subKey: value
$.x[key][subKey]: value
|
deep dictionary modification
|
Block constructs¶
Block constructs control a program flow. They are dictionaries that have strings as all their keys.
The following block constructs are available:
Assignment | Explanation |
---|---|
Return: value
|
Returns value from a method
|
If: predicate()
Then:
- code
- block
Else:
- code
- block
|
predicate() is a YAQL expression that must be evaluated to True or False The
Else section is optionalOne-line code blocks can be written as scalars rather than an array.
|
While: predicate()
Do:
- code
- block
|
predicate() must be evaluated to True or False |
For: variableName
In: collection
Do:
- code
- block
|
collection must be a YAQL expression returning iterable collection or
evaluatable array as in assignment instructions, for example, [1, 2, $x] Inside a code block loop, a variable is accessible as
$variableName |
Repeat:
Do:
- code
- block
|
Repeats the code block specified number of times
|
Break:
|
Breaks from loop
|
Match:
case1:
- code
- block
case2:
- code
- block
Value: $valExpression()
Default:
- code
- block
|
Matches the result of
$valExpression() against a set of possible values
(cases). The code block of first matched case is executed.If no case matched and the default key is present
than the
Default code block get executed.The case values are constant values (not expressions).
|
Switch:
$predicate1():
- code
- block
$predicate2():
- code
- block
Default:
- code
- block
|
All code blocks that have their predicate evaluated to
True are executed,
but the order of predicate evaluation is not fixed.The
Default key is optional.If no predicate evaluated to
True , the Default code block get executed. |
Parallel:
- code
- block
Limit: 5
|
Executes all instructions in code block in a separate green threads in parallel.
The limit is optional and means the maximum number of concurrent green threads.
|
Try:
- code
- block
Catch:
With: keyError
As: e
Do:
- code
- block
Else:
- code
- block
Finally:
- code
- block
|
Try and Catch are keywords that represent the handling of exceptions due to data
or coding errors during program execution. A
Try block is the block of code in
which exceptions occur. A Catch block is the block of code, that is executed if
an exception occurred.Exceptions are not declared in Murano PL. It means that exceptions of any types can
be handled and generated. Generating of exception can be done with construct:
Throw: keyError .The
Else is optional block. Else block is executed if no exception occurred.The
Finally also is optional. It’s a place to put any code that will
be executed, whether the try-block raised an exception or not. |
Notice, that if you have more then one block construct in your workflow, you need to insert dashes before each construct. For example:
Body:
- If: predicate1()
Then:
- code
- block
- While: predicate2()
Do:
- code
- block
Object model¶
Object model is a JSON serialized representation of objects and their properties. Everything you do in the OpenStack dashboard is reflected in an object model. The object model is sent to the Application catalog engine when the user decides to deploy the built environment. On the engine side, MuranoPL objects are constructed and initialized from the received Object model, and a predefined method is executed on the root object.
Objects are serialized to JSON using the following template:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | {
"?": {
"id": "globally unique object ID (UUID)",
"type": "fully namespace-qualified class name",
"optional designer-related entries can be placed here": {
"key": "value"
}
},
"classProperty1": "propertyValue",
"classProperty2": 123,
"classProperty3": ["value1", "value2"],
"reference1": {
"?": {
"id": "object id",
"type": "object type"
},
"property": "value"
},
"reference2": "referenced object id"
}
|
Objects can be identified as dictionaries that contain the ?
entry.
All system fields are hidden in that entry.
There are two ways to specify references:
reference1
as in the example above. This method allows inline definition of an object. When the instance of the referenced object is created, an outer object becomes its parent/owner that is responsible for the object. The object itself may require that its parent (direct or indirect) be of a specified type, like all applications require to haveEnvironment
somewhere in a parent chain.- Referring to an object by specifying other object ID. That object must
be defined elsewhere in an object tree. Object references distinguished
from strings having the same value by evaluating property contracts.
The former case would have
$.class(Name)
while the later - the$.string()
contract.
MuranoPL Core Library¶
Some objects and actions can be used in several application deployments. All common parts are grouped into MuranoPL libraries. Murano core library is a set of classes needed in each deployment. Class names from core library can be used in the application definitions. This library is located under the meta directory.
Classes included in the Murano core library are as follows:
io.murano
io.murano.resources
io.murano.system
Class: Object¶
A parent class for all MuranoPL classes. It implements the initialize
,
setAttr
, and getAttr
methods defined in the pythonic part of the Object class.
All MuranoPL classes are implicitly inherited from this class.
See also
Source Object.yaml file.
Class: Application¶
Defines an application itself. All custom applications must be derived from this class.
See also
Source Application.yaml file.
Class: SecurityGroupManager¶
Manages security groups during an application deployment.
See also
Source SecurityGroupManager.yaml file.
Class: Environment¶
Defines an environment in terms of the deployment process and groups all Applications and their related infrastructures. It also able to deploy them at once.
Environments is intent to group applications to manage them easily.
Property | Description | Default usage |
---|---|---|
name |
An environment name. | In |
applications |
A list of applications belonging to an environment. | In |
agentListener |
A property containing the io.murano.system.AgentListener object
that can be used to interact with Murano Agent. |
Runtime |
stack |
A property containing a HeatStack object that can be used to interact with Heat. | Runtime |
instanceNotifier |
A property containing the io.murano.system.InstanceNotifier object
that can be used to keep track of the amount of deployed instances. |
Runtime |
defaultNetworks |
A property containing user-defined Networks
(io.murano.resources.Network ) that can be used as default networks
for the instances in this environment. |
In |
securityGroupManager |
A property containing the SecurityGroupManager object that can
be used to construct a security group associated with this environment. |
Runtime |
See also
Source Environment.yaml file.
Class: Instance¶
Defines virtual machine parameters and manages an instance lifecycle: spawning, deploying, joining to the network, applying security group, and deleting.
Property | Description | Default usage |
---|---|---|
name |
An instance name. | In |
flavor |
An instance flavor defining virtual machine hardware parameters. | In |
image |
An instance image defining operation system. | In |
keyname |
Optional. A key pair name used to connect easily to the instance. | In |
agent |
Configures interaction with the Murano agent using
io.murano.system.Agent . |
Runtime |
ipAddresses |
A list of all IP addresses assigned to an instance. | Out |
networks |
Specifies the networks that an instance will be joined to.
Custom networks that extend Network class can be
specified. An instance will be connected to them and for the default
environment network or flat network if corresponding values are set
to True . Without additional configuration, instance will be joined
to the default network that is set in the current environment. |
In |
volumes |
Specifies the mapping of a mounting path to volume implementations
that must be attached to the instance. Custom volumes that extend
Volume class can be specified. |
In |
blockDevices |
Specifies the list of block device mappings that an instance will use
to boot from. Each mapping defines a volume that must be an instance of
Volume class, device name, device type, and boot order.
Either the blockDevices property or image property must be
specified in order to boot an instance |
In |
assignFloatingIp |
Determines if floating IP is required. Default is False . |
In |
floatingIpAddress |
IP addresses assigned to an instance after an application deployment. | Out |
securityGroupName |
Optional. A security group that an instance will be joined to. | In |
See also
Source Instance.yaml file.
Resources¶
Instance class uses the following resources:
- Agent-v2.template
Python Murano Agent template.
Note
This agent is supposed to be unified. Currently, only Linux-based machines are supported. Windows support will be added later.
- linux-init.sh
- Python Murano Agent initialization script that sets up an agent with valid information containing an updated agent template.
- Agent-v1.template
- Windows Murano Agent template.
- windows-init.sh
- Windows Murano Agent initialization script.
Class: Network¶
The basic abstract class for all MuranoPL classes representing networks.
See also
Source Network.yaml file.
Class: Logger¶
Logging API is the part of core library since Liberty release. It was introduced to improve debuggability of MuranoPL programs.
You can get a logger instance by calling a logger
function which
is located in io.murano.system
namespace. The logger
function takes
a logger name as the only parameter. It is a common recommendation to use full
class name as a logger name within that class. This convention avoids names
conflicts in logs and ensures a better logging subsystem configurability.
Logger class instantiation:
$log: logger('io.murano.apps.activeDirectory.ActiveDirectory')
Level | Description |
---|---|
CRITICAL | Very severe error events that will presumably lead the application to abort. |
ERROR | Error events that might not prevent the application from running. |
WARNING | Events that are potentially harmful but will allow the application to continue running. |
INFO | Informational messages highlighting the progress of the application at the coarse-grained level. |
DEBUG | Detailed informational events that are useful when debugging an application. |
TRACE | Even more detailed informational events comparing to the DEBUG level. |
There are several methods that fully correspond to the log levels you can use
for logging events. They are debug
, trace
, info
, warning
,
error
, and critical
.
Logging example:
$log.info('print my info message {message}', message=>message)
Logging methods use the same format rules as the YAQL format function. Thus the line above is equal to the:
$log.info('print my info message {message}'.format(message=>message))
To print an exception stacktrace, use the exception method. This method uses the ERROR level:
Try:
- Throw: exceptionName
Message: exception message
Catch:
With: exceptionName
As: e
Do:
- $log.exception($e, 'something bad happen "{message}"', message=>message)
Note
You can configure the logging subsystem through the logging.conf
file
of the Murano Engine.
See also
- Source Logger.yaml file.
- OpenStack networking logging configuration.
Class: StatusReporter¶
Provides feedback feature. To follow the deployment process in the UI, all status changes should be included in the application configuration.
See also
Source StatusReporter.yaml file.
Reflection capabilities in MuranoPL.¶
Reflection provides objects that describes MuranoPL classes and packages.
The first important function is typeinfo
. Usage:
$typeInfo: typeinfo($someObject)
Now $typeInfo
variable contains instance of type of $someObject
(MuranoClass
instance).
MuranoPL provide following abilities to reflection:
Types¶
Property | Description |
---|---|
name |
name of MuranoPL class |
version |
version (SemVer) of MuranoPL class. |
ancestors |
list of class ancestors |
properties |
list of class properties. See Properties |
package |
package information. See Packages |
methods |
list of methods. See Methods |
type |
reference to type, which can be used as argument in engine functions |
Example
- $typeInfo: typeinfo($)
...
# log name, version and package name of this class
- $log.info("This is "{class_name}/{version} from {package}",
class_name => $typeInfo.name,
version => str($typeInfo.version),
package => $typeInfo.package.name))
- $log.info("Ancestors:")
- For: ancestor
In: $typeInfo.ancestors
Do:
#log all ancestors names
- $log.info("{ancestor_name}", ancestor_name => $ancestor.name)
# log full class version
- $log.info("{version}", version => str($typeInfo.version))
# create object with same class
- $newObject = new($typeInfo.type)
Properties¶
Property introspection¶
Property | Description |
---|---|
name |
name of property |
hasDefault |
boolean value. True, if property has default value, False otherwise |
usage |
Usage property’s field. See Property usage for details |
declaringType |
type - owner of declared property |
Property access¶
Methods | Description |
---|---|
$property.setValue($target, $value) |
set value of $property for object $target to $value |
$property.getValue($target) |
get value of $property for object $target |
Example
- $typeInfo: typeinfo($)
...
# select first property
- $selectedPropety: $typeInfo.properties.first()
# log property name
- $log.info("Hi, my name is {p_name}, p_name => $selectedProperty.name)
# set new property value
- $selectedProperty.setValue($, "new_value")
# log new property value using reflection
- $log.info("My new value is {value}", value => $selectedProperty.getValue($))
# also, if property static, $target can be null
- $log.info("Static property value is {value},
value => $staticProperty.getValue(null))
Packages¶
Property | Description |
---|---|
types |
list of types, declared in package |
name |
package name |
version |
package version |
Example
- $typeInfo: typeinfo($)
...
- $packageRef: $typeInfo.package
- $log.info("This is package {p_name}/{p_version}",
p_name => $packageRef.name,
p_version => str($packageRef.version))
- $log.info("Types in package:")
- For: type_
In: $packageRef.types
Do:
- $log.info("{typename}", typename => type_.name)
Methods¶
Methods properties¶
Property | Description |
---|---|
name |
method’s name |
declaringType |
type - owner of declared method |
arguments |
list of method’s arguments. See Method arguments |
Method invoking¶
Methods | Description |
---|---|
$method.invoke($target, $arg1, ... $argN, kwarg1 => value1, ..., kwargN => valueN) |
call $target ‘s method $method with $arg1 , ..., $argN positional arguments and kwarg1 , .... kwargN named arguments |
Example
- $typeInfo: typeinfo($)
...
# select single method by name
- $selectedMethod: $typeInfo.methods.where($.name = sampleMethodName).single()
# log method name
- $log.info("Method name: {m_name}", m_name => $selectedMethod.name)
# log method arguments names
- For: argument
In: $selectedMethod.arguments
Do:
- $log.info("{name}", name => $argument.name)
# call method with positional argument 'bar' and named `baz` == 'baz'
- $selectedMethod.invoke($, 'bar', baz => baz)
Method arguments¶
Property | Description |
---|---|
name |
argument’s name |
hasDefault |
True if argument has default value, False otherwise |
declaringMethod |
method - owner of argument |
usage |
argument’s usage type. See Method arguments for details |
- $firstArgument: $selectedMethod.arguments.first()
# store argument's name
- $argName: $firstArgument.name
# store owner's name
- $methodName: $firstArgument.declaringMethod.name
- $log.info("Hi, my name is {a_name} ! My owner is {m_name}",
a_name => $argName,
m_name => $methodName)
Static methods and properties¶
In MuranoPL, static denotes class methods and class properties (as opposed to instance methods and instance properties). These methods and properties can be accessed without an instance present.
Static methods are often used for helper methods that are not bound to any object (that is, do not maintain a state) or as a convenient way to write a class factory.
Type objects¶
Usually static methods and properties are accessed using type object. That is, an object that represents the class rather than class instance.
For any given class foo.Bar its type object may be retrieved using any of the following ways:
- Using
ns:Bar
notation considering that ns is declared in Namespaces section (and it is foo in this case),- Using
:Bar
syntax if Bar is in the current namespace (that is, what=:Bar
would mean if=
was a valid namespace prefix),- Using
type()
function with a fully qualified class name:type('foo.Bar')
,- By obtaining a type of class instance:
type($object)
(available for packages with format version starting from 1.3),- Through reflection:
typeinfo($object).type
.
No matter what method was used to get type object, the returned object will be the same because there can be only one type object per class.
All functions that accept type name, for example new()
function, also
accept type objects.
Accessing static methods and properties¶
Static methods can be invoked using one of the two ways:
- Using type object:
ns:Bar.foo(arg)
,:Bar.foo(arg)
, and so on,- On a class instance similar to normal methods:
$obj.foo(arg)
.
- Access to properties is similar to that:
- Using type object:
ns:Bar.property
,:Bar.property
, and so on, - On a class instance:
$obj.property
.
- Using type object:
Static properties are defined on a class rather than on an instance. Therefore, their values will be the same for all class instances (for particular version of the class).
Declaration of static methods and properties¶
Methods and properties are declared to be static by specifying
Usage: Static
on them.
For example:
Properties:
property:
Contract: $.string()
Usage: Static
Methods:
foo:
Usage: Static
Body:
- Return: $.property
Static properties are never initialized from object model but can be modified
from within MuranoPL code (i.e. they are not immutable).
Static methods also can be executed as an action from outside using
Scope: Public
. Within static method Body $this
(and $
if not
set to something else in expression) are set to type object rather than to
instance, as it is for regular methods.
Static methods written in Python¶
For MuranoPL classes entirely or partially written in Python, all methods
that have either @staticmethod
or @classmethod
decorators are
automatically imported as static methods and work as they normally do in
Python.
Extension methods¶
Extension methods are a special kind of static methods that can act as if they were regular instance methods of some other type.
Extension methods enable you to “add” methods to existing types without modifying the original type.
Defining extension methods¶
Extension methods are declared with the Usage: Extension
modifier.
For example:
Name: SampleClass
Methods:
mul:
Usage: Extension
Arguments:
- self:
Contract: $.int().notNull()
- arg:
Contract: $.int().notNull()
Body:
Return: $self * $arg
Extension method are said to extend some other type and that type is deducted from the first method argument contract. Thus extension methods must have at least one argument.
Extension methods can also be written in Python just the same way as static methods. However one should be careful in method declaration and use precise yaql specification of the type of first method argument otherwise the method will become an extension of any type.
To turn Python static method into extension method it must be decorated with
@yaql.language.specs.meta('Usage', 'Extension')
decorator.
Using extension methods¶
The example above defines a method that extends integer type. Therefore, with
the method above it becomes possible to say 2.mul(3)
. However, the most
often usage is to extend some existing MuranoPL class using class()
contract.
If the first argument contract does not have notNull()
, then the method
can be invoked on the null
object as well (like null.foo()
).
Extension methods are static methods and, therefore,can be invoked in a usual
way on type object: :SampleClass.mul(2, 3)
. However, unlike regular static
methods extensions cannot be invoked on a class instance because this can
result in ambiguity.
Using extension lookup order¶
When somewhere in the code the $foo.bar()
expression is encountered, MuranoPL
uses the following order to locate bar() implementation
:
- If there is an instance or static method in
$foo
‘s class, it will be used.- Otherwise if the current class (where this expression was encountered) has an extension method called
bar
and$foo
satisfies the contract of its first argument, then this method will be called.
Normally, if no method was found an exception will be raised. However,
additional extension methods can be imported into the current context. This is
done using the Import
keyword on a class level. The Import
section
specifies either a list or a single type name (or type object) which extension
methods will be available anywhere within the class code:
Name: MyClass
Import:
- ns:SomeOtherType
- :ClassFomCurrentContext
- 'io.murano.foo.Bar'
If no method was found with the algorithm above, the search continues on
extension methods of all classes listed in the Import
section in the order
types are listed.
MuranoPL Metadata¶
MuranoPL metadata is a way to attach additional information to various MuranoPL entities such as classes, packages, properties, methods, and method arguments. That information can be used by both applications (to implement dynamic programming techniques) or by the external callers (API consumers like UI or even by the Murano Engine itself to impose some runtime behavior based on well known meta values). Thus, metadata is a flexible alternative to adding new keyword for every new feature.
Work with metadata includes the following cases:
- Defining your own metadata classes
- Attaching metadata to various parts of MuranoPL code
- Obtaining metadata and its usage
Define metadata classes¶
Define MuranoPL class with the description of arbitrary metadata. The class that can be used as metadata differs from the regular class:
- The
Usage
attribute of the former equals toMeta
, while theUsage
attribute of the latter equals toClass
. The default value of theUsage
attribute isClass
. - Metadata class has additional attributes (
Cardinality
,Applies
andInherited
) to control how and where instances of that class can be attached.
Cardinality¶
The Cardinality
attribute can be set to either One
or Many
and
indicates the possibility to attach two or more instances of metadata to a
single language entity. The default value is One
.
Applies¶
The Applies
attribute can be set to one of Package
, Type
,
Method
, Property
, Argument
or All
and controls the possible
language entities which instances of metadata class can be attached to. It is
possible to specify several values using YAML list notation. The default value
is All
.
Inherited¶
The Inherited
attribute can be set to true
or false
and specifies
if there is metadata retained for child classes, overridden methods and
properties. The default value is false
.
Using of Inherited: true
has the following consequences.
If some class inherits from two classes with the same metadata attached and
this metadata has Cardinality: One
, it will lead to emerging of two
metadata objects with Cardinality: One
within a single entity and will
throw an exception. However, if the child class has this metadata attached
explicitly, it will override the inherited metas and there is no conflict.
If the child class has the same meta as its parent (attached explicitly),
then in case of Cardinatity: One
the meta of the child overrides the
meta of the parent as it is mentioned above. And in case of
Cardinatity: Many
meta of the parent is added to the list of the child’s
metas.
Example¶
The following example shows a simple meta-class implementation:
Name: MetaClassOne
Usage: Meta
Cardinality: One
Applies: All
Properties:
description:
Contract: $.string()
Default: null
count:
Contract: $.int().check($ >= 0)
Default: 0
MetaClassOne
is defined as a metadata class by setting the Usage
attribute to Meta
. The Cardinality
and Applies
attributes determine
that only one instance of MetaClassOne
can be attached to object of any
type. The Inherited
attribute is omitted so there is no metadata
retained for child classes, overridden methods and properties. In the
example above, Cardinality
and Applies
can be omitted as well, as
their values are set to default but in this case the author wants to be
explicit.
The following example shows metadata class with different values of attributes:
Name: MetaClassMany
Usage: Meta
Cardinality: Many
Applies: [Property, Method]
Inherited: true
Properties:
description:
Contract: $.string()
Default: null
count:
Contract: $.int().check($ >= 0)
Default: 0
An instance (or several instances) of MetaClassMany
can be attached to
either property or method. Overridden methods and properties inherit
metadata from its parents.
Attach metadata to a MuranoPL entity¶
To attach metadata to MuranoPL class, package, property, method or method
argument, add the Meta
keyword to its description. Under the
description, specify a list of metadata class instances which you want to
attach to the entity. To attach only one metadata class instance, use a single
scalar instead of a list.
Consider the example of attaching previously defined metadata to different entities in a class definition:
Namespaces:
=: io.murano.bar
std: io.murano
res: io.murano.resources
sys: io.murano.system
Name: Bar
Extends: std:Application
Meta:
MetaClassOne:
description: "Just an empty application class with some metadata"
count: 1
Properties:
name:
Contract: $.string().notNull()
Meta:
- MetaClassOne:
description: "Name of the app"
count: 1
- MetaClassMany:
count: 2
- MetaClassMany:
count: 3
Methods:
initialize:
Body:
- $._environment: $.find(std:Environment).require()
Meta:
MetaClassOne:
description: "Method for initializing app"
count: 1
deploy:
Body:
- If: not $.getAttr(deployed, false)
Then:
- $._environment.reporter.report($this, 'Deploy started')
- $._environment.reporter.report($this, 'Deploy finished')
- $.setAttr(deployed, true)
The Bar
class has an instance of metadata class MetaClassOne
attached.
For this, the Meta
keyword is added to the Bar
class description and
the instance of the MetaClassOne
class is specified under it. This
instance’s properties are description
and count
.
There are three meta-objects attached to the name
property of the Bar
class. One of it is a MetaclassOne
object and the other two are
MetaClassMany
objects. There can be more than one instance of
MetaClassMany
attached to a single entity since the Cardinality
attribute of MetaClassMany
is set to Many
.
The initialize
method of Bar
also has its metadata.
To attach metadata to the package, add the Meta
keyword to
manifest.yaml
file.
Example:
Format: 1.0
Type: Application
FullName: io.murano.bar.Bar
Name: Bar
Description: |
Empty Description
Author: author
Tags: [bar]
Classes:
io.murano.bar.Bar: Bar.yaml
io.murano.bar.MetaClassOne: MetaClassOne.yaml
io.murano.bar.MetaClassMany: MetaClassMany.yaml
Supplier:
Name: Name
Description: Description
Summary: Summary
Meta:
io.murano.bar.MetaClassOne:
description: "Just an empty application with some metadata"
count: 1
Obtain metadata in runtime¶
Metadata can be accessed from MuranoPL using reflection capabilities and from Python code using existing YAQL mechanism.
The following example shows how applications can access attached metadata:
Namespaces:
=: io.murano.bar
std: io.murano
res: io.murano.resources
sys: io.murano.system
Name: Bar
Extends: std:Application
Meta:
MetaClassOne:
description: "Just an empty application class with some metadata"
Methods:
sampleAction:
Scope: Public
Body:
- $._environment.reporter.report($this, typeinfo($).meta.
where($ is MetaClassOne).single().description)
The sampleAction
method is added to the Bar
class definition. This
makes use of metadata attached to the Bar
class.
The information about the Bar
class is received by calling the
typeinfo
function. Then metadata is accessed through the meta
property which returns the collection of all meta attached to the property.
Then it is checked that the meta is a MetaClassOne
object to ensure that
it has description
. While executing the action, the phrase “Just an
empty application class with some metadata” is reported to a log. Some
advanced usages of MuranoPL reflection capabilities can be found in the
corresponding section of this reference.
By using metadata, an application can get information of any type attached to any object and use this information to change its own behavior. The most valuable use-cases of metadata can be:
- Providing information about capabilities of application and its parts
- Setting application requirements
Capabilities can include version of software, information for use in UI or CLI, permissions, and any other. Metadata can also be used in requirements as a part of the contract.
The following example demonstrates the possible use cases for the metadata:
Name: BlogApp
Meta:
m:SomeFeatureSupport:
support: true
Properties:
volumeName:
Contract: $.string().notNull()
Meta:
m:Deprecated:
text: "volumeName property is deprecated"
server:
Contract: $.class(srv:CoolServer).notNull().check(typeinfo($).meta.
where($ is m:SomeFeatureSupport and $.support = true).any())
Methods:
importantAction:
Scope: Public
Meta:
m:CallerMustBeAdmin
Note, that the classes in the example do not exist as of Murano Mitaka, and therefore the example is not a real working code.
The SomeFeatureSupport
metadata with support: true
says that the
BlogApp
application supports some feature. The Deprecated
metadata
attached to the volumeName
property informs that this
property has a better alternative and it will not be used in the future
versions anymore. The CallerMustBeAdmin
metadata attached to the
importantAction
method sets permission to execute this method to the
admin users only.
In the contract of the server
property it is specified that the server
application must be of the srv:CoolServer
class and must have the
attached meta-object of the m:SomeFeatureSupport
class with the
support
property set to true
.
Versioning¶
Versioning is an ability to assign a version number to some particular package (and, in turn, to a class) and then distinguish packages with different versions.
Package version¶
It is possible to specify a version for packages. You can import several versions of the same package simultaneously and even deploy them inside a single environment. To do this, you should use Glare as a storage for packages. But if you’re going to keep only the latest version API is still good enough and both FormatVersion and Version rules will still be there. For more information about using Glare, refer to Using Glare as a storage for packages.
To specify the version of your package, add a new section to the manifest file:
Version: 0.1.0
It should be standard SemVer format version string consisting of 3 parts:
Major.Minor.Patch
and optional SemVer suffixes
[-dev-build.label[+metadata.label]]
.
All MuranoPL classes have the version of the package they are contained in.
If no version is specified, the package version is 0.0.0.
Note
It is impossible to show multiple versions of the same application in murano dashboard: only the last one is shown if the multiple versions are present.
Package requirements¶
In some cases, packages may require other packages for their work. You need to list such packages in the Require section of the manifest file:
Require: package1_FQN: version_spec_1 ... packageN_FQN: version_spec_N
version_spec
here denotes the allowed version range. It can be either in
semantic_version specification pip-like format or as a partial version string.
If you do not want to specify the package version, leave this value empty:
Require: package1_FQN: >=0.0.3 package2_FQN:
In this case, version specification is equal to 0.
Note
All packages depend on the io.murano package (Core Library). If you do not
specify this requirement in the list (or the list is empty, or there is
no Require
key in the package manifest), then dependency io.murano: 0
will be automatically added.
Object version¶
You can specify the version of the objects in UI definition when your
application requires a specific version of some class. To do this, add a new key
classVersion
to section ?
describing the object:
?: type: io.test.apps.TestApp classVersion: version_spec
Side-by-side versioning of packages¶
In some cases it might happen that several different versions of the same class are simultaneously present in a single environment:
- There are different versions of the same MuranoPL class inside a single object model (environment).
- Several class versions encounter within class parents. For example, class A extends B and C and class C inherits B2, where B and B2 are two different versions of the same class.
The first case, when two different versions of the same class need to communicate
with each other, is handled by the fact that in order to do that there is a
class()
contract for that value. class()
contract validates object
version against package requirements. If class A has a property with contract
$.class(B), then an object passed in this property when upcasted to B must have a
version compatible with requirement specification in A’s package (requesting
B’s package).
For the second case, where a single class attempts to inherit from two different versions of the same class engine (DSL), it attempts to find a version of this class which satisfies all parties and use it instead. However, if it is impossible, all remained different versions of the same class are treated as if they are unrelated classes.
For example: classA inherits classB from packageX and classC from packageY. Both classB and classC inherit from classD from packageZ; however, packageX depends on the version 1.2.0 of packageZ, while packageY depends on the version 1.3.0. This leads to a situation when classA transitively inherits classD of both versions 1.2 and 1.3. Therefore, an exception is thrown. However, if packageY’s dependency would be just “1” (which means any of the 1.x.x family), the conflict would be resolved and the 1.2 would be used as it satisfies both inheritance chains.
Murano engine is free to use any package version that is valid for the spec. For example, one application requires packageX with version spec < 0.3 and another package with the spec > 0. If both packages are get used in the same environment and the engine already loaded version 0.3 it can still use it for the second requirement even if there is a package with version 0.4 in the catalog and the classes from both classes are never interfere. In other words, engine always tries to minimize the number of versions in use for the single package to avoid conflicts and unnecessary package downloads. However, it also means that packages not always get the latest requirements.
Manifest format versioning¶
The manifests of packages are versioned using Format attribute. Currently, available versions are: 1.0, 1.1, 1.2 and 1.3. The versioning of manifest format is directly connected with YAQL and version of murano itself.
The short description of versions:
Format version | Description |
---|---|
1.0 | supported by all versions of murano. Use this version if you are planning to use yaql 0.2 in your application |
1.1 | supported since Liberty. yaql 0.2 is supported in legacy mode. Specify it, if you want to use features from yaql 0.2 and yaql 1.0.0 at the same time in your application. |
1.2 | supported since Liberty. Do not use yaql 0.2 in applications with this format. |
1.3 | supported since Mitaka. yaql 1.1 is available. It’s recommended specifying this format in new applications, where compatibility with older versions of murano is not required. |
1.4 | supported since Newton. Keyword Scope is introduced
for class methods to declare method’s accessibility from
outside through the API call. |
UI forms versioning¶
UI forms are versioned using Format attribute inside YAML definition. For more information, refer to corresponding documentation.
Murano actions¶
Murano action is a type of MuranoPL method. The differences from a regular MuranoPL method are:
- Action is executed on deployed objects.
- Action execution is initiated by API request, you do not have to call the method manually.
So murano action allows performing any operations on objects:
- Getting information from the VM, like a config that is generated during the deployment
- VM rebooting
- Scaling
A list of available actions is formed during the environment deployment. Right after the deployment is finished, you can call action asynchronously. Murano engine generates a task for every action. Therefore, the action status can be tracked.
Note
Actions may be called against any MuranoPL object, including Environment
,
Application
, and any other objects.
Note
Now murano doesn’t support big files download during action execution. This is because action results are stored in murano database and are limited by approximately 10kb size.
To mark a method as an action, use Scope: Public
or Usage: Action
.
The latter option is deprecated for the package format versions > 1.3 and
occasionally will be no longer supported. Also, you cannot use both
Usage: Action
and Scope: Session
in one method.
The following example shows an action that returns an archive with a configuration file:
exportConfig:
Scope: Public
Body:
- $._environment.reporter.report($this, 'Action exportConfig called')
- $resources: new(sys:Resources)
- $template: $resources.yaml('ExportConfig.template')
- $result: $.masterNode.instance.agent.call($template, $resources)
- $._environment.reporter.report($this, 'Got archive from Kubernetes')
- Return: new(std:File, base64Content => $result.content,
filename => 'application.tar.gz')
List of available actions can be found with environment details or application details API calls. It’s located in object model special data. Take a look at the following example:
Request:
http://localhost:8082/v1/environments/<id>/services/<id>
Response:
{
"name": "SimpleVM",
"?": {
"_26411a1861294160833743e45d0eaad9": {
"name": "SimpleApp"
},
"type": "com.example.Simple",
"id": "e34c317a-f5ee-4f3d-ad2f-d07421b13d67",
"_actions": {
"e34c317a-f5ee-4f3d-ad2f-d07421b13d67_exportConfig": {
"enabled": true,
"name": "exportConfig"
}
}
}
}
Static actions¶
Static methods (Static methods and properties) can also be called
through the API if they are exposed by specifying Scope: Public
, and the
result of its execution will be returned.
Consider the following example of the static action that makes use both of static class property and user’s input as an argument:
Name: Bar
Properties:
greeting:
Usage: Static
Contract: $.string()
Default: 'Hello, '
Methods:
staticAction:
Scope: Public
Usage: Static
Arguments:
- myName:
Contract: $.string().notNull()
Body:
- Return: concat($.greeting, $myName)
Request:
http://localhost:8082/v1/actions
Request body:
{
"className": "ns.Bar",
"methodName": "staticAction",
"parameters": {"myName": "John"}
}
Responce:
"Hello, John"