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. We also make use of ContextV2Wrapper allocator and lsm::context_v2_unwrapper plugin as we do allocate values in en embedded entity. If it wasn’t the case, if we were only allocating values on the service instance, we could get rid of those two.

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
55
plugins/__init__.py
 1"""
 2    Inmanta LSM
 3
 4    :copyright: 2020 Inmanta
 5    :contact: code@inmanta.com
 6    :license: Inmanta EULA
 7"""
 8from inmanta_plugins.lsm.allocation import AllocationSpecV2
 9from inmanta_plugins.lsm.allocation_v2 import dict_path
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)

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, regardless of the current limitations of the server which doesn’t support such updates natively.

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 end 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.

Embedded entities

Thanks to the allocator ContextV2Wrapper and the plugin lsm::context_v2_unwrapper it is also now possible to do allocation on an embedded entity’s attributes. The server doesn’t support it natively, so as workaround, we will save 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.

When running the allocation on embedded entities, 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.

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
28implement ValueService using parents
29implement EmbeddedValue using std::none
30
31binding = lsm::ServiceEntityBinding(
32    service_entity="__config__::ValueService",
33    lifecycle=lsm::fsm::simple,
34    service_entity_name="value-service",
35    allocation_spec="value_allocation",
36)
37
38for assignment in lsm::context_v2_unwrapper(
39    assignments=lsm::all(binding),
40    fallback_attribute="allocated",
41    track_deletes=true,
42):
43    attributes = assignment["attributes"]
44
45    service = ValueService(
46        instance_id=assignment["id"],
47        entity_binding=binding,
48        name=attributes["name"],
49        first_value=attributes["first_value"],
50        allocated=attributes["allocated"],
51    )
52
53    for embedded_value in attributes["embedded_values"]:
54        service.embedded_values += EmbeddedValue(
55            **embedded_value
56        )
57    end
58end
plugins/__init__.py
 1"""
 2    Inmanta LSM
 3
 4    :copyright: 2020 Inmanta
 5    :contact: code@inmanta.com
 6    :license: Inmanta EULA
 7"""
 8from inmanta_plugins.lsm.allocation import AllocationSpecV2
 9from inmanta_plugins.lsm.allocation_v2 import dict_path
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)