Template Extensions - Nautobot Design Builder Documentation

Skip to content

# Template Extensions

## `nautobot_design_builder.ext`

Extensions API for the object creator.

### `AttributeExtension`

Bases: `Extension`, `ABC`

An `AttributeExtension` will be evaluated when the design key matches the `tag`.

Source code in `nautobot_design_builder/ext.py`

     99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    class AttributeExtension(Extension, ABC):
        """An `AttributeExtension` will be evaluated when the design key matches the `tag`."""

        @abstractmethod
        def attribute(self, *args: List[Any], value: Any = None, model_instance: "ModelInstance" = None) -> None:
            """This method is called when the `attribute_tag` is encountered.

            Note: The method signature must match the above for the extension to work. The
            extension name is parsed by splitting on `:` symbols and the result is passed as the
            varargs. For instance, if the attribute tag is `my_tag` and it is called with `!my_tag:arg1`: {} then
            `*args` will be ['arg1'] and `value` will be the empty dictionary.

            Args:
                *args (List[Any]): Any additional arguments following the tag name. These are `:`
                    delimited.

                value (Any): The value of the data structure at this key's point in the design YAML.
                    This could be a scalar, a dict or a list.

                model_instance (ModelInstance): Object is the ModelInstance that would ultimately
                    contain the values.
            """

#### `attribute(*args, value=None, model_instance=None)` `abstractmethod`

This method is called when the `attribute_tag` is encountered.

Note: The method signature must match the above for the extension to work. The extension name is parsed by splitting on `:` symbols and the result is passed as the varargs. For instance, if the attribute tag is `my_tag` and it is called with `!my_tag:arg1`: {} then `*args` will be ['arg1'] and `value` will be the empty dictionary.

Parameters:

Name | Type | Description | Default
--- | --- | --- | ---
`*args` | `List[Any]` | Any additional arguments following the tag name. These are `:` delimited. | `()`
`value` | `Any` | The value of the data structure at this key's point in the design YAML. This could be a scalar, a dict or a list. | `None`
`model_instance` | `ModelInstance` | Object is the ModelInstance that would ultimately contain the values. | `None`

Source code in `nautobot_design_builder/ext.py`

    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    @abstractmethod
    def attribute(self, *args: List[Any], value: Any = None, model_instance: "ModelInstance" = None) -> None:
        """This method is called when the `attribute_tag` is encountered.

        Note: The method signature must match the above for the extension to work. The
        extension name is parsed by splitting on `:` symbols and the result is passed as the
        varargs. For instance, if the attribute tag is `my_tag` and it is called with `!my_tag:arg1`: {} then
        `*args` will be ['arg1'] and `value` will be the empty dictionary.

        Args:
            *args (List[Any]): Any additional arguments following the tag name. These are `:`
                delimited.

            value (Any): The value of the data structure at this key's point in the design YAML.
                This could be a scalar, a dict or a list.

            model_instance (ModelInstance): Object is the ModelInstance that would ultimately
                contain the values.
        """

### `Extension`

Bases: `ABC`

Base class for attribute and value extensions.

Extensions add capabilities to the object creator. For instance, the ReferenceExtension provides both the `!ref` attribute (which saves references to ORM objects) as well as the `!ref` value lookup that returns previously saved references.

To develop a new extension, simply extend this base class and provide ether an `attribute` or `value` to be handled by the extension. For `attribute` extensions, a class variable named `attribute_tag` must be provided to indicate the tag name (such as "ref" for the `!ref` tag). A corresponding instance method named `attribute` must also be provided.

Likewise, for a value extension the `value_tag` and `value` instance method must be provided.

The `__init__` method is called only once. The extension is initialized when the first tag matching `tag_name` or `value_name` is encountered.

Parameters:

Name | Type | Description | Default
--- | --- | --- | ---
`environment` | `Environment` | The object creator that is implementing the current design. | required

