Allocation V3#

Allocation V3 is a new framework that changes significantly compared to Allocation V2. The purpose is the same as V2: filling up the read-only values of a service instance during the first validation compile of the lifecycle. Allocation is now performed via a plugin call.

The advantage of this approach is that it simplifies greatly the process: you don’t need anymore to write allocator classes and all the required functions (needs_allocation, allocate, etc.). You also don’t need to instantiate many AllocationSpecV2 with your allocators inside. Instead, you just need to write one plugin per attribute you want to allocate and register it as an allocator, it is less verbose and a much more straightforward approach. LSM comes with build-in allocators that can be used out of the box, e.g. get_first_free_integer.

Create an allocator#

In the allocation V3 framework, an allocator is a python function returning the value to be set for a specific read-only attribute on a specific service instance. To register this function as an allocator, use the allocation_helpers.allocator() decorator:

1from inmanta_plugins.lsm.allocation_helpers import allocator
2
3@allocator()
4def get_service_id(
5    service: "lsm::ServiceEntity",
6    attribute_path: "string",
7) -> "int":
8    return 5
An allocator must accept exactly two positional arguments:
  1. service, the service instance for which the value is being allocated.

2. attribute_path, the attribute of the service instance in which the allocated value should be saved, as a DictPath expression. The decorated function can define a default value.

After those two positional arguments, the function is free of accepting any keyword argument it needs from the model and they will be passed transparently. The function can also define default values, that will be passed transparently as well.

Once an allocator is registered, it can be reused for other instances and attributes that require the same type of allocation by passing the appropriate parameters to the plugin call.

It is also possible to enforce an order in the allocators call by passing values that are returned by other plugins in the model:

main.cf (Plugin call ordering)#
 1"""
 2    Inmanta LSM
 3    :copyright: 2024 Inmanta
 4    :contact: code@inmanta.com
 5    :license: Inmanta EULA
 6"""
 7
 8
 9import lsm
10import lsm::fsm
11
12entity ServiceWithOrderedAllocation extends lsm::ServiceEntity:
13    """
14    This service entity demonstrates how to enforce a specific order during
15    the allocation process. Here we want to allocate some attributes in a
16    specific order: value_allocated_first and then value_allocated_last.
17
18    :attr name: The name identifying the service instance.
19    :attr value_allocated_first: A read-only value, automatically assigned by the api
20        before value_allocated_last.
21    :attr value_allocated_last: A read-only value, automatically assigned by the api
22        after value_allocated_first.
23    """
24
25    string                      name
26    lsm::attribute_modifier     name__modifier="rw"
27    string?                     value_allocated_first=null
28    lsm::attribute_modifier     value_allocated_first__modifier="r"
29    string?                     value_allocated_last=null
30    lsm::attribute_modifier     value_allocated_last__modifier="r"
31end
32
33# Inherit parent entity's implementations
34implement ServiceWithOrderedAllocation using parents
35
36
37# Create a binding to enable service creation through the service catalog
38ordered_allocation_binding = lsm::ServiceEntityBindingV2(
39    service_entity="allocatorv3_demo::ServiceWithOrderedAllocation",
40    lifecycle=lsm::fsm::simple,
41    service_entity_name="allocation_order_enforcement",
42)
43
44# Collect all service instances
45for assignment in lsm::all(ordered_allocation_binding):
46    service = ServiceWithOrderedAllocation(
47        instance_id=assignment["id"],
48        entity_binding=ordered_allocation_binding,
49        name=assignment["attributes"]["name"],
50
51        # Regular allocation:
52        value_allocated_first=ordered_allocation(
53            service,
54            "value_allocated_first"
55        ),
56
57        # Passing value_allocated_first as a parameter to this allocator
58        # will enforce the ordering:
59        value_allocated_last = ordered_allocation(
60            service,
61            "value_allocated_last",
62            requires=[service.value_allocated_first]
63        )
64
65    )
66end

