Embedded entities¶
In some situations, the attributes of a ServiceEntity contain a lot of duplication. Consider the following example:
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:
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 attributestrict_modifier_enforcement
explicitly to true.Or, create a service binding using the
lsm::ServiceEntityBindingV2
entity (recommended approach). This entity has thestrict_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:
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:
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.
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.
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.
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:
Initial desired state: data lives in cluster A
Intermediate desired state: data is replicated in cluster A and cluster B
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 |
|
|
instance is not being validated |
|
‘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.
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 |
Fill in states before the update and states where we are actually deploying the update. The
current attributes
will always beactive
andprevious attributes
depends on the direction we are moving in. For theup
state, we are not updating, so there is noprevious attributes
. For updates,previous attributes
is alwaysrollback
(the old active set has been promoted to therollback
set). For rollback scenarios, theprevious attributes
are alwayscandidate
.
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 |
– |
– |
Copy over the state of the
is like
column and apply the operations. e.g. for thevalidating
update_start
state:Copy attributes from the state in the
is like
column (update_inprogress
)state
validating
current attributes
previous attributes
update_start
yes
active
rollback
Apply the operation: the ‘forwards’ ‘promote’ operation shifts the
active
set torollback
and thecandidate
set toactive
, but here we apply it in reverse, i.e. theactive
set becomescandidate
and therollback
set becomesactive
: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 |
– |
– |
Finally, translate to the state variables as follows:
For all non-validating states, double check that
current_attributes==active
For all non-validating states, set
previous_attr_set_on_export
to the value ofprevious attributes
For all validating states, double check that
current_attributes==state.validate_self
For all validating states, set
previous_attr_set_on_validate
to the value ofprevious attributes
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.