Unmanaged Resources

Unmanaged resources are resources that live in the network that are not yet managed by the orchestrator. They may be of the same type as other resources that are already managed, or something else entirely. The orchestrator can discover unmanaged resources in the network, given proper guidance. This discovery is driven by discovery resources in the model. They express the intent to discover resources of the associated type in the network.

Terminology

  • Discovery resource: The category of resources that express the intent to discover resources in the network. This is an actual resource that is part of the configuration model.

  • Discovered resource: This is the unit of data that is generated by the discovery process. Each time the discovery process discovers a resource, it creates a record in the inventory for discovered resources. This record contains the set of attributes that define the current state of the discovered resource. Discovered resources only exist in the discovered resources database. They don’t exist in the configuration model.

Example

The code snippet below defines a discovery resource called InterfaceDiscovery. Instances of this resource will discover the interfaces present on a specific host. A discovery resource must always inherit from std::DiscoveryResource. Note that discovery resources are defined in exactly the same way as a regular resource, except that they inherit from std::DiscoveryResource instead of std::PurgeableResource or std::Resource.

my_project/main.cf
 1import ip
 2
 3entity InterfaceDiscovery extends std::DiscoveryResource:
 4    """
 5    A discovery resource that discovers interfaces on a specific host.
 6
 7    :attr name_filter: If not null, only discover interfaces for which the
 8                       name matches this regular expression.
 9                       Otherwise discover all the interfaces on the host.
10    """
11    string? name_filter = null
12end
13
14InterfaceDiscovery.host [1] -- ip::Host
15
16index InterfaceDiscovery(host)
17
18implement InterfaceDiscovery using parents, std::none

The associated handler code is shown below:

my_module/inmanta_plugins/__init__.py
 1import re
 2from collections import abc
 3
 4import pydantic
 5
 6from inmanta import resources
 7from inmanta.data.model import ResourceIdStr
 8from inmanta.agent.handler import provider, DiscoveryHandler, HandlerContext
 9from inmanta.resources import resource, DiscoveryResource
10
11
12@resource("my_module::InterfaceDiscovery", agent="host.name", id_attribute="host")
13class InterfaceDiscovery(DiscoveryResource):
14    fields = ("host", "name_filter")
15
16    host: str
17    name_filter: str
18
19    @staticmethod
20    def get_host(exporter, resource):
21        return resource.host.name
22
23
24class UnmanagedInterface(pydantic.BaseModel):
25    """
26    Datastructure used by the InterfaceDiscoveryHandler to return the attributes
27    of its discovered resources.
28    """
29
30    host: str
31    interface_name: str
32    ip_address: str
33
34
35@provider("my_module::InterfaceDiscovery", name="interface_discovery_handler")
36class InterfaceDiscoveryHandler(DiscoveryHandler[InterfaceDiscovery, UnmanagedInterface]):
37    def discover_resources(
38        self, ctx: HandlerContext, discovery_resource: InterfaceDiscovery
39    ) -> dict[ResourceIdStr, UnmanagedInterface]:
40        """
41        Entrypoint that is called by the agent when the discovery resource is deployed.
42        """
43        discovered: abc.Iterator[UnmanagedInterface] = (
44            UnmanagedInterface(**attributes)
45            for attributes in self._get_discovered_interfaces(discovery_resource)
46            if discovery_resource.name_filter is None or re.match(discovery_resource.name_filter, attributes["interface_name"])
47        )
48        return {
49            resources.Id(
50                entity_type="my_module::Interface",
51                agent_name=res.host,
52                attribute="interface_name",
53                attribute_value=res.interface_name,
54            ).resource_str(): res
55            for res in discovered
56        }
57
58    def _get_discovered_interfaces(self, discovery_resource: InterfaceDiscovery) -> list[dict[str, object]]:
59        """
60        A helper method that contains the logic to discover the unmanaged interfaces in the network.
61        It returns a list of dictionaries where each dictionary contains the attributes of an unmanaged resource.
62        """
63        raise NotImplementedError()

