Embedded entities

In some situations, the attributes of a ServiceEntity contain a lot of duplication. Consider the following example:

main.cf
 1import lsm
 2import lsm::fsm
 3
 4entity ServiceX extends lsm::ServiceEntity:
 5    """
 6        The API of ServiceX.
 7
 8        :attr service_id: A unique ID for this service.
 9
10        :attr customer_router_name: The name of the router on the customer side.
11        :attr customer_router_system_ip: The system ip of the router on the customer side.
12        :attr customer_router_vendor: The vendor of the router on the customer side.
13        :attr customer_router_chassis: The chassis of the router on the customer side.
14
15        :attr provider_router_name: The name of the router on the provider side.
16        :attr provider_router_system_ip: The system ip of the router on the provider side.
17        :attr provider_router_vendor: The vendor of the router on the provider side.
18        :attr provider_router_chassis: The chassis of the router on the provider side.
19    """
20    string service_id
21
22    string customer_router_name
23    std::ipv4_address customer_router_system_ip
24    lsm::attribute_modifier customer_router_system_ip__modifier="rw+"
25    string customer_router_vendor
26    string customer_router_chassis
27
28    string provider_router_name
29    std::ipv4_address provider_router_system_ip
30    lsm::attribute_modifier provider_router_system_ip__modifier="rw+"
31    string provider_router_vendor
32    string provider_router_chassis
33end
34
35index ServiceX(service_id)
36
37implement ServiceX using parents
38
39binding = lsm::ServiceEntityBindingV2(
40    service_entity="__config__::ServiceX",
41    lifecycle=lsm::fsm::service,
42    service_entity_name="service_x",
43)
44
45for instance in lsm::all(binding):
46    ServiceX(
47        instance_id=instance["id"],
48        entity_binding=binding,
49        **instance["attributes"],
50    )
51end

Specifying the router details multiple times, results in code that is hard to read and hard to maintain. Embedded entities provide a mechanism to define a set of attributes in a separate entity. These attributes can be included in a ServiceEntity or in another embedded entity via an entity relationship. The code snippet below rewrite the above-mentioned example using the embedded entity Router:

main.cf
 1import lsm
 2import lsm::fsm
 3
 4entity ServiceX extends lsm::ServiceEntity:
 5    """
 6        The API of ServiceX.
 7
 8        :attr service_id: A unique ID for this service.
 9    """
10    string service_id
11end
12
13index ServiceX(service_id)
14
15ServiceX.customer_router [1] -- Router
16ServiceX.provider_router [1] -- Router
17
18entity Router extends lsm::EmbeddedEntity:
19    """
20        Router details.
21
22        :attr name: The name of the router.
23        :attr system_ip: The system ip of the router.
24        :attr vendor: The vendor of the router.
25        :attr chassis: The chassis of the router.
26    """
27    string name
28    std::ipv4_address system_ip
29    lsm::attribute_modifier system_ip__modifier="rw+"
30    string vendor
31    string chassis
32end
33
34index Router(name)
35
36implement ServiceX using parents
37implement Router using parents
38
39binding = lsm::ServiceEntityBindingV2(
40    service_entity="__config__::ServiceX",
41    lifecycle=lsm::fsm::service,
42    service_entity_name="service_x",
43)
44
45for instance in lsm::all(binding):
46    ServiceX(
47        instance_id=instance["id"],
48        entity_binding=binding,
49        service_id=instance["attributes"]["service_id"],
50        customer_router=Router(**instance["attributes"]["customer_router"]),
51        provider_router=Router(**instance["attributes"]["provider_router"]),
52    )
53end

Note, that the Router entity also defines an index on the name attribute.

Modelling embedded entities

This section describes the different parts of the model that are relevant when modelling an embedded entity.

Strict modifier enforcement

Each entity binding (lsm::ServiceEntityBinding and lsm::ServiceEntityBindingV2) has a feature flag called strict_modifier_enforcement. This flag indicates whether attribute modifiers should be enforced recursively on embedded entities or not. For new projects, it’s recommended to enable this flag. Enabling it can be done in two different ways:

  • Create a service binding using the lsm::ServiceEntityBinding entity and set the value of the attribute strict_modifier_enforcement explicitly to true.

  • Or, create a service binding using the lsm::ServiceEntityBindingV2 entity (recommended approach). This entity has the strict_modifier_enforcement flag enabled by default.

