From 2ab54857625a09ae13087ba1fe9813b77465c94c Mon Sep 17 00:00:00 2001 From: "Reimann, Timo" Date: Tue, 14 Sep 2021 17:09:57 +0200 Subject: [PATCH] added functional sync feature --- develop/ACS.json | 1 - .../management/commands/sync_eox_data.py | 187 ++++++++++++++---- .../migrations/0005_auto_20210914_1344.py | 48 +++++ netbox_cisco_support/models.py | 28 ++- netbox_cisco_support/templatetags/filters.py | 3 + 5 files changed, 223 insertions(+), 44 deletions(-) delete mode 100644 develop/ACS.json create mode 100644 netbox_cisco_support/migrations/0005_auto_20210914_1344.py diff --git a/develop/ACS.json b/develop/ACS.json deleted file mode 100644 index 69088e1..0000000 --- a/develop/ACS.json +++ /dev/null @@ -1 +0,0 @@ -{"PaginationResponseRecord":{"PageIndex":1,"LastIndex":1,"TotalRecords":2,"PageRecords":2},"EOXRecord":[{"EOLProductID":"CSACS-1121-K9","ProductIDDescription":"ACS 1121 Appliance With 5.x SW And Base license","ProductBulletinNumber":"EOL9097","LinkToProductBulletinURL":"http://www.cisco.com/en/US/prod/collateral/netmgtsw/ps5698/ps6767/ps9911/eol__C51-726880.html","EOXExternalAnnouncementDate":{"value":"2013-02-26","dateFormat":"YYYY-MM-DD"},"EndOfSaleDate":{"value":"2013-08-27","dateFormat":"YYYY-MM-DD"},"EndOfSWMaintenanceReleases":{"value":"2014-08-27","dateFormat":"YYYY-MM-DD"},"EndOfSecurityVulSupportDate":{"value":"2016-08-26","dateFormat":"YYYY-MM-DD"},"EndOfRoutineFailureAnalysisDate":{"value":"2014-08-27","dateFormat":"YYYY-MM-DD"},"EndOfServiceContractRenewal":{"value":"2017-11-22","dateFormat":"YYYY-MM-DD"},"LastDateOfSupport":{"value":"2018-08-31","dateFormat":"YYYY-MM-DD"},"EndOfSvcAttachDate":{"value":"2014-08-27","dateFormat":"YYYY-MM-DD"},"UpdatedTimeStamp":{"value":"2013-02-26","dateFormat":"YYYY-MM-DD"},"EOXMigrationDetails":{"PIDActiveFlag":"Y","MigrationInformation":"Small Secure Network Server for ISE, NAC, & ACS Applications","MigrationOption":"Enter PID(s)","MigrationProductId":"SNS-3415-K9","MigrationProductName":"","MigrationStrategy":"","MigrationProductInfoURL":"http://www.cisco.com/en/US/prod/collateral/netmgtsw/ps5698/ps6767/ps9911/data_sheet_c78-715717.html"},"EOXInputType":"ShowEOXByPids","EOXInputValue":"CSACS-1121-K9 "},{"EOLProductID":"CSACS-1121-K9","ProductIDDescription":"ACS 1121 Appliance With 5.x SW And Base license","ProductBulletinNumber":"EOL9097","LinkToProductBulletinURL":"http://www.cisco.com/en/US/prod/collateral/netmgtsw/ps5698/ps6767/ps9911/eol__C51-726880.html","EOXExternalAnnouncementDate":{"value":"2013-02-26","dateFormat":"YYYY-MM-DD"},"EndOfSaleDate":{"value":"2013-08-27","dateFormat":"YYYY-MM-DD"},"EndOfSWMaintenanceReleases":{"value":"2014-08-27","dateFormat":"YYYY-MM-DD"},"EndOfSecurityVulSupportDate":{"value":"2016-08-26","dateFormat":"YYYY-MM-DD"},"EndOfRoutineFailureAnalysisDate":{"value":"2014-08-27","dateFormat":"YYYY-MM-DD"},"EndOfServiceContractRenewal":{"value":"2017-11-22","dateFormat":"YYYY-MM-DD"},"LastDateOfSupport":{"value":"2018-08-31","dateFormat":"YYYY-MM-DD"},"EndOfSvcAttachDate":{"value":"2014-08-27","dateFormat":"YYYY-MM-DD"},"UpdatedTimeStamp":{"value":"2013-02-26","dateFormat":"YYYY-MM-DD"},"EOXMigrationDetails":{"PIDActiveFlag":"Y","MigrationInformation":"ACS application & BASE license for SNS-3415-K9 appliance","MigrationOption":"Enter PID(s)","MigrationProductId":"CSACS-3415-K9","MigrationProductName":"","MigrationStrategy":"","MigrationProductInfoURL":"http://www.cisco.com/en/US/prod/collateral/netmgtsw/ps5698/ps6767/ps9911/data_sheet_c78-715717.html"},"EOXInputType":"ShowEOXByPids","EOXInputValue":"CSACS-1121-K9 "}]} diff --git a/netbox_cisco_support/management/commands/sync_eox_data.py b/netbox_cisco_support/management/commands/sync_eox_data.py index 7fefea8..e7d0d47 100644 --- a/netbox_cisco_support/management/commands/sync_eox_data.py +++ b/netbox_cisco_support/management/commands/sync_eox_data.py @@ -4,9 +4,11 @@ import django.utils.text from django.core.management.base import BaseCommand, CommandError +from datetime import datetime from requests import api from dcim.models import Manufacturer from dcim.models import Device, DeviceType +from netbox_cisco_support.models import CiscoDeviceTypeSupport class Command(BaseCommand): @@ -22,30 +24,127 @@ def add_arguments(self, parser): ) def update_device_type_eox_data(self, pid, eox_data): - print(eox_data['EOXRecord'][0]['EOXInputValue']) - - end_of_sale_date = eox_data['EOXRecord'][0]['EndOfSaleDate']['value'] - end_of_sw_maintenance_releases = eox_data['EOXRecord'][0]['EndOfSWMaintenanceReleases']['value'] - end_of_security_vul_support_date = eox_data['EOXRecord'][0]['EndOfSecurityVulSupportDate']['value'] - end_of_routine_failure_analysis_date = eox_data['EOXRecord'][0]['EndOfRoutineFailureAnalysisDate']['value'] - end_of_service_contract_renewal = eox_data['EOXRecord'][0]['EndOfServiceContractRenewal']['value'] - last_date_of_support = eox_data['EOXRecord'][0]['LastDateOfSupport']['value'] - end_of_svc_attach_date = eox_data['EOXRecord'][0]['EndOfSvcAttachDate']['value'] - - print("End Of Sale: %s" % end_of_sale_date) - print("End Of SW Maintenance: %s" % end_of_sw_maintenance_releases) - print("End Of Security Vuln Support: %s" % end_of_security_vul_support_date) - print("End Of Routine Failure Analysis: %s" % end_of_routine_failure_analysis_date) - print("End Of Service Contract Renewal: %s" % end_of_service_contract_renewal) - print("Last Date Of Support: %s" % last_date_of_support) - print("End Of Service Attach Date: %s" % end_of_svc_attach_date) + # Get the device type object for the supplied PID + dt = DeviceType.objects.get(part_number=pid) - return + # Check if CiscoDeviceTypeSupport record already exists + try: + dts = CiscoDeviceTypeSupport.objects.get(device_type=dt) + # If not, create a new one for this Device Type + except CiscoDeviceTypeSupport.DoesNotExist: + dts = CiscoDeviceTypeSupport(device_type=dt) - def get_product_ids(self, manufacturer): - i = 0 - product_ids = [] + # Only save if something has changed + value_changed = False + + try: + # Check if JSON contains EndOfSaleDate with value field + if not eox_data["EOXRecord"][0]["EndOfSaleDate"]["value"]: + self.stdout.write(self.style.NOTICE("%s has no end_of_sale_date" % pid)) + else: + end_of_sale_date_string = eox_data["EOXRecord"][0]["EndOfSaleDate"]["value"] + # Cast this value to datetime.date object + end_of_sale_date = datetime.strptime(end_of_sale_date_string, '%Y-%m-%d').date() + self.stdout.write(self.style.SUCCESS("%s - end_of_sale_date: %s" % (pid, end_of_sale_date))) + + # Check if CiscoDeviceTypeSupport end_of_sale_date differs from JSON EndOfSaleDate, update if true + if dts.end_of_sale_date != end_of_sale_date: + dts.end_of_sale_date = end_of_sale_date + value_changed = True + # Do nothing when JSON field does not exist + except KeyError: + self.stdout.write(self.style.NOTICE("%s has no end_of_sale_date" % pid)) + + try: + if not eox_data["EOXRecord"][0]["EndOfSWMaintenanceReleases"]["value"]: + self.stdout.write(self.style.NOTICE("%s has no end_of_sw_maintenance_releases" % pid)) + else: + end_of_sw_maintenance_releases_string = eox_data["EOXRecord"][0]["EndOfSaleDate"]["value"] + end_of_sw_maintenance_releases = datetime.strptime(end_of_sw_maintenance_releases_string, '%Y-%m-%d').date() + self.stdout.write(self.style.SUCCESS("%s - end_of_sw_maintenance_releases: %s" % (pid, end_of_sw_maintenance_releases))) + + if dts.end_of_sw_maintenance_releases != end_of_sw_maintenance_releases: + dts.end_of_sw_maintenance_releases = end_of_sw_maintenance_releases + value_changed = True + except KeyError: + self.stdout.write(self.style.NOTICE("%s has no end_of_sw_maintenance_releases" % pid)) + + try: + if not eox_data["EOXRecord"][0]["EndOfSecurityVulSupportDate"]["value"]: + self.stdout.write(self.style.NOTICE("%s has no end_of_security_vul_support_date" % pid)) + else: + end_of_security_vul_support_date_string = eox_data["EOXRecord"][0]["EndOfSecurityVulSupportDate"]["value"] + end_of_security_vul_support_date = datetime.strptime(end_of_security_vul_support_date_string, '%Y-%m-%d').date() + self.stdout.write(self.style.SUCCESS("%s - end_of_security_vul_support_date: %s" % (pid, end_of_security_vul_support_date))) + + if dts.end_of_security_vul_support_date != end_of_security_vul_support_date: + dts.end_of_security_vul_support_date = end_of_security_vul_support_date + value_changed = True + except KeyError: + self.stdout.write(self.style.NOTICE("%s has no end_of_security_vul_support_date" % pid)) + + try: + if not eox_data["EOXRecord"][0]["EndOfRoutineFailureAnalysisDate"]["value"]: + self.stdout.write(self.style.NOTICE("%s has no end_of_routine_failure_analysis_date" % pid)) + else: + end_of_routine_failure_analysis_date_string = eox_data["EOXRecord"][0]["EndOfRoutineFailureAnalysisDate"]["value"] + end_of_routine_failure_analysis_date = datetime.strptime(end_of_routine_failure_analysis_date_string, '%Y-%m-%d').date() + self.stdout.write(self.style.SUCCESS("%s - end_of_routine_failure_analysis_date: %s" % (pid, end_of_routine_failure_analysis_date))) + + if dts.end_of_routine_failure_analysis_date != end_of_routine_failure_analysis_date: + dts.end_of_routine_failure_analysis_date = end_of_routine_failure_analysis_date + value_changed = True + except KeyError: + self.stdout.write(self.style.NOTICE("%s has no end_of_routine_failure_analysis_date" % pid)) + + try: + if not eox_data["EOXRecord"][0]["EndOfServiceContractRenewal"]["value"]: + self.stdout.write(self.style.NOTICE("%s has no end_of_service_contract_renewal" % pid)) + else: + end_of_service_contract_renewal_string = eox_data["EOXRecord"][0]["EndOfServiceContractRenewal"]["value"] + end_of_service_contract_renewal = datetime.strptime(end_of_service_contract_renewal_string, '%Y-%m-%d').date() + self.stdout.write(self.style.SUCCESS("%s - end_of_service_contract_renewal: %s" % (pid, end_of_service_contract_renewal))) + + if dts.end_of_service_contract_renewal != end_of_service_contract_renewal: + dts.end_of_service_contract_renewal = end_of_service_contract_renewal + value_changed = True + except KeyError: + self.stdout.write(self.style.NOTICE("%s has no end_of_service_contract_renewal" % pid)) + + try: + if not eox_data["EOXRecord"][0]["LastDateOfSupport"]["value"]: + self.stdout.write(self.style.NOTICE("%s has no last_date_of_support" % pid)) + else: + last_date_of_support_string = eox_data["EOXRecord"][0]["EndOfSaleDate"]["value"] + last_date_of_support = datetime.strptime(last_date_of_support_string, '%Y-%m-%d').date() + self.stdout.write(self.style.SUCCESS("%s - last_date_of_support: %s" % (pid, last_date_of_support))) + + if dts.last_date_of_support != last_date_of_support: + dts.last_date_of_support = last_date_of_support + value_changed = True + except KeyError: + self.stdout.write(self.style.NOTICE("%s has no last_date_of_support" % pid)) + + try: + if not eox_data["EOXRecord"][0]["EndOfSvcAttachDate"]["value"]: + self.stdout.write(self.style.NOTICE("%s has no end_of_svc_attach_date" % pid)) + else: + end_of_svc_attach_date_string = eox_data["EOXRecord"][0]["EndOfSvcAttachDate"]["value"] + end_of_svc_attach_date = datetime.strptime(end_of_svc_attach_date_string, '%Y-%m-%d').date() + self.stdout.write(self.style.SUCCESS("%s - end_of_svc_attach_date: %s" % (pid, end_of_svc_attach_date))) + + if dts.end_of_svc_attach_date != end_of_svc_attach_date: + dts.end_of_svc_attach_date = end_of_svc_attach_date + value_changed = True + except KeyError: + self.stdout.write(self.style.NOTICE("%s has no end_of_svc_attach_date" % pid)) + + if value_changed: + dts.save() + + return + def get_device_types(self, manufacturer): # trying to get the right manufacturer for this plugin try: m = Manufacturer.objects.get(name=manufacturer) @@ -60,6 +159,14 @@ def get_product_ids(self, manufacturer): except DeviceType.DoesNotExist: raise CommandError('Manufacturer "%s" has no Device Types' % m) + return dt + + def get_product_ids(self, manufacturer): + i = 0 + product_ids = [] + + dt = self.get_device_types(manufacturer) + for device_type in dt: # Skip if the device type has no valid part number. Part numbers must match the exact Cisco Base PID @@ -71,21 +178,25 @@ def get_product_ids(self, manufacturer): self.stdout.write(self.style.SUCCESS('Found device type "%s" with Part Number "%s"' % (device_type, device_type.part_number))) product_ids.append(device_type.part_number) - # trying to get all devices and its serial numbers for this device type (for contract data) - try: - d = Device.objects.filter(device_type=device_type) - except Device.DoesNotExist: - raise CommandError('Device "%s" does not exist' % d) + return product_ids + + def get_serial_numbers(self, dtype): + # trying to get all devices and its serial numbers for this device type (for contract data) + try: + d = Device.objects.filter(device_type=dtype) for device in d: + + # Skip if the device has no valid serial number. if not device.serial: self.stdout.write(self.style.WARNING('Found device "%s" WITHOUT Serial Number - SKIPPING' % (device))) continue - self.stdout.write(self.style.SUCCESS('%s: Found device "%s" with Serial Number "%s"' % (i, device, device.serial))) - i += 1 - - return product_ids + # TODO - Add serial number to a list and do something with it rather than displaying. + self.stdout.write(self.style.SUCCESS('Found device "%s" with Serial Number "%s"' % (device, device.serial))) + except Device.DoesNotExist: + raise CommandError('Device with device type "%s" does not exist' % dtype) + return def logon(self): token_url = "https://cloudsso.cisco.com/as/token.oauth2" @@ -104,16 +215,16 @@ def logon(self): def handle(self, *args, **kwargs): product_ids = self.get_product_ids(kwargs['manufacturer']) - # with open('/source/netbox_cisco_support/ACS.json') as json_file: - # data = json.load(json_file) - - # print(json.dumps(data, sort_keys=True, indent=4)) - - # finished collecting data, making API calls + self.stdout.write(self.style.SUCCESS('Gathering data for these PIDs: ' + ', '.join(product_ids))) api_call_headers = self.logon() + i = 1 + for pid in product_ids: + # if i == 10: + # break + url = 'https://api.cisco.com/supporttools/eox/rest/5/EOXByProductID/1/%s?responseencoding=json' % pid api_call_response = requests.get(url, headers=api_call_headers) self.stdout.write(self.style.SUCCESS('Call ' + url)) @@ -126,3 +237,7 @@ def handle(self, *args, **kwargs): outfile.write(api_call_response.text) data = json.loads(api_call_response.text) + + self.update_device_type_eox_data(pid, data) + + i += 1 diff --git a/netbox_cisco_support/migrations/0005_auto_20210914_1344.py b/netbox_cisco_support/migrations/0005_auto_20210914_1344.py new file mode 100644 index 0000000..a8d5001 --- /dev/null +++ b/netbox_cisco_support/migrations/0005_auto_20210914_1344.py @@ -0,0 +1,48 @@ +# Generated by Django 3.2.6 on 2021-09-14 13:44 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('netbox_cisco_support', '0004_ciscosupport_is_covered'), + ] + + operations = [ + migrations.AlterField( + model_name='ciscodevicetypesupport', + name='end_of_routine_failure_analysis_date', + field=models.DateField(blank=True, null=True), + ), + migrations.AlterField( + model_name='ciscodevicetypesupport', + name='end_of_sale_date', + field=models.DateField(blank=True, null=True), + ), + migrations.AlterField( + model_name='ciscodevicetypesupport', + name='end_of_security_vul_support_date', + field=models.DateField(blank=True, null=True), + ), + migrations.AlterField( + model_name='ciscodevicetypesupport', + name='end_of_service_contract_renewal', + field=models.DateField(blank=True, null=True), + ), + migrations.AlterField( + model_name='ciscodevicetypesupport', + name='end_of_svc_attach_date', + field=models.DateField(blank=True, null=True), + ), + migrations.AlterField( + model_name='ciscodevicetypesupport', + name='end_of_sw_maintenance_releases', + field=models.DateField(blank=True, null=True), + ), + migrations.AlterField( + model_name='ciscodevicetypesupport', + name='last_date_of_support', + field=models.DateField(blank=True, null=True), + ), + ] diff --git a/netbox_cisco_support/models.py b/netbox_cisco_support/models.py index a9aad25..0f7c89c 100644 --- a/netbox_cisco_support/models.py +++ b/netbox_cisco_support/models.py @@ -16,39 +16,53 @@ def __str__(self): end_of_sale_date = models.DateField( help_text='Last date to order the requested product through Cisco point-of-sale mechanisms. The product is no ' - 'longer for sale after this date.' + 'longer for sale after this date.', + blank=True, + null=True ) end_of_sw_maintenance_releases = models.DateField( help_text='Last date that Cisco Engineering might release any software maintenance releases or bug fixes to ' 'the software product. After this date, Cisco Engineering no longer develops, repairs, maintains, or ' - 'tests the product software.' + 'tests the product software.', + blank=True, + null=True ) end_of_security_vul_support_date = models.DateField( help_text='Last date that Cisco Engineering may release a planned maintenance release or scheduled software ' - 'remedy for a security vulnerability issue.' + 'remedy for a security vulnerability issue.', + blank=True, + null=True ) end_of_routine_failure_analysis_date = models.DateField( help_text='Last date Cisco might perform a routine failure analysis to determine the root cause of an ' - 'engineering-related or manufacturing-related issue.' + 'engineering-related or manufacturing-related issue.', + blank=True, + null=True ) end_of_service_contract_renewal = models.DateField( help_text='Last date to extend or renew a service contract for the product. The extension or renewal period ' - 'cannot extend beyond the last date of support.' + 'cannot extend beyond the last date of support.', + blank=True, + null=True ) last_date_of_support = models.DateField( help_text='Last date to receive service and support for the product. After this date, all support services for ' - 'the product are unavailable, and the product becomes obsolete.' + 'the product are unavailable, and the product becomes obsolete.', + blank=True, + null=True ) end_of_svc_attach_date = models.DateField( help_text='Last date to order a new service-and-support contract or add the equipment and/or software to an ' 'existing service-and-support contract for equipment and software that is not covered by a ' - 'service-and-support contract.' + 'service-and-support contract.', + blank=True, + null=True ) diff --git a/netbox_cisco_support/templatetags/filters.py b/netbox_cisco_support/templatetags/filters.py index 637a3a9..1448131 100644 --- a/netbox_cisco_support/templatetags/filters.py +++ b/netbox_cisco_support/templatetags/filters.py @@ -15,6 +15,9 @@ def expires_next_year(value): @register.filter(is_safe=True) def expiration_class(value): + if not value: + return + if is_expired(value): return mark_safe('class="danger"') elif expires_next_year(value):