Allocation V2#

Allocation V2 is a new framework, similar to allocation (v1). It happens in the same lifecycle stage and serves the same purpose: filling up read-only values of a service instance.

It comes to fill some gaps in the functionalities of allocation (v1) and takes advantages of the experience and learnings that using allocation (v1) taught us. It is a more complete, functional, and elegant framework.

Example#

The example below show you the use case where a single allocator is used the same way on both the service instance and an embedded entity.

main.cf#
 1import lsm
 2import lsm::fsm
 3
 4
 5entity ValueService extends lsm::ServiceEntity:
 6    string                      name
 7    lsm::attribute_modifier     name__modifier="rw"
 8
 9    int?                        first_value
10    lsm::attribute_modifier     first_value__modifier="r"
11end
12ValueService.embedded_values [0:] -- EmbeddedValue
13
14entity EmbeddedValue extends lsm::EmbeddedEntity:
15    string                      id
16    lsm::attribute_modifier     id__modifier="rw"
17
18    int?                        third_value
19    lsm::attribute_modifier     third_value__modifier="r"
20
21    string[]? __lsm_key_attributes = ["id"]
22end
23
24index EmbeddedValue(id)
25
26implement ValueService using parents
27implement EmbeddedValue using std::none
28
29binding = lsm::ServiceEntityBinding(
30    service_entity="__config__::ValueService",
31    lifecycle=lsm::fsm::simple,
32    service_entity_name="value-service",
33    allocation_spec="value_allocation",
34    strict_modifier_enforcement=true,
35)
36
37for assignment in lsm::all(binding):
38    attributes = assignment["attributes"]
39
40    service = ValueService(
41        instance_id=assignment["id"],
42        entity_binding=binding,
43        name=attributes["name"],
44        first_value=attributes["first_value"],
45    )
46
47    for embedded_value in attributes["embedded_values"]:
48        service.embedded_values += EmbeddedValue(
49            **embedded_value
50        )
51    end
52end
plugins/__init__.py#
 1"""
 2    Inmanta LSM
 3
 4    :copyright: 2022 Inmanta
 5    :contact: code@inmanta.com
 6    :license: Inmanta EULA
 7"""
 8from inmanta.util import dict_path
 9from inmanta_plugins.lsm.allocation import AllocationSpecV2
10from inmanta_plugins.lsm.allocation_v2.framework import AllocatorV2, ContextV2, ForEach
11
12
13class IntegerAllocator(AllocatorV2):
14    def __init__(self, value: int, attribute: str) -> None:
15        self.value = value
16        self.attribute = dict_path.to_path(attribute)
17
18    def needs_allocation(self, context: ContextV2) -> bool:
19        try:
20            if not context.get_instance().get(self.attribute):
21                # Attribute not present
22                return True
23        except IndexError:
24            return True
25        return False
26
27    def allocate(self, context: ContextV2) -> None:
28        context.set_value(self.attribute, self.value)
29
30
31AllocationSpecV2(
32    "value_allocation",
33    IntegerAllocator(value=1, attribute="first_value"),
34    ForEach(
35        item="item",
36        in_list="embedded_values",
37        identified_by="id",
38        apply=[
39            IntegerAllocator(
40                value=3,
41                attribute="third_value",
42            ),
43        ],
44    ),
45)

Allocation V2 features#

The two main additions to allocation v2 when compared to v1 are:
  • The new ContextV2 object (replacement for AllocationContext object), which goes in pair with AllocatorV2 and AllocationSpecV2

  • The support for allocating attributes in embedded entities.

Setting a read-only attribute on an embedded entity, like done in the above-mentioned example, is only possible when strict_modifier_enforcement is enabled. On legacy services, where strict_modifier_enforcement is not enabled, read-only attributes can be set on embedded entities using the workaround mentioned the Section Legacy: Set attributes on embedded entities.

Warning

To use allocation safely, allocators should not keep any state between invocations, but pass all state via the ContextV2 object.

ContextV2#

A context object that will be passed to each allocator and that should be used to set values. This context always shows the attributes the allocator should have access to, based on its level in the allocators tree. This means a top level allocator will see all the attributes, but an allocator used on embedded entities will only see the attributes of such embedded entity (as if it was a standalone entity). The context object can also be used to store values at each “level of allocation”, reachable by all allocators at the same level.

