Language Reference — Inmanta 2017.4dev documentation

# Language Reference

The Inmanta language is a declarative language to model the configuration of an infrastructure.

The evaluation order of statements is determined by their dependencies on other statements and not based on the lexical order. i.e. The code is not necessarily executed top to bottom.

## Modules

The source is organized in modules. Each module is a git repository with the following structure:

    module/
    +-- files/
    +-- model/
    |  +-- _init.cf
    +-- plugins/
    +-- templates/
    +-- module.yaml

The `module.yaml` file, the `model` directory and the `model/_init.cf` are required.

For example:

    test/
    +-- files/
    +-- model/
    |  +-- _init.cf
    |  +-- services.cf
    |  +-- policy
    |  |  +-- _init.cf
    |  |  +-- other.cf
    +-- plugins/
    +-- templates/
    +-- module.yaml

The model code is in the `.cf` files. Each file forms a namespace. The namespaces for the files are the following.

File | Namespace
--- | ---
test/model/_init.cf | test
test/model/services.cf | test::services
test/model/policy/_init.cf | test::policy
test/model/policy/other.cf | test::policy::other

Modules are only loaded when they are imported by a loaded module or the `main.cf` file of the project.

To access members from another namespace, it must be imported into the current namespace.:

    import test::services

Imports can also define an alias, to shorten long names:

    import test::services as services

## Variables

Variables can be defined in any lexical scope. They are visible in their defining scope and its children. A lexical scope is either a namespaces or a code block (area between `:` and `end`).

Variable names must start with a lower case character and can consist of the characters: `a-zA-Z_0-9-`

A value can be assigned to a variable exactly once. The type of the variable is the type of the value. Assigning a value to the same variable twice will produce a compiler error, unless the values are identical.

Variables from other modules can be referenced by prefixing them with the module name (or alias)

    import redhat
    os = redhat::fedora23
    import ubuntu as ubnt
    os2 = ubnt::ubuntu1204

## Literals values

Literal values can be assigned to variables

    var1 = 1 # assign an integer, var1 contains now a number
    var2 = 3.14 # assign a float, var2 also contains a number
    var3 = "This is a string" # var3 contains a string

    # var 4 and 5 are both booleans
    var4 = true
    var5 = false

    # var6 is a list of values
    var6 = ["fedora", "ubuntu", "rhel"]

    # a dictionary with string keys and any type of values is also a primitive
    var7 = { "foo":"bar", "baz": 1}

    # var8 contains the same value as var2
    var8 = var2

    # next assignment will not return an error because var1 already contains this value
    var1 = 1

    # next assignment would return an error because var1 already has a different value
    #var1 = "test"

    #ref to a variable from another namespace
    import ip::services
    sshservice = ip::services::ssh

## Primitive types

The basic primitive types are `string`, `number` or `bool`.

Constrained primitive types can be derived from the basic primitive type with a typedef statement. Constrained primitive types add additional constraints to the basic primitive type with either a regex or a logical condition. The name of the constrained primitive type must not collide with the name of a variable or type in the same lexical scope.

    typedef : 'typedef' ID 'as' PRIMITIVE 'matching' condition|regex;

For example

    typedef tcp_port as number matching self > 0 and self < 65565
    typedef mac_addr as string matching /([0-9a-fA-F]{2})(:[0-9a-fA-F]{2}){5}$/

Lists of primitive types are also primitive types: `string[]`, `number[]`, `bool[]` or `mac_addr[]`

`dict` is the primitive type that represents a dictionary

## Conditions

Conditions can have the following forms

    condition : '(' condition ')'
        | condition 'or' condition
        | condition 'and' condition
        | 'not' condition
        | value ('>' | '>=' | '<' | '<=' | '==' | '!=') value
        | value 'in' value
        | 'true'
        | 'false'
        | functioncall
        ;

## Function calls / Plugins

Each module can define plugins. Plugins can contribute functions to the module’s namespace. The function call syntax is

    functioncall : moduleref '.' ID '(' arglist? ')';
    arglist : value
            | arglist ',' value

For example

    std::familyof(host.os, "rhel")
    a = param::one("region", "demo::forms::AWSForm")

## Entities

Entities model configuration concepts. They are like classes in other object oriented languages: they can be instantiated and they define the structure of their instances.

Entity names must start with an upper case character and can consist of the characters: `a-zA-Z_0-9-`

Entities can have a number of attributes and relations to other entities. Entity attributes have primitive types, with an optional default value. An attribute has to have a value unless the nulable variant of the primitive type is used. An attribute that can be null uses a primitive type with a `?` such as `string?`. A value can also be assigned only once to an attribute that can be null. To indicate that no value will be assigned, the literal `null` is available. `null` can also be the default value of an attribute.

Entities can inherit from multiple other entities. Entities inherits attributes and relations from parent entities. All entities inherit from `std::Entity`.

It is not possible to override or rename attributes or relations. However, it is possible to override defaults. Default values for attributes defined in the class take precedence over those in the parent classes. When a class has multiple parents, the left parent takes precedence over the others. A default value can be removed by setting its value to `undef`.

The syntax for defining entities is:

    entity: 'entity' ID ('extends' classlist)? ':' attribute* 'end';

    classlist: class
              | class ',' classlist;

    attribute: primitve_type ID ('=' literal)?;

Defining entities in a configuration model

    entity File:
       string path
       string content
       number mode = 640
       string[] list = []
       dict things = {}
    end

## Relations

A Relation is a unidirectional or bidirectional relation between two entities. The consistency of a bidirectional double binding is maintained by the compiler: assignment to one side of the relation is an implicit assignment of the reverse relation.

Relations are defined by specifying each end of the relation together with the multiplicity of each relation end. Each end of the relation is named and is maintained as a double binding by the compiler.