On the plugin side, add an optional argument to enforce ordering:

__init__.py (Plugin call ordering)#
 1"""
 2    Copyright 2024 Inmanta
 3
 4    Licensed under the Apache License, Version 2.0 (the "License");
 5    you may not use this file except in compliance with the License.
 6    You may obtain a copy of the License at
 7
 8        http://www.apache.org/licenses/LICENSE-2.0
 9
10    Unless required by applicable law or agreed to in writing, software
11    distributed under the License is distributed on an "AS IS" BASIS,
12    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13    See the License for the specific language governing permissions and
14    limitations under the License.
15
16    Contact: code@inmanta.com
17"""
18
19from datetime import datetime
20
21from inmanta_plugins.lsm.allocation_helpers import allocator
22
23
24@allocator()
25def ordered_allocation(
26    service: "lsm::ServiceEntity",
27    attribute_path: "string",
28    *,
29    requires: "list?" = None
30) -> "string":
31    """
32    For demonstration purposes, this allocator returns the current time.
33
34    :param service: The service instance for which the attribute value
35        is being allocated.
36    :param attribute_path: DictPath to the attribute of the service
37        instance in which the allocated value will be stored.
38    :param requires: Optional list containing the results of allocator calls
39        that should happen before the current call.
40    """
41    return str(datetime.now())

V2 to V3 migration#

Moving from allocation V2 to allocation V3 boils down to the following steps:

In the plugins directory:

  1. Create a specific allocator for each property of the service that requires allocation.

  2. Make sure to register these allocators by decorating them with the @allocator() decorator.

In the model:

  1. Call the relevant allocator plugin for each value requiring allocation in the lsm::all unwrapping.

Basic example#

Here is an example of a V2 to V3 migration. For both the model and the plugin, first the old V2 version is shown and then the new version using V3 framework:

Plugin#

Baseline V2 allocation in the plugins directory:

__init__.py (V2 allocation)#
 1"""
 2    Copyright 2024 Inmanta
 3
 4    Licensed under the Apache License, Version 2.0 (the "License");
 5    you may not use this file except in compliance with the License.
 6    You may obtain a copy of the License at
 7
 8        http://www.apache.org/licenses/LICENSE-2.0
 9
10    Unless required by applicable law or agreed to in writing, software
11    distributed under the License is distributed on an "AS IS" BASIS,
12    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13    See the License for the specific language governing permissions and
14    limitations under the License.
15
16    Contact: code@inmanta.com
17"""
18
19from inmanta.util import dict_path
20from inmanta_plugins.lsm.allocation import AllocationSpecV2
21from inmanta_plugins.lsm.allocation_v2.framework import AllocatorV2, ContextV2, ForEach
22
23
24class IntegerAllocator(AllocatorV2):
25    """
26    Custom allocator class to set an integer value for an attribute.
27    """
28
29    def __init__(self, value: int, attribute: str) -> None:
30        """
31        :param value: The value to store for this attribute of this service.
32        :param attribute: Attribute of the service instance in which the
33            value will be stored.
34        """
35        self.value = value
36        self.attribute = dict_path.to_path(attribute)
37
38    def needs_allocation(self, context: ContextV2) -> bool:
39        """
40        Determine if this allocator has any work to do or if all
41        values have already been allocated correctly for the instance
42        exposed through the context object.
43
44        :param context: Interface with the current instance
45        being unwrapped in an lsm::all call.
46        """
47        try:
48            if not context.get_instance().get(self.attribute):
49                # Attribute not present
50                return True
51        except IndexError:
52            return True
53
54        return False
55
56    def allocate(self, context: ContextV2) -> None:
57        """
58        Allocate the value for the attribute via the context object.
59
60        :param context: Interface with the current instance
61            being unwrapped in an lsm::all call.
62        """
63        context.set_value(self.attribute, self.value)
64
65
66# In the allocation V2 framework, AllocationSpecV2 objects
67# are used to configure the allocation process:
68AllocationSpecV2(
69    "value_allocation",
70    IntegerAllocator(value=1, attribute="top_level_value"),
71    ForEach(
72        item="item",
73        in_list="embedded_services",
74        identified_by="id",
75        apply=[
76            IntegerAllocator(
77                value=3,
78                attribute="embedded_value",
79            ),
80        ],
81    ),
82)