In the example at the beginning of this page, the same allocator can be used to set a value on the service entity and an embedded entity. In needs_allocation, when calling context.get_instance(), we receive as dict the full service entity when allocating first_value and the embedded entity when allocating third_value.

AllocatorV2#

A base class for all v2 allocators, they are provided with a ContextV2 object for those two methods: needs_allocation and allocate. The main difference with v1, is that the allocate method doesn’t return any value to allocate, it sets them using the context object: context.set_value(name, value).

AllocationSpecV2#

The collector for all AllocatorV2.

Legacy: Set attributes on embedded entities#

The server doesn’t have support to set read-only attributes on embedded entities when strict_modifier_enforcement is disabled. Thanks to the allocator ContextV2Wrapper and the plugin lsm::context_v2_unwrapper a workaround exists to do allocation on an embedded entity’s attributes with strict_modifier_enforcement disabled. This workaround saves all the allocated values in a dict, in an attribute of the service instance (added to the instance for this single purpose). That way, the server accepts the update.

The ContextV2Wrapper, which has to be used at the root of the allocation tree, will collect and save all the allocated value in a single dict. And when getting all the service instances in your model, with lsm::all, you can simply wrap the call to lsm::all with a call to lsm::context_v2_unwrapper, which will place all the allocated values saved in the dict, directly where they belong, in the embedded entities.

When using the ContextV2Wrapper and the lsm::context_v2_unwrapper plugin, you will have to specify in which attributes all the allocated values should be saved.

main.cf#
 1import lsm
 2import lsm::fsm
 3
 4
 5entity ValueService extends lsm::ServiceEntity:
 6    string                      name
 7    lsm::attribute_modifier     name__modifier="rw"
 8
 9    int?                        first_value
10    lsm::attribute_modifier     first_value__modifier="r"
11
12    dict?                       allocated
13    lsm::attribute_modifier     allocated__modifier="r"
14end
15ValueService.embedded_values [0:] -- EmbeddedValue
16
17entity EmbeddedValue extends lsm::EmbeddedEntity:
18    string                      id
19    lsm::attribute_modifier     id__modifier="rw"
20
21    int?                        third_value
22    lsm::attribute_modifier     third_value__modifier="r"
23end
24
25implement ValueService using parents
26implement EmbeddedValue using std::none
27
28binding = lsm::ServiceEntityBinding(
29    service_entity="__config__::ValueService",
30    lifecycle=lsm::fsm::simple,
31    service_entity_name="value-service",
32    allocation_spec="value_allocation",
33)
34
35for assignment in lsm::context_v2_unwrapper(
36    assignments=lsm::all(binding),
37    fallback_attribute="allocated",
38):
39    attributes = assignment["attributes"]
40
41    service = ValueService(
42        instance_id=assignment["id"],
43        entity_binding=binding,
44        name=attributes["name"],
45        first_value=attributes["first_value"],
46        allocated=attributes["allocated"],
47    )
48
49    for embedded_value in attributes["embedded_values"]:
50        service.embedded_values += EmbeddedValue(
51            **embedded_value
52        )
53    end
54end
plugins/__init__.py#
 1"""
 2    Inmanta LSM
 3
 4    :copyright: 2022 Inmanta
 5    :contact: code@inmanta.com
 6    :license: Inmanta EULA
 7"""
 8from inmanta.util import dict_path
 9from inmanta_plugins.lsm.allocation import AllocationSpecV2
10from inmanta_plugins.lsm.allocation_v2.framework import (
11    AllocatorV2,
12    ContextV2,
13    ContextV2Wrapper,
14    ForEach,
15)
16
17
18class IntegerAllocator(AllocatorV2):
19    def __init__(self, value: int, attribute: str) -> None:
20        self.value = value
21        self.attribute = dict_path.to_path(attribute)
22
23    def needs_allocation(self, context: ContextV2) -> bool:
24        try:
25            if not context.get_instance().get(self.attribute):
26                # Attribute not present
27                return True
28        except IndexError:
29            return True
30        return False
31
32    def allocate(self, context: ContextV2) -> None:
33        context.set_value(self.attribute, self.value)
34
35
36AllocationSpecV2(
37    "value_allocation",
38    IntegerAllocator(value=1, attribute="first_value"),
39    ContextV2Wrapper(
40        "allocated",
41        ForEach(
42            item="item",
43            in_list="embedded_values",
44            identified_by="id",
45            apply=[
46                IntegerAllocator(
47                    value=3,
48                    attribute="third_value",
49                ),
50            ],
51        ),
52    ),
53)