Defining relations between entities in the domain model

    relation: class '.' ID multi '--' class '.' ID multi
            | class '.' ID multi annotation_list class '.' ID multi ;
    annotation_list: value
            | annotation_list ',' value

For example a bidirectional relation:

    File.service [1] -- Service.file [1:]

Or a unidirectional relation

    uni_relation : class '.' ID multi '--' class
           | class '.' ID multi annotation_list class;

For example

    Service.file [1:] -- File

Relation multiplicities are enforced by the compiler. If they are violated a compilation error is issued.

Note

In previous version another relation syntax was used that was less natural to read and allowed only bidirectional relations. The relation above was defined as `File file [1:] -- [1] Service service` This synax is deprecated but still widely used in many modules.

## Instantiation

Instances of an entity are created with a constructor statement

    File(path="/etc/motd")

A constructor can assign values to any of the properties (attributes or relations) of the entity. It can also leave the properties unassigned. For attributes with default values, the constructor is the only place where the defaults can be overridden.

Values can be assigned to the remaining properties as if they are variables. To relations with a higher arity, multiple values can be assigned

    Host.files [0:] -- File.host [1]

    h1 = Host("test")
    f1 = File(host=h1, path="/opt/1")
    f2 = File(host=h1, path="/opt/2")
    f3 = File(host=h1, path="/opt/3")

    // h1.files equals [f1, f2, f3]

    FileSet.files [0:] -- File.set [1]

    s1 = FileSet()
    s1.files = [f1,f2]
    s1.files = f3

    // s1.files equals [f1, f2, f3]

    s1.files = f3
    // adding a value twice does not affect the relation,
    // s1.files still equals [f1, f2, f3]

## Refinements

Entities define what should be deployed. Entities can either be deployed directly (such as files and packages) or they can be refined. Refinement expands an abstract entity into one or more more concrete entities.

For example, `apache::Server` is refined as follows

    implementation apacheServerDEB for Server:
        pkg = std::Package(host=host, name="apache2-mpm-worker", state="installed")
        pkg2 = std::Package(host=host, name="apache2", state="installed")
        svc = std::Service(host=host, name="apache2", state="running", onboot=true, reload=true, requires=[pkg, pkg2])
        svc.requires = self.requires

        # put an empty index.html in the default documentroot so health checks do not fail
        index_html = std::ConfigFile(host=host, path="/var/www/html/index.html", content="",
                                 requires=pkg)
        self.user = "www-data"
        self.group = "www-data"
    end

    implement Server using apacheServerDEB when std::familyof(host.os, "ubuntu")

For each entity one or more refinements can be defined with the `implementation` statement. Implementation are connected to entities using the `implement` statement.

When an instance of an entity is constructed, the runtime searches for refinements. One or more refinements are selected based on the associated conditions. When no implementation is found, an exception is raised. Entities for which no implementation is required are implemented using `std::none`.

In the implementation block, the entity instance itself can be accessed through the variable self.

`implement` statements are not inherited.

The syntax for implements and implementation is:

    implementation: 'implementation' ID 'for' class ':' statement* 'end';
    implement: 'implement' class 'using' ID ('when' condition)?;

## Indexes and queries

Index definitions make sure that an entity is unique. An index definition defines a list of properties that uniquely identify an instance of an entity. If a second instance is constructed with the same identifying properties, the first instance is returned instead.

All identifying properties must be set in the constructor.

Indices are inherited. i.e. all identifying properties of all parent types must be set in the constructor.

Defining an index

    entity Host:
        string  name
    end

    index Host(name)

Explicit index lookup is performed with a query statement

    testhost = Host[name="test"]

For indices on relations (instead of attributes) an alternative syntax can be used

    entity File:
        string path
    end

    Host.files [0:] -- File.host [1]

    index File(host, path)

    a = File[host=vm1, path="/etc/passwd"]  # normal index lookup
    b = vm1.files[path="/etc/passwd"]  # selector style index lookup
    # a == b

## For loop

To iterate over the items of a list, a for loop can be used

    for i in std::sequence(size, 1):
        app_vm = Host(name="app{{i}}")
    end

The syntax is:

    for: 'for' ID 'in' value ':' statement* 'end';

## Transformations

At the lowest level of abstraction the configuration of an infrastructure often consists of configuration files. To construct configuration files, templates and string interpolation can be used.

### String interpolation

String interpolation allows variables to be include as parameters inside a string.

The included variables are resolved in the lexical scope of the string they are included in.

Interpolating strings

    hostname = "serv1.example.org"
    motd = """Welcome to {{hostname}}\n"""

### Templates

Inmanta integrates the Jinja2 template engine. A template is evaluated in the lexical scope where the `std::template` function is called. This function accepts as an argument the path of a template file. The first part of the path is the module that contains the template and the remainder of the path is the path within the template directory of the module.

The integrated Jinja2 engine supports to the entire Jinja feature set, except for subtemplates. During execution Jinja2 has access to all variables and plug-ins that are available in the scope where the template is evaluated. However, the `::` in paths needs to be replaced with a `.`. The result of the template is returned by the template function.

Using a template to transform variables to a configuration file

    hostname = "wwwserv1.example.com"
    admin = "joe@example.com"
    motd_content = std::template("motd/message.tmpl")

The template used in the previous listing

    Welcome to {{ hostname }}
    This machine is maintainted by {{ admin }}

## Plug-ins

For more complex operations, python plugins can be used. Plugins are exposed in the Inmanta language as function calls, such as the template function call. A template accepts parameters and returns a value that it computed out of the variables. Each module that is included can also provide plug-ins. These plug-ins are accessible within the namespace of the module. The Plugins section of the module guid provides more details about how to write a plugin.