The remainder of this section assumes the strict_modifier_enforcement flag is enabled. If your project has strict_modifier_enforcement disabled for legacy reasons, consult the Section Legacy: Embedded entities without strict_modifier_enforcement for more information.

Defining an embedded entity

The following constraints should be satisfied for each embedded entity defined in a model:

  • The embedded entity must inherit from lsm::EmbeddedEntity.

  • When a bidirectional relationship is used between the embedding entity and the embedded entity, the variable name referencing the embedding entity should start with an underscore (See code snippet below).

  • When a bidirectional relationship is used, the arity of the relationship towards the embedding entity should be 0 or 1.

  • Relation attributes, where the other side is an embedded entity, should be prefixed with an underscore when the relation should not be included in the service definition.

  • An index must be defined on an embedded entity if the relationship towards that embedded entity has an upper arity larger than one. This index is used to uniquely identify an embedded entity in a relationship. More information regarding this is available in section Attribute modifiers on a relationship.

  • When an embedded entity is defined with the attribute modifier __r__, all sub-attributes of that embedded entity need to have the attribute modifier set to read-only as well. More information regarding attribute modifiers on embedded entities is available in section Attribute modifiers on a relationship.

The following code snippet gives an example of a bidirectional relationship to an embedded entity. Note that the name of the relationship to the embedding entity starts with an underscore as required by the above-mentioned constraints:

main.cf
 1import lsm
 2import lsm::fsm
 3
 4entity ServiceX extends lsm::ServiceEntity:
 5    """
 6        The API of ServiceX.
 7
 8        :attr service_id: A unique ID for this service.
 9    """
10    string service_id
11end
12
13index ServiceX(service_id)
14
15ServiceX.router [1] -- Router._service [1]
16
17entity Router extends lsm::EmbeddedEntity:
18    """
19        Router details.
20
21        :attr name: The name of the router.
22        :attr system_ip: The system ip of the router.
23        :attr vendor: The vendor of the router.
24        :attr chassis: The chassis of the router.
25    """
26    string name
27    std::ipv4_address system_ip
28    lsm::attribute_modifier system_ip__modifier="rw+"
29    string vendor
30    string chassis
31end
32
33index Router(name)
34
35implement ServiceX using parents
36implement Router using parents
37
38binding = lsm::ServiceEntityBindingV2(
39    service_entity="__config__::ServiceX",
40    lifecycle=lsm::fsm::service,
41    service_entity_name="service_x",
42)
43
44for instance in lsm::all(binding):
45    ServiceX(
46        instance_id=instance["id"],
47        entity_binding=binding,
48        service_id=instance["attributes"]["service_id"],
49        router=Router(**instance["attributes"]["router"]),
50    )
51end

Attribute modifiers on a relationship

Attribute modifiers can also be specified on relational attributes. The -- part of the relationship definition can be replaced with either lsm::__r__, lsm::__rw__ or lsm::__rwplus__. These attribute modifiers have the following semantics when set on a relationship:

  • __r__: The embedded entity/entities can only be set by an allocator. If an embedded entity has this attribute modifier, all its sub-attributes should have the read-only modifier as well.

  • __rw__: The embedded entities, part of the relationship, should be set on service instantiation. After creation, no embedded entities can be added or removed from the relationship anymore. Note that this doesn’t mean that the attributes of the embedded entity cannot be updated. The latter is determined by the attribute modifiers defined on the attributes of the embedded entity.

  • __rwplus__: After service instantiation, embedded entities can be added or removed from the relationship.

When the relationship definition contains a -- instead of one of the above-mentioned keywords, the default attribute modifier __rw__ is applied on the relationship. The code snippet below gives an example on the usage of attribute modifiers on relationships:

main.cf
 1import lsm
 2import lsm::fsm
 3
 4entity ServiceX extends lsm::ServiceEntity:
 5    """
 6        The API of ServiceX.
 7
 8        :attr service_id: A unique ID for this service.
 9    """
