Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Clean up asset classes #2494

Open
wants to merge 16 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions care/facility/api/viewsets/asset.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@
from care.facility.models.bed import AssetBed, ConsultationBed
from care.users.models import User
from care.utils.assetintegration.asset_classes import AssetClasses
from care.utils.assetintegration.base import BaseAssetIntegration
from care.utils.cache.cache_allowed_facilities import get_accessible_facilities
from care.utils.filters.choicefilter import CareChoiceFilter, inverse_choices
from care.utils.queryset.asset_bed import get_asset_queryset
Expand Down Expand Up @@ -389,7 +390,6 @@ def operate_assets(self, request, *args, **kwargs):
This API is used to operate assets. API accepts the asset_id and action as parameters.
"""
try:
action = request.data["action"]
asset: Asset = self.get_object()
middleware_hostname = (
asset.meta.get(
Expand All @@ -405,7 +405,7 @@ def operate_assets(self, request, *args, **kwargs):
"middleware_hostname": middleware_hostname,
}
)
result = asset_class.handle_action(action)
result = asset_class.handle_action(**request.data["action"])
return Response({"result": result}, status=status.HTTP_200_OK)

except ValidationError as e:
Expand Down
141 changes: 141 additions & 0 deletions care/facility/tests/test_asset_api.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
from django.utils.timezone import now, timedelta
from rest_framework import status
from rest_framework.exceptions import ValidationError
from rest_framework.test import APITestCase

from care.facility.models import Asset, Bed
from care.users.models import User
from care.utils.assetintegration.asset_classes import AssetClasses
from care.utils.assetintegration.hl7monitor import HL7MonitorAsset
from care.utils.assetintegration.onvif import OnvifAsset
from care.utils.assetintegration.ventilator import VentilatorAsset
from care.utils.tests.test_utils import TestUtils


Expand All @@ -31,6 +35,143 @@ def setUp(self) -> None:
super().setUp()
self.asset = self.create_asset(self.asset_location)

def validate_invalid_meta(self, asset_class, meta):
with self.assertRaises(ValidationError):
asset_class(meta)

def test_meta_validations_for_onvif_asset(self):
valid_meta = {
"local_ip_address": "192.168.0.1",
"camera_access_key": "username:password:access_key",
"middleware_hostname": "middleware.local",
"insecure_connection": True,
}
onvif_asset = OnvifAsset(valid_meta)
self.assertEqual(onvif_asset.middleware_hostname, "middleware.local")
self.assertEqual(onvif_asset.host, "192.168.0.1")
self.assertEqual(onvif_asset.username, "username")
self.assertEqual(onvif_asset.password, "password")
self.assertEqual(onvif_asset.access_key, "access_key")
self.assertTrue(onvif_asset.insecure_connection)

invalid_meta_cases = [
# Invalid format for camera_access_key
{
"id": "123",
"local_ip_address": "192.168.0.1",
"middleware_hostname": "middleware.local",
"camera_access_key": "invalid_format",
},
# Missing username/password in camera_access_key
{
"local_ip_address": "192.168.0.1",
"middleware_hostname": "middleware.local",
"camera_access_key": "invalid_format",
},
# Missing middleware_hostname
{
"local_ip_address": "192.168.0.1",
"camera_access_key": "username:password:access_key",
},
# Missing local_ip_address
{
"middleware_hostname": "middleware.local",
"camera_access_key": "username:password:access_key",
},
# Invalid value for insecure_connection
{
"local_ip_address": "192.168.0.1",
"camera_access_key": "username:password:access_key",
"middleware_hostname": "middleware.local",
"insecure_connection": "invalid_value",
},
]
for meta in invalid_meta_cases:
self.validate_invalid_meta(OnvifAsset, meta)

def test_meta_validations_for_ventilator_asset(self):
valid_meta = {
"id": "123",
"local_ip_address": "192.168.0.1",
"middleware_hostname": "middleware.local",
"insecure_connection": True,
}
ventilator_asset = VentilatorAsset(valid_meta)
self.assertEqual(ventilator_asset.middleware_hostname, "middleware.local")
self.assertEqual(ventilator_asset.host, "192.168.0.1")
self.assertTrue(ventilator_asset.insecure_connection)

invalid_meta_cases = [
# Missing id
{
"local_ip_address": "192.168.0.1",
"middleware_hostname": "middleware.local",
},
# Missing middleware_hostname
{"id": "123", "local_ip_address": "192.168.0.1"},
# Missing local_ip_address
{"id": "123", "middleware_hostname": "middleware.local"},
# Invalid insecure_connection
{
"id": "123",
"local_ip_address": "192.168.0.1",
"middleware_hostname": "middleware.local",
"insecure_connection": "invalid_value",
},
# Camera access key not required for ventilator, invalid meta
{
"id": "21",
"local_ip_address": "192.168.0.1",
"camera_access_key": "username:password:access_key",
"middleware_hostname": "middleware.local",
"insecure_connection": True,
},
]
for meta in invalid_meta_cases:
self.validate_invalid_meta(VentilatorAsset, meta)

def test_meta_validations_for_hl7monitor_asset(self):
valid_meta = {
"id": "123",
"local_ip_address": "192.168.0.1",
"middleware_hostname": "middleware.local",
"insecure_connection": True,
}
hl7monitor_asset = HL7MonitorAsset(valid_meta)
self.assertEqual(hl7monitor_asset.middleware_hostname, "middleware.local")
self.assertEqual(hl7monitor_asset.host, "192.168.0.1")
self.assertEqual(hl7monitor_asset.id, "123")
self.assertTrue(hl7monitor_asset.insecure_connection)

invalid_meta_cases = [
# Missing id
{
"local_ip_address": "192.168.0.1",
"middleware_hostname": "middleware.local",
},
# Missing middleware_hostname
{"id": "123", "local_ip_address": "192.168.0.1"},
# Missing local_ip_address
{"id": "123", "middleware_hostname": "middleware.local"},
# Invalid insecure_connection
{
"id": "123",
"local_ip_address": "192.168.0.1",
"middleware_hostname": "middleware.local",
"insecure_connection": "invalid_value",
},
# Camera access key not required for HL7Monitor, invalid meta
{
"id": "123",
"local_ip_address": "192.168.0.1",
"camera_access_key": "username:password:access_key",
"middleware_hostname": "middleware.local",
"insecure_connection": True,
},
]
for meta in invalid_meta_cases:
self.validate_invalid_meta(HL7MonitorAsset, meta)

def test_list_assets(self):
response = self.client.get("/api/v1/asset/")
self.assertEqual(response.status_code, status.HTTP_200_OK)
Expand Down
44 changes: 33 additions & 11 deletions care/utils/assetintegration/base.py
Original file line number Diff line number Diff line change
@@ -1,26 +1,46 @@
import json
from typing import TypedDict

import jsonschema
import requests
from django.conf import settings
from jsonschema import ValidationError as JSONValidationError
from rest_framework import status
from rest_framework.exceptions import APIException
from rest_framework.exceptions import APIException, ValidationError

from care.utils.jwks.token_generator import generate_jwt


class ActionParams(TypedDict, total=False):
type: str
data: dict | None
timeout: int | None


class BaseAssetIntegration:
auth_header_type = "Care_Bearer "

def __init__(self, meta):
from care.utils.schema import (
meta_object_schema, # Lazy import to avoid circular import issues
)
sainak marked this conversation as resolved.
Show resolved Hide resolved

try:
meta["_name"] = self._name
jsonschema.validate(instance=meta, schema=meta_object_schema)
except JSONValidationError as e:
error_message = f"Invalid metadata: {e.message}"
raise ValidationError(error_message) from e

self.meta = meta
self.id = self.meta.get("id", "")
self.host = self.meta["local_ip_address"]
self.middleware_hostname = self.meta["middleware_hostname"]
self.insecure_connection = self.meta.get("insecure_connection", False)
self.timeout = settings.MIDDLEWARE_REQUEST_TIMEOUT

def handle_action(self, action):
pass
def handle_action(self, **kwargs):
"""Handle actions using kwargs instead of dict."""

def get_url(self, endpoint):
protocol = "http"
Expand Down Expand Up @@ -48,16 +68,18 @@ def _validate_response(self, response: requests.Response):
{"error": "Invalid Response"}, response.status_code
) from e

def api_post(self, url, data=None):
def api_post(self, url, data=None, timeout=None):
if timeout is None:
timeout = self.timeout
timeout = min(timeout, 60)
sainak marked this conversation as resolved.
Show resolved Hide resolved
return self._validate_response(
requests.post(
url, json=data, headers=self.get_headers(), timeout=self.timeout
)
requests.post(url, json=data, headers=self.get_headers(), timeout=timeout)
)

def api_get(self, url, data=None):
def api_get(self, url, data=None, timeout=None):
if timeout is None:
timeout = self.timeout
timeout = min(timeout, 60)
sainak marked this conversation as resolved.
Show resolved Hide resolved
return self._validate_response(
requests.get(
url, params=data, headers=self.get_headers(), timeout=self.timeout
)
requests.get(url, params=data, headers=self.get_headers(), timeout=timeout)
)
10 changes: 6 additions & 4 deletions care/utils/assetintegration/hl7monitor.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from rest_framework.exceptions import ValidationError

from care.utils.assetintegration.base import BaseAssetIntegration
from care.utils.assetintegration.base import ActionParams, BaseAssetIntegration


class HL7MonitorAsset(BaseAssetIntegration):
Expand All @@ -20,12 +20,13 @@ def __init__(self, meta):
{key: f"{key} not found in asset metadata" for key in e.args}
) from e

def handle_action(self, action):
action_type = action["type"]
def handle_action(self, **kwargs: ActionParams):
action_type = kwargs["type"]
timeout = kwargs.get("timeout")

if action_type == self.HL7MonitorActions.GET_VITALS.value:
request_params = {"device_id": self.host}
return self.api_get(self.get_url("vitals"), request_params)
return self.api_get(self.get_url("vitals"), request_params, timeout)

if action_type == self.HL7MonitorActions.GET_STREAM_TOKEN.value:
return self.api_post(
Expand All @@ -34,6 +35,7 @@ def handle_action(self, action):
"asset_id": self.id,
"ip": self.host,
},
timeout,
)

raise ValidationError({"action": "invalid action type"})
20 changes: 11 additions & 9 deletions care/utils/assetintegration/onvif.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from rest_framework.exceptions import ValidationError

from care.utils.assetintegration.base import BaseAssetIntegration
from care.utils.assetintegration.base import ActionParams, BaseAssetIntegration


class OnvifAsset(BaseAssetIntegration):
Expand All @@ -27,9 +27,10 @@ def __init__(self, meta):
{key: f"{key} not found in asset metadata" for key in e.args}
) from e

def handle_action(self, action):
action_type = action["type"]
action_data = action.get("data", {})
def handle_action(self, **kwargs: ActionParams):
action_type = kwargs["type"]
action_data = kwargs.get("data", {})
timeout = kwargs.get("timeout")

request_body = {
"hostname": self.host,
Expand All @@ -41,26 +42,27 @@ def handle_action(self, action):
}

if action_type == self.OnvifActions.GET_CAMERA_STATUS.value:
return self.api_get(self.get_url("status"), request_body)
return self.api_get(self.get_url("status"), request_body, timeout)

if action_type == self.OnvifActions.GET_PRESETS.value:
return self.api_get(self.get_url("presets"), request_body)
return self.api_get(self.get_url("presets"), request_body, timeout)

if action_type == self.OnvifActions.GOTO_PRESET.value:
return self.api_post(self.get_url("gotoPreset"), request_body)
return self.api_post(self.get_url("gotoPreset"), request_body, timeout)

if action_type == self.OnvifActions.ABSOLUTE_MOVE.value:
return self.api_post(self.get_url("absoluteMove"), request_body)
return self.api_post(self.get_url("absoluteMove"), request_body, timeout)

if action_type == self.OnvifActions.RELATIVE_MOVE.value:
return self.api_post(self.get_url("relativeMove"), request_body)
return self.api_post(self.get_url("relativeMove"), request_body, timeout)

if action_type == self.OnvifActions.GET_STREAM_TOKEN.value:
return self.api_post(
self.get_url("api/stream/getToken/videoFeed"),
{
"stream_id": self.access_key,
},
timeout,
)

raise ValidationError({"action": "invalid action type"})
10 changes: 6 additions & 4 deletions care/utils/assetintegration/ventilator.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from rest_framework.exceptions import ValidationError

from care.utils.assetintegration.base import BaseAssetIntegration
from care.utils.assetintegration.base import ActionParams, BaseAssetIntegration


class VentilatorAsset(BaseAssetIntegration):
Expand All @@ -20,12 +20,13 @@ def __init__(self, meta):
{key: f"{key} not found in asset metadata" for key in e.args}
) from e

def handle_action(self, action):
action_type = action["type"]
def handle_action(self, **kwargs: ActionParams):
action_type = kwargs["type"]
timeout = kwargs.get("timeout")

if action_type == self.VentilatorActions.GET_VITALS.value:
request_params = {"device_id": self.host}
return self.api_get(self.get_url("vitals"), request_params)
return self.api_get(self.get_url("vitals"), request_params, timeout)

if action_type == self.VentilatorActions.GET_STREAM_TOKEN.value:
return self.api_post(
Expand All @@ -34,6 +35,7 @@ def handle_action(self, action):
"asset_id": self.id,
"ip": self.host,
},
timeout,
)

raise ValidationError({"action": "invalid action type"})
Loading
Loading