Source code in `nautobot_design_builder/ext.py`

    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    class Extension(ABC):
        """Base class for attribute and value extensions.

        Extensions add capabilities to the object creator. For instance,
        the ReferenceExtension provides both the `!ref` attribute (which
        saves references to ORM objects) as well as the `!ref` value lookup
        that returns previously saved references.

        To develop a new extension, simply extend this base class and provide
        ether an `attribute` or `value` to be handled by the extension. For
        `attribute` extensions, a class variable named `attribute_tag` must
        be provided to indicate the tag name (such as "ref" for the `!ref` tag). A
        corresponding instance method named `attribute` must also be provided.

        Likewise, for a value extension the `value_tag` and `value` instance
        method must be provided.

        The `__init__` method is called only once. The extension is initialized when the first
        tag matching `tag_name` or `value_name` is encountered.

        Args:
            environment (Environment): The object creator that is implementing the
                current design.
        """

        environment: "Environment"

        @property
        @abstractmethod
        def tag(self):
            """All Extensions must specify their tag name.

            The `tag` method indicates to the Environment what the
            tag name is for this extensions. For instance, a `tag`
            of `ref` will match `!ref` in the design.
            """

        def __init__(self, environment: "Environment"):  # noqa: D107
            self.environment = environment

        def commit(self) -> None:
            """Optional method that is called once a design has been implemented and committed to the database.

            Note: Commit is called once for each time Environment.implement_design is called. For a design job with
            multiple design files, commit will be called once for each design file. It is up to the extension
            to track internal state so that multiple calls to `commit` don't introduce an inconsistency.
            """

        def roll_back(self) -> None:
            """Optional method that is called if the design has failed and the database transaction will be rolled back."""

#### `tag` `abstractmethod` `property`

All Extensions must specify their tag name.

The `tag` method indicates to the Environment what the tag name is for this extensions. For instance, a `tag` of `ref` will match `!ref` in the design.

#### `commit()`

Optional method that is called once a design has been implemented and committed to the database.

Note: Commit is called once for each time Environment.implement_design is called. For a design job with multiple design files, commit will be called once for each design file. It is up to the extension to track internal state so that multiple calls to `commit` don't introduce an inconsistency.

Source code in `nautobot_design_builder/ext.py`

    87
    88
    89
    90
    91
    92
    93
    def commit(self) -> None:
        """Optional method that is called once a design has been implemented and committed to the database.

        Note: Commit is called once for each time Environment.implement_design is called. For a design job with
        multiple design files, commit will be called once for each design file. It is up to the extension
        to track internal state so that multiple calls to `commit` don't introduce an inconsistency.
        """

#### `roll_back()`

Optional method that is called if the design has failed and the database transaction will be rolled back.

Source code in `nautobot_design_builder/ext.py`

    95
    96
    def roll_back(self) -> None:
        """Optional method that is called if the design has failed and the database transaction will be rolled back."""

### `GitContextExtension`

Bases: `AttributeExtension`

Provides the "!git_context" attribute extension that will save content to a git repo.

Parameters:

Name | Type | Description | Default
--- | --- | --- | ---
`environment` | `Environment` | The object creator that is implementing the current design. | required

Example

    devices:
        - name: "My Device"
            "!git_context":
            destination: "config/my_device.yml"
            data:
                bgp_asn: 64495

The above will implement the design and when the `!git_context` tag is encountered it will marshal the dictionary at `data` to `config/my_device.yml` to the base directory of the git repository. TODO: explain how the git repo is configured. Potentially change the configuration to have git_slug directly in the tag content?

