From c52fd0802ed2395fa3a62ec41ec0297b0bca373e Mon Sep 17 00:00:00 2001 From: Jon Date: Mon, 20 May 2024 16:42:31 +0930 Subject: [PATCH] feat(api): API accept computer inventory !8 closes #2 --- app/api/urls.py | 6 +- app/api/views/itam/device/__init__.py | 0 .../itam/{device.py => device/detail.py} | 0 app/api/views/itam/device/inventory.py | 259 ++++++++++++++++++ app/core/http/common.py | 17 ++ docs/projects/django-template/api.md | 44 ++- 6 files changed, 323 insertions(+), 3 deletions(-) create mode 100644 app/api/views/itam/device/__init__.py rename app/api/views/itam/{device.py => device/detail.py} (100%) create mode 100644 app/api/views/itam/device/inventory.py create mode 100644 app/core/http/common.py diff --git a/app/api/urls.py b/app/api/urls.py index 2eb1a6a5..f4ffe80c 100644 --- a/app/api/urls.py +++ b/app/api/urls.py @@ -3,7 +3,9 @@ from rest_framework.urlpatterns import format_suffix_patterns from .views import access, index -from .views.itam import device as itam_device, software as itam_software, config as itam_config +from .views.itam import software as itam_software, config as itam_config +from .views.itam.device import detail as itam_device +from .views.itam.device import inventory urlpatterns = [ path("", index.IndexView.as_view(), name='_api_home'), @@ -21,6 +23,8 @@ urlpatterns = [ path("software/", itam_software.List.as_view(), name="_api_softwares"), path("software//", itam_software.Detail.as_view(), name="_api_software_view"), + path("device/inventory/", inventory.Collect.as_view(), name="_api_device_inventory"), + ] urlpatterns = format_suffix_patterns(urlpatterns) diff --git a/app/api/views/itam/device/__init__.py b/app/api/views/itam/device/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/app/api/views/itam/device.py b/app/api/views/itam/device/detail.py similarity index 100% rename from app/api/views/itam/device.py rename to app/api/views/itam/device/detail.py diff --git a/app/api/views/itam/device/inventory.py b/app/api/views/itam/device/inventory.py new file mode 100644 index 00000000..bf9d615e --- /dev/null +++ b/app/api/views/itam/device/inventory.py @@ -0,0 +1,259 @@ +# from django.contrib.auth.mixins import PermissionRequiredMixin, LoginRequiredMixin +import json +import re + +from django.http import JsonResponse +from django.utils import timezone + +from rest_framework import generics, views +from rest_framework.response import Response + +from access.models import Organization + +from core.http.common import Http + +from itam.models.device import Device, DeviceType, DeviceOperatingSystem, DeviceSoftware +from itam.models.operating_system import OperatingSystem, OperatingSystemVersion +from itam.models.software import Software, SoftwareCategory, SoftwareVersion + + + +class Collect(views.APIView): + + def post(self, request, *args, **kwargs): + + data = json.loads(request.body) + + status = Http.Status.BAD_REQUEST + + device = None + device_operating_system = None + operating_system = None + operating_system_version = None + + organization = Organization.objects.get(pk=1) + + try: + + if Device.objects.filter(name=data['details']['name']).exists(): + + device = Device.objects.get(name=data['details']['name']) + + else: # Create the device + + device = Device.objects.create( + name = data['details']['name'], + device_type = DeviceType.objects.get(pk=1), + serial_number = data['details']['serial_number'], + uuid = data['details']['uuid'], + organization = organization, + ) + + status = Http.Status.CREATED + + + if OperatingSystem.objects.filter( slug=data['os']['name'] ).exists(): + + operating_system = OperatingSystem.objects.get( slug=data['os']['name'] ) + + else: # Create Operating System + + operating_system = OperatingSystem.objects.create( + name = data['os']['name'], + organization = device.organization, + is_global = True + ) + + + if OperatingSystemVersion.objects.filter( name=data['os']['version_major'], operating_system=operating_system ).exists(): + + operating_system_version = OperatingSystemVersion.objects.get( + organization = device.organization, + is_global = True, + name = data['os']['version_major'], + operating_system = operating_system + ) + + else: # Create Operating System Version + + operating_system_version = OperatingSystemVersion.objects.create( + organization = device.organization, + is_global = True, + name = data['os']['version_major'], + operating_system = operating_system, + ) + + + if DeviceOperatingSystem.objects.filter( version=data['os']['version'], device=device, operating_system_version=operating_system_version ).exists(): + + device_operating_system = DeviceOperatingSystem.objects.get( + device=device, + version = data['os']['version'], + operating_system_version = operating_system_version, + ) + + if not device_operating_system.installdate: # Only update install date if empty + + device_operating_system.installdate = timezone.now() + + device_operating_system.save() + + else: # Create Operating System Version + + device_operating_system = DeviceOperatingSystem.objects.create( + organization = device.organization, + device=device, + version = data['os']['version'], + operating_system_version = operating_system_version, + installdate = timezone.now() + ) + + + for inventory in list(data['software']): + + software = None + software_category = None + software_version = None + + device_software = None + + + if SoftwareCategory.objects.filter( name = inventory['category'] ).exists(): + + software_category = SoftwareCategory.objects.get( + name = inventory['category'] + ) + + else: # Create Software Category + + software_category = SoftwareCategory.objects.create( + organization = device.organization, + is_global = True, + name = inventory['category'], + ) + + + if Software.objects.filter( name = inventory['name'] ).exists(): + + software = Software.objects.get( + name = inventory['name'] + ) + + if not software.category: + + software.category = software_category + software.save() + + else: # Create Software + + software = Software.objects.create( + organization = device.organization, + is_global = True, + name = inventory['name'], + category = software_category, + ) + + + pattern = r"^(\d+:)?(?P\d+\.\d+(\.\d+)?)" + + semver = re.search(pattern, str(inventory['version']), re.DOTALL) + + + if semver: + + semver = semver['semver'] + + else: + semver = inventory['version'] + + + if SoftwareVersion.objects.filter( name = semver, software = software ).exists(): + + software_version = SoftwareVersion.objects.get( + name = semver, + software = software, + ) + + else: # Create Software Category + + software_version = SoftwareVersion.objects.create( + organization = device.organization, + is_global = True, + name = semver, + software = software, + ) + + + if DeviceSoftware.objects.filter( software = software, device=device ).exists(): + + device_software = DeviceSoftware.objects.get( + device = device, + software = software + ) + + else: # Create Software + + device_software = DeviceSoftware.objects.create( + organization = device.organization, + is_global = True, + installedversion = software_version, + software = software, + device = device, + action=None + ) + + + if device_software: # Update the Inventoried software + + clear_installed_software = DeviceSoftware.objects.filter( + device = device, + software = software + ) + + # Clear installed version of all installed software + # any found later with no version to be removed + clear_installed_software.update(installedversion=None) + + + if not device_software.installed: # Only update install date if blank + + device_software.installed = timezone.now() + + device_software.save() + + device_software.installedversion = software_version + + device_software.save() + + + if not device_software.installed: # Only update install date if blank + + device_software.installed = timezone.now() + + device_software.save() + + + if device and operating_system and operating_system_version and device_operating_system: + + # Remove software no longer installed + DeviceSoftware.objects.filter( + device = device, + software = software, + ).delete() + + status = Http.Status.OK + + + except Exception as e: + + print(f'An error occured{e}') + + status = Http.Status.SERVER_ERROR + + + return Response(data='OK',status=status) + + + + def get_view_name(self): + return "Inventory" diff --git a/app/core/http/common.py b/app/core/http/common.py new file mode 100644 index 00000000..1f5e4c8a --- /dev/null +++ b/app/core/http/common.py @@ -0,0 +1,17 @@ +from enum import IntEnum + + + +class Http(): + """Common HTTP Related objects""" + + + class Status(IntEnum): + """HTTP server status codes.""" + + OK = 200 + CREATED = 201 + + BAD_REQUEST = 400 + + SERVER_ERROR = 500 diff --git a/docs/projects/django-template/api.md b/docs/projects/django-template/api.md index 56cf3b50..3de67ad8 100644 --- a/docs/projects/django-template/api.md +++ b/docs/projects/django-template/api.md @@ -7,8 +7,6 @@ about: https://gitlab.com/nofusscomputing/infrastructure/configuration-managemen --- -## Access - to access the api, it can be done with the following command: ``` bash @@ -18,6 +16,48 @@ curl -X GET http://127.0.0.1:8000/api/ -H 'Authorization: Token ' ``` +## Features + +- Inventory Report Collection + + +## Inventory Reports + +- url `/api/device/inventory/` + +- method `POST` + +- content `application/json` + +Passing a valid inventory report to this endpoint will update the device within the app if the device already exists. + +Report Format + +``` json + +{ + "details": { + "name": "string", + "serial_number": "string", + "uuid": "string" + }, + "os": { + "name": "debian|ubuntu", + "version_major": "major version number", + "version": "as reported" + }, + "software": [ + { + "name": "string", + "category": "string", + "version": "string" + } + ] +} + + +``` + ## User Token To generate a user token to access the api, use command `python3 manage.py drf_create_token `