feat(itam): Add Inventory API v2 endpoint

ref: #248 #383
This commit is contained in:
2024-11-08 18:29:53 +09:30
parent 5987e62063
commit 1c5fb0de18
6 changed files with 302 additions and 0 deletions

View File

@ -54,6 +54,7 @@ from itam.viewsets import (
device_model as device_model_v2,
device_type as device_type_v2,
device_software as device_software_v2,
inventory,
operating_system as operating_system_v2,
operating_system_version as operating_system_version_v2,
software as software_v2,
@ -134,6 +135,7 @@ router.register('itam/device', device_v2.ViewSet, basename='_api_v2_device')
router.register('itam/device/(?P<device_id>[0-9]+)/software', device_software_v2.ViewSet, basename='_api_v2_device_software')
router.register('itam/device/(?P<device_id>[0-9]+)/service', service_device_v2.ViewSet, basename='_api_v2_service_device')
router.register('itam/device/(?P<device_id>[0-9]+)/notes', notes_v2.ViewSet, basename='_api_v2_device_notes')
router.register('itam/inventory', inventory.ViewSet, basename='_api_v2_inventory')
router.register('itam/operating_system', operating_system_v2.ViewSet, basename='_api_v2_operating_system')
router.register('itam/operating_system/(?P<operating_system_id>[0-9]+)/notes', notes_v2.ViewSet, basename='_api_v2_operating_system_notes')
router.register('itam/operating_system/(?P<operating_system_id>[0-9]+)/version', operating_system_version_v2.ViewSet, basename='_api_v2_operating_system_version')

View File

@ -33,6 +33,7 @@ class InventoryPermissions(OrganizationPermissionAPI):
@extend_schema( deprecated = True )
class Collect(OrganizationPermissionAPI, views.APIView):
queryset = Device.objects.all()

View File

@ -225,6 +225,15 @@ class ModelViewSet(
class ModelCreateViewSet(
ModelViewSetBase,
viewsets.mixins.CreateModelMixin,
):
pass
class ModelListRetrieveDeleteViewSet(
viewsets.mixins.ListModelMixin,
viewsets.mixins.RetrieveModelMixin,

View File

@ -0,0 +1,93 @@
from django.urls import reverse
from rest_framework import serializers
from core import exceptions as centurion_exceptions
from itam.models.device import Device
class InventorySerializer(serializers.Serializer):
""" Serializer for Inventory Upload """
class DetailsSerializer(serializers.Serializer):
name = serializers.CharField(
help_text = 'Host name',
required = True
)
serial_number = serializers.CharField(
default = None,
help_text = 'Devices serial number',
required = False
)
uuid = serializers.CharField(
default = None,
help_text = 'Device system UUID',
required = False
)
def validate(self, data):
if(
data['serial_number'] is None
and data['uuid'] is None
):
raise centurion_exceptions.ValidationError(
detail = 'Serial Number or UUID is required',
code = 'no_serial_or_uuid'
)
return data
class OperatingSystemSerializer(serializers.Serializer):
name = serializers.CharField(
help_text='Name of the operating system installed on the device',
required = True,
)
version_major = serializers.IntegerField(
help_text='Major semver version number of the OS version',
required = True,
)
version = serializers.CharField(
help_text='semver version number of the OS',
required = True
)
class SoftwareSerializer(serializers.Serializer):
name = serializers.CharField(
help_text='Name of the software',
required = True
)
category = serializers.CharField(
help_text='Category of the software',
default = None,
required = False
)
version = serializers.CharField(
default = None,
help_text='semver version number of the software',
required = False
)
details = DetailsSerializer()
os = OperatingSystemSerializer( required = False )
software = SoftwareSerializer( many = True, required = False )

View File

@ -26,6 +26,7 @@ class Index(CommonViewSet):
return Response(
{
"device": reverse('v2:_api_v2_device-list', request=request),
"inventory": reverse('v2:_api_v2_inventory-list', request=request),
"operating_system": reverse('v2:_api_v2_operating_system-list', request=request),
"software": reverse('v2:_api_v2_software-list', request=request)
}

View File

@ -0,0 +1,196 @@
import json
from drf_spectacular.utils import extend_schema, extend_schema_view, OpenApiResponse
from rest_framework.response import Response
from api.tasks import process_inventory
from api.viewsets.common import ModelCreateViewSet
from core import exceptions as centurion_exception
from core.http.common import Http
from itam.models.device import Device
from itam.serializers.inventory import InventorySerializer
from settings.models.user_settings import UserSettings
@extend_schema_view(
create=extend_schema(
summary = "Upload a device's inventory",
description = """After inventorying a device, it's inventory file, `.json` is uploaded to this endpoint.
If the device does not exist, it will be created. If the device does exist the existing
device will be updated with the information within the inventory.
matching for an existing device is by slug which is the hostname converted to lower case
letters. This conversion is automagic.
**NOTE:** _for device creation, the API user must have user setting 'Default Organization'. Without
this setting populated, no device will be created and the endpoint will return HTTP/403_
## Permissions
- `itam.add_device` Required to upload inventory
""",
request = InventorySerializer,
responses = {
200: OpenApiResponse(
description='Inventory upload successful',
response = {
'OK'
}
),
400: OpenApiResponse(description='Error Occured, see output retured'),
401: OpenApiResponse(description='User Not logged in'),
403: OpenApiResponse(description='User is missing permission or in different organization'),
500: OpenApiResponse(description='Exception occured. View server logs for the Stack Trace'),
}
)
)
class ViewSet( ModelCreateViewSet ):
"""Device Inventory
Use this endpoint to upload your device inventories.
"""
model = Device
serializer_class = InventorySerializer
documentation: str = 'https://nofusscomputing.com/docs/not_model_docs'
view_name = 'Device Inventory'
view_description = __doc__
inventory_action: str = None
"""Inventory action, choice. new|update"""
def create(self, request, *args, **kwargs):
"""Upload a device inventory
Raises:
centurion_exceptions.PermissionDenied: User is missing the required permissions
Returns:
Response: string denoting what has occured
"""
status = Http.Status.OK
response_data = 'OK'
try:
data = InventorySerializer(
data = request.data
)
device = None
if not data.is_valid():
raise centurion_exception.ValidationError(
detail = 'Uploaded inventory is not valid',
code = 'invalid_inventory'
)
self.default_organization = UserSettings.objects.get(user=request.user).default_organization
if Device.objects.filter(slug=str(data.validated_data['details']['name']).lower()).exists():
self.obj = Device.objects.get(slug=str(data.validated_data['details']['name']).lower())
device = self.obj
task = process_inventory.delay(data.validated_data, self.default_organization.id)
response_data: dict = {"task_id": f"{task.id}"}
except centurion_exception.PermissionDenied as e:
status = Http.Status.FORBIDDEN
response_data = e.detail
except centurion_exception.ValidationError as e:
status = Http.Status.BAD_REQUEST
response_data = e.detail
except Exception as e:
print(f'An error occured{e}')
status = Http.Status.SERVER_ERROR
response_data = f'Unknown Server Error occured: {e}'
return Response(data=response_data,status=status)
def get_dynamic_permissions(self):
"""Obtain the permissions required to upload an inventory.
Returns:
list: Permissions required for Inventory Upload
"""
organization = None
device_search = None
if 'details' in self.request.data:
if 'name' in self.request.data['details']:
device_search = Device.objects.filter(
slug = str(self.request.data['details']['name']).lower()
)
else:
centurion_exception.ParseError(
detail = {
'name': 'Device name is required'
},
code = 'missing_device_name'
)
else:
centurion_exception.ParseError(
detail = {
'details': 'Details dict is required'
},
code = 'missing_details_dict'
)
if device_search: # Existing device
if len(list(device_search)) == 1:
self.obj = list(device_search)[0]
self.permission_required = [
'itam.change_device'
]
self.inventory_action = 'update'
else: # New device
self.permission_required = [
'itam.add_device'
]
self.inventory_action = 'new'
return super().get_permission_required()