10    string service_id
11end
12
13index ServiceX(service_id)
14
15ServiceX.primary [1] -- SubService
16ServiceX.secondary [0:1] lsm::__rwplus__ SubService
17
18entity SubService extends lsm::EmbeddedEntity:
19    """
20        :attr ip: The IP address of the service
21    """
22    std::ipv4_address ip
23end
24
25index SubService(ip)
26
27implement ServiceX using parents
28implement SubService using parents
29
30binding = lsm::ServiceEntityBindingV2(
31    service_entity="__config__::ServiceX",
32    lifecycle=lsm::fsm::service,
33    service_entity_name="service_x",
34)
35
36for instance in lsm::all(binding):
37    service_x = ServiceX(
38        instance_id=instance["id"],
39        entity_binding=binding,
40        service_id=instance["attributes"]["service_id"],
41        primary=SubService(**instance["attributes"]["primary"]),
42    )
43    if instance["attributes"]["secondary"] != null:
44        service_x.secondary=SubService(**instance["attributes"]["secondary"])
45    end
46end

In order to enforce the above-mentioned attribute modifiers, the inmanta server needs to be able to determine whether the embedded entities, provided in an attribute update, are an update of an existing embedded entity or a new embedded entity is being created. For that reason, each embedded entity needs to define the set of attributes that uniquely identify the embedded entity if the upper arity of the relationship is larger than one. This set of attributes is defined via an index on the embedded entity. The index should satisfy the following constraints:

  • At least one non-relational attribute should be included in the index.

  • Each non-relational attribute, part of the index, is exposed via the north-bound API (i.e. the name of the attribute doesn’t start with an underscore).

  • The index can include no other relational attributes except for the relation to the embedding entity.

The attributes that uniquely identify an embedded entity can never be updated. As such, they cannot have the attribute modifier __rwplus__.

If multiple indices are defined on the embedded entity that satisfy the above-mentioned constraints, one index needs to be selected explicitly by defining the string[]? __lsm_key_attributes attribute in the embedded entity. The default value of this attribute should contain all the attributes of the index that should be used to uniquely identify the embedded entity.

The example below defines an embedded entity SubService with two indices that satisfy the above-mentioned constraints. The __lsm_key_attributes attribute is used to indicate that the name attribute should be used to uniquely identify the embedded entity.

main.cf
 1import lsm
 2import lsm::fsm
 3
 4entity ServiceX extends lsm::ServiceEntity:
 5    """
 6        The API of ServiceX.
 7
 8        :attr service_id: A unique ID for this service.
 9    """
10    string service_id
11end
12
13index ServiceX(service_id)
14
15ServiceX.primary [1] -- SubService
16ServiceX.secondary [0:1] lsm::__rwplus__ SubService
17
18entity SubService extends lsm::EmbeddedEntity:
19    """
20        :attr name: The name of the sub-service
21        :attr ip: The IP address of the service
22    """
23    string name
24    std::ipv4_address ip
25    string[]? __lsm_key_attributes = ["name"]
26end
27
28index SubService(name)
29index SubService(ip)
30
31implement ServiceX using parents
32implement SubService using parents
33
34binding = lsm::ServiceEntityBindingV2(
35    service_entity="__config__::ServiceX",
36    lifecycle=lsm::fsm::service,
37    service_entity_name="service_x",
38)
39
40for instance in lsm::all(binding):
41    service_x = ServiceX(
42        instance_id=instance["id"],
43        entity_binding=binding,
44        service_id=instance["attributes"]["service_id"],
45        primary=SubService(**instance["attributes"]["primary"]),
46    )
47    if instance["attributes"]["secondary"] != null:
48        service_x.secondary=SubService(**instance["attributes"]["secondary"])
49    end
50end

If the upper arity of the relationship towards an embedded entity is one, it’s not required to define an index on the embedded entity. In that case, the embedded entity will always have the same identity, no matter what the values of its attributes are. This means that there will be no difference in behavior whether the attribute modifier is set to rw or rw+. If an index is defined on the embedded entity, the attribute modifiers will be enforced in the same way as for relationships with an upper arity larger than one.

Tracking embedded entities across updates