Source code in `nautobot_design_builder/ext.py`

    223
    224
    225
    226
    227
    228
    229
    230
    231
    232
    233
    234
    235
    236
    237
    238
    239
    240
    241
    242
    243
    244
    245
    246
    247
    248
    249
    250
    251
    252
    253
    254
    255
    256
    257
    258
    259
    260
    261
    262
    263
    264
    265
    266
    267
    268
    269
    270
    271
    272
    273
    274
    275
    276
    277
    278
    279
    280
    281
    282
    283
    284
    285
    286
    287
    288
    289
    290
    291
    292
    293
    294
    295
    296
    297
    298
    299
    300
    301
    302
    303
    304
    305
    306
    307
    308
    309
    310
    311
    312
    313
    314
    class GitContextExtension(AttributeExtension):
        """Provides the "!git_context" attribute extension that will save content to a git repo.

        Args:
            environment (Environment): The object creator that is implementing the
                current design.

        Example:
            ```yaml
            devices:
                - name: "My Device"
                    "!git_context":
                    destination: "config/my_device.yml"
                    data:
                        bgp_asn: 64495
            ```

            The above will implement the design and when the `!git_context` tag is encountered
            it will marshal the dictionary at `data` to `config/my_device.yml` to the base directory
            of the git repository. TODO: explain how the git repo is configured. Potentially change
            the configuration to have git_slug directly in the tag content?
        """

        tag = "git_context"

        def __init__(self, environment: "Environment"):  # noqa: D107
            super().__init__(environment)
            slug = NautobotDesignBuilderConfig.context_repository
            self.context_repo = GitRepo(slug, environment.job_result)
            self._env = {}
            self._reset()

        def _reset(self):
            """Reset the internal state for commit/rollback tracking."""
            self._env = {
                "files": [],
                "directories": [],
            }

        def attribute(self, *args, value=None, model_instance: "ModelInstance" = None):
            """Provide the attribute tag functionality for git_context.

            Args:
                *args (Any): Unused

                value (Any): Value should be a dictionary with the required fields `destination` and
                    `data`. The `destination` field of the dictionary indicates the relative path to
                    store information in the git repo. The `data` field contains the information that
                    should be written to the git repository.

                model_instance (CreatorObject): The object containing the data.

            Raises:
                DesignImplementationError: raised if a required field is missing from the attribute's dictionary.
            """
            required_fields = set("destination", "data")
            missing_fields = required_fields - set(value.keys())
            if missing_fields:
                missing_fields = ", ".join(missing_fields)
                raise DesignImplementationError(f"git-context is missing {missing_fields}")

            base_dir = self.context_repo.path
            output_dir = os.path.join(base_dir, os.path.dirname(value["destination"]))
            try:
                os.makedirs(output_dir)
                self._env["directories"].append(output_dir)
            except FileExistsError:
                # this just means the directory exists
                # prior to this particular change, so don't
                # record that the directory was created so we
                # don't accidentally remove it during roll back
                pass

            output_file = os.path.join(base_dir, value["destination"])
            with open(output_file, "w", encoding="UTF-8") as context_file:
                yaml.dump(value["data"], context_file)
            self._env["files"].append(output_file)

        def commit(self):
            """Commit the added files to the git repository and push the changes."""
            self.context_repo.commit_with_added("Created by design builder")
            self.context_repo.push()
            self._reset()

        def roll_back(self):
            """Delete any files and directories that were created by the tag."""
            for file in self._env["files"]:
                os.remove(file)

            for dirpath in self._env["directories"]:
                os.rmdir(dirpath)
            self._reset()

#### `attribute(*args, value=None, model_instance=None)`

Provide the attribute tag functionality for git_context.

Parameters:

Name | Type | Description | Default
--- | --- | --- | ---
`*args` | `Any` | Unused | `()`
`value` | `Any` | Value should be a dictionary with the required fields `destination` and `data`. The `destination` field of the dictionary indicates the relative path to store information in the git repo. The `data` field contains the information that should be written to the git repository. | `None`
`model_instance` | `CreatorObject` | The object containing the data. | `None`

Raises:

Type | Description
--- | ---
`DesignImplementationError` | raised if a required field is missing from the attribute's dictionary.