When moving to V3, register an allocator in the plugin:

__init__.py (V3 allocation)#
 1"""
 2    Copyright 2024 Inmanta
 3
 4    Licensed under the Apache License, Version 2.0 (the "License");
 5    you may not use this file except in compliance with the License.
 6    You may obtain a copy of the License at
 7
 8        http://www.apache.org/licenses/LICENSE-2.0
 9
10    Unless required by applicable law or agreed to in writing, software
11    distributed under the License is distributed on an "AS IS" BASIS,
12    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13    See the License for the specific language governing permissions and
14    limitations under the License.
15
16    Contact: code@inmanta.com
17"""
18
19from inmanta_plugins.lsm.allocation_helpers import allocator
20
21
22@allocator()
23def get_value(
24    service: "lsm::ServiceEntity",
25    attribute_path: "string",
26    *,
27    value: "any",
28) -> "any":
29    """
30    Store a given value in the attributes of a service.
31
32    :param service: The service instance for which the attribute value
33        is being allocated.
34    :param attribute_path: DictPath to the attribute of the service
35        instance in which the allocated value will be stored.
36    :param value: The value to store for this attribute of this service.
37    """
38
39    return value

Model#

Baseline V2 allocation in the model:

main.cf (V2 allocation)#
 1"""
 2    Inmanta LSM
 3    :copyright: 2024 Inmanta
 4    :contact: code@inmanta.com
 5    :license: Inmanta EULA
 6"""
 7
 8import lsm
 9import lsm::fsm
10
11entity TopLevelService extends lsm::ServiceEntity:
12    """
13    Top-level service to demonstrate V2 allocation.
14
15    :attr name: The name identifying the service instance.
16    :attr top_level_value: A read-only value, automatically assigned by the api.
17    """
18    string                      name
19    lsm::attribute_modifier     name__modifier="rw"
20    int?                        top_level_value=null
21    lsm::attribute_modifier     top_level_value__modifier="r"
22end
23
24# Uniquely identify top level services through their name attribute
25index TopLevelService(name)
26
27# Each top level service may have zero or more embedded services attached to it
28TopLevelService.embedded_services [0:] -- EmbeddedService
29
30entity EmbeddedService extends lsm::EmbeddedEntity:
31    """
32    An embedded service, attached to a TopLevelService instance.
33
34    :attr id: Identifier for this embedded service instance.
35    :attr embedded_value: A read-only value, automatically assigned by the api.
36    """
37    string                      id
38    lsm::attribute_modifier     id__modifier="rw"
39    int?                        embedded_value=null
40    lsm::attribute_modifier     embedded_value__modifier="r"
41    string[]? __lsm_key_attributes = ["id"]
42end
43
44# Uniquely identify embedded services through their id attribute
45index EmbeddedService(id)
46
47# Inherit parent entity's implementations
48implement TopLevelService using parents
49
50implement EmbeddedService using parents
51
52# Create a binding to enable service creation through the service catalog
53value_binding = lsm::ServiceEntityBindingV2(
54    service_entity="allocatorv3_demo::TopLevelService",
55    lifecycle=lsm::fsm::simple,
56    service_entity_name="value-service",
57    # V2 allocation requires passing the allocation_spec argument.
58    # The value_allocation is defined in the plugin:
59    allocation_spec="value_allocation",
60    service_identity="name",
61    service_identity_display_name="Name",
62)
63
64# Collect all service instances
65for assignment in lsm::all(value_binding):
66    attributes = assignment["attributes"]
67
68    service = TopLevelService(
69        instance_id=assignment["id"],
70        entity_binding=value_binding,
71        name=attributes["name"],
72        top_level_value=attributes["top_level_value"],
73        embedded_services=[
74            EmbeddedService(
75                **embedded_service
76            )
77            for embedded_service in attributes["embedded_services"]
78        ],
79    )
80end