The handler code consists of three parts:

  • Lines 12-21: The class that describes how the discovery resource InterfaceDiscovery should be serialized. This resource definition is analogous to the definition of a regular PurgeableResource or Resource, except that the class inherits from DiscoveryResource.

  • Lines 24-32: A Pydantic BaseModel that represents the datastructure that will be used by the discovery handler to return the attributes of the discovered resources. This specific example uses a Pydantic BaseModel, but discovery handlers can use any json serializable datastructure.

  • Line: 35-63: This is the handler for the discovery resource. A discovery handler class must satisfy the following requirements:

    • It must be annotated with the @provider annotation, like a regular CRUDHandler or ResourceHandler.

    • It must inherit from the DiscoveryHandler class. This is a generic class with two parameters. The first parameter is the class of the associated DiscoveryResource and the second parameter is the type of datastructure that the discovery handler will use to return the attributes of discovered resources.

    • It must implement a method called discover_resources that contains the logic to discover the resources in the network. This method returns a dictionary. The keys of this dictionary contain the resource ids of the discovered resources and the values the associated attributes.

Sharing attributes

In some situations there is a need to share behavior or attributes between a resource X and the discovery resource for X. For example, both might require credentials to authenticate to their remote host. This can be done by making both entities inherit from a shared parent entity. An example is provided below.

my_project/main.cf
 1import ip
 2
 3entity Credentials:
 4    """
 5    An entity that holds the shared attributes between the Interface and InterfaceDiscovery entity.
 6    """
 7    string username
 8    string password
 9end
10
11implement Credentials using std::none
12
13entity InterfaceBase:
14    """
15    Base entity for the Interface and InterfaceDiscovery handler.
16    """
17end
18
19InterfaceBase.credentials [1] -- Credentials
20InterfaceBase.host [1] -- ip::Host
21
22implement InterfaceBase using std::none
23
24entity Interface extends InterfaceBase, std::PurgeableResource:
25    """
26    An entity that represents an interface that is managed by the Inmanta server.
27    """
28    string name
29    std::ipv4_address ip_address
30end
31
32index Interface(host, name)
33
34implement Interface using parents, std::none
35
36entity InterfaceDiscovery extends InterfaceBase, std::DiscoveryResource:
37    """
38    A discovery resource used to discover interfaces that exist on a specific host.
39
40    :attr name_filter: If not null, this resource only discovers the interfaces for which the name matches this
41                       regular expression. Otherwise discover all the interfaces on the host.
42    """
43    string? name_filter = null
44end
45
46index InterfaceDiscovery(host)
47
48implement InterfaceDiscovery using parents, std::none

The Credentials entity, in the above-mentioned snippet, contains the shared attributes between the PurgeableResource Interface and the DiscoveryResource InterfaceDiscovery.

The associated handler code is provided below:

my_module/inmanta_plugins/__init__.py
  1import re
  2from collections import abc
  3from typing import Optional
  4
  5import pydantic
  6
  7from inmanta import resources
  8from inmanta.data.model import ResourceIdStr
  9from inmanta.agent.handler import provider, DiscoveryHandler, HandlerContext, CRUDHandler
 10from inmanta.resources import resource, DiscoveryResource, PurgeableResource
 11
 12
 13class InterfaceBase:
 14    fields = ("host", "username", "password")
 15
 16    host: str
 17    username: str
 18    password: str
 19
 20    @staticmethod
 21    def get_host(exporter, resource):
 22        return resource.host.name
 23
 24    @staticmethod
 25    def get_username(exporter, resource):
 26        return resource.credentials.username
 27
 28    @staticmethod
 29    def get_password(exporter, resource):
 30        return resource.credentials.password
 31
 32
 33@resource("my_module::Interface", agent="host.name", id_attribute="name")
 34class Interface(InterfaceBase, PurgeableResource):
 35    fields = ("name", "ip_address")
 36
 37    name: str
 38    ip_address: str
 39
 40
 41@resource("my_module::InterfaceDiscovery", agent="host.name", id_attribute="host")
 42class InterfaceDiscovery(InterfaceBase, DiscoveryResource):
 43    fields = ("name_filter",)
 44
 45    name_filter: Optional[str]
 46
 47
 48class UnmanagedInterface(pydantic.BaseModel):
 49    """
 50    Datastructure used by the InterfaceDiscoveryHandler to return the attributes
 51    of the discovered resources.
 52    """
 53
 54    host: str
 55    interface_name: str
 56    ip_address: str
 57
 58
 59class Authenticator:
 60    """
 61    Helper class that handles the authentication to the remote host.
 62    """
 63
 64    def login(self, credentials: InterfaceBase) -> None:
 65        raise NotImplementedError()
 66
 67    def logout(self, credentials: InterfaceBase) -> None:
 68        raise NotImplementedError()
 69
 70
 71@provider("my_module::Interface", name="interface_handler")
 72class InterfaceHandler(Authenticator, CRUDHandler[Interface]):
 73    """
 74    Handler for the interfaces managed by the orchestrator.
 75    """
 76
 77    def pre(self, ctx: HandlerContext, resource: Interface) -> None:
 78        self.login(resource)
 79
 80    def post(self, ctx: HandlerContext, resource: Interface) -> None:
 81        self.logout(resource)
 82
 83    def read_resource(self, ctx: HandlerContext, resource: Interface) -> None:
 84        raise NotImplementedError()
 85
 86    def create_resource(self, ctx: HandlerContext, resource: Interface) -> None:
 87        raise NotImplementedError()
 88
 89    def delete_resource(self, ctx: HandlerContext, resource: Interface) -> None:
 90        raise NotImplementedError()
 91
 92    def update_resource(self, ctx: HandlerContext, changes: dict, resource: Interface) -> None:
 93        raise NotImplementedError()
 94
 95
 96@provider("my_module::InterfaceDiscovery", name="interface_discovery_handler")
 97class InterfaceDiscoveryHandler(Authenticator, DiscoveryHandler[InterfaceDiscovery, UnmanagedInterface]):
 98
 99    def pre(self, ctx: HandlerContext, resource: InterfaceDiscovery) -> None:
100        self.login(resource)
101
102    def post(self, ctx: HandlerContext, resource: InterfaceDiscovery) -> None:
103        self.logout(resource)
104
105    def discover_resources(
106        self, ctx: HandlerContext, discovery_resource: InterfaceDiscovery
107    ) -> abc.Mapping[ResourceIdStr, UnmanagedInterface]:
108        """
109        Entrypoint that is called by the agent when the discovery resource is deployed.
110        """
111        discovered: abc.Iterator[UnmanagedInterface] = (
112            UnmanagedInterface(**attributes)
113            for attributes in self._get_discovered_interfaces(discovery_resource)
114            if discovery_resource.name_filter is None or re.match(discovery_resource.name_filter, attributes["interface_name"])
115        )
116        return {
117            resources.Id(
118                entity_type="my_module::Interface",
119                agent_name=res.host,
120                attribute="interface_name",
121                attribute_value=res.interface_name,
122            ).resource_str(): res
123            for res in discovered
124        }
125
126    def _get_discovered_interfaces(self, discovery_resource: InterfaceDiscovery) -> list[dict[str, object]]:
127        """
128        A helper method that contains the logic to discover the unmanaged interfaces in the network.
129        It returns a list of dictionaries where each dictionary contains the attributes of a discovered interface.
130        """
131        raise NotImplementedError()

In the above-mentioned code snippet the Credentials class contains the shared attributes between the Interface resource and the InterfaceDiscovery resource. The Authenticator class on the other hand contains the shared logic between the InterfaceHandler and the InterfaceDiscoveryHandler class.