Depending on what the embedded entities are modeling, you might want to keep track of which embedded entities were added or removed during an update, in order to apply custom logic to them. This section describes how to track embedded entities during the update flow of a service.

When using the ‘simple’ lifecycle, this is supported out of the box by passing include_purged_embedded_entities=true to the lsm::all() plugin call.

Note

This feature requires strict_modifier_enforcement=true to be able to correctly identify which embedded entities are added or removed. Make sure this is set for all the relevant bindings.

During each step of the update, two sets of attributes (the ‘current’ set and the ‘previous’ set) will be compared to determine which embedded entities were added or removed. The plugin will accordingly set the following boolean attributes on the relevant embedded entities: _removed and _added. These values can then be used in the model to implement custom logic.

Note

The ‘simple’ lifecycle defines out-of-the-box which pair of sets should be compared at each step of the update. Please refer to the Tracking embedded entities when using a custom lifecycle section below for more information on how to define which pairs should be compared when using a custom lifecycle.

Note

To set a different naming scheme for these tracking attributes, use the removed_attribute and added_attribute parameters of the lsm::all() plugin.

The following sections describe 3 flavours of update flows through examples.

Update flow with implicit deletion

In this update flow, the embedded entities are side-effect free and fully under control of the parent entity. The following model demonstrate this case:

  • The parent entity is a file on a file system

  • The embedded entities represent individual lines in this file

In this example, the deployed resources (i.e. the deployed files) will mirror exactly the embedded entities present in the model since the content of the file is derived from the set of embedded entities. If an embedded entity is removed during an update, the file content will reflect this accordingly.

main.cf
 1import std::testing
 2import lsm
 3import lsm::fsm
 4import fs
 5import mitogen
 6
 7entity File extends lsm::ServiceEntity:
 8    """
 9        Top-level service representing a file on a file system.
10
11        :attr path: Unique path to this file
12    """
13    string path
14end
15
16index File(path)
17File.lines [0:] lsm::__rwplus__ Line._file [1]
18
19entity Line extends lsm::EmbeddedEntity:
20    """
21        Embedded entity representing a single line in a file.
22
23        :attr line_no: The line number
24        :attr content: Content of this line
25    """
26    int line_no
27    string content = ""
28    lsm::attribute_modifier content__modifier="rw+"
29
30end
31
32index Line(_file, line_no)
33
34binding = lsm::ServiceEntityBindingV2(
35    service_entity="__config__::File",
36    lifecycle=lsm::fsm::simple,
37    service_entity_name="file",
38)
39
40for instance in lsm::all(binding, include_purged_embedded_entities=false):
41    file = File(
42        instance_id=instance["id"],
43        entity_binding=binding,
44        path=instance["attributes"]["path"],
45    )
46    for line in instance["attributes"]["lines"]:
47        file.lines += Line(**line)
48    end
49end
50
51mitogen_local=mitogen::Local()
52
53implementation file for File:
54    self.resources += std::testing::NullResource(name=self.path)
55    self.resources += fs::File(
56        path=self.path,
57        host=std::Host(
58            via=mitogen_local, name="internal", os=std::linux,
59        ),
60        mode=777,
61        owner="inmanta",
62        group="inmanta",
63        content = string([string(l.content) for l in std::key_sort(self.lines, 'line_no')]),
64    )
65end
66
67implement File using parents
68implement File using file
69implement Line using parents

Update flow with explicit deletion

In this update flow, the embedded entities are not side-effect free or not fully under control of the parent entity. The following model demonstrate this case:

  • The parent entity is a directory on a file system

  • The embedded entities represent individual files in this directory

In this example, we have to take extra steps to make sure the deployed resources (i.e. the deployed directories and files below them) match the embedded entities present in the model. The content of the directories is derived from the set of embedded entities. If an embedded entity is removed during an update, we have to make sure to remove it from disk explicitly.