When moving to V3 allocation, on the model side, call the allocators for the values requiring allocation:

main.cf (V3 allocation)#
 1"""
 2    Inmanta LSM
 3    :copyright: 2024 Inmanta
 4    :contact: code@inmanta.com
 5    :license: Inmanta EULA
 6"""
 7
 8import lsm
 9import lsm::fsm
10
11entity TopLevelService extends lsm::ServiceEntity:
12    """
13    This service entity demonstrates how a single allocator
14    can be used for both a service entity and its embedded
15    entities.
16
17    :attr name: The name identifying the service instance.
18    :attr top_level_value: A read-only value, automatically assigned by the api.
19    """
20
21    string                      name
22    lsm::attribute_modifier     name__modifier="rw"
23    int?                        top_level_value=null
24    lsm::attribute_modifier     top_level_value__modifier="r"
25end
26
27# Uniquely identify top level services through their name attribute
28index TopLevelService(name)
29
30# Each top level service may have zero or more embedded services attached to it
31TopLevelService.embedded_services [0:] -- EmbeddedService
32
33
34entity EmbeddedService extends lsm::EmbeddedEntity:
35    """
36    An embedded service, attached to a TopLevelService instance.
37
38    :attr id: Identifier for this embedded service instance.
39    :attr embedded_value: A read-only value, automatically assigned by the api.
40    """
41    string                      id
42    lsm::attribute_modifier     id__modifier="rw"
43    int?                        embedded_value=null
44    lsm::attribute_modifier     embedded_value__modifier="r"
45    string[]? __lsm_key_attributes = ["id"]
46end
47
48# Uniquely identify embedded services through their id attribute
49index EmbeddedService(id)
50
51# Inherit parent entity's implementations
52implement TopLevelService using parents
53
54implement EmbeddedService using parents
55
56
57# Create a binding to enable service creation through the service catalog
58top_level_service_binding = lsm::ServiceEntityBindingV2(
59    service_entity="allocatorv3_demo::TopLevelService",
60    lifecycle=lsm::fsm::simple,
61    service_entity_name="top-level-service",
62    service_identity="name",
63    service_identity_display_name="Name",
64)
65
66
67# Collect all service instances
68for assignment in lsm::all(top_level_service_binding):
69    attributes = assignment["attributes"]
70    service = TopLevelService(
71        instance_id=assignment["id"],
72        entity_binding=top_level_service_binding,
73        name=attributes["name"],
74        # Allocator call
75        top_level_value=get_value(service, "top_level_value", value=1),
76        embedded_services=[
77            EmbeddedService(
78                id=embedded_service["id"],
79                # Allocator call
80                embedded_value=get_value(
81                    service,
82                    lsm::format(
83                        "embedded_services[id={id}].embedded_value",
84                        args=[],
85                        kwargs=embedded_service,
86                    ),
87                    value=3,
88                ),
89            )
90            for embedded_service in attributes["embedded_services"]
91        ],
92    )
93end

In-depth example#

This is a more complex example ensuring uniqueness for an attribute across instances within a given range of values:

Plugin#

Baseline V2 allocation in the plugins directory:

__init__.py (V2 allocation)#
 1"""
 2    Copyright 2024 Inmanta
 3
 4    Licensed under the Apache License, Version 2.0 (the "License");
 5    you may not use this file except in compliance with the License.
 6    You may obtain a copy of the License at
 7
 8        http://www.apache.org/licenses/LICENSE-2.0
 9
10    Unless required by applicable law or agreed to in writing, software
11    distributed under the License is distributed on an "AS IS" BASIS,
12    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13    See the License for the specific language governing permissions and
14    limitations under the License.
15
16    Contact: code@inmanta.com
17"""
18
19from inmanta_plugins.lsm.allocation import AllocationSpec, AnyUniqueInt, LSM_Allocator
20
21# Define an AllocationSpec using the build-in LSM_Allocator allocator:
22AllocationSpec(
23    "allocate_vlan",
24    LSM_Allocator(attribute="vlan_id", strategy=AnyUniqueInt(lower=50000, upper=70000)),
25)

