diff --git a/docs/images/lcm_software_validation_report_run_jobs2.png b/docs/images/lcm_software_validation_report_run_jobs2.png new file mode 100644 index 00000000..5bd3ef37 Binary files /dev/null and b/docs/images/lcm_software_validation_report_run_jobs2.png differ diff --git a/docs/user/lifecycle_reporting.md b/docs/user/lifecycle_reporting.md index 51271f46..bafac781 100644 --- a/docs/user/lifecycle_reporting.md +++ b/docs/user/lifecycle_reporting.md @@ -12,7 +12,7 @@ You can run these reports two ways: ![](../images/lcm_software_validation_report_run.png) -- The "Jobs" dropdown and navigating to **Device/Sofware Lifecycle Reporting** section. The jobs will appear here and all you will need to do is click the play button. +- The "Jobs" dropdown and navigating to **Device/Sofware Lifecycle Reporting** section. The jobs will appear here and all you will need to do is click the play button. You'll then be able to select the platforms and tenants to run the report for, and click on **Run Job Now** execute button on right side of screen. ![](../images/lcm_software_validation_report_run_jobs.png) diff --git a/nautobot_device_lifecycle_mgmt/choices.py b/nautobot_device_lifecycle_mgmt/choices.py index 80be3eb9..38b146b0 100644 --- a/nautobot_device_lifecycle_mgmt/choices.py +++ b/nautobot_device_lifecycle_mgmt/choices.py @@ -55,10 +55,12 @@ class ReportRunTypeChoices(ChoiceSet): REPORT_SINGLE_OBJECT_RUN = "single-object-run" REPORT_FULL_RUN = "full-report-run" + REPORT_FILTERED_RUN = "filtered-report-run" CHOICES = ( (REPORT_SINGLE_OBJECT_RUN, "Single Object Run"), (REPORT_FULL_RUN, "Full Report Run"), + (REPORT_FILTERED_RUN, "Filtered Report Run"), ) diff --git a/nautobot_device_lifecycle_mgmt/jobs/lifecycle_reporting.py b/nautobot_device_lifecycle_mgmt/jobs/lifecycle_reporting.py index ab00a4eb..39b1fb63 100644 --- a/nautobot_device_lifecycle_mgmt/jobs/lifecycle_reporting.py +++ b/nautobot_device_lifecycle_mgmt/jobs/lifecycle_reporting.py @@ -2,9 +2,11 @@ """Jobs for the Lifecycle Management app.""" from datetime import datetime +from itertools import product -from nautobot.dcim.models import Device, InventoryItem -from nautobot.extras.jobs import Job +from nautobot.dcim.models import Device, InventoryItem, Platform +from nautobot.extras.jobs import Job, MultiObjectVar +from nautobot.tenancy.models import Tenant from nautobot_device_lifecycle_mgmt import choices from nautobot_device_lifecycle_mgmt.models import ( @@ -84,34 +86,89 @@ class DeviceSoftwareValidationFullReport(Job): description = "Validates software version on devices." read_only = False + # Add dropdowns for platform and tenant filters; Defaults to all platforms and tenants + platform = MultiObjectVar( + model=Platform, + label="Platform", + description="Filter by platform; defaults to all platforms", + required=False, + ) + tenant = MultiObjectVar( + model=Tenant, + label="Tenant", + description="Filter by tenant; defaults to all tenants", + required=False, + ) + + filters = [platform, tenant] + class Meta: """Meta class for the job.""" has_sensitive_variables = False - def run(self) -> None: # pylint: disable=arguments-differ + def run(self, **filters) -> None: # pylint: disable=arguments-differ """Check if software assigned to each device is valid. If no software is assigned return warning message.""" job_run_time = datetime.now() validation_count = 0 - for device in Device.objects.filter(software_version__isnull=True): + # Create empty lists for versioned and non-versioned devices + versioned_devices = [] # Devices with software version + non_versioned_devices = [] # Devices without software version + all_tenants = False + all_platforms = False + + # Get filters + platforms = filters.get("platform") + tenants = filters.get("tenant") + + # If no platforms or tenants are provided, use all platforms and tenants + if not platforms: + platforms = Platform.objects.all() + all_platforms = True + + if not tenants: + tenants = Tenant.objects.all() + all_tenants = True + + # Get all combinations of platforms and tenants + filtered_products = product(platforms, tenants) + + # Get versioned and non-versioned devices + for platform, tenant in filtered_products: + versioned_devices.extend( + Device.objects.filter(platform=platform, tenant=tenant, software_version__isnull=False) + ) + non_versioned_devices.extend( + Device.objects.filter(platform=platform, tenant=tenant, software_version__isnull=True) + ) + + # Validate devices without software version + for device in non_versioned_devices: validate_obj, _ = DeviceSoftwareValidationResult.objects.get_or_create(device=device) validate_obj.is_validated = False validate_obj.valid_software.set(ValidatedSoftwareLCM.objects.get_for_object(device)) validate_obj.software = None validate_obj.last_run = job_run_time - validate_obj.run_type = choices.ReportRunTypeChoices.REPORT_FULL_RUN + if all_tenants and all_platforms: + validate_obj.run_type = choices.ReportRunTypeChoices.REPORT_FULL_RUN + else: + validate_obj.run_type = choices.ReportRunTypeChoices.REPORT_FILTERED_RUN validate_obj.validated_save() validation_count += 1 - for device in Device.objects.filter(software_version__isnull=False): + # Validate devices with software version + for device in versioned_devices: device_software = DeviceSoftware(device) validate_obj, _ = DeviceSoftwareValidationResult.objects.get_or_create(device=device) validate_obj.is_validated = device_software.validate_software() validate_obj.valid_software.set(ValidatedSoftwareLCM.objects.get_for_object(device)) validate_obj.software = device.software_version validate_obj.last_run = job_run_time - validate_obj.run_type = choices.ReportRunTypeChoices.REPORT_FULL_RUN + if all_tenants and all_platforms: + validate_obj.run_type = choices.ReportRunTypeChoices.REPORT_FULL_RUN + else: + validate_obj.run_type = choices.ReportRunTypeChoices.REPORT_FILTERED_RUN validate_obj.validated_save() validation_count += 1 diff --git a/nautobot_device_lifecycle_mgmt/migrations/0023_cvelcm_affected_softwares_tmp_and_more.py b/nautobot_device_lifecycle_mgmt/migrations/0023_cvelcm_affected_softwares_tmp_and_more.py new file mode 100644 index 00000000..49572cda --- /dev/null +++ b/nautobot_device_lifecycle_mgmt/migrations/0023_cvelcm_affected_softwares_tmp_and_more.py @@ -0,0 +1,37 @@ +# Generated by Django 4.2.15 on 2024-12-13 15:10 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("nautobot_device_lifecycle_mgmt", "0022_alter_softwareimagelcm_inventory_items_and_more"), + ] + + operations = [ + migrations.AddField( + model_name="cvelcm", + name="affected_softwares_tmp", + field=models.JSONField(default=list), + ), + migrations.AddField( + model_name="devicesoftwarevalidationresult", + name="software_tmp", + field=models.UUIDField(blank=True, null=True), + ), + migrations.AddField( + model_name="inventoryitemsoftwarevalidationresult", + name="software_tmp", + field=models.UUIDField(blank=True, null=True), + ), + migrations.AddField( + model_name="validatedsoftwarelcm", + name="software_tmp", + field=models.UUIDField(blank=True, null=True), + ), + migrations.AddField( + model_name="vulnerabilitylcm", + name="software_tmp", + field=models.UUIDField(blank=True, null=True), + ), + ] diff --git a/nautobot_device_lifecycle_mgmt/migrations/0024_migrate_to_core_software_models_check.py b/nautobot_device_lifecycle_mgmt/migrations/0024_migrate_to_core_software_models_check.py new file mode 100644 index 00000000..b7cd251e --- /dev/null +++ b/nautobot_device_lifecycle_mgmt/migrations/0024_migrate_to_core_software_models_check.py @@ -0,0 +1,295 @@ +from django.core.exceptions import ValidationError +from django.db import migrations +from django.db.models import Q + + +def verify_dlm_models_migated_to_core(apps, schema_editor): + """Verifies whether the objects for the following DLM models have been migrated to the corresponding Core models + DLM SoftwareLCM -> Core SoftwareVersion + DLM SoftwareImageLCM -> Core SoftwareImageFile + DLM Contact -> Core Contact + """ + ContentType = apps.get_model("contenttypes", "ContentType") + DLMContact = apps.get_model("nautobot_device_lifecycle_mgmt", "ContactLCM") + DLMSoftwareVersion = apps.get_model("nautobot_device_lifecycle_mgmt", "SoftwareLCM") + DLMSoftwareImage = apps.get_model("nautobot_device_lifecycle_mgmt", "SoftwareImageLCM") + CoreContact = apps.get_model("extras", "Contact") + CoreSoftwareVersion = apps.get_model("dcim", "SoftwareVersion") + CoreSoftwareImage = apps.get_model("dcim", "SoftwareImageFile") + + dlm_contact_ct = ContentType.objects.get_for_model(DLMContact) + dlm_software_version_ct = ContentType.objects.get_for_model(DLMSoftwareVersion) + dlm_software_image_ct = ContentType.objects.get_for_model(DLMSoftwareImage) + core_contact_ct = ContentType.objects.get_for_model(CoreContact) + core_software_version_ct = ContentType.objects.get_for_model(CoreSoftwareVersion) + core_software_image_ct = ContentType.objects.get_for_model(CoreSoftwareImage) + + # Verify nautobot_device_lifecycle_mgmt.SoftwareLCM instances were migrated to dcim.SoftwareVersion + for dlm_software_version in DLMSoftwareVersion.objects.all(): + _verify_software_version_migrated(apps, dlm_software_version) + _verify_content_type_references_migrated_to_new_model(apps, dlm_software_version_ct, core_software_version_ct) + + # Verify nautobot_device_lifecycle_mgmt.SoftwareImageLCM instances were migrated to dcim.SoftwareImageFile + for dlm_software_image in DLMSoftwareImage.objects.all(): + _verify_software_image_migrated(apps, dlm_software_image) + _verify_content_type_references_migrated_to_new_model(apps, dlm_software_image_ct, core_software_image_ct) + + # Verify nautobot_device_lifecycle_mgmt.ContactLCM instances were migrated to extras.Contact + for dlm_contact in DLMContact.objects.all(): + _verify_contact_migrated(apps, dlm_contact) + _verify_content_type_references_migrated_to_new_model(apps, dlm_contact_ct, core_contact_ct) + + +def _verify_software_version_migrated(apps, dlm_software_version): + """Verifies instances of DLM SoftwareLCM were migrated to Core SoftwareVersion.""" + CoreSoftwareVersion = apps.get_model("dcim", "SoftwareVersion") + + core_software_version_q = CoreSoftwareVersion.objects.filter( + platform=dlm_software_version.device_platform, version=dlm_software_version.version + ) + if not core_software_version_q.exists(): + raise ValidationError( + f"DLM Migration Error: Did not find Core SoftwareVersion object matching DLM Software object: {dlm_software_version}" + ) + return + + +def _verify_software_image_migrated(apps, dlm_software_image): + """Verifies instances of DLM SoftwareImageLCM were migrated to Core SoftwareImageFile.""" + SoftwareVersion = apps.get_model("dcim", "SoftwareVersion") + SoftwareImageFile = apps.get_model("dcim", "SoftwareImageFile") + + dlm_software_version = dlm_software_image.software + core_software_version_q = SoftwareVersion.objects.filter( + platform=dlm_software_version.device_platform, version=dlm_software_version.version + ) + if not core_software_version_q.exists(): + raise ValidationError( + f"DLM Migration Error: Did not find Core SoftwareVersion matching DLM SoftwareVersion on DLM SoftwareImage object: {dlm_software_image}" + ) + core_software_image_q = SoftwareImageFile.objects.filter( + image_file_name=dlm_software_image.image_file_name, software_version=core_software_version_q.first() + ) + if not core_software_image_q.exists(): + raise ValidationError( + f"DLM Migration Error: Did not find Core SoftwareImage object matching DLM SoftwareImage object: {dlm_software_image}" + ) + return + + +def _verify_contact_migrated(apps, dlm_contact): + """Verifies instances of DLM Contact were migrated to Core Contact.""" + CoreContact = apps.get_model("extras", "Contact") + + core_contact_q = CoreContact.objects.filter(name=dlm_contact.name, phone=dlm_contact.phone, email=dlm_contact.email) + if not core_contact_q.exists(): + raise ValidationError( + f"DLM Migration Error: Did not find Core Contact object matching DLM Contact object: {dlm_contact}" + ) + return + + +def _verify_content_type_references_migrated_to_new_model(apps, old_ct, new_ct): + """Verify Nautobot extension objects and relationships linked to deprecated DLM models were migrated.""" + ComputedField = apps.get_model("extras", "ComputedField") + CustomField = apps.get_model("extras", "CustomField") + CustomLink = apps.get_model("extras", "CustomLink") + ExportTemplate = apps.get_model("extras", "ExportTemplate") + JobButton = apps.get_model("extras", "JobButton") + JobHook = apps.get_model("extras", "JobHook") + Note = apps.get_model("extras", "Note") + ObjectChange = apps.get_model("extras", "ObjectChange") + ObjectPermission = apps.get_model("users", "ObjectPermission") + Relationship = apps.get_model("extras", "Relationship") + RelationshipAssociation = apps.get_model("extras", "RelationshipAssociation") + Status = apps.get_model("extras", "Status") + Tag = apps.get_model("extras", "Tag") + TaggedItem = apps.get_model("extras", "TaggedItem") + WebHook = apps.get_model("extras", "WebHook") + + old_ct_computed_fields = ComputedField.objects.filter(content_type=old_ct) + for computed_field in old_ct_computed_fields: + print( + f"Migration error. The Computed Field '{computed_field.label}' has not been migrated to the new model '{str(new_ct)}'" + ) + if old_ct_computed_fields.exists(): + raise ValidationError( + f"DLM Migration Error: Found computed fields that have not been migrated from DLM to Core model: {old_ct_computed_fields}." + ) + + # Migrate CustomField content type + old_ct_custom_fields = CustomField.objects.filter(content_types=old_ct).exclude(content_types=new_ct) + for custom_field in old_ct_custom_fields: + print( + f"Migration error. The Custom Field '{custom_field.label}' has not been migrated to Core model '{str(new_ct)}'" + ) + if old_ct_custom_fields.exists(): + raise ValidationError( + f"DLM Migration Error: Found custom fields that have not been migrated from DLM to Core model: {old_ct_custom_fields}." + ) + + # Migrate CustomLink content type + old_ct_custom_links = CustomLink.objects.filter(content_type=old_ct) + for custom_link in old_ct_custom_links: + print( + f"Migration error. The Custom Link '{custom_link.name}' has not been migrated to Core model '{str(new_ct)}'" + ) + if old_ct_custom_links.exists(): + raise ValidationError( + f"DLM Migration Error: Found custom links that have not been migrated from DLM to Core model: {old_ct_custom_links}." + ) + + # Migrate ExportTemplate content type - skip git export templates + old_ct_export_templates = ExportTemplate.objects.filter(content_type=old_ct, owner_content_type=None) + for export_template in old_ct_export_templates: + print( + f"Migration error. The Export Template '{export_template.name}' has not been migrated to Core model '{str(new_ct)}'" + ) + if old_ct_export_templates.exists(): + raise ValidationError( + f"DLM Migration Error: Found export templates that have not been migrated from DLM to Core model: {old_ct_export_templates}." + ) + + # Migrate JobButton content type + old_ct_job_buttons = JobButton.objects.filter(content_types=old_ct).exclude(content_types=new_ct) + for job_button in old_ct_job_buttons: + print( + f"Migration error. The Job Button '{job_button.name}' has not been migrated to Core model '{str(new_ct)}'" + ) + if old_ct_job_buttons.exists(): + raise ValidationError( + f"DLM Migration Error: Found job buttons that have not been migrated from DLM to Core model: {old_ct_job_buttons}." + ) + + # Migrate JobHook content type + old_ct_job_hooks = JobHook.objects.filter(content_types=old_ct).exclude(content_types=new_ct) + for job_hook in old_ct_job_hooks: + print(f"Migration error. The Job Hook '{job_hook.name}' has not been migrated to Core model '{str(new_ct)}'") + if old_ct_job_hooks.exists(): + raise ValidationError( + f"DLM Migration Error: Found job hooks that have not been migrated from DLM to Core model: {old_ct_job_hooks}." + ) + + # Migrate Note content type + old_ct_notes = Note.objects.filter(assigned_object_type=old_ct) + for note in old_ct_notes: + print(f"Migration error.The Note '{str(note)}' has not been migrated to Core model '{str(new_ct)}'") + if old_ct_notes.exists(): + raise ValidationError( + f"DLM Migration Error: Found notes that have not been migrated from DLM to Core model: {old_ct_notes}." + ) + + # Migrate ObjectChange content type + old_ct_object_changes = ObjectChange.objects.filter(changed_object_type=old_ct) + for object_change in old_ct_object_changes: + print( + f"Migration error. The ObjectChange {str(object_change)} has not been migrated to Core model '{str(new_ct)}'" + ) + if old_ct_object_changes.exists(): + raise ValidationError( + f"DLM Migration Error: Found object changes that have not been migrated from DLM to Core model: {old_ct_object_changes}." + ) + + # Migrate Status content type + old_ct_statuses = Status.objects.filter(content_types=old_ct).exclude(content_types=new_ct) + for status in old_ct_statuses: + print(f"Migration error. The Status '{status.name}' has not been migrated to Core model '{str(new_ct)}'") + if old_ct_statuses.exists(): + raise ValidationError( + f"DLM Migration Error: Found statuses that have not been migrated from DLM to Core model: {old_ct_statuses}." + ) + + # Migrate Tag content type + old_ct_tags = Tag.objects.filter(content_types=old_ct).exclude(content_types=new_ct) + for tag in old_ct_tags: + print(f"Migration error. The Tag '{tag.name}' has not been migrated to Core model '{str(new_ct)}'") + if old_ct_tags.exists(): + raise ValidationError( + f"DLM Migration Error: Found tags that have not been migrated from DLM to Core model: {old_ct_tags}." + ) + + # Migrate TaggedItem content type + old_ct_tagged_items = TaggedItem.objects.filter(content_type=old_ct) + for tagged_item in old_ct_tagged_items: + print( + f"Migration error. The Tagged Item '{str(tagged_item)}' has not been migrated to Core model '{str(new_ct)}'" + ) + if old_ct_tagged_items.exists(): + raise ValidationError( + f"DLM Migration Error: Found tagged items that have not been migrated from DLM to Core model: {old_ct_tagged_items}." + ) + + # Migrate WebHook content type + old_ct_web_hooks = WebHook.objects.filter(content_types=old_ct).exclude(content_types=new_ct) + for web_hook in old_ct_web_hooks: + print(f"Migration error. The Web Hook '{web_hook.name}' has not been migrated to Core model '{str(new_ct)}'") + if old_ct_web_hooks.exists(): + raise ValidationError( + f"DLM Migration Error: Found web hooks that have not been migrated from DLM to Core model: {old_ct_web_hooks}." + ) + + # Migrate ObjectPermission content type + old_ct_permissions = ObjectPermission.objects.filter(object_types=old_ct).exclude(object_types=new_ct) + for object_permission in old_ct_permissions: + print( + f"Migration error. The Object Permission '{str(object_permission)}' has not been migrated to Core model '{str(new_ct)}'", + ) + if old_ct_permissions.exists(): + raise ValidationError( + f"DLM Migration Error: Found object permissions that have not been migrated from DLM to Core models: {old_ct_permissions}." + ) + + # These are migrated separately as they follow specific business logic + excluded_relationships = ("device_soft", "inventory_item_soft") + old_ct_relationships = Relationship.objects.filter( + ~Q(key__in=excluded_relationships) & (Q(source_type=old_ct) | Q(destination_type=old_ct)) + ) + for relationship in old_ct_relationships: + print( + f"Migration error. The Relationship '{relationship.label}' has not been migrated to Core model '{str(new_ct)}'" + ) + + old_ct_relationship_assoc_src = RelationshipAssociation.objects.filter( + source_type=old_ct, relationship=relationship + ) + for relationship_association in old_ct_relationship_assoc_src: + print( + f"Migration error. The Relationship Association '{str(relationship_association)}' has not been migrated to Core model '{str(new_ct)}'" + ) + old_ct_relationship_assoc_src = RelationshipAssociation.objects.filter( + source_type=old_ct, relationship=relationship + ) + if old_ct_relationship_assoc_src: + raise ValidationError( + f"DLM Migration Error: Found relationship associations that have not been migrated from DLM to Core model: {old_ct_relationship_assoc_src}." + ) + + old_ct_relationship_assoc_dst = RelationshipAssociation.objects.filter( + destination_type=old_ct, relationship=relationship + ) + for relationship_association in old_ct_relationship_assoc_dst: + print( + f"Migration error. The Relationship Association '{str(relationship_association)}' has not been migrated to Core model '{str(new_ct)}'" + ) + if old_ct_relationship_assoc_dst: + raise ValidationError( + f"DLM Migration Error: Found relationship associations that have not been migrated from DLM to Core models: {old_ct_relationship_assoc_dst}." + ) + if old_ct_relationships.exists(): + raise ValidationError( + f"DLM Migration Error: Found relationships that have not been migrated from DLM to Core model: {old_ct_relationships}." + ) + + +class Migration(migrations.Migration): + dependencies = [ + ("dcim", "0055_softwareimage_softwareversion_data_migration"), + ("contenttypes", "0002_remove_content_type_name"), + ("extras", "0057_jobbutton"), + ("nautobot_device_lifecycle_mgmt", "0023_cvelcm_affected_softwares_tmp_and_more"), + ("users", "0001_initial"), + ] + + operations = [ + migrations.RunPython(code=verify_dlm_models_migated_to_core, reverse_code=migrations.RunPython.noop), + ] diff --git a/nautobot_device_lifecycle_mgmt/migrations/0025_migrate_soft_references_p1.py b/nautobot_device_lifecycle_mgmt/migrations/0025_migrate_soft_references_p1.py new file mode 100644 index 00000000..8107f898 --- /dev/null +++ b/nautobot_device_lifecycle_mgmt/migrations/0025_migrate_soft_references_p1.py @@ -0,0 +1,180 @@ +# Generated by Django 3.2.25 on 2024-05-01 13:08 +import django.db.models.deletion +from django.core.exceptions import ValidationError +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("nautobot_device_lifecycle_mgmt", "0024_migrate_to_core_software_models_check"), + ] + + def nullify_software_fields(apps, schema_editor): + """ + Nullify fields holding Software references. + """ + ValidatedSoftware = apps.get_model("nautobot_device_lifecycle_mgmt", "ValidatedSoftwareLCM") + DeviceSoftwareValidationResult = apps.get_model( + "nautobot_device_lifecycle_mgmt", "DeviceSoftwareValidationResult" + ) + InventoryItemSoftwareValidationResult = apps.get_model( + "nautobot_device_lifecycle_mgmt", "InventoryItemSoftwareValidationResult" + ) + CVE = apps.get_model("nautobot_device_lifecycle_mgmt", "CVELCM") + Vulnerability = apps.get_model("nautobot_device_lifecycle_mgmt", "VulnerabilityLCM") + + for validated_software in ValidatedSoftware.objects.all(): + validated_software.software = None + validated_software.save() + + for device_soft_val_res in DeviceSoftwareValidationResult.objects.all(): + if device_soft_val_res.software: + device_soft_val_res.software = None + device_soft_val_res.save() + + for invitem_soft_val_res in InventoryItemSoftwareValidationResult.objects.all(): + if invitem_soft_val_res.software: + invitem_soft_val_res.software = None + invitem_soft_val_res.save() + + for vuln in Vulnerability.objects.all(): + if vuln.software: + vuln.software = None + vuln.save() + + for cve in CVE.objects.all(): + cve.affected_softwares.clear() + cve.save() + + def migrate_dlm_software_references(apps, schema_editor): + """ + Map migrated DLM Software references to corresponding Core SoftwareVersion ones. + """ + ValidatedSoftwareLCM = apps.get_model("nautobot_device_lifecycle_mgmt", "ValidatedSoftwareLCM") + DeviceSoftwareValidationResult = apps.get_model( + "nautobot_device_lifecycle_mgmt", "DeviceSoftwareValidationResult" + ) + InventoryItemSoftwareValidationResult = apps.get_model( + "nautobot_device_lifecycle_mgmt", "InventoryItemSoftwareValidationResult" + ) + CVE = apps.get_model("nautobot_device_lifecycle_mgmt", "CVELCM") + Vulnerability = apps.get_model("nautobot_device_lifecycle_mgmt", "VulnerabilityLCM") + + for validated_software in ValidatedSoftwareLCM.objects.all(): + if validated_software.software_tmp is None: + raise ValidationError( + f"ValidatedSoftware {validated_software} is missing reference to the Core Software. You must run migration job first before installing v3 of the DLM app." + ) + validated_software.software_id = validated_software.software_tmp + validated_software.save() + + for device_soft_val_res in DeviceSoftwareValidationResult.objects.all(): + if device_soft_val_res.software is None: + continue + if device_soft_val_res.software_tmp is None: + raise ValidationError( + f"DeviceSoftwareValidationResult {device_soft_val_res} is missing reference to the Core Software. You must run migration job first before installing v3 of the DLM app." + ) + if device_soft_val_res.software_tmp: + device_soft_val_res.software_id = device_soft_val_res.software_tmp + device_soft_val_res.save() + + for invitem_soft_val_res in InventoryItemSoftwareValidationResult.objects.all(): + if invitem_soft_val_res.software is None: + continue + if invitem_soft_val_res.software_tmp is None: + raise ValidationError( + f"InventoryItemSoftwareValidationResult {invitem_soft_val_res} is missing reference to the Core Software. You must run migration job first before installing v3 of the DLM app." + ) + if invitem_soft_val_res.software_tmp: + invitem_soft_val_res.software_id = invitem_soft_val_res.software_tmp + invitem_soft_val_res.save() + + for cve in CVE.objects.all(): + if cve.affected_softwares_tmp: + softwares = [soft_id for soft_id in cve.affected_softwares_tmp] + cve.affected_softwares.set(softwares) + cve.save() + + for vuln in Vulnerability.objects.all(): + if vuln.software is None: + continue + if vuln.software_tmp is None: + raise ValidationError( + f"ValidVulnerabilityatedSoftware {vuln} is missing reference to the Core Software. You must run migration job first before installing v3 of the DLM app." + ) + if vuln.software_tmp: + vuln.software_id = vuln.software_tmp + vuln.save() + + def delete_migrated_dlm_objects(apps, schema_editor): + """ + Delete migrated DLM objects and objects created by the migration job. + """ + ContactLCM = apps.get_model("nautobot_device_lifecycle_mgmt", "ContactLCM") + SoftwareLCM = apps.get_model("nautobot_device_lifecycle_mgmt", "SoftwareLCM") + SoftwareImageLCM = apps.get_model("nautobot_device_lifecycle_mgmt", "SoftwareImageLCM") + Tag = apps.get_model("extras", "Tag") + Relationship = apps.get_model("extras", "Relationship") + + # Delete DLM relationships from software to devices and inventory items + Relationship.objects.filter(key="device_soft").delete() + Relationship.objects.filter(key="inventory_item_soft").delete() + + # Delete DLM SoftwareLCM, SoftwareImageLCM and ContactLCM instances + ContactLCM.objects.all().delete() + SoftwareLCM.objects.all().delete() + SoftwareImageLCM.objects.all().delete() + + # Delete tags created by migration Job. These are no longer needed. + Tag.objects.filter(name__istartswith="DLM_migration-ContactLCM").delete() + Tag.objects.filter(name__istartswith="DLM_migration-SoftwareLCM").delete() + Tag.objects.filter(name__istartswith="DLM_migration-SoftwareImageLCM").delete() + + operations = [ + migrations.AlterField( + model_name="validatedsoftwarelcm", + name="software", + field=models.ForeignKey( + null=True, on_delete=django.db.models.deletion.SET_NULL, to="nautobot_device_lifecycle_mgmt.softwarelcm" + ), + ), + migrations.RunPython(nullify_software_fields), + migrations.AlterField( + model_name="cvelcm", + name="affected_softwares", + field=models.ManyToManyField(blank=True, related_name="corresponding_cves", to="dcim.softwareversion"), + ), + migrations.AlterField( + model_name="devicesoftwarevalidationresult", + name="software", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="+", + to="dcim.softwareversion", + ), + ), + migrations.AlterField( + model_name="inventoryitemsoftwarevalidationresult", + name="software", + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to="dcim.softwareversion" + ), + ), + migrations.AlterField( + model_name="validatedsoftwarelcm", + name="software", + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to="dcim.softwareversion"), + ), + migrations.AlterField( + model_name="vulnerabilitylcm", + name="software", + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to="dcim.softwareversion" + ), + ), + migrations.RunPython(migrate_dlm_software_references), + migrations.RunPython(delete_migrated_dlm_objects), + ] diff --git a/nautobot_device_lifecycle_mgmt/migrations/0026_migrate_soft_references_p2.py b/nautobot_device_lifecycle_mgmt/migrations/0026_migrate_soft_references_p2.py new file mode 100644 index 00000000..c11241df --- /dev/null +++ b/nautobot_device_lifecycle_mgmt/migrations/0026_migrate_soft_references_p2.py @@ -0,0 +1,38 @@ +# Generated by Django 4.2.16 on 2024-11-07 20:28 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("nautobot_device_lifecycle_mgmt", "0025_migrate_soft_references_p1"), + ] + + operations = [ + migrations.RemoveField( + model_name="cvelcm", + name="affected_softwares_tmp", + ), + migrations.RemoveField( + model_name="devicesoftwarevalidationresult", + name="software_tmp", + ), + migrations.RemoveField( + model_name="inventoryitemsoftwarevalidationresult", + name="software_tmp", + ), + migrations.RemoveField( + model_name="validatedsoftwarelcm", + name="software_tmp", + ), + migrations.RemoveField( + model_name="vulnerabilitylcm", + name="software_tmp", + ), + migrations.AlterField( + model_name="validatedsoftwarelcm", + name="software", + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="dcim.softwareversion"), + ), + ] diff --git a/nautobot_device_lifecycle_mgmt/migrations/0027_delete_models_migrated_to_core.py b/nautobot_device_lifecycle_mgmt/migrations/0027_delete_models_migrated_to_core.py new file mode 100644 index 00000000..e300caa9 --- /dev/null +++ b/nautobot_device_lifecycle_mgmt/migrations/0027_delete_models_migrated_to_core.py @@ -0,0 +1,58 @@ +# Generated by Django 3.2.25 on 2024-05-01 13:08 + +from django.db import migrations + + +class Migration(migrations.Migration): + dependencies = [ + ("dcim", "0058_controller_data_migration"), + ("nautobot_device_lifecycle_mgmt", "0026_migrate_soft_references_p2"), + ] + + operations = [ + migrations.AlterUniqueTogether( + name="softwareimagelcm", + unique_together=None, + ), + migrations.RemoveField( + model_name="softwareimagelcm", + name="device_types", + ), + migrations.RemoveField( + model_name="softwareimagelcm", + name="inventory_items", + ), + migrations.RemoveField( + model_name="softwareimagelcm", + name="object_tags", + ), + migrations.RemoveField( + model_name="softwareimagelcm", + name="software", + ), + migrations.RemoveField( + model_name="softwareimagelcm", + name="tags", + ), + migrations.AlterUniqueTogether( + name="softwarelcm", + unique_together=None, + ), + migrations.RemoveField( + model_name="softwarelcm", + name="device_platform", + ), + migrations.RemoveField( + model_name="softwarelcm", + name="tags", + ), + migrations.DeleteModel( + name="ContactLCM", + ), + migrations.DeleteModel( + name="SoftwareImageLCM", + ), + migrations.DeleteModel( + name="SoftwareLCM", + ), + ] diff --git a/nautobot_device_lifecycle_mgmt/migrations/0029_devicehardwarenoticeresult.py b/nautobot_device_lifecycle_mgmt/migrations/0029_devicehardwarenoticeresult.py index ebe9517b..41a69b1d 100755 --- a/nautobot_device_lifecycle_mgmt/migrations/0029_devicehardwarenoticeresult.py +++ b/nautobot_device_lifecycle_mgmt/migrations/0029_devicehardwarenoticeresult.py @@ -13,7 +13,7 @@ class Migration(migrations.Migration): dependencies = [ ("extras", "0106_populate_default_statuses_and_roles_for_contact_associations"), ("dcim", "0058_controller_data_migration"), - ("nautobot_device_lifecycle_mgmt", "0028_delete_models_migrated_to_core"), + ("nautobot_device_lifecycle_mgmt", "0027_delete_models_migrated_to_core"), ] operations = [ diff --git a/pyproject.toml b/pyproject.toml index a5ec35b6..5ce54f3f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "nautobot-device-lifecycle-mgmt" -version = "3.0.0" +version = "3.0.1" description = "Manages device lifecycles for platforms and software." authors = ["Network to Code, LLC "] license = "Apache-2.0" diff --git a/tasks.py b/tasks.py index 414c4e85..b5d27fc5 100644 --- a/tasks.py +++ b/tasks.py @@ -780,7 +780,8 @@ def ruff(context, action=None, target=None, fix=False, output_format="concise"): if not run_command(context, command, warn=True): exit_code = 1 - raise Exit(code=exit_code) + if exit_code: + raise Exit(code=exit_code) @task