main.cf
 1import std::testing
 2import lsm
 3import lsm::fsm
 4import fs
 5import mitogen
 6
 7entity Folder extends lsm::ServiceEntity:
 8    """
 9        Top-level service representing a folder on a file system.
10
11        :attr path: Unique path to this folder
12    """
13    string path
14end
15
16index Folder(path)
17Folder.files [0:] lsm::__rwplus__ File._folder [1]
18
19entity File extends lsm::EmbeddedEntity:
20    """
21        Embedded entity representing a file in a folder.
22
23        :attr name: name of this line
24        :attr content: Content of this file
25
26    """
27    string name = ""
28    string content
29    lsm::attribute_modifier content__modifier="rw+"
30
31    # These private attributes keep track of added/removed
32    # embedded entities across updates
33    bool _added = false
34    bool _removed = false
35end
36
37index File(_folder, name)
38
39binding = lsm::ServiceEntityBindingV2(
40    service_entity="__config__::Folder",
41    lifecycle=lsm::fsm::simple,
42    service_entity_name="folder",
43)
44
45for instance in lsm::all(binding, include_purged_embedded_entities=true):
46    folder = Folder(
47        instance_id=instance["id"],
48        entity_binding=binding,
49        path=instance["attributes"]["path"],
50    )
51    for file in instance["attributes"]["files"]:
52        folder.files += File(**file)
53    end
54end
55
56implementation folder for Folder:
57    self.resources += std::testing::NullResource(name=self.path)
58end
59
60mitogen_local = mitogen::Local()
61
62implementation file for File:
63    self._folder.resources += fs::File(
64        path=self._folder.path+"/"+self.name,
65        host=std::Host(
66            via=mitogen_local, name="internal", os=std::linux,
67        ),
68        mode=777,
69        owner="inmanta",
70        group="inmanta",
71        content = self.content,
72        # By keeping track of embedded entities removed during an update
73        # we can purge the underlying resources accordingly.
74        # Alternatively, if the parent folder is removed, we want to
75        # purge all embedded entities.
76        purged=self._removed or self._folder.purge_resources,
77    )
78end
79
80implement File using parents
81implement File using file
82implement Folder using parents, folder

Update flow with mutually explicit desired state

The last possible update scenario is one with mutually exclusive desired state throughout the update, e.g. a database migration from cluster A to cluster B:

  1. Initial desired state: data lives in cluster A

  2. Intermediate desired state: data is replicated in cluster A and cluster B

  3. Final desired state: data lives in cluster B

For these more involved update scenarios we recommend updating the lifecycle specifically for this update.

Tracking embedded entities when using a custom lifecycle

To track updates, lsm needs to know which attribute set is considered the ‘previous’ state and which is the ‘current’ state. This depends on the lifecycle and which direction we are moving: are we updating or rolling back.

It also depends on if an instance is being validated or not. When doing a compile where the instance is in a validation state, if the instance is not being validated, it pretends to be in the state it was in prior to the update i.e. it pretends its current attributes are the ones it had before the update. If the instance is being validated, it pretends to be in the post-update state i.e. it pretends its current attributes are the ones it will have after the update.

The lsm::all() plugin derives this from the following attributes of the lifecycle states on the update path:

  • previous_attr_set_on_validate

  • previous_attr_set_on_export

The domain of valid values for these attributes is ['candidate', 'active', 'rollback', null].

The following logic is used to determine which is the current and which is the previous attribute set

instance is being validated

previous

current

instance is being validated

previous_attr_set_on_validate

validate_self

instance is not being validated

previous_attr_set_on_export

‘active’ attribute set

When building a custom lifecycle, to be able to use the tracking plugin, these fields have to be set correctly. To do so, the lifecycle has to be analyzed. The remainder of this chapter describes a method to perform this analysis by starting from the main states, and working towards the validation states. We will apply this to the lsm::fsm::simple lifecycle.

1. First step is to have clear view of the lifecycle. This can be done by plotting a graph of it. This can be done by adding lsm::render_dot(lsm::fsm::simple) to a model and compiling it. This will create a file called fsm.svg that contains the lifecycle. For reference, here’s a simplified representation (it doesn’t contain the failure states) of the ‘update’ part of the lsm::fsm::simple lifecycle, to follow along the example.

Simple lifecycle update subgraph
  1. Second step is to make a table for each state involved in the update, including the state just before the start of the update and the one after it. Ignore _failed states, as their config will be identical to the associated success state. For each validating transfer, add the source state a second time.