This example will demonstrate how to use the get_first_free_integer allocator from the lsm module. Since we are using a plugin that is already defined, no extra plugin code is required. We will simply call this plugin from the model with the appropriate arguments.

Model#

Baseline V2 allocation in the model:

main.cf (V2 allocation)#
 1"""
 2    Inmanta LSM
 3    :copyright: 2024 Inmanta
 4    :contact: code@inmanta.com
 5    :license: Inmanta EULA
 6"""
 7
 8import lsm
 9import lsm::fsm
10
11entity VlanAssignment extends lsm::ServiceEntity:
12    """
13    This service entity demonstrates allocation using the LSM_Allocator
14    build in lsm.
15
16    :attr name: The name identifying the service instance.
17    :attr vlan_id: A read-only value, automatically assigned by the api.
18    """
19    string name
20    int? vlan_id=null
21    lsm::attribute_modifier vlan_id__modifier="r"
22end
23
24# Inherit parent entity's implementations
25implement VlanAssignment using parents
26
27# Create a binding to enable service creation through the service catalog
28vlan_binding = lsm::ServiceEntityBinding(
29    service_entity="allocatorv3_demo::VlanAssignment",
30    lifecycle=lsm::fsm::simple,
31    service_entity_name="vlan-assignment",
32    # V2 allocation requires passing the allocation_spec argument.
33    # The allocate_vlan is defined in the plugin:
34    allocation_spec="allocate_vlan",
35)
36
37# Collect all service instances
38for assignment in lsm::all(vlan_binding):
39    VlanAssignment(
40        instance_id=assignment["id"],
41        entity_binding=vlan_binding,
42        **assignment["attributes"]
43    )
44end

When moving to V3 allocation, on the model side, call the allocators for the values requiring allocation:

main.cf (V3 allocation)#
 1"""
 2    Inmanta LSM
 3    :copyright: 2024 Inmanta
 4    :contact: code@inmanta.com
 5    :license: Inmanta EULA
 6"""
 7
 8
 9import lsm
10import lsm::fsm
11import lsm::allocators
12
13
14entity VlanAssignment extends lsm::ServiceEntity:
15    """
16    This service entity demonstrates allocation using the get_first_free_integer
17    allocator build in lsm.
18
19    :attr name: The name identifying the service instance.
20    :attr vlan_id: A read-only value, automatically assigned by the api.
21    """
22
23    string name
24    int? vlan_id=null
25    lsm::attribute_modifier vlan_id__modifier="r"
26end
27
28
29# Inherit parent entity's implementations
30implement VlanAssignment using parents
31
32# Create a binding to enable service creation through the service catalog
33vlan_binding = lsm::ServiceEntityBindingV2(
34    service_entity="allocatorv3_demo::VlanAssignment",
35    lifecycle=lsm::fsm::simple,
36    service_entity_name="vlan-assignment",
37)
38
39# Collect all service instances
40for assignment in lsm::all(vlan_binding):
41    service = VlanAssignment(
42        instance_id=assignment["id"],
43        entity_binding=vlan_binding,
44        name=assignment["attributes"]["name"],
45        # Allocator call
46        vlan_id=lsm::allocators::get_first_free_integer(
47            service,
48            "vlan_id",
49            range_start=50000,
50            range_end=70000,
51            # Retrieve the values already in use across services in the binding
52            # and pass them as a parameter to the allocator call
53            used_values=lsm::allocators::get_used_values(vlan_binding, "vlan_id"),
54        )
55    )
56end