Source code in `nautobot_design_builder/ext.py`

    262
    263
    264
    265
    266
    267
    268
    269
    270
    271
    272
    273
    274
    275
    276
    277
    278
    279
    280
    281
    282
    283
    284
    285
    286
    287
    288
    289
    290
    291
    292
    293
    294
    295
    296
    297
    298
    299
    def attribute(self, *args, value=None, model_instance: "ModelInstance" = None):
        """Provide the attribute tag functionality for git_context.

        Args:
            *args (Any): Unused

            value (Any): Value should be a dictionary with the required fields `destination` and
                `data`. The `destination` field of the dictionary indicates the relative path to
                store information in the git repo. The `data` field contains the information that
                should be written to the git repository.

            model_instance (CreatorObject): The object containing the data.

        Raises:
            DesignImplementationError: raised if a required field is missing from the attribute's dictionary.
        """
        required_fields = set("destination", "data")
        missing_fields = required_fields - set(value.keys())
        if missing_fields:
            missing_fields = ", ".join(missing_fields)
            raise DesignImplementationError(f"git-context is missing {missing_fields}")

        base_dir = self.context_repo.path
        output_dir = os.path.join(base_dir, os.path.dirname(value["destination"]))
        try:
            os.makedirs(output_dir)
            self._env["directories"].append(output_dir)
        except FileExistsError:
            # this just means the directory exists
            # prior to this particular change, so don't
            # record that the directory was created so we
            # don't accidentally remove it during roll back
            pass

        output_file = os.path.join(base_dir, value["destination"])
        with open(output_file, "w", encoding="UTF-8") as context_file:
            yaml.dump(value["data"], context_file)
        self._env["files"].append(output_file)

#### `commit()`

Commit the added files to the git repository and push the changes.

Source code in `nautobot_design_builder/ext.py`

    301
    302
    303
    304
    305
    def commit(self):
        """Commit the added files to the git repository and push the changes."""
        self.context_repo.commit_with_added("Created by design builder")
        self.context_repo.push()
        self._reset()

#### `roll_back()`

Delete any files and directories that were created by the tag.

Source code in `nautobot_design_builder/ext.py`

    307
    308
    309
    310
    311
    312
    313
    314
    def roll_back(self):
        """Delete any files and directories that were created by the tag."""
        for file in self._env["files"]:
            os.remove(file)

        for dirpath in self._env["directories"]:
            os.rmdir(dirpath)
        self._reset()

### `ReferenceExtension`

Bases: `AttributeExtension`, `ValueExtension`

An ObjectCreator extension the creates references to objects and retrieves them.

This extension is both an attribute extension and a value extension that is associated with the `ref` tag in both cases. When using this extension as an attribute, the value should be a string representing the name of the reference to be used later. The name of the reference must not include any dots, as dotted notation in the reference lookup indicates that the model attribute should be returned rather than the model itself.

When used as a value extension, the syntax is `!ref:name_previously_used` where `name_previously_used` matches the string value provided to the original `!ref` attribute. With only the reference name, the entire CreatorObject is returned for assignment, when the name is followed by a dot and an attribute name, then only that matching attribute is returned from the stored creator object.

Parameters:

Name | Type | Description | Default
--- | --- | --- | ---
`environment` | `Environment` | The object creator that is implementing the current design. | required