state

validating

current attributes

previous attributes

is like

operation since is like

up

update_start

update_start

yes

update_rejected

update_acknowledged

update_inprogress

rollback

  1. Fill in states before the update and states where we are actually deploying the update. The current attributes will always be active and previous attributes depends on the direction we are moving in. For the up state, we are not updating, so there is no previous attributes. For updates, previous attributes is always rollback (the old active set has been promoted to the rollback set). For rollback scenarios, the previous attributes are always candidate.

state

validating

current attributes

previous attributes

is like

operation since is like

up

active

update_start

update_start

yes

update_rejected

update_acknowledged

update_inprogress

active

rollback

rollback

active

candidate

4. For each state that remains, indicate which other state it pretends to be like: the state prior to the update or the state after the update. Also add any operation (‘promote’ or ‘rollback’) performed between the state and the ‘pretended’ state, and its direction (‘forwards’ or ‘backwards’).

  • If the pretend state is chronologically before the current state, the operation will be applied in the ‘forwards’ direction.

  • If the pretend state is chronologically after the current state, the operation will be applied in the ‘backwards’ direction.

e.g. for the validating update_start state: its is like state, update_inprogress comes after it chronologically and a ‘promote’ operation will happen. Following the above rules, we will apply a ‘promote’ operation in the ‘backwards’ direction to find the current attributes and the previous attributes of this state given those of the update_inprogress state.

state

validating

current attributes

previous attributes

is like

operation since is like

up

active

update_start

up

update_start

yes

update_inprogress

promote/backwards

update_rejected

up

update_acknowledged

up

update_inprogress

active

rollback

rollback

active

candidate

  1. Copy over the state of the is like column and apply the operations. e.g. for the validating update_start state:

    1. Copy attributes from the state in the is like column (update_inprogress)

      state

      validating

      current attributes

      previous attributes

      update_start

      yes

      active

      rollback

    2. Apply the operation: the ‘forwards’ ‘promote’ operation shifts the active set to rollback and the candidate set to active, but here we apply it in reverse, i.e. the active set becomes candidate and the rollback set becomes active:

      state

      validating

      current attributes

      previous attributes

      update_start

      yes

      candidate

      active

state

validating

current attributes

previous attributes

is like

operation since is like

up

active

update_start

active

up

update_start

yes

candidate

active

update_inprogress

promote/backwards

update_rejected

active

up

update_acknowledged

active

up

update_inprogress

active

rollback

rollback

active

candidate

  1. Finally, translate to the state variables as follows:

    1. For all non-validating states, double check that current_attributes==active

    2. For all non-validating states, set previous_attr_set_on_export to the value of previous attributes

    3. For all validating states, double check that current_attributes==state.validate_self

    4. For all validating states, set previous_attr_set_on_validate to the value of previous attributes

    5. Perform the same operations on the associated _failed states

If any of the checks above failed, either you made a mistake or the state tracking plugin can’t be used for this feature and you will have to build one yourself.

Legacy: Embedded entities without strict modifier enforcement

When the strict_modifier_enforcement flag is disabled on a service entity binding, the attribute modifiers defined on embedded entities are not enforced recursively. In that case, only the attribute modifiers defined on top-level service attributes are enforced. The following meaning applies to attribute modifiers associated with top-level relational attributes to embedded entities:

  • __r__: The embedded entity/entities can only be set by an allocator.

  • __rw__: The embedded entity/entities should be set on service instantiation. Afterwards the relationship object cannot be altered anymore. This means it will be impossible to add/remove entities from the relationship as well as modify any of the attributes of the embedded entity in the relationship.

  • __rwplus__: After service instantiation, embedded entities can be updated and embedded entities can be added/removed from the relationship.

The modelling rules that apply when the strict_modifier_enforcement flag is disabled are less strict compared to the rules defined in Defining an embedded entity. The following changes apply:

  • No index should be defined on an embedded entity to indicate the set of attributes that uniquely identify that embedded entity. There is also no need to set the __lsm_key_attributes attribute either.

  • When the attribute modifier on an embedded entity is set to __r__, it’s not required to set the attribute modifiers of all sub-attribute to read-only as well.