To facilitate allocation on embedded entities, the ForEach allocator can be used.

Deleting of Embedded entities#

When you want to support deletion of embedded entities during updates, a slightly different configuration is needed. Because all allocated values are stored in a single attribute, the deleted entities will be recreated when unwrapping.

To prevent this, use track_deletes=true on both the the allocator ContextV2Wrapper and the plugin lsm::context_v2_unwrapper

Additionally, to re-trigger allocation when an item is deleted, use SetSensitiveForEach instead of ForEach.

main.cf#
 1import lsm
 2import lsm::fsm
 3
 4
 5entity ValueService extends lsm::ServiceEntity:
 6    string                      name
 7    lsm::attribute_modifier     name__modifier="rw"
 8
 9    int?                        first_value
10    lsm::attribute_modifier     first_value__modifier="r"
11
12    dict?                       allocated
13    lsm::attribute_modifier     allocated__modifier="r"
14end
15ValueService.embedded_values [0:] lsm::__rwplus__ EmbeddedValue
16
17entity EmbeddedValue extends lsm::EmbeddedEntity:
18    string                      id
19    lsm::attribute_modifier     id__modifier="rw"
20
21    int?                        third_value
22    lsm::attribute_modifier     third_value__modifier="r"
23
24    string                      other_value
25    lsm::attribute_modifier     other_value__modifier="rw"
26end
27
28index EmbeddedValue(id)
29
30implement ValueService using parents
31implement EmbeddedValue using std::none
32
33binding = lsm::ServiceEntityBindingV2(
34    service_entity="__config__::ValueService",
35    lifecycle=lsm::fsm::simple,
36    service_entity_name="value-service",
37    allocation_spec="value_allocation",
38)
39
40for assignment in lsm::context_v2_unwrapper(
41    assignments=lsm::all(binding),
42    fallback_attribute="allocated",
43    track_deletes=true,
44):
45    attributes = assignment["attributes"]
46
47    service = ValueService(
48        instance_id=assignment["id"],
49        entity_binding=binding,
50        name=attributes["name"],
51        first_value=attributes["first_value"],
52        allocated=attributes["allocated"],
53    )
54
55    for embedded_value in attributes["embedded_values"]:
56        service.embedded_values += EmbeddedValue(
57            **embedded_value
58        )
59    end
60end
plugins/__init__.py#
 1"""
 2    Inmanta LSM
 3
 4    :copyright: 2020 Inmanta
 5    :contact: code@inmanta.com
 6    :license: Inmanta EULA
 7"""
 8from inmanta.util import dict_path
 9from inmanta_plugins.lsm.allocation import AllocationSpecV2
10from inmanta_plugins.lsm.allocation_v2.framework import (
11    AllocatorV2,
12    ContextV2,
13    ContextV2Wrapper,
14    SetSensitiveForEach,
15)
16
17
18class IntegerAllocator(AllocatorV2):
19    def __init__(self, value: int, attribute: str) -> None:
20        self.value = value
21        self.attribute = dict_path.to_path(attribute)
22
23    def needs_allocation(self, context: ContextV2) -> bool:
24        try:
25            if not context.get_instance().get(self.attribute):
26                # Attribute not present
27                return True
28        except IndexError:
29            return True
30        return False
31
32    def allocate(self, context: ContextV2) -> None:
33        if self.needs_allocation(context):
34            context.set_value(self.attribute, self.value)
35
36
37AllocationSpecV2(
38    "value_allocation",
39    IntegerAllocator(value=1, attribute="first_value"),
40    ContextV2Wrapper(
41        "allocated",
42        SetSensitiveForEach(
43            item="item",
44            in_list="embedded_values",
45            identified_by="id",
46            apply=[
47                IntegerAllocator(
48                    value=3,
49                    attribute="third_value",
50                ),
51            ],
52        ),
53        track_deletes=True,
54    ),
55)