From 3d94ba2118baff16ac9f7a01f4eb8fc0bfea8787 Mon Sep 17 00:00:00 2001 From: Christian Adell Date: Thu, 4 Apr 2024 11:48:18 +0200 Subject: [PATCH 01/29] style: move the Designs into its own navigation tab --- nautobot_design_builder/navigation.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/nautobot_design_builder/navigation.py b/nautobot_design_builder/navigation.py index aa286886..cf32081e 100644 --- a/nautobot_design_builder/navigation.py +++ b/nautobot_design_builder/navigation.py @@ -9,11 +9,11 @@ menu_items = ( NavMenuTab( - name="Jobs", - weight=150, + name="Designs", + weight=1000, groups=( NavMenuGroup( - name="Designs", + name="Design Builder", weight=100, items=( NavMenuItem( From fdcf825682a3fbfb95dfbfd78bf306cd2b58003c Mon Sep 17 00:00:00 2001 From: Christian Adell Date: Thu, 4 Apr 2024 12:12:36 +0200 Subject: [PATCH 02/29] style: define a Jobs group name for design builder jobs, different from design jobs --- nautobot_design_builder/jobs.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/nautobot_design_builder/jobs.py b/nautobot_design_builder/jobs.py index 24f6d385..63f22ada 100644 --- a/nautobot_design_builder/jobs.py +++ b/nautobot_design_builder/jobs.py @@ -6,6 +6,9 @@ from .models import DesignInstance +name = "Design Builder" + + class DesignInstanceDecommissioning(Job): """Job to decommission Design Instances.""" @@ -18,7 +21,7 @@ class DesignInstanceDecommissioning(Job): class Meta: # pylint: disable=too-few-public-methods """Meta class.""" - name = "Decommission Design Instances." + name = "Decommission Design Instances" description = """Job to decommission one or many Design Instances from Nautobot.""" def run(self, data, commit): From 681a1225c38e7eaf19c9af0390e7eece8013eda0 Mon Sep 17 00:00:00 2001 From: Christian Adell Date: Thu, 4 Apr 2024 12:17:16 +0200 Subject: [PATCH 03/29] style: use play button --- nautobot_design_builder/tables.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nautobot_design_builder/tables.py b/nautobot_design_builder/tables.py index b20cc345..85c5d80d 100644 --- a/nautobot_design_builder/tables.py +++ b/nautobot_design_builder/tables.py @@ -10,7 +10,7 @@ DESIGNTABLE = """ - + """ From ee9d7b2781b2bd65f3b61fdc7b4e2c8dfcb8bb07 Mon Sep 17 00:00:00 2001 From: Christian Adell Date: Thu, 4 Apr 2024 14:13:09 +0200 Subject: [PATCH 04/29] style: change last implemented to last updated --- nautobot_design_builder/jobs.py | 2 +- .../nautobot_design_builder/designinstance_retrieve.html | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/nautobot_design_builder/jobs.py b/nautobot_design_builder/jobs.py index 63f22ada..01660844 100644 --- a/nautobot_design_builder/jobs.py +++ b/nautobot_design_builder/jobs.py @@ -6,7 +6,7 @@ from .models import DesignInstance -name = "Design Builder" +name = "Design Builder" # pylint: disable=invalid-name class DesignInstanceDecommissioning(Job): diff --git a/nautobot_design_builder/templates/nautobot_design_builder/designinstance_retrieve.html b/nautobot_design_builder/templates/nautobot_design_builder/designinstance_retrieve.html index 1dc68240..2c128a9c 100644 --- a/nautobot_design_builder/templates/nautobot_design_builder/designinstance_retrieve.html +++ b/nautobot_design_builder/templates/nautobot_design_builder/designinstance_retrieve.html @@ -16,11 +16,11 @@ {{ object.owner|placeholder }} - First implemented + Deployment Time {{ object.first_implemented|placeholder }} - Last implemented + Last Update Time {{ object.last_implemented|placeholder }} From 4630a25d834be005079e06ea5a0de3f46fcecff8 Mon Sep 17 00:00:00 2001 From: Christian Adell Date: Thu, 4 Apr 2024 14:29:29 +0200 Subject: [PATCH 05/29] style: move from live state to operational state --- nautobot_design_builder/tables.py | 4 +++- .../nautobot_design_builder/designinstance_retrieve.html | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/nautobot_design_builder/tables.py b/nautobot_design_builder/tables.py index 85c5d80d..9f2f2033 100644 --- a/nautobot_design_builder/tables.py +++ b/nautobot_design_builder/tables.py @@ -46,7 +46,9 @@ class DesignInstanceTable(StatusTableMixin, BaseTable): name = Column(linkify=True) design = Column(linkify=True) - live_state = ColoredLabelColumn() + first_implemented = Column(verbose_name="Deployment Time") + last_implemented = Column(verbose_name="Last Update Time") + live_state = ColoredLabelColumn(verbose_name="Operational State") actions = ButtonsColumn( DesignInstance, buttons=( diff --git a/nautobot_design_builder/templates/nautobot_design_builder/designinstance_retrieve.html b/nautobot_design_builder/templates/nautobot_design_builder/designinstance_retrieve.html index 2c128a9c..6ee9b7f1 100644 --- a/nautobot_design_builder/templates/nautobot_design_builder/designinstance_retrieve.html +++ b/nautobot_design_builder/templates/nautobot_design_builder/designinstance_retrieve.html @@ -34,7 +34,7 @@ - Live State + Operational State {{ object.live_state }} From d2b3f84e259674c76ddd9cf910aaee11a41771f8 Mon Sep 17 00:00:00 2001 From: Christian Adell Date: Fri, 5 Apr 2024 12:27:27 +0200 Subject: [PATCH 06/29] feat: adding version representation --- .../designs/initial_data/jobs.py | 1 + nautobot_design_builder/design_job.py | 1 + nautobot_design_builder/filters.py | 14 +++++++++-- nautobot_design_builder/forms.py | 6 +++-- .../migrations/0005_auto_20240405_0938.py | 24 +++++++++++++++++++ nautobot_design_builder/models.py | 5 ++-- nautobot_design_builder/signals.py | 3 ++- nautobot_design_builder/tables.py | 4 ++-- .../design_retrieve.html | 4 ++++ .../designinstance_retrieve.html | 4 ++++ 10 files changed, 56 insertions(+), 10 deletions(-) create mode 100644 nautobot_design_builder/migrations/0005_auto_20240405_0938.py diff --git a/examples/custom_design/designs/initial_data/jobs.py b/examples/custom_design/designs/initial_data/jobs.py index 39f01ee1..60679fab 100644 --- a/examples/custom_design/designs/initial_data/jobs.py +++ b/examples/custom_design/designs/initial_data/jobs.py @@ -19,3 +19,4 @@ class Meta: commit_default = False design_file = "designs/0001_design.yaml.j2" context_class = InitialDesignContext + version = "1.0.0" diff --git a/nautobot_design_builder/design_job.py b/nautobot_design_builder/design_job.py index 7478b656..f84103e0 100644 --- a/nautobot_design_builder/design_job.py +++ b/nautobot_design_builder/design_job.py @@ -207,6 +207,7 @@ def _setup_journal(self, instance_name: str, design_owner: str): live_state=Status.objects.get( content_types=content_type, name=choices.DesignInstanceLiveStateChoices.PENDING ), + version=self.design_model().version, ) instance.validated_save() diff --git a/nautobot_design_builder/filters.py b/nautobot_design_builder/filters.py index 66d2cec3..4c0ce327 100644 --- a/nautobot_design_builder/filters.py +++ b/nautobot_design_builder/filters.py @@ -22,7 +22,7 @@ class Meta: """Meta attributes for filter.""" model = Design - fields = ["id", "job"] + fields = ["id", "job", "version"] class DesignInstanceFilterSet(NautobotFilterSet, StatusModelFilterSetMixin): @@ -40,7 +40,17 @@ class Meta: """Meta attributes for filter.""" model = DesignInstance - fields = ["id", "design", "name", "owner", "first_implemented", "last_implemented", "status", "live_state"] + fields = [ + "id", + "design", + "name", + "owner", + "first_implemented", + "last_implemented", + "status", + "live_state", + "version", + ] class JournalFilterSet(NautobotFilterSet): diff --git a/nautobot_design_builder/forms.py b/nautobot_design_builder/forms.py index fe45868e..e36dbeed 100644 --- a/nautobot_design_builder/forms.py +++ b/nautobot_design_builder/forms.py @@ -1,6 +1,6 @@ """Forms for the design builder app.""" -from django.forms import NullBooleanField +from django.forms import NullBooleanField, CharField from nautobot.extras.forms import NautobotFilterForm from nautobot.extras.models import Job, JobResult from nautobot.utilities.forms import TagFilterField, DynamicModelChoiceField, StaticSelect2, BOOLEAN_WITH_BLANK_CHOICES @@ -13,8 +13,9 @@ class DesignFilterForm(NautobotFilterForm): model = Design - job = DynamicModelChoiceField(queryset=Job.objects.all()) + job = DynamicModelChoiceField(queryset=Job.objects.all(), required=False) tag = TagFilterField(model) + version = CharField(max_length=20, required=False) class DesignInstanceFilterForm(NautobotFilterForm): @@ -24,6 +25,7 @@ class DesignInstanceFilterForm(NautobotFilterForm): design = DynamicModelChoiceField(queryset=Design.objects.all()) tag = TagFilterField(model) + version = CharField(max_length=20, required=False) class JournalFilterForm(NautobotFilterForm): diff --git a/nautobot_design_builder/migrations/0005_auto_20240405_0938.py b/nautobot_design_builder/migrations/0005_auto_20240405_0938.py new file mode 100644 index 00000000..d5bd9518 --- /dev/null +++ b/nautobot_design_builder/migrations/0005_auto_20240405_0938.py @@ -0,0 +1,24 @@ +# Generated by Django 3.2.20 on 2024-04-05 09:38 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("nautobot_design_builder", "0004_support_update_design"), + ] + + operations = [ + migrations.AddField( + model_name="design", + name="version", + field=models.CharField(default="0.0.1", max_length=20), + preserve_default=False, + ), + migrations.AddField( + model_name="designinstance", + name="version", + field=models.CharField(default="0.0.1", max_length=20), + preserve_default=False, + ), + ] diff --git a/nautobot_design_builder/models.py b/nautobot_design_builder/models.py index 9426ffd6..17bba4a7 100644 --- a/nautobot_design_builder/models.py +++ b/nautobot_design_builder/models.py @@ -103,10 +103,10 @@ class Design(PrimaryModel): to a saved graphql query at some point in the future. """ - # TODO: Add version field (future feature) # TODO: Add saved graphql query (future feature) # TODO: Add a template mapping to get custom payload (future feature) job = models.ForeignKey(to=JobModel, on_delete=models.PROTECT, editable=False) + version = models.CharField(max_length=20) objects = DesignQuerySet.as_manager() @@ -167,14 +167,13 @@ class DesignInstance(PrimaryModel, StatusModel): post_decommission = Signal() - # TODO: add version field to indicate which version of a design - # this instance is on. (future feature) design = models.ForeignKey(to=Design, on_delete=models.PROTECT, editable=False, related_name="instances") name = models.CharField(max_length=DESIGN_NAME_MAX_LENGTH) owner = models.CharField(max_length=DESIGN_OWNER_MAX_LENGTH, blank=True, default="") first_implemented = models.DateTimeField(blank=True, null=True, auto_now_add=True) last_implemented = models.DateTimeField(blank=True, null=True) live_state = StatusField(blank=False, null=False, on_delete=models.PROTECT) + version = models.CharField(max_length=20) objects = DesignInstanceQuerySet.as_manager() diff --git a/nautobot_design_builder/signals.py b/nautobot_design_builder/signals.py index 1c3ce072..b77f345f 100644 --- a/nautobot_design_builder/signals.py +++ b/nautobot_design_builder/signals.py @@ -67,7 +67,8 @@ def create_design_model(sender, instance: Job, **kwargs): # pylint:disable=unus instance (Job): Job instance that has been created or updated. """ if instance.job_class and issubclass(instance.job_class, DesignJob): - _, created = Design.objects.get_or_create(job=instance) + version = instance.job_class.Meta.version if hasattr(instance.job_class.Meta, "version") else "Not defined" + _, created = Design.objects.get_or_create(job=instance, defaults={"version": version}) if created: _LOGGER.debug("Created design from %s", instance) diff --git a/nautobot_design_builder/tables.py b/nautobot_design_builder/tables.py index 9f2f2033..9333320e 100644 --- a/nautobot_design_builder/tables.py +++ b/nautobot_design_builder/tables.py @@ -27,7 +27,7 @@ class Meta(BaseTable.Meta): # pylint: disable=too-few-public-methods """Meta attributes.""" model = Design - fields = ("name", "job", "instance_count") + fields = ("name", "version", "job", "instance_count") DESIGNINSTANCETABLE = """ @@ -62,7 +62,7 @@ class Meta(BaseTable.Meta): # pylint: disable=too-few-public-methods """Meta attributes.""" model = DesignInstance - fields = ("name", "design", "owner", "first_implemented", "last_implemented", "status", "live_state") + fields = ("name", "design", "version", "owner", "first_implemented", "last_implemented", "status", "live_state") class JournalTable(BaseTable): diff --git a/nautobot_design_builder/templates/nautobot_design_builder/design_retrieve.html b/nautobot_design_builder/templates/nautobot_design_builder/design_retrieve.html index 3ec300c1..154e2eaf 100644 --- a/nautobot_design_builder/templates/nautobot_design_builder/design_retrieve.html +++ b/nautobot_design_builder/templates/nautobot_design_builder/design_retrieve.html @@ -17,6 +17,10 @@ Job {{ object.job|hyperlinked_object }} + + Version + {{ object.version }} + {% endblock content_left_page %} diff --git a/nautobot_design_builder/templates/nautobot_design_builder/designinstance_retrieve.html b/nautobot_design_builder/templates/nautobot_design_builder/designinstance_retrieve.html index 6ee9b7f1..8b202f1e 100644 --- a/nautobot_design_builder/templates/nautobot_design_builder/designinstance_retrieve.html +++ b/nautobot_design_builder/templates/nautobot_design_builder/designinstance_retrieve.html @@ -11,6 +11,10 @@ Name {{ object.name }} + + Version + {{ object.version }} + Owner {{ object.owner|placeholder }} From fc1d197556738aa7a5d2ed4d08573f1fee727956 Mon Sep 17 00:00:00 2001 From: Christian Adell Date: Fri, 5 Apr 2024 14:22:20 +0200 Subject: [PATCH 07/29] tests: fix version tests --- nautobot_design_builder/tests/test_decommissioning_job.py | 4 +++- nautobot_design_builder/tests/test_model_design_instance.py | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/nautobot_design_builder/tests/test_decommissioning_job.py b/nautobot_design_builder/tests/test_decommissioning_job.py index da84f7be..77453392 100644 --- a/nautobot_design_builder/tests/test_decommissioning_job.py +++ b/nautobot_design_builder/tests/test_decommissioning_job.py @@ -64,7 +64,7 @@ def setUp(self): ) self.job1.validated_save() - self.design1, _ = models.Design.objects.get_or_create(job=self.job1) + self.design1, _ = models.Design.objects.get_or_create(job=self.job1, defaults={"version": "0.0.1"}) self.content_type = ContentType.objects.get_for_model(models.DesignInstance) self.design_instance = models.DesignInstance( design=self.design1, @@ -73,6 +73,7 @@ def setUp(self): live_state=Status.objects.get( content_types=self.content_type, name=choices.DesignInstanceLiveStateChoices.PENDING ), + version=self.design1.version, ) self.design_instance.validated_save() @@ -83,6 +84,7 @@ def setUp(self): live_state=Status.objects.get( content_types=self.content_type, name=choices.DesignInstanceLiveStateChoices.PENDING ), + version=self.design1.version, ) self.design_instance_2.validated_save() diff --git a/nautobot_design_builder/tests/test_model_design_instance.py b/nautobot_design_builder/tests/test_model_design_instance.py index 17588e37..7cd332d1 100644 --- a/nautobot_design_builder/tests/test_model_design_instance.py +++ b/nautobot_design_builder/tests/test_model_design_instance.py @@ -28,6 +28,7 @@ def create_design_instance(design_name, design): live_state=Status.objects.get( content_types=content_type, name=choices.DesignInstanceLiveStateChoices.PENDING ), + version=design.version, ) design_instance.validated_save() return design_instance From c2b799da23f6593131f485d3b56fd8120b1bb2d0 Mon Sep 17 00:00:00 2001 From: Christian Adell Date: Fri, 5 Apr 2024 18:15:25 +0200 Subject: [PATCH 08/29] refactor: add reference to change owner reference in Nautobot 2.0 --- nautobot_design_builder/design_job.py | 1 + nautobot_design_builder/models.py | 1 + 2 files changed, 2 insertions(+) diff --git a/nautobot_design_builder/design_job.py b/nautobot_design_builder/design_job.py index f84103e0..445048c6 100644 --- a/nautobot_design_builder/design_job.py +++ b/nautobot_design_builder/design_job.py @@ -38,6 +38,7 @@ class DesignJob(Job, ABC, LoggingMixin): # pylint: disable=too-many-instance-at """ instance_name = StringVar(label="Instance Name", max_length=models.DESIGN_NAME_MAX_LENGTH) + # TODO: In Nautobot 2.1, replace by the Contacts model owner = StringVar(label="Implementation Owner", required=False, max_length=models.DESIGN_OWNER_MAX_LENGTH) if nautobot_version >= "2.0.0": diff --git a/nautobot_design_builder/models.py b/nautobot_design_builder/models.py index 17bba4a7..d876791e 100644 --- a/nautobot_design_builder/models.py +++ b/nautobot_design_builder/models.py @@ -169,6 +169,7 @@ class DesignInstance(PrimaryModel, StatusModel): design = models.ForeignKey(to=Design, on_delete=models.PROTECT, editable=False, related_name="instances") name = models.CharField(max_length=DESIGN_NAME_MAX_LENGTH) + # TODO: In Nautobot 2.1, replace by the Contacts model owner = models.CharField(max_length=DESIGN_OWNER_MAX_LENGTH, blank=True, default="") first_implemented = models.DateTimeField(blank=True, null=True, auto_now_add=True) last_implemented = models.DateTimeField(blank=True, null=True) From 2e76123848da6fea44173fa3ab8fd0870760f9cb Mon Sep 17 00:00:00 2001 From: Christian Adell Date: Mon, 8 Apr 2024 10:48:18 +0200 Subject: [PATCH 09/29] refactor: replace 'owner' by computed 'created_by' and 'last_updated_by' --- nautobot_design_builder/api/serializers.py | 13 ++++- nautobot_design_builder/design_job.py | 7 +-- nautobot_design_builder/filters.py | 1 - .../0006_remove_designinstance_owner.py | 16 ++++++ nautobot_design_builder/models.py | 16 ++++-- nautobot_design_builder/tables.py | 15 +++++- .../designinstance_retrieve.html | 8 ++- nautobot_design_builder/tests/__init__.py | 1 - nautobot_design_builder/tests/util.py | 9 +--- nautobot_design_builder/util.py | 51 +++++++++++++++++++ 10 files changed, 113 insertions(+), 24 deletions(-) create mode 100644 nautobot_design_builder/migrations/0006_remove_designinstance_owner.py diff --git a/nautobot_design_builder/api/serializers.py b/nautobot_design_builder/api/serializers.py index a34daf8f..e033b410 100644 --- a/nautobot_design_builder/api/serializers.py +++ b/nautobot_design_builder/api/serializers.py @@ -40,6 +40,8 @@ class DesignInstanceSerializer(NautobotModelSerializer, TaggedModelSerializerMix url = HyperlinkedIdentityField(view_name="plugins-api:nautobot_design_builder-api:design-detail") design = NestedDesignSerializer() live_state = NestedStatusSerializer() + created_by = SerializerMethodField(read_only=True) + last_updated_by = SerializerMethodField(read_only=True) class Meta: """Serializer options for the design model.""" @@ -50,13 +52,22 @@ class Meta: "url", "design", "name", - "owner", + "created_by", "first_implemented", + "last_updated_by", "last_implemented", "status", "live_state", ] + def get_created_by(self, instance): + """Get the username of the user who created the object.""" + return instance.get_created_by() + + def get_last_updated_by(self, instance): + """Get the username of the user who update the object last time.""" + return instance.get_last_updated_by() + class JournalSerializer(NautobotModelSerializer, TaggedModelSerializerMixin): """Serializer for the journal model.""" diff --git a/nautobot_design_builder/design_job.py b/nautobot_design_builder/design_job.py index 445048c6..bf22a9ba 100644 --- a/nautobot_design_builder/design_job.py +++ b/nautobot_design_builder/design_job.py @@ -38,8 +38,6 @@ class DesignJob(Job, ABC, LoggingMixin): # pylint: disable=too-many-instance-at """ instance_name = StringVar(label="Instance Name", max_length=models.DESIGN_NAME_MAX_LENGTH) - # TODO: In Nautobot 2.1, replace by the Contacts model - owner = StringVar(label="Implementation Owner", required=False, max_length=models.DESIGN_OWNER_MAX_LENGTH) if nautobot_version >= "2.0.0": from nautobot.extras.jobs import DryRunVar # pylint: disable=no-name-in-module,import-outside-toplevel @@ -191,7 +189,7 @@ def implement_design(self, context, design_file, commit): self.builder.implement_design_changes(design, deprecated_design, design_file, commit) - def _setup_journal(self, instance_name: str, design_owner: str): + def _setup_journal(self, instance_name: str): try: instance = models.DesignInstance.objects.get(name=instance_name, design=self.design_model()) self.log_info(message=f'Existing design instance of "{instance_name}" was found, re-running design job.') @@ -201,7 +199,6 @@ def _setup_journal(self, instance_name: str, design_owner: str): content_type = ContentType.objects.get_for_model(models.DesignInstance) instance = models.DesignInstance( name=instance_name, - owner=design_owner, design=self.design_model(), last_implemented=datetime.now(), status=Status.objects.get(content_types=content_type, name=choices.DesignInstanceStatusChoices.ACTIVE), @@ -240,7 +237,7 @@ def run(self, **kwargs): # pylint: disable=arguments-differ,too-many-branches,t else: self.job_result.job_kwargs = self.serialize_data(data) - journal = self._setup_journal(data.pop("instance_name"), data.pop("owner")) + journal = self._setup_journal(data.pop("instance_name")) self.log_info(message=f"Building {getattr(self.Meta, 'name')}") extensions = getattr(self.Meta, "extensions", []) self.builder = Builder( diff --git a/nautobot_design_builder/filters.py b/nautobot_design_builder/filters.py index 4c0ce327..590a9ec2 100644 --- a/nautobot_design_builder/filters.py +++ b/nautobot_design_builder/filters.py @@ -44,7 +44,6 @@ class Meta: "id", "design", "name", - "owner", "first_implemented", "last_implemented", "status", diff --git a/nautobot_design_builder/migrations/0006_remove_designinstance_owner.py b/nautobot_design_builder/migrations/0006_remove_designinstance_owner.py new file mode 100644 index 00000000..5c85486f --- /dev/null +++ b/nautobot_design_builder/migrations/0006_remove_designinstance_owner.py @@ -0,0 +1,16 @@ +# Generated by Django 3.2.20 on 2024-04-08 07:15 + +from django.db import migrations + + +class Migration(migrations.Migration): + dependencies = [ + ("nautobot_design_builder", "0005_auto_20240405_0938"), + ] + + operations = [ + migrations.RemoveField( + model_name="designinstance", + name="owner", + ), + ] diff --git a/nautobot_design_builder/models.py b/nautobot_design_builder/models.py index d876791e..8d70d23f 100644 --- a/nautobot_design_builder/models.py +++ b/nautobot_design_builder/models.py @@ -16,7 +16,7 @@ from nautobot.utilities.querysets import RestrictedQuerySet from nautobot.utilities.choices import ColorChoices -from .util import nautobot_version +from .util import nautobot_version, get_created_and_last_updated_usernames_for_model from . import choices from .errors import DesignValidationError @@ -150,8 +150,6 @@ def get_by_natural_key(self, design_name, instance_name): DESIGN_NAME_MAX_LENGTH = 100 -DESIGN_OWNER_MAX_LENGTH = 100 - @extras_features("statuses") class DesignInstance(PrimaryModel, StatusModel): @@ -169,8 +167,6 @@ class DesignInstance(PrimaryModel, StatusModel): design = models.ForeignKey(to=Design, on_delete=models.PROTECT, editable=False, related_name="instances") name = models.CharField(max_length=DESIGN_NAME_MAX_LENGTH) - # TODO: In Nautobot 2.1, replace by the Contacts model - owner = models.CharField(max_length=DESIGN_OWNER_MAX_LENGTH, blank=True, default="") first_implemented = models.DateTimeField(blank=True, null=True, auto_now_add=True) last_implemented = models.DateTimeField(blank=True, null=True) live_state = StatusField(blank=False, null=False, on_delete=models.PROTECT) @@ -236,6 +232,16 @@ def delete(self, *args, **kwargs): raise ValidationError("A Design Instance can only be delete if it's Decommissioned and not Deployed.") return super().delete(*args, **kwargs) + def get_created_by(self): + """Get the username of the user who created the object.""" + created_by, _ = get_created_and_last_updated_usernames_for_model(self) + return created_by + + def get_last_updated_by(self): + """Get the username of the user who update the object last time.""" + _, last_updated_by = get_created_and_last_updated_usernames_for_model(self) + return last_updated_by + class Journal(PrimaryModel): """The Journal represents a single execution of a design instance. diff --git a/nautobot_design_builder/tables.py b/nautobot_design_builder/tables.py index 9333320e..483a573d 100644 --- a/nautobot_design_builder/tables.py +++ b/nautobot_design_builder/tables.py @@ -7,7 +7,6 @@ from nautobot_design_builder.models import Design, DesignInstance, Journal, JournalEntry - DESIGNTABLE = """ @@ -48,6 +47,8 @@ class DesignInstanceTable(StatusTableMixin, BaseTable): design = Column(linkify=True) first_implemented = Column(verbose_name="Deployment Time") last_implemented = Column(verbose_name="Last Update Time") + created_by = Column(accessor="get_created_by", verbose_name="Deployed by") + updated_by = Column(accessor="get_last_updated_by", verbose_name="Last Updated by") live_state = ColoredLabelColumn(verbose_name="Operational State") actions = ButtonsColumn( DesignInstance, @@ -62,7 +63,17 @@ class Meta(BaseTable.Meta): # pylint: disable=too-few-public-methods """Meta attributes.""" model = DesignInstance - fields = ("name", "design", "version", "owner", "first_implemented", "last_implemented", "status", "live_state") + fields = ( + "name", + "design", + "version", + "created_by", + "first_implemented", + "updated_by", + "last_implemented", + "status", + "live_state", + ) class JournalTable(BaseTable): diff --git a/nautobot_design_builder/templates/nautobot_design_builder/designinstance_retrieve.html b/nautobot_design_builder/templates/nautobot_design_builder/designinstance_retrieve.html index 8b202f1e..f24486bf 100644 --- a/nautobot_design_builder/templates/nautobot_design_builder/designinstance_retrieve.html +++ b/nautobot_design_builder/templates/nautobot_design_builder/designinstance_retrieve.html @@ -16,13 +16,17 @@ {{ object.version }} - Owner - {{ object.owner|placeholder }} + Deployed by + {{ object.get_created_by|placeholder }} Deployment Time {{ object.first_implemented|placeholder }} + + Last Updated by + {{ object.get_last_updated_by|placeholder }} + Last Update Time {{ object.last_implemented|placeholder }} diff --git a/nautobot_design_builder/tests/__init__.py b/nautobot_design_builder/tests/__init__.py index baada528..1368ffaa 100644 --- a/nautobot_design_builder/tests/__init__.py +++ b/nautobot_design_builder/tests/__init__.py @@ -21,7 +21,6 @@ def setUp(self): super().setUp() self.data = { "instance_name": "Test Design", - "owner": "", } self.logged_messages = [] self.git_patcher = patch("nautobot_design_builder.ext.GitRepo") diff --git a/nautobot_design_builder/tests/util.py b/nautobot_design_builder/tests/util.py index c790be93..d9887e35 100644 --- a/nautobot_design_builder/tests/util.py +++ b/nautobot_design_builder/tests/util.py @@ -15,17 +15,12 @@ def populate_sample_data(): ) design, _ = Design.objects.get_or_create(job=job) - design_instance, _ = DesignInstance.objects.get_or_create(design=design, name="Initial Data", owner="Test User") + design_instance, _ = DesignInstance.objects.get_or_create(design=design, name="Initial Data") Journal.objects.get_or_create(design_instance=design_instance, job_result=job_result) def create_test_view_data(): """Creates test data for view and API view test cases.""" - owners = [ - "Peter Müller", - "Maria Meyer", - "Otto Fischer", - ] for i in range(1, 4): # Core models job = Job.objects.create(name=f"Fake Design Job {i}") @@ -36,7 +31,7 @@ def create_test_view_data(): # Design Builder models design = Design.objects.create(job=job) - instance = DesignInstance.objects.create(design=design, name=f"Test Instance {i}", owner=owners[i - 1]) + instance = DesignInstance.objects.create(design=design, name=f"Test Instance {i}") journal = Journal.objects.create(design_instance=instance, job_result=job_result) full_control = i == 1 # Have one record where full control is given, more than one where its not. JournalEntry.objects.create(journal=journal, design_object=object_created_by_job, full_control=full_control) diff --git a/nautobot_design_builder/util.py b/nautobot_design_builder/util.py index e19d4c1d..ffe14e11 100644 --- a/nautobot_design_builder/util.py +++ b/nautobot_design_builder/util.py @@ -1,5 +1,6 @@ """Main design builder app module, contains DesignJob and base methods and functions.""" +# pylint: disable=import-outside-toplevel import functools import importlib import inspect @@ -14,6 +15,8 @@ from packaging.specifiers import Specifier import yaml +from django.contrib.contenttypes.models import ContentType +from django.db.models import Model from django.conf import settings import nautobot from nautobot.extras.models import GitRepository @@ -350,6 +353,54 @@ def custom_delete_order(key: str) -> int: return 0 +# TODO: this is only available in Nautobot 2.x, recreating it here to reuse for Nautobot 1.x +def get_changes_for_model(model): + """Return a queryset of ObjectChanges for a model or instance. + + The queryset will be filtered by the model class. If an instance is provided, + the queryset will also be filtered by the instance id. + """ + from nautobot.extras.models import ObjectChange # prevent circular import + + if isinstance(model, Model): + return ObjectChange.objects.filter( + changed_object_type=ContentType.objects.get_for_model(model._meta.model), + changed_object_id=model.pk, + ) + if issubclass(model, Model): + return ObjectChange.objects.filter(changed_object_type=ContentType.objects.get_for_model(model._meta.model)) + raise TypeError(f"{model!r} is not a Django Model class or instance") + + +def get_created_and_last_updated_usernames_for_model(instance): + """Get the user who created and last updated an instance. + + Args: + instance (Model): A model class instance + + Returns: + created_by (str): Username of the user that created the instance + last_updated_by (str): Username of the user that last modified the instance + """ + from nautobot.extras.choices import ObjectChangeActionChoices + from nautobot.extras.models import ObjectChange + + object_change_records = get_changes_for_model(instance) + created_by = None + last_updated_by = None + try: + created_by_record = object_change_records.get(action=ObjectChangeActionChoices.ACTION_CREATE) + created_by = created_by_record.user_name + except ObjectChange.DoesNotExist: + pass + + last_updated_by_record = object_change_records.first() + if last_updated_by_record: + last_updated_by = last_updated_by_record.user_name + + return created_by, last_updated_by + + @functools.total_ordering class _NautobotVersion: """Utility for comparing Nautobot versions.""" From 06550d96682912867464c6870704beacffaaf0a8 Mon Sep 17 00:00:00 2001 From: Christian Adell Date: Mon, 8 Apr 2024 10:56:01 +0200 Subject: [PATCH 10/29] feat: add last time the design jobs were synced --- nautobot_design_builder/tables.py | 3 ++- .../templates/nautobot_design_builder/design_retrieve.html | 4 ++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/nautobot_design_builder/tables.py b/nautobot_design_builder/tables.py index 483a573d..5b83624c 100644 --- a/nautobot_design_builder/tables.py +++ b/nautobot_design_builder/tables.py @@ -21,12 +21,13 @@ class DesignTable(BaseTable): name = Column(linkify=True) instance_count = Column(linkify=True, accessor=Accessor("instance_count"), verbose_name="Instances") actions = ButtonsColumn(Design, buttons=("changelog",), prepend_template=DESIGNTABLE) + job_last_synced = Column(accessor="job.last_updated", verbose_name="Job Last Synced Time") class Meta(BaseTable.Meta): # pylint: disable=too-few-public-methods """Meta attributes.""" model = Design - fields = ("name", "version", "job", "instance_count") + fields = ("name", "version", "job", "job_last_synced", "instance_count") DESIGNINSTANCETABLE = """ diff --git a/nautobot_design_builder/templates/nautobot_design_builder/design_retrieve.html b/nautobot_design_builder/templates/nautobot_design_builder/design_retrieve.html index 154e2eaf..7f3274dc 100644 --- a/nautobot_design_builder/templates/nautobot_design_builder/design_retrieve.html +++ b/nautobot_design_builder/templates/nautobot_design_builder/design_retrieve.html @@ -17,6 +17,10 @@ Job {{ object.job|hyperlinked_object }} + + Job Last Synced + {{ object.job.last_updated }} + Version {{ object.version }} From 45aef4f86a8b40707bc94be568f43fa356a1b945 Mon Sep 17 00:00:00 2001 From: Christian Adell Date: Mon, 8 Apr 2024 11:59:46 +0200 Subject: [PATCH 11/29] feat: add and optional description metadata for designjobs --- .../custom_design/designs/initial_data/jobs.py | 1 + .../migrations/0007_design_description.py | 18 ++++++++++++++++++ nautobot_design_builder/models.py | 1 + nautobot_design_builder/signals.py | 7 ++++++- nautobot_design_builder/tables.py | 2 +- .../design_retrieve.html | 4 ++++ 6 files changed, 31 insertions(+), 2 deletions(-) create mode 100644 nautobot_design_builder/migrations/0007_design_description.py diff --git a/examples/custom_design/designs/initial_data/jobs.py b/examples/custom_design/designs/initial_data/jobs.py index 60679fab..6b41bea0 100644 --- a/examples/custom_design/designs/initial_data/jobs.py +++ b/examples/custom_design/designs/initial_data/jobs.py @@ -20,3 +20,4 @@ class Meta: design_file = "designs/0001_design.yaml.j2" context_class = InitialDesignContext version = "1.0.0" + description = "It establishes the devices and site information for four sites: IAD5, LGA1, LAX11, SEA11." diff --git a/nautobot_design_builder/migrations/0007_design_description.py b/nautobot_design_builder/migrations/0007_design_description.py new file mode 100644 index 00000000..de07ef0a --- /dev/null +++ b/nautobot_design_builder/migrations/0007_design_description.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.20 on 2024-04-08 09:26 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("nautobot_design_builder", "0006_remove_designinstance_owner"), + ] + + operations = [ + migrations.AddField( + model_name="design", + name="description", + field=models.CharField(default="Not defined", max_length=255), + preserve_default=False, + ), + ] diff --git a/nautobot_design_builder/models.py b/nautobot_design_builder/models.py index 8d70d23f..91f3b280 100644 --- a/nautobot_design_builder/models.py +++ b/nautobot_design_builder/models.py @@ -107,6 +107,7 @@ class Design(PrimaryModel): # TODO: Add a template mapping to get custom payload (future feature) job = models.ForeignKey(to=JobModel, on_delete=models.PROTECT, editable=False) version = models.CharField(max_length=20) + description = models.CharField(max_length=255) objects = DesignQuerySet.as_manager() diff --git a/nautobot_design_builder/signals.py b/nautobot_design_builder/signals.py index b77f345f..c8a3aae9 100644 --- a/nautobot_design_builder/signals.py +++ b/nautobot_design_builder/signals.py @@ -67,8 +67,13 @@ def create_design_model(sender, instance: Job, **kwargs): # pylint:disable=unus instance (Job): Job instance that has been created or updated. """ if instance.job_class and issubclass(instance.job_class, DesignJob): + description = ( + instance.job_class.Meta.version if hasattr(instance.job_class.Meta, "description") else "Not defined" + ) version = instance.job_class.Meta.version if hasattr(instance.job_class.Meta, "version") else "Not defined" - _, created = Design.objects.get_or_create(job=instance, defaults={"version": version}) + _, created = Design.objects.get_or_create( + job=instance, defaults={"version": version, "description": description} + ) if created: _LOGGER.debug("Created design from %s", instance) diff --git a/nautobot_design_builder/tables.py b/nautobot_design_builder/tables.py index 5b83624c..c6abad85 100644 --- a/nautobot_design_builder/tables.py +++ b/nautobot_design_builder/tables.py @@ -27,7 +27,7 @@ class Meta(BaseTable.Meta): # pylint: disable=too-few-public-methods """Meta attributes.""" model = Design - fields = ("name", "version", "job", "job_last_synced", "instance_count") + fields = ("name", "version", "job", "job_last_synced", "description", "instance_count") DESIGNINSTANCETABLE = """ diff --git a/nautobot_design_builder/templates/nautobot_design_builder/design_retrieve.html b/nautobot_design_builder/templates/nautobot_design_builder/design_retrieve.html index 7f3274dc..6b86b358 100644 --- a/nautobot_design_builder/templates/nautobot_design_builder/design_retrieve.html +++ b/nautobot_design_builder/templates/nautobot_design_builder/design_retrieve.html @@ -25,6 +25,10 @@ Version {{ object.version }} + + Description + {{ object.description }} + {% endblock content_left_page %} From 34224f97d9b701e410567f4b3e5771be623fcc25 Mon Sep 17 00:00:00 2001 From: Christian Adell Date: Mon, 8 Apr 2024 15:15:29 +0200 Subject: [PATCH 12/29] refactor: remove direct access to Jounral and fix view from DesignInstance --- nautobot_design_builder/navigation.py | 6 ------ nautobot_design_builder/tables.py | 6 +++--- .../nautobot_design_builder/designinstance_retrieve.html | 4 ++-- nautobot_design_builder/views.py | 6 +++++- 4 files changed, 10 insertions(+), 12 deletions(-) diff --git a/nautobot_design_builder/navigation.py b/nautobot_design_builder/navigation.py index cf32081e..9810f744 100644 --- a/nautobot_design_builder/navigation.py +++ b/nautobot_design_builder/navigation.py @@ -28,12 +28,6 @@ permissions=["nautobot_design_builder.view_designinstance"], buttons=(), ), - NavMenuItem( - link="plugins:nautobot_design_builder:journal_list", - name="Journals", - permissions=["nautobot_design_builder.view_journal"], - buttons=(), - ), ), ), ), diff --git a/nautobot_design_builder/tables.py b/nautobot_design_builder/tables.py index c6abad85..68142e12 100644 --- a/nautobot_design_builder/tables.py +++ b/nautobot_design_builder/tables.py @@ -48,8 +48,8 @@ class DesignInstanceTable(StatusTableMixin, BaseTable): design = Column(linkify=True) first_implemented = Column(verbose_name="Deployment Time") last_implemented = Column(verbose_name="Last Update Time") - created_by = Column(accessor="get_created_by", verbose_name="Deployed by") - updated_by = Column(accessor="get_last_updated_by", verbose_name="Last Updated by") + created_by = Column(accessor=Accessor("get_created_by"), verbose_name="Deployed by") + updated_by = Column(accessor=Accessor("get_last_updated_by"), verbose_name="Last Updated by") live_state = ColoredLabelColumn(verbose_name="Operational State") actions = ButtonsColumn( DesignInstance, @@ -82,7 +82,7 @@ class JournalTable(BaseTable): pk = Column(linkify=True, verbose_name="ID") design_instance = Column(linkify=True) - job_result = Column(linkify=True) + job_result = Column(accessor=Accessor("job_result.created"), linkify=True, verbose_name="Design Job Result") journal_entry_count = Column(accessor=Accessor("journal_entry_count"), verbose_name="Journal Entries") active = BooleanColumn(verbose_name="Active Journal") diff --git a/nautobot_design_builder/templates/nautobot_design_builder/designinstance_retrieve.html b/nautobot_design_builder/templates/nautobot_design_builder/designinstance_retrieve.html index f24486bf..6f14a268 100644 --- a/nautobot_design_builder/templates/nautobot_design_builder/designinstance_retrieve.html +++ b/nautobot_design_builder/templates/nautobot_design_builder/designinstance_retrieve.html @@ -51,7 +51,7 @@ {% endblock content_left_page %} -{% block content_full_width_page %} +{% block content_right_page %} {% include 'utilities/obj_table.html' with table=journals_table table_template='panel_table.html' heading='Journals' %}
-{% endblock content_full_width_page %} +{% endblock content_right_page %} diff --git a/nautobot_design_builder/views.py b/nautobot_design_builder/views.py index 2650cf66..8af13a79 100644 --- a/nautobot_design_builder/views.py +++ b/nautobot_design_builder/views.py @@ -92,7 +92,11 @@ def get_extra_context(self, request, instance=None): """Extend UI.""" context = super().get_extra_context(request, instance) if self.action == "retrieve": - journals = Journal.objects.restrict(request.user, "view").filter(design_instance=instance) + journals = ( + Journal.objects.restrict(request.user, "view") + .filter(design_instance=instance) + .annotate(journal_entry_count=count_related(JournalEntry, "journal")) + ) journals_table = JournalTable(journals) journals_table.columns.hide("design_instance") From 60861987d68c0fd3f62d79080d65ada7be6400b3 Mon Sep 17 00:00:00 2001 From: Christian Adell Date: Mon, 8 Apr 2024 17:08:29 +0200 Subject: [PATCH 13/29] refactor: replace design instance by deployment without renaming the model --- nautobot_design_builder/filters.py | 2 +- nautobot_design_builder/jobs.py | 4 ++-- nautobot_design_builder/models.py | 2 ++ nautobot_design_builder/navigation.py | 2 +- nautobot_design_builder/tables.py | 4 ++-- .../templates/nautobot_design_builder/design_retrieve.html | 2 +- .../nautobot_design_builder/designinstance_retrieve.html | 2 +- nautobot_design_builder/views.py | 2 ++ 8 files changed, 12 insertions(+), 8 deletions(-) diff --git a/nautobot_design_builder/filters.py b/nautobot_design_builder/filters.py index 590a9ec2..9acbeba2 100644 --- a/nautobot_design_builder/filters.py +++ b/nautobot_design_builder/filters.py @@ -59,7 +59,7 @@ class JournalFilterSet(NautobotFilterSet): design_instance = NaturalKeyOrPKMultipleChoiceFilter( queryset=DesignInstance.objects.all(), - label="Design Instance (ID)", + label="Design Deployment (ID)", ) job_result = NaturalKeyOrPKMultipleChoiceFilter( diff --git a/nautobot_design_builder/jobs.py b/nautobot_design_builder/jobs.py index 01660844..beecca2c 100644 --- a/nautobot_design_builder/jobs.py +++ b/nautobot_design_builder/jobs.py @@ -15,13 +15,13 @@ class DesignInstanceDecommissioning(Job): design_instances = MultiObjectVar( model=DesignInstance, query_params={"status": "active"}, - description="Design Instances to decommission.", + description="Design Deployments to decommission.", ) class Meta: # pylint: disable=too-few-public-methods """Meta class.""" - name = "Decommission Design Instances" + name = "Decommission Design Deployments" description = """Job to decommission one or many Design Instances from Nautobot.""" def run(self, data, commit): diff --git a/nautobot_design_builder/models.py b/nautobot_design_builder/models.py index 91f3b280..a3b7122f 100644 --- a/nautobot_design_builder/models.py +++ b/nautobot_design_builder/models.py @@ -187,6 +187,8 @@ class Meta: unique_together = [ ("design", "name"), ] + verbose_name = "Design Deployment" + verbose_name_plural = "Design Deployments" def clean(self): """Guarantee that the design field cannot be changed.""" diff --git a/nautobot_design_builder/navigation.py b/nautobot_design_builder/navigation.py index 9810f744..d8061a53 100644 --- a/nautobot_design_builder/navigation.py +++ b/nautobot_design_builder/navigation.py @@ -24,7 +24,7 @@ ), NavMenuItem( link="plugins:nautobot_design_builder:designinstance_list", - name="Design Instances", + name="Design Deployments", permissions=["nautobot_design_builder.view_designinstance"], buttons=(), ), diff --git a/nautobot_design_builder/tables.py b/nautobot_design_builder/tables.py index 68142e12..4d250a37 100644 --- a/nautobot_design_builder/tables.py +++ b/nautobot_design_builder/tables.py @@ -19,7 +19,7 @@ class DesignTable(BaseTable): job = Column(linkify=True) name = Column(linkify=True) - instance_count = Column(linkify=True, accessor=Accessor("instance_count"), verbose_name="Instances") + instance_count = Column(linkify=True, accessor=Accessor("instance_count"), verbose_name="Deployments") actions = ButtonsColumn(Design, buttons=("changelog",), prepend_template=DESIGNTABLE) job_last_synced = Column(accessor="job.last_updated", verbose_name="Job Last Synced Time") @@ -81,7 +81,7 @@ class JournalTable(BaseTable): """Table for list view.""" pk = Column(linkify=True, verbose_name="ID") - design_instance = Column(linkify=True) + design_instance = Column(linkify=True, verbose_name="Deployment") job_result = Column(accessor=Accessor("job_result.created"), linkify=True, verbose_name="Design Job Result") journal_entry_count = Column(accessor=Accessor("journal_entry_count"), verbose_name="Journal Entries") active = BooleanColumn(verbose_name="Active Journal") diff --git a/nautobot_design_builder/templates/nautobot_design_builder/design_retrieve.html b/nautobot_design_builder/templates/nautobot_design_builder/design_retrieve.html index 6b86b358..11455519 100644 --- a/nautobot_design_builder/templates/nautobot_design_builder/design_retrieve.html +++ b/nautobot_design_builder/templates/nautobot_design_builder/design_retrieve.html @@ -34,6 +34,6 @@ {% endblock content_left_page %} {% block content_full_width_page %} -{% include 'utilities/obj_table.html' with table=instances_table table_template='panel_table.html' heading='Instances' %} +{% include 'utilities/obj_table.html' with table=instances_table table_template='panel_table.html' heading='Design Deployments' %}
{% endblock content_full_width_page %} diff --git a/nautobot_design_builder/templates/nautobot_design_builder/designinstance_retrieve.html b/nautobot_design_builder/templates/nautobot_design_builder/designinstance_retrieve.html index 6f14a268..016b49c3 100644 --- a/nautobot_design_builder/templates/nautobot_design_builder/designinstance_retrieve.html +++ b/nautobot_design_builder/templates/nautobot_design_builder/designinstance_retrieve.html @@ -4,7 +4,7 @@ {% block content_left_page %}
- Design Instance + Design Deployment
diff --git a/nautobot_design_builder/views.py b/nautobot_design_builder/views.py index 8af13a79..c046e686 100644 --- a/nautobot_design_builder/views.py +++ b/nautobot_design_builder/views.py @@ -87,6 +87,8 @@ class DesignInstanceUIViewSet( # pylint:disable=abstract-method table_class = DesignInstanceTable action_buttons = () lookup_field = "pk" + verbose_name = "Design Deployment" + verbose_name_plural = "Design Deployments" def get_extra_context(self, request, instance=None): """Extend UI.""" From 645ff072a3b2d787d0ebcc75dff63e7c233aa8a8 Mon Sep 17 00:00:00 2001 From: Christian Adell Date: Tue, 9 Apr 2024 15:44:08 +0200 Subject: [PATCH 14/29] feat: add support for docs per design --- .../designs/initial_data/jobs.py | 8 +++ .../migrations/0008_auto_20240409_1315.py | 31 +++++++++ nautobot_design_builder/models.py | 6 +- nautobot_design_builder/signals.py | 16 +++-- nautobot_design_builder/tables.py | 6 +- .../nautobot_design_builder/design_list.html | 67 ++++++++++++++++++ .../design_retrieve.html | 68 +++++++++++-------- .../markdown_render.html | 23 +++++++ nautobot_design_builder/views.py | 23 ++++++- 9 files changed, 207 insertions(+), 41 deletions(-) create mode 100644 nautobot_design_builder/migrations/0008_auto_20240409_1315.py create mode 100644 nautobot_design_builder/templates/nautobot_design_builder/design_list.html create mode 100644 nautobot_design_builder/templates/nautobot_design_builder/markdown_render.html diff --git a/examples/custom_design/designs/initial_data/jobs.py b/examples/custom_design/designs/initial_data/jobs.py index 6b41bea0..d5ae3f1c 100644 --- a/examples/custom_design/designs/initial_data/jobs.py +++ b/examples/custom_design/designs/initial_data/jobs.py @@ -21,3 +21,11 @@ class Meta: context_class = InitialDesignContext version = "1.0.0" description = "It establishes the devices and site information for four sites: IAD5, LGA1, LAX11, SEA11." + docs = """This design creates the following objects in the source of truth to establish the initia network environment in four sites: IAD5, LGA1, LAX11, SEA11. + +These sites belong to the America region (and different subregions), and use Juniper PTX10016 devices. + +The user input data is: + - Number of devices per site (integer) + - The description for one of the regions (string) +""" diff --git a/nautobot_design_builder/migrations/0008_auto_20240409_1315.py b/nautobot_design_builder/migrations/0008_auto_20240409_1315.py new file mode 100644 index 00000000..017094ee --- /dev/null +++ b/nautobot_design_builder/migrations/0008_auto_20240409_1315.py @@ -0,0 +1,31 @@ +# Generated by Django 3.2.20 on 2024-04-09 13:15 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("nautobot_design_builder", "0007_design_description"), + ] + + operations = [ + migrations.AlterModelOptions( + name="designinstance", + options={"verbose_name": "Design Deployment", "verbose_name_plural": "Design Deployments"}, + ), + migrations.AddField( + model_name="design", + name="docs", + field=models.CharField(blank=True, default="", editable=False, max_length=4096), + ), + migrations.AlterField( + model_name="design", + name="description", + field=models.CharField(blank=True, default="", max_length=255), + ), + migrations.AlterField( + model_name="design", + name="version", + field=models.CharField(default="0.0.0", max_length=20), + ), + ] diff --git a/nautobot_design_builder/models.py b/nautobot_design_builder/models.py index a3b7122f..8932cef3 100644 --- a/nautobot_design_builder/models.py +++ b/nautobot_design_builder/models.py @@ -106,9 +106,9 @@ class Design(PrimaryModel): # TODO: Add saved graphql query (future feature) # TODO: Add a template mapping to get custom payload (future feature) job = models.ForeignKey(to=JobModel, on_delete=models.PROTECT, editable=False) - version = models.CharField(max_length=20) - description = models.CharField(max_length=255) - + version = models.CharField(max_length=20, default="0.0.0") + description = models.CharField(max_length=255, blank=True, default="") + docs = models.CharField(max_length=4096, blank=True, default="", editable=False) objects = DesignQuerySet.as_manager() class Meta: diff --git a/nautobot_design_builder/signals.py b/nautobot_design_builder/signals.py index c8a3aae9..2dde04b6 100644 --- a/nautobot_design_builder/signals.py +++ b/nautobot_design_builder/signals.py @@ -67,13 +67,15 @@ def create_design_model(sender, instance: Job, **kwargs): # pylint:disable=unus instance (Job): Job instance that has been created or updated. """ if instance.job_class and issubclass(instance.job_class, DesignJob): - description = ( - instance.job_class.Meta.version if hasattr(instance.job_class.Meta, "description") else "Not defined" - ) - version = instance.job_class.Meta.version if hasattr(instance.job_class.Meta, "version") else "Not defined" - _, created = Design.objects.get_or_create( - job=instance, defaults={"version": version, "description": description} - ) + default_data = {} + if hasattr(instance.job_class.Meta, "description"): + default_data["description"] = instance.job_class.Meta.description + if hasattr(instance.job_class.Meta, "version"): + default_data["version"] = instance.job_class.Meta.version + if hasattr(instance.job_class.Meta, "version"): + default_data["docs"] = instance.job_class.Meta.docs + + _, created = Design.objects.get_or_create(job=instance, defaults=default_data) if created: _LOGGER.debug("Created design from %s", instance) diff --git a/nautobot_design_builder/tables.py b/nautobot_design_builder/tables.py index 4d250a37..8f24debe 100644 --- a/nautobot_design_builder/tables.py +++ b/nautobot_design_builder/tables.py @@ -8,8 +8,12 @@ from nautobot_design_builder.models import Design, DesignInstance, Journal, JournalEntry DESIGNTABLE = """ + + + + - + """ diff --git a/nautobot_design_builder/templates/nautobot_design_builder/design_list.html b/nautobot_design_builder/templates/nautobot_design_builder/design_list.html new file mode 100644 index 00000000..e9d381f3 --- /dev/null +++ b/nautobot_design_builder/templates/nautobot_design_builder/design_list.html @@ -0,0 +1,67 @@ +{% extends 'generic/object_list.html' %} +{% load buttons %} +{% load static %} +{% load helpers %} + + +{% block content %} + {{ block.super }} + + + +{% endblock %} + +{% block javascript %} + + +{% endblock %} diff --git a/nautobot_design_builder/templates/nautobot_design_builder/design_retrieve.html b/nautobot_design_builder/templates/nautobot_design_builder/design_retrieve.html index 11455519..cb5324ee 100644 --- a/nautobot_design_builder/templates/nautobot_design_builder/design_retrieve.html +++ b/nautobot_design_builder/templates/nautobot_design_builder/design_retrieve.html @@ -2,37 +2,47 @@ {% load helpers %} {% block content_left_page %} -
-
- Design -
-
- - - - - - - - - - - - - - - - - - - - -
Status - {{ object.get_status_display }} -
Job{{ object.job|hyperlinked_object }}
Job Last Synced{{ object.job.last_updated }}
Version{{ object.version }}
Description{{ object.description }}
-
+
+
+ Design +
+ + + + + + + + + + + + + + + + + +
Job{{ object.job|hyperlinked_object }}
Job Last Synced{{ object.job.last_updated }}
Version{{ object.version }}
Description{{ object.description }}
+
{% endblock content_left_page %} +{% block content_right_page %} +
+
+ Documentation +
+ + + + +
+ {{ object.docs | render_markdown }} +
+
+ +{% endblock content_right_page %} + {% block content_full_width_page %} {% include 'utilities/obj_table.html' with table=instances_table table_template='panel_table.html' heading='Design Deployments' %}
diff --git a/nautobot_design_builder/templates/nautobot_design_builder/markdown_render.html b/nautobot_design_builder/templates/nautobot_design_builder/markdown_render.html new file mode 100644 index 00000000..dbab3b28 --- /dev/null +++ b/nautobot_design_builder/templates/nautobot_design_builder/markdown_render.html @@ -0,0 +1,23 @@ +{% load helpers %} +{% load static %} + + + + + + +

{{ design_name }} design

+ + +
+
+
{{ text_content | render_markdown }}
+
+
diff --git a/nautobot_design_builder/views.py b/nautobot_design_builder/views.py index c046e686..29aec7d6 100644 --- a/nautobot_design_builder/views.py +++ b/nautobot_design_builder/views.py @@ -2,6 +2,9 @@ from django_tables2 import RequestConfig from django.apps import apps as global_apps +from django.shortcuts import render + +from rest_framework.decorators import action from nautobot.core.views.mixins import ( ObjectDetailViewMixin, @@ -13,7 +16,7 @@ from nautobot.utilities.paginator import EnhancedPaginator, get_paginate_count from nautobot.utilities.utils import count_related from nautobot.core.views.generic import ObjectView - +from nautobot.core.views.mixins import PERMISSIONS_ACTION_MAP from nautobot_design_builder.api.serializers import ( DesignSerializer, @@ -37,6 +40,13 @@ from nautobot_design_builder.tables import DesignTable, DesignInstanceTable, JournalTable, JournalEntryTable +PERMISSIONS_ACTION_MAP.update( + { + "docs": "view", + } +) + + class DesignUIViewSet( # pylint:disable=abstract-method ObjectDetailViewMixin, ObjectListViewMixin, @@ -70,6 +80,17 @@ def get_extra_context(self, request, instance=None): context["instances_table"] = instances_table return context + @action(detail=True, methods=["get"]) + def docs(self, request, pk, *args, **kwargs): + """Additional action to handle docs.""" + design = Design.objects.get(pk=pk) + context = { + "design_name": design.name, + "is_modal": request.GET.get("modal"), # TODO: not sure what modal means + "text_content": design.docs, + } + return render(request, "nautobot_design_builder/markdown_render.html", context) + class DesignInstanceUIViewSet( # pylint:disable=abstract-method ObjectDetailViewMixin, From 37245d629596daaa25c847f1b99e672a26ce66d6 Mon Sep 17 00:00:00 2001 From: Christian Adell Date: Tue, 9 Apr 2024 16:28:17 +0200 Subject: [PATCH 15/29] fix: clean up Tag of desing instance after deletion --- nautobot_design_builder/signals.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/nautobot_design_builder/signals.py b/nautobot_design_builder/signals.py index 2dde04b6..cc932e47 100644 --- a/nautobot_design_builder/signals.py +++ b/nautobot_design_builder/signals.py @@ -5,14 +5,14 @@ from django.apps import apps from django.contrib.contenttypes.models import ContentType -from django.db.models.signals import post_save +from django.db.models.signals import post_save, post_delete from django.dispatch import receiver from django.conf import settings from django.db.models.signals import pre_delete from django.db.models import ProtectedError from nautobot.core.signals import nautobot_database_ready -from nautobot.extras.models import Job, Status +from nautobot.extras.models import Job, Status, Tag from nautobot.utilities.choices import ColorChoices from nautobot.extras.registry import registry from nautobot_design_builder.models import JournalEntry @@ -72,7 +72,7 @@ def create_design_model(sender, instance: Job, **kwargs): # pylint:disable=unus default_data["description"] = instance.job_class.Meta.description if hasattr(instance.job_class.Meta, "version"): default_data["version"] = instance.job_class.Meta.version - if hasattr(instance.job_class.Meta, "version"): + if hasattr(instance.job_class.Meta, "docs"): default_data["docs"] = instance.job_class.Meta.docs _, created = Design.objects.get_or_create(job=instance, defaults=default_data) @@ -113,3 +113,9 @@ def load_pre_delete_signals(): load_pre_delete_signals() + + +@receiver(signal=post_delete, sender=DesignInstance) +def handle_post_delete_design_instance(sender, instance, **kwargs): # pylint: disable: unused-argument + """Cleaning up the Tag created for a design instance.""" + Tag.objects.get(name=f"Managed by {instance}").delete() From 1777b066fc115adee6ae90ed6f1e97706a1bdf22 Mon Sep 17 00:00:00 2001 From: Christian Adell Date: Tue, 9 Apr 2024 18:04:01 +0200 Subject: [PATCH 16/29] feat: allow desing job edition from design table --- nautobot_design_builder/signals.py | 2 +- nautobot_design_builder/tables.py | 3 +++ nautobot_design_builder/views.py | 2 +- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/nautobot_design_builder/signals.py b/nautobot_design_builder/signals.py index cc932e47..0d43857c 100644 --- a/nautobot_design_builder/signals.py +++ b/nautobot_design_builder/signals.py @@ -116,6 +116,6 @@ def load_pre_delete_signals(): @receiver(signal=post_delete, sender=DesignInstance) -def handle_post_delete_design_instance(sender, instance, **kwargs): # pylint: disable: unused-argument +def handle_post_delete_design_instance(sender, instance, **kwargs): # pylint: disable=unused-argument """Cleaning up the Tag created for a design instance.""" Tag.objects.get(name=f"Managed by {instance}").delete() diff --git a/nautobot_design_builder/tables.py b/nautobot_design_builder/tables.py index 8f24debe..f497ad49 100644 --- a/nautobot_design_builder/tables.py +++ b/nautobot_design_builder/tables.py @@ -15,6 +15,9 @@ + + + """ diff --git a/nautobot_design_builder/views.py b/nautobot_design_builder/views.py index 29aec7d6..75a0d594 100644 --- a/nautobot_design_builder/views.py +++ b/nautobot_design_builder/views.py @@ -86,7 +86,7 @@ def docs(self, request, pk, *args, **kwargs): design = Design.objects.get(pk=pk) context = { "design_name": design.name, - "is_modal": request.GET.get("modal"), # TODO: not sure what modal means + "is_modal": request.GET.get("modal"), "text_content": design.docs, } return render(request, "nautobot_design_builder/markdown_render.html", context) From 5945177db2ab2f2417942974fff37c4d3e4fff2d Mon Sep 17 00:00:00 2001 From: Christian Adell Date: Wed, 10 Apr 2024 09:17:54 +0200 Subject: [PATCH 17/29] ci: fix duplicated middleware addition --- development/nautobot_config.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/development/nautobot_config.py b/development/nautobot_config.py index b04a2f43..91126021 100644 --- a/development/nautobot_config.py +++ b/development/nautobot_config.py @@ -25,7 +25,8 @@ if "debug_toolbar.middleware.DebugToolbarMiddleware" not in MIDDLEWARE: # noqa: F405 MIDDLEWARE.insert(0, "debug_toolbar.middleware.DebugToolbarMiddleware") # noqa: F405 -MIDDLEWARE.insert(0, "nautobot_design_builder.middleware.GlobalRequestMiddleware") # noqa: F405 +if "nautobot_design_builder.middleware.GlobalRequestMiddleware" not in MIDDLEWARE: # noqa: F405 + MIDDLEWARE.insert(0, "nautobot_design_builder.middleware.GlobalRequestMiddleware") # noqa: F405 # # Misc. settings From ab967a1657f6a4710fd7fd4b0b021551a8612e28 Mon Sep 17 00:00:00 2001 From: Christian Adell Date: Wed, 10 Apr 2024 09:18:19 +0200 Subject: [PATCH 18/29] fix: use the last used journal input data --- nautobot_design_builder/tables.py | 3 ++- nautobot_design_builder/templatetags/__init__.py | 0 nautobot_design_builder/templatetags/utils.py | 14 ++++++++++++++ 3 files changed, 16 insertions(+), 1 deletion(-) create mode 100644 nautobot_design_builder/templatetags/__init__.py create mode 100644 nautobot_design_builder/templatetags/utils.py diff --git a/nautobot_design_builder/tables.py b/nautobot_design_builder/tables.py index f497ad49..3dfd840e 100644 --- a/nautobot_design_builder/tables.py +++ b/nautobot_design_builder/tables.py @@ -38,10 +38,11 @@ class Meta(BaseTable.Meta): # pylint: disable=too-few-public-methods DESIGNINSTANCETABLE = """ +{% load utils %} - diff --git a/nautobot_design_builder/templatetags/__init__.py b/nautobot_design_builder/templatetags/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/nautobot_design_builder/templatetags/utils.py b/nautobot_design_builder/templatetags/utils.py new file mode 100644 index 00000000..113cef7b --- /dev/null +++ b/nautobot_design_builder/templatetags/utils.py @@ -0,0 +1,14 @@ +"""Jinja filters for design_builder.""" + +from django import template +from django_jinja import library + + +register = template.Library() + + +@library.filter() +@register.filter() +def get_last_journal(design_instance): + """Get last run journal in a design instance.""" + return design_instance.journals.order_by("created").last() From 6ab74400bb5d74b54d079fdbf51c6ceeffdfafae Mon Sep 17 00:00:00 2001 From: Christian Adell Date: Wed, 10 Apr 2024 09:22:05 +0200 Subject: [PATCH 19/29] chore: remove nonrelevant comment --- nautobot_design_builder/design.py | 1 - 1 file changed, 1 deletion(-) diff --git a/nautobot_design_builder/design.py b/nautobot_design_builder/design.py index b9c67bdf..6dd5724e 100644 --- a/nautobot_design_builder/design.py +++ b/nautobot_design_builder/design.py @@ -617,7 +617,6 @@ def get_extension(self, ext_type: str, tag: str) -> ext.Extension: extn["object"] = extn["class"](self) return extn["object"] - # TODO: this is a breaking change that needs to be revisited because it's used by Django commands directly @transaction.atomic def implement_design_changes(self, design: Dict, deprecated_design: Dict, design_file: str, commit: bool = False): """Iterates through items in the design and creates them. From e93470eefd4c81d5c8a8a20490a8d9c551b80c0a Mon Sep 17 00:00:00 2001 From: Christian Adell Date: Wed, 10 Apr 2024 09:32:38 +0200 Subject: [PATCH 20/29] feat: support design deletion --- nautobot_design_builder/tables.py | 2 +- nautobot_design_builder/views.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/nautobot_design_builder/tables.py b/nautobot_design_builder/tables.py index 3dfd840e..27ea9c44 100644 --- a/nautobot_design_builder/tables.py +++ b/nautobot_design_builder/tables.py @@ -27,7 +27,7 @@ class DesignTable(BaseTable): job = Column(linkify=True) name = Column(linkify=True) instance_count = Column(linkify=True, accessor=Accessor("instance_count"), verbose_name="Deployments") - actions = ButtonsColumn(Design, buttons=("changelog",), prepend_template=DESIGNTABLE) + actions = ButtonsColumn(Design, buttons=("changelog", "delete"), prepend_template=DESIGNTABLE) job_last_synced = Column(accessor="job.last_updated", verbose_name="Job Last Synced Time") class Meta(BaseTable.Meta): # pylint: disable=too-few-public-methods diff --git a/nautobot_design_builder/views.py b/nautobot_design_builder/views.py index 75a0d594..c71e75ec 100644 --- a/nautobot_design_builder/views.py +++ b/nautobot_design_builder/views.py @@ -52,6 +52,7 @@ class DesignUIViewSet( # pylint:disable=abstract-method ObjectListViewMixin, ObjectChangeLogViewMixin, ObjectNotesViewMixin, + ObjectDestroyViewMixin, ): """UI views for the design model.""" From 13dc7dc728254ed82b2487a2422891b9baac1eba Mon Sep 17 00:00:00 2001 From: Christian Adell Date: Wed, 10 Apr 2024 09:34:37 +0200 Subject: [PATCH 21/29] chore: squash migrations --- .../migrations/0005_auto_20240405_0938.py | 24 ----------- .../migrations/0005_auto_20240410_0734.py | 42 +++++++++++++++++++ .../0006_remove_designinstance_owner.py | 16 ------- .../migrations/0007_design_description.py | 18 -------- .../migrations/0008_auto_20240409_1315.py | 31 -------------- 5 files changed, 42 insertions(+), 89 deletions(-) delete mode 100644 nautobot_design_builder/migrations/0005_auto_20240405_0938.py create mode 100644 nautobot_design_builder/migrations/0005_auto_20240410_0734.py delete mode 100644 nautobot_design_builder/migrations/0006_remove_designinstance_owner.py delete mode 100644 nautobot_design_builder/migrations/0007_design_description.py delete mode 100644 nautobot_design_builder/migrations/0008_auto_20240409_1315.py diff --git a/nautobot_design_builder/migrations/0005_auto_20240405_0938.py b/nautobot_design_builder/migrations/0005_auto_20240405_0938.py deleted file mode 100644 index d5bd9518..00000000 --- a/nautobot_design_builder/migrations/0005_auto_20240405_0938.py +++ /dev/null @@ -1,24 +0,0 @@ -# Generated by Django 3.2.20 on 2024-04-05 09:38 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - dependencies = [ - ("nautobot_design_builder", "0004_support_update_design"), - ] - - operations = [ - migrations.AddField( - model_name="design", - name="version", - field=models.CharField(default="0.0.1", max_length=20), - preserve_default=False, - ), - migrations.AddField( - model_name="designinstance", - name="version", - field=models.CharField(default="0.0.1", max_length=20), - preserve_default=False, - ), - ] diff --git a/nautobot_design_builder/migrations/0005_auto_20240410_0734.py b/nautobot_design_builder/migrations/0005_auto_20240410_0734.py new file mode 100644 index 00000000..c2462e17 --- /dev/null +++ b/nautobot_design_builder/migrations/0005_auto_20240410_0734.py @@ -0,0 +1,42 @@ +# Generated by Django 3.2.20 on 2024-04-10 07:34 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('nautobot_design_builder', '0004_support_update_design'), + ] + + operations = [ + migrations.AlterModelOptions( + name='designinstance', + options={'verbose_name': 'Design Deployment', 'verbose_name_plural': 'Design Deployments'}, + ), + migrations.RemoveField( + model_name='designinstance', + name='owner', + ), + migrations.AddField( + model_name='design', + name='description', + field=models.CharField(blank=True, default='', max_length=255), + ), + migrations.AddField( + model_name='design', + name='docs', + field=models.CharField(blank=True, default='', editable=False, max_length=4096), + ), + migrations.AddField( + model_name='design', + name='version', + field=models.CharField(default='0.0.0', max_length=20), + ), + migrations.AddField( + model_name='designinstance', + name='version', + field=models.CharField(default='0.0.0', max_length=20), + preserve_default=False, + ), + ] diff --git a/nautobot_design_builder/migrations/0006_remove_designinstance_owner.py b/nautobot_design_builder/migrations/0006_remove_designinstance_owner.py deleted file mode 100644 index 5c85486f..00000000 --- a/nautobot_design_builder/migrations/0006_remove_designinstance_owner.py +++ /dev/null @@ -1,16 +0,0 @@ -# Generated by Django 3.2.20 on 2024-04-08 07:15 - -from django.db import migrations - - -class Migration(migrations.Migration): - dependencies = [ - ("nautobot_design_builder", "0005_auto_20240405_0938"), - ] - - operations = [ - migrations.RemoveField( - model_name="designinstance", - name="owner", - ), - ] diff --git a/nautobot_design_builder/migrations/0007_design_description.py b/nautobot_design_builder/migrations/0007_design_description.py deleted file mode 100644 index de07ef0a..00000000 --- a/nautobot_design_builder/migrations/0007_design_description.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 3.2.20 on 2024-04-08 09:26 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - dependencies = [ - ("nautobot_design_builder", "0006_remove_designinstance_owner"), - ] - - operations = [ - migrations.AddField( - model_name="design", - name="description", - field=models.CharField(default="Not defined", max_length=255), - preserve_default=False, - ), - ] diff --git a/nautobot_design_builder/migrations/0008_auto_20240409_1315.py b/nautobot_design_builder/migrations/0008_auto_20240409_1315.py deleted file mode 100644 index 017094ee..00000000 --- a/nautobot_design_builder/migrations/0008_auto_20240409_1315.py +++ /dev/null @@ -1,31 +0,0 @@ -# Generated by Django 3.2.20 on 2024-04-09 13:15 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - dependencies = [ - ("nautobot_design_builder", "0007_design_description"), - ] - - operations = [ - migrations.AlterModelOptions( - name="designinstance", - options={"verbose_name": "Design Deployment", "verbose_name_plural": "Design Deployments"}, - ), - migrations.AddField( - model_name="design", - name="docs", - field=models.CharField(blank=True, default="", editable=False, max_length=4096), - ), - migrations.AlterField( - model_name="design", - name="description", - field=models.CharField(blank=True, default="", max_length=255), - ), - migrations.AlterField( - model_name="design", - name="version", - field=models.CharField(default="0.0.0", max_length=20), - ), - ] From f7e4da1d9224ad755f938c7d43ed064bbe7330d3 Mon Sep 17 00:00:00 2001 From: Christian Adell Date: Wed, 10 Apr 2024 09:36:00 +0200 Subject: [PATCH 22/29] fix: black for migrations --- .../migrations/0005_auto_20240410_0734.py | 35 +++++++++---------- 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/nautobot_design_builder/migrations/0005_auto_20240410_0734.py b/nautobot_design_builder/migrations/0005_auto_20240410_0734.py index c2462e17..d5d9fd70 100644 --- a/nautobot_design_builder/migrations/0005_auto_20240410_0734.py +++ b/nautobot_design_builder/migrations/0005_auto_20240410_0734.py @@ -4,39 +4,38 @@ class Migration(migrations.Migration): - dependencies = [ - ('nautobot_design_builder', '0004_support_update_design'), + ("nautobot_design_builder", "0004_support_update_design"), ] operations = [ migrations.AlterModelOptions( - name='designinstance', - options={'verbose_name': 'Design Deployment', 'verbose_name_plural': 'Design Deployments'}, + name="designinstance", + options={"verbose_name": "Design Deployment", "verbose_name_plural": "Design Deployments"}, ), migrations.RemoveField( - model_name='designinstance', - name='owner', + model_name="designinstance", + name="owner", ), migrations.AddField( - model_name='design', - name='description', - field=models.CharField(blank=True, default='', max_length=255), + model_name="design", + name="description", + field=models.CharField(blank=True, default="", max_length=255), ), migrations.AddField( - model_name='design', - name='docs', - field=models.CharField(blank=True, default='', editable=False, max_length=4096), + model_name="design", + name="docs", + field=models.CharField(blank=True, default="", editable=False, max_length=4096), ), migrations.AddField( - model_name='design', - name='version', - field=models.CharField(default='0.0.0', max_length=20), + model_name="design", + name="version", + field=models.CharField(default="0.0.0", max_length=20), ), migrations.AddField( - model_name='designinstance', - name='version', - field=models.CharField(default='0.0.0', max_length=20), + model_name="designinstance", + name="version", + field=models.CharField(default="0.0.0", max_length=20), preserve_default=False, ), ] From 45273c7b80697205d279efe70c91104ae4fea51f Mon Sep 17 00:00:00 2001 From: Christian Adell Date: Wed, 10 Apr 2024 09:58:37 +0200 Subject: [PATCH 23/29] feat: small improvements --- nautobot_design_builder/jobs.py | 2 +- nautobot_design_builder/tables.py | 7 +++---- nautobot_design_builder/templatetags/utils.py | 2 +- nautobot_design_builder/views.py | 1 + 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/nautobot_design_builder/jobs.py b/nautobot_design_builder/jobs.py index beecca2c..8e9952be 100644 --- a/nautobot_design_builder/jobs.py +++ b/nautobot_design_builder/jobs.py @@ -22,7 +22,7 @@ class Meta: # pylint: disable=too-few-public-methods """Meta class.""" name = "Decommission Design Deployments" - description = """Job to decommission one or many Design Instances from Nautobot.""" + description = """Job to decommission one or many Design Deployments from Nautobot.""" def run(self, data, commit): """Execute Decommissioning job.""" diff --git a/nautobot_design_builder/tables.py b/nautobot_design_builder/tables.py index 27ea9c44..c5fa3793 100644 --- a/nautobot_design_builder/tables.py +++ b/nautobot_design_builder/tables.py @@ -15,7 +15,7 @@ - + """ @@ -24,17 +24,16 @@ class DesignTable(BaseTable): """Table for list view.""" - job = Column(linkify=True) name = Column(linkify=True) instance_count = Column(linkify=True, accessor=Accessor("instance_count"), verbose_name="Deployments") actions = ButtonsColumn(Design, buttons=("changelog", "delete"), prepend_template=DESIGNTABLE) - job_last_synced = Column(accessor="job.last_updated", verbose_name="Job Last Synced Time") + job_last_synced = Column(accessor="job.last_updated", verbose_name="Last Synced Time") class Meta(BaseTable.Meta): # pylint: disable=too-few-public-methods """Meta attributes.""" model = Design - fields = ("name", "version", "job", "job_last_synced", "description", "instance_count") + fields = ("name", "version", "job_last_synced", "description", "instance_count") DESIGNINSTANCETABLE = """ diff --git a/nautobot_design_builder/templatetags/utils.py b/nautobot_design_builder/templatetags/utils.py index 113cef7b..a41f3252 100644 --- a/nautobot_design_builder/templatetags/utils.py +++ b/nautobot_design_builder/templatetags/utils.py @@ -11,4 +11,4 @@ @register.filter() def get_last_journal(design_instance): """Get last run journal in a design instance.""" - return design_instance.journals.order_by("created").last() + return design_instance.journals.order_by("last_updated").last() diff --git a/nautobot_design_builder/views.py b/nautobot_design_builder/views.py index c71e75ec..1f6a096e 100644 --- a/nautobot_design_builder/views.py +++ b/nautobot_design_builder/views.py @@ -119,6 +119,7 @@ def get_extra_context(self, request, instance=None): journals = ( Journal.objects.restrict(request.user, "view") .filter(design_instance=instance) + .order_by("last_updated") .annotate(journal_entry_count=count_related(JournalEntry, "journal")) ) From 130f6749a5ca90ccbe0500b505fb3f6abf5a6fb9 Mon Sep 17 00:00:00 2001 From: Christian Adell Date: Thu, 11 Apr 2024 10:30:22 +0200 Subject: [PATCH 24/29] docs: improve docs --- README.md | 2 + docs/user/app_getting_started.md | 2 + docs/user/app_overview.md | 5 ++- docs/user/design_development.md | 50 ++++++------------------ docs/user/design_lifecycle.md | 67 ++++++++++++++++++++++++++++++++ docs/user/design_quickstart.md | 2 + mkdocs.yml | 1 + 7 files changed, 90 insertions(+), 39 deletions(-) create mode 100644 docs/user/design_lifecycle.md diff --git a/README.md b/README.md index 86cd5e80..0883879d 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,8 @@ Design Builder is a Nautobot application for easily populating data within Nautobot using standardized design files. These design files are just Jinja templates that describe the Nautobot objects to be created or updated. +It also introduces the concept of a design-oriented Source of Truth with a complete lifecycle management of the design deployments (i.e., an instantiation of a design with concrete input data). With this approach, the users of the application can not only create (or populate) data within Nautobot but also update or decommission it while enforcing data protection and dependency. + ## Documentation Full documentation for this App can be found over on the [Nautobot Docs](https://docs.nautobot.com) website: diff --git a/docs/user/app_getting_started.md b/docs/user/app_getting_started.md index 4c6dccc3..8e0aa8b3 100644 --- a/docs/user/app_getting_started.md +++ b/docs/user/app_getting_started.md @@ -12,6 +12,8 @@ The easiest way to experience Design Builder is to either add the [demo-designs] ## What are the next steps? + + The Design Builder demo designs ship with some sample designs to demonstrate capabilities. Once the application stack is ready, you should have several jobs listed under the "Jobs" -> "Jobs" menu item. ![Jobs list](../images/screenshots/sample-design-jobs-list.png) diff --git a/docs/user/app_overview.md b/docs/user/app_overview.md index d7f79768..c1b31c2c 100644 --- a/docs/user/app_overview.md +++ b/docs/user/app_overview.md @@ -7,12 +7,15 @@ This document provides an overview of the App including critical information and ## Description -Design Builder provides a system where standardized network designs can be developed to produce collections of objects within Nautobot. These designs are text based templates that can create and update hierarchical data structures within Nautobot. +Design Builder provides a system where standardized network designs can be developed to produce or update collections of objects within Nautobot. These designs are text based templates that can create and update hierarchical data structures within Nautobot. + +The deployment of a design comes with a complete lifecycle management of all the changes connected as a single entity. Thus, the design deployment can be updated or decommissioned after its creation, and the all the changes introduced can be honored when accessing the data outside of the design builder app. ## Audience (User Personas) - Who should use this App? - Network engineers who want to have reproducible sets of Nautobot objects based on some standard design. - Automation engineers who want to be able to automate the creation of Nautobot objects based on a set of standard designs. +- Users who want to leverage abstracted network services defined by network engineers in a simplfied way. ## Authors and Maintainers diff --git a/docs/user/design_development.md b/docs/user/design_development.md index 19bb8c9e..f6d0cc71 100644 --- a/docs/user/design_development.md +++ b/docs/user/design_development.md @@ -114,6 +114,18 @@ The value of the `context_class` metadata attribute should be any Python class t This attribute is optional. A report is a Jinja template that is rendered once the design has been implemented. Like `design_file` the design builder will look for this template relative to the filename that defines the design job. This is helpful to generate a custom view of the data that was built during the design build. +### `version` + +It's an optional string attribute that is used to define the versioning reference of a design job. This will enable in the future the versioning lifecycle of design deployments. For example, one a design evolves from one version to another, the design deployment will be able to accommodate the new changes. + +### `description` + +It's an optional string attribute that will be used in the `Job` / `Design` to provide a high-level overview of the intend of the design job. + +### `docs` + +It's an optional string, in markdown format, that will be added to the `Design` to provide more detailed information than the one from the description. This should help the users of the `Design` to understand the goal of the design and the impact of the input data. + ## Design Context Primary Purpose: @@ -339,41 +351,3 @@ class DesignJobWithExtensions(DesignJob): extensions = [ext.BGPPeeringExtension] ``` -## Design LifeCycle - -Design implementations can have a full life cycle: creation, update, and decommission. - - - -Once a design is "deployed" in Nautobot, a Design Instance is created with the report of the changes implemented, and with actions to decommission or update it. - -### Design Decommission - -This feature allows to rollback all the changes implemented by a design instance to the previous state. This rollback depends on the scope of the change: - -- If the object was created by the design implementation, this object will be removed. -- If only some attributes were changes, the affected attributes will be rolled back to the previous state. - -The decommissioning feature takes into account potential dependencies between design implementations. For example, if a new l3vpn design depends on devices that were created by another design, this previous design won't be decommissioned until the l3vpn dependencies are also decommissioned to warrant consistency. - -Once a design instance is decommissioned, it's still visible in the API/UI to check the history of changes but without any active relationship with Nautobot objects. After decommissioning, the design instance can be deleted completely from Nautobot. - -### Design Updates - -This feature allows to re run a design instance with different input data to update the implemented design with the new changes: additions and removals. - -It leverages a complete tracking of previous design implementation and a reduce function for the new design to understand the changes to be implemented and the objects to be decommissioned (leveraging the previous decommissioning feature for only a specific object). - -The update feature comes with a few assumptions: - -- All the design objects that have an identifier have to use identifier keys to identify the object to make them comparable across designs. -- Object identifiers should keep consistent in multiple design runs. For example, you can't target a device with the device name and update the name on the same design. -- When design provides a list of objects, the objects are assumed to be in the same order. For example, if the first design creates `[deviceA1, deviceB1]`, if expanded, it should be `[deviceA1, deviceB1, deviceA2, deviceB2]`, not `[deviceA1, deviceA2, deviceB1, deviceB2]`. - - diff --git a/docs/user/design_lifecycle.md b/docs/user/design_lifecycle.md new file mode 100644 index 00000000..79ea94c0 --- /dev/null +++ b/docs/user/design_lifecycle.md @@ -0,0 +1,67 @@ +# Design LifeCycle + + + +According to a design-oriented approach, the Design Builder App provides not only with the capacity to create and update data in Nautobot but also with a complete lifecycle management of each deployment: update, versioning (in the future), and decommissioning. + + + +All the Design Builder UI navigation menus are under the Design Builder tab. + +## `Design` + +A `Design` is a one to one mapping with a Nautobot `Job`, enriched with some data from the Design Builder `DesignJob` definition. In concrete, it stores: + +- A `Job` reference. +- A `version` string from the `DesignJob`. +- A `description` string from the `DesignJob`. +- A `docs` string from the `DesignJob`. + + + +From the `Design`, the user can manage the associated `Job`, and trigger its execution to deploy it creating a `DesignInstance` or Design Deployment + +## Design Deployment or `DesignInstance` + +Once a design is "deployed" in Nautobot, a Design Deployment (or `DesignInstance`) is created with the report of the changes implemented (i.e. `Journals`), and with actions to update or decommissioning it (see next subsections). + +The `DesignInstance` stores: + +- The `name` of the deployment, within the context of the `Design`. +- The `Design` reference. +- The `version` from the `Design` when it was deployed or updated. +- When it was initially deployed or last updated. +- The `status` of the design, and the `live_state` or operational status to signal its state in the actual network. + + + +### Design Deployment Update + +This feature allows to re run a design instance with different input data to update the implemented design with the new changes: additions and removals. + +It leverages a complete tracking of previous design implementation and a reduce function for the new design to understand the changes to be implemented and the objects to be decommissioned (leveraging the previous decommissioning feature for only a specific object). + +The update feature comes with a few assumptions: + +- All the design objects that have an identifier have to use identifier keys to identify the object to make them comparable across designs. +- Object identifiers should keep consistent in multiple design runs. For example, you can't target a device with the device name and update the name on the same design. +- When design provides a list of objects, the objects are assumed to be in the same order. For example, if the first design creates `[deviceA1, deviceB1]`, if expanded, it should be `[deviceA1, deviceB1, deviceA2, deviceB2]`, not `[deviceA1, deviceA2, deviceB1, deviceB2]`. + + + +### Design Deployment Decommission + +This feature allows to rollback all the changes implemented by a design instance to the previous state. This rollback depends on the scope of the change: + +- If the object was created by the design implementation, this object will be removed. +- If only some attributes were changes, the affected attributes will be rolled back to the previous state. + +The decommissioning feature takes into account potential dependencies between design implementations. For example, if a new l3vpn design depends on devices that were created by another design, this previous design won't be decommissioned until the l3vpn dependencies are also decommissioned to warrant consistency. + +Once a design instance is decommissioned, it's still visible in the API/UI to check the history of changes but without any active relationship with Nautobot objects. After decommissioning, the design instance can be deleted completely from Nautobot. diff --git a/docs/user/design_quickstart.md b/docs/user/design_quickstart.md index 545556ef..fed93c42 100644 --- a/docs/user/design_quickstart.md +++ b/docs/user/design_quickstart.md @@ -10,6 +10,8 @@ To add a new design you will need (at a minimum) a class extending `nautobot_des For more information on creating designs see [Getting Started with Designs](design_development.md). +Once the designs are loaded, you can start managing them from the "Design Builder" navigation tab. + ## Sample Data Much of the time, designs will need some data to exist in Nautobot before they can be built. In a development and testing environment it is necessary to generate this data for testing purposes. The Design Builder application comes with a `load_design` management command that will read a design YAML file (not a template) and will build the design in Nautobot. This can be used to produce sample data for a development environment. Simply create a YAML file that includes all of the object definitions needed for testing and load the file with `invoke build-design `. This should read the file and build all of the objects within Nautobot. diff --git a/mkdocs.yml b/mkdocs.yml index 9c07e15c..8c0173d4 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -107,6 +107,7 @@ nav: - Getting Started: "user/app_getting_started.md" - Design Quick Start: "user/design_quickstart.md" - Design Development: "user/design_development.md" + - Design LifeCycle: "user/design_lifecycle.md" - Frequently Asked Questions: "user/faq.md" - Git-based Config Context: "user/git_config_context.md" - Administrator Guide: From a6fcf77dfa1c1bbd9a4d74ecc392a67580e642c9 Mon Sep 17 00:00:00 2001 From: Christian Adell Date: Fri, 12 Apr 2024 10:09:02 +0200 Subject: [PATCH 25/29] fix: used design deployment in templates --- .../templates/nautobot_design_builder/designprotection_tab.html | 2 +- .../templates/nautobot_design_builder/journal_retrieve.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/nautobot_design_builder/templates/nautobot_design_builder/designprotection_tab.html b/nautobot_design_builder/templates/nautobot_design_builder/designprotection_tab.html index 99e18d0c..b84d8b94 100644 --- a/nautobot_design_builder/templates/nautobot_design_builder/designprotection_tab.html +++ b/nautobot_design_builder/templates/nautobot_design_builder/designprotection_tab.html @@ -15,7 +15,7 @@ Attribute - Referencing Design Instance + Referencing Design Deployments diff --git a/nautobot_design_builder/templates/nautobot_design_builder/journal_retrieve.html b/nautobot_design_builder/templates/nautobot_design_builder/journal_retrieve.html index 0417ec50..645b50b8 100644 --- a/nautobot_design_builder/templates/nautobot_design_builder/journal_retrieve.html +++ b/nautobot_design_builder/templates/nautobot_design_builder/journal_retrieve.html @@ -12,7 +12,7 @@ {{ object.job_result|hyperlinked_object }} - Design Instance + Design Deployment {{ object.design_instance|hyperlinked_object }} From 7b561218ec4b9bd9ccc795e00fefdd3305ea6d1a Mon Sep 17 00:00:00 2001 From: Christian Adell Date: Mon, 15 Apr 2024 06:23:36 +0200 Subject: [PATCH 26/29] Apply suggestions from code review Co-authored-by: Andrew Bates --- docs/user/design_development.md | 4 +- docs/user/design_lifecycle.md | 10 ++-- .../designs/initial_data/jobs.py | 2 +- nautobot_design_builder/api/serializers.py | 8 --- nautobot_design_builder/models.py | 6 +- nautobot_design_builder/tables.py | 4 +- .../nautobot_design_builder/design_list.html | 58 ++++++++++--------- .../designinstance_retrieve.html | 4 +- 8 files changed, 46 insertions(+), 50 deletions(-) diff --git a/docs/user/design_development.md b/docs/user/design_development.md index f6d0cc71..6eac1c71 100644 --- a/docs/user/design_development.md +++ b/docs/user/design_development.md @@ -120,11 +120,11 @@ It's an optional string attribute that is used to define the versioning referenc ### `description` -It's an optional string attribute that will be used in the `Job` / `Design` to provide a high-level overview of the intend of the design job. +This optional attribute that is a string that provides a high-level overview of the intend of the design job. This description is displayed int the design detail view. ### `docs` -It's an optional string, in markdown format, that will be added to the `Design` to provide more detailed information than the one from the description. This should help the users of the `Design` to understand the goal of the design and the impact of the input data. +This attribute is also displayed on the design detail view. The `docs` attribute can utilize markdown format and should provide more detailed information than the description. This should help the users of the `Design` to understand the goal of the design and the impact of the input data. ## Design Context diff --git a/docs/user/design_lifecycle.md b/docs/user/design_lifecycle.md index 79ea94c0..2b5689fa 100644 --- a/docs/user/design_lifecycle.md +++ b/docs/user/design_lifecycle.md @@ -2,7 +2,7 @@ -According to a design-oriented approach, the Design Builder App provides not only with the capacity to create and update data in Nautobot but also with a complete lifecycle management of each deployment: update, versioning (in the future), and decommissioning. +According to a design-oriented approach, the Design Builder App provides not only the capacity to create and update data in Nautobot but also a complete lifecycle management of each deployment: update, versioning (in the future), and decommissioning. -From the `Design`, the user can manage the associated `Job`, and trigger its execution to deploy it creating a `DesignInstance` or Design Deployment +From the `Design`, the user can manage the associated `Job`, and trigger its execution, which creates a `DesignInstance` or Design Deployment ## Design Deployment or `DesignInstance` -Once a design is "deployed" in Nautobot, a Design Deployment (or `DesignInstance`) is created with the report of the changes implemented (i.e. `Journals`), and with actions to update or decommissioning it (see next subsections). +Once a design is "deployed" in Nautobot, a Design Deployment (or `DesignInstance`) is created with the report of the changes implemented (i.e. `Journals`), and with actions to update or decommission it (see next subsections). The `DesignInstance` stores: @@ -40,9 +40,9 @@ The `DesignInstance` stores: ### Design Deployment Update -This feature allows to re run a design instance with different input data to update the implemented design with the new changes: additions and removals. +This feature provides a means to re-run a design instance with different input data. Re-running the job will update the implemented design with the new changes: additions and removals. -It leverages a complete tracking of previous design implementation and a reduce function for the new design to understand the changes to be implemented and the objects to be decommissioned (leveraging the previous decommissioning feature for only a specific object). +It leverages a complete tracking of previous design implementations and a reduce function for the new design to understand the changes to be implemented and the objects to be decommissioned (leveraging the previous decommissioning feature for only a specific object). The update feature comes with a few assumptions: diff --git a/examples/custom_design/designs/initial_data/jobs.py b/examples/custom_design/designs/initial_data/jobs.py index d5ae3f1c..941719d4 100644 --- a/examples/custom_design/designs/initial_data/jobs.py +++ b/examples/custom_design/designs/initial_data/jobs.py @@ -20,7 +20,7 @@ class Meta: design_file = "designs/0001_design.yaml.j2" context_class = InitialDesignContext version = "1.0.0" - description = "It establishes the devices and site information for four sites: IAD5, LGA1, LAX11, SEA11." + description = "Establish the devices and site information for four sites: IAD5, LGA1, LAX11, SEA11." docs = """This design creates the following objects in the source of truth to establish the initia network environment in four sites: IAD5, LGA1, LAX11, SEA11. These sites belong to the America region (and different subregions), and use Juniper PTX10016 devices. diff --git a/nautobot_design_builder/api/serializers.py b/nautobot_design_builder/api/serializers.py index e033b410..2fd45329 100644 --- a/nautobot_design_builder/api/serializers.py +++ b/nautobot_design_builder/api/serializers.py @@ -60,14 +60,6 @@ class Meta: "live_state", ] - def get_created_by(self, instance): - """Get the username of the user who created the object.""" - return instance.get_created_by() - - def get_last_updated_by(self, instance): - """Get the username of the user who update the object last time.""" - return instance.get_last_updated_by() - class JournalSerializer(NautobotModelSerializer, TaggedModelSerializerMixin): """Serializer for the journal model.""" diff --git a/nautobot_design_builder/models.py b/nautobot_design_builder/models.py index 8932cef3..99646791 100644 --- a/nautobot_design_builder/models.py +++ b/nautobot_design_builder/models.py @@ -235,12 +235,14 @@ def delete(self, *args, **kwargs): raise ValidationError("A Design Instance can only be delete if it's Decommissioned and not Deployed.") return super().delete(*args, **kwargs) - def get_created_by(self): + @property + def created_by(self): """Get the username of the user who created the object.""" created_by, _ = get_created_and_last_updated_usernames_for_model(self) return created_by - def get_last_updated_by(self): + @property + def last_updated_by(self): """Get the username of the user who update the object last time.""" _, last_updated_by = get_created_and_last_updated_usernames_for_model(self) return last_updated_by diff --git a/nautobot_design_builder/tables.py b/nautobot_design_builder/tables.py index c5fa3793..8b4345a9 100644 --- a/nautobot_design_builder/tables.py +++ b/nautobot_design_builder/tables.py @@ -55,8 +55,8 @@ class DesignInstanceTable(StatusTableMixin, BaseTable): design = Column(linkify=True) first_implemented = Column(verbose_name="Deployment Time") last_implemented = Column(verbose_name="Last Update Time") - created_by = Column(accessor=Accessor("get_created_by"), verbose_name="Deployed by") - updated_by = Column(accessor=Accessor("get_last_updated_by"), verbose_name="Last Updated by") + created_by = Column(verbose_name="Deployed by") + updated_by = Column(verbose_name="Last Updated by") live_state = ColoredLabelColumn(verbose_name="Operational State") actions = ButtonsColumn( DesignInstance, diff --git a/nautobot_design_builder/templates/nautobot_design_builder/design_list.html b/nautobot_design_builder/templates/nautobot_design_builder/design_list.html index e9d381f3..21fdc25d 100644 --- a/nautobot_design_builder/templates/nautobot_design_builder/design_list.html +++ b/nautobot_design_builder/templates/nautobot_design_builder/design_list.html @@ -3,17 +3,42 @@ {% load static %} {% load helpers %} - +{% block extra_styles %} +{{ block.super }} + +{% endblock %} {% block content %} {{ block.super }} -