Source code in `nautobot_design_builder/ext.py`

    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    177
    178
    179
    180
    181
    182
    183
    184
    185
    186
    187
    188
    189
    190
    191
    192
    193
    194
    195
    196
    197
    198
    199
    200
    201
    202
    203
    204
    205
    206
    207
    208
    209
    210
    211
    212
    213
    214
    215
    216
    217
    218
    219
    220
    class ReferenceExtension(AttributeExtension, ValueExtension):
        """An ObjectCreator extension the creates references to objects and retrieves them.

        This extension is both an attribute extension and a value extension that is
        associated with the `ref` tag in both cases. When using this extension as an attribute,
        the value should be a string representing the name of the reference to be used later. The name
        of the reference must not include any dots, as dotted notation in the reference lookup indicates that
        the model attribute should be returned rather than the model itself.

        When used as a value extension, the syntax is `!ref:name_previously_used` where
        `name_previously_used` matches the string value provided to the original `!ref` attribute. With
        only the reference name, the entire CreatorObject is returned for assignment, when the name is
        followed by a dot and an attribute name, then only that matching attribute is returned from the
        stored creator object.

        Args:
            environment (Environment): The object creator that is implementing the
                current design.
        """

        tag = "ref"

        def __init__(self, environment: "Environment"):  # noqa: D107
            super().__init__(environment)
            self._env = {}

        def attribute(self, *args: List[Any], value, model_instance):
            """This method is called when the `!ref` tag is encountered.

            Args:
                *args (List[Any]): Any additional arguments following the tag name. These are `:` delimited.

                value (Any): Value should be a string name (the reference) to refer to the object
                model_instance (CreatorObject): The object that will be later referenced

            Example:
                ```yaml
                devices:
                    - name: "My Device"
                      "!ref": "my_device"
                ```

                The `ReferenceExtension.attribute` method is called when the `!ref` attribute
                is encountered. The `value` argument of the method will be the string `my_device` and
                the `object` argument will be set to a `CreatorObject` containing the "My Device"
                `dcim.models.Device` instance that is being created. If the value is a list, then
                one reference will be created for each of the items in the list.
            """
            if isinstance(value, list):
                for item in value:
                    self._env[item] = model_instance
            else:
                self._env[value] = model_instance

        def value(self, key) -> "ModelInstance":
            """Return the CreatorObject that is stored at `key`.

            Args:
                key (str): The reference name

            Returns:
                CreatorObject: The object stored at `reference_name`
            """
            keys = key.split(".", 1)
            attribute = None
            if len(keys) == 2:
                key, attribute = keys
            try:
                model_instance = self._env[key]
            except KeyError:
                # pylint: disable=raise-missing-from
                raise DesignImplementationError(f"No ref named {key} has been saved in the design.")
            if model_instance.design_instance is None:
                return model_instance
            adding = model_instance.design_instance._state.adding  # pylint: disable=protected-access
            if model_instance.design_instance and not adding:
                model_instance.design_instance.refresh_from_db()
            if attribute:
                # TODO: I think the result of the reduce operation needs to (potentially)
                # be wrapped up in a ModelInstance object
                return reduce(getattr, [model_instance.design_instance, *attribute.split(".")])
            return model_instance

#### `attribute(*args, value, model_instance)`

This method is called when the `!ref` tag is encountered.

Parameters:

Name | Type | Description | Default
--- | --- | --- | ---
`*args` | `List[Any]` | Any additional arguments following the tag name. These are `:` delimited. | `()`
`value` | `Any` | Value should be a string name (the reference) to refer to the object | required
`model_instance` | `CreatorObject` | The object that will be later referenced | required

Example

    devices:
        - name: "My Device"
          "!ref": "my_device"

The `ReferenceExtension.attribute` method is called when the `!ref` attribute is encountered. The `value` argument of the method will be the string `my_device` and the `object` argument will be set to a `CreatorObject` containing the "My Device" `dcim.models.Device` instance that is being created. If the value is a list, then one reference will be created for each of the items in the list.

Source code in `nautobot_design_builder/ext.py`

    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    177
    178
    179
    180
    181
    182
    183
    184
    185
    186
    187
    188
    189
    190
    191
    def attribute(self, *args: List[Any], value, model_instance):
        """This method is called when the `!ref` tag is encountered.

        Args:
            *args (List[Any]): Any additional arguments following the tag name. These are `:` delimited.

            value (Any): Value should be a string name (the reference) to refer to the object
            model_instance (CreatorObject): The object that will be later referenced

        Example:
            ```yaml
            devices:
                - name: "My Device"
                  "!ref": "my_device"
            ```

            The `ReferenceExtension.attribute` method is called when the `!ref` attribute
            is encountered. The `value` argument of the method will be the string `my_device` and
            the `object` argument will be set to a `CreatorObject` containing the "My Device"
            `dcim.models.Device` instance that is being created. If the value is a list, then
            one reference will be created for each of the items in the list.
        """
        if isinstance(value, list):
            for item in value:
                self._env[item] = model_instance
        else:
            self._env[value] = model_instance

#### `value(key)`

Return the CreatorObject that is stored at `key`.

Parameters:

Name | Type | Description | Default
--- | --- | --- | ---
`key` | `str` | The reference name | required

Returns:

Name | Type | Description
--- | --- | ---
`CreatorObject` | `ModelInstance` | The object stored at `reference_name`

Source code in `nautobot_design_builder/ext.py`

    193
    194
    195
    196
    197
    198
    199
    200
    201
    202
    203
    204
    205
    206
    207
    208
    209
    210
    211
    212
    213
    214
    215
    216
    217
    218
    219
    220
    def value(self, key) -> "ModelInstance":
        """Return the CreatorObject that is stored at `key`.

        Args:
            key (str): The reference name

        Returns:
            CreatorObject: The object stored at `reference_name`
        """
        keys = key.split(".", 1)
        attribute = None
        if len(keys) == 2:
            key, attribute = keys
        try:
            model_instance = self._env[key]
        except KeyError:
            # pylint: disable=raise-missing-from
            raise DesignImplementationError(f"No ref named {key} has been saved in the design.")
        if model_instance.design_instance is None:
            return model_instance
        adding = model_instance.design_instance._state.adding  # pylint: disable=protected-access
        if model_instance.design_instance and not adding:
            model_instance.design_instance.refresh_from_db()
        if attribute:
            # TODO: I think the result of the reduce operation needs to (potentially)
            # be wrapped up in a ModelInstance object
            return reduce(getattr, [model_instance.design_instance, *attribute.split(".")])
        return model_instance

### `ValueExtension`

Bases: `Extension`, `ABC`

A `ValueExtension` will be matched when a design value matches the `tag`.

Source code in `nautobot_design_builder/ext.py`

    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    class ValueExtension(Extension, ABC):
        """A `ValueExtension` will be matched when a design value matches the `tag`."""

        @abstractmethod
        def value(self, key: str) -> "ModelInstance":
            """Retrieve a CreatorObject to be assigned to the design.

            Args:
                key (str): The key to lookup the Creator Object.

            Returns:
                CreatorObject: A CreatorObject must be returned that will be used
                in place of the `!attribute_tag` placeholder.
            """

#### `value(key)` `abstractmethod`

Retrieve a CreatorObject to be assigned to the design.

Parameters:

Name | Type | Description | Default
--- | --- | --- | ---
`key` | `str` | The key to lookup the Creator Object. | required

Returns:

Name | Type | Description
--- | --- | ---
`CreatorObject` | `ModelInstance` | A CreatorObject must be returned that will be used
| `ModelInstance` | in place of the `!attribute_tag` placeholder.

Source code in `nautobot_design_builder/ext.py`

    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    @abstractmethod
    def value(self, key: str) -> "ModelInstance":
        """Retrieve a CreatorObject to be assigned to the design.

        Args:
            key (str): The key to lookup the Creator Object.

        Returns:
            CreatorObject: A CreatorObject must be returned that will be used
            in place of the `!attribute_tag` placeholder.
        """

### `extensions(module=None)`

Get all the extensions defined in a module.

Parameters:

Name | Type | Description | Default
--- | --- | --- | ---
`module` | `ModuleType` | Module to search for extensions. If left as `None` then the ext.py module is searched. | `None`

Returns:

Type | Description
--- | ---
`List[Extension]` | List[Extension]: List of extensions found in the module.

Source code in `nautobot_design_builder/ext.py`

    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    def extensions(module: ModuleType = None) -> List["Extension"]:
        """Get all the extensions defined in a module.

        Args:
            module (ModuleType): Module to search for extensions. If left as `None` then the ext.py module is searched.

        Returns:
            List[Extension]: List of extensions found in the module.
        """
        if module is None:
            module = sys.modules[__name__]

        def matches(value):
            if hasattr(value, "__module__"):
                if value.__module__ == module.__name__:
                    return is_extension(value)
            return False

        return [extension[1] for extension in inspect.getmembers(module, matches)]

### `is_extension(cls)`

Determine if a class is an Extension.

Source code in `nautobot_design_builder/ext.py`

    21
    22
    23
    def is_extension(cls):
        """Determine if a class is an Extension."""
        return inspect.isclass(cls) and issubclass(cls, Extension) and cls is not Extension