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

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

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 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
 3import ip
 4
 5entity ServiceX extends lsm::ServiceEntity:
 6    """
 7        The API of ServiceX.
 8
 9        :attr service_id: A unique ID for this service.
10    """
11    string service_id
12end
13
14index ServiceX(service_id)
15
16ServiceX.router [1] -- Router._service [1]
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    ip::ip 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        router=Router(**instance["attributes"]["router"]),
51    )
52end

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

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

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.

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.