Skip to content
104 changes: 104 additions & 0 deletions openwisp_controller/connection/api/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
DeviceConnection = load_model("connection", "DeviceConnection")
Credentials = load_model("connection", "Credentials")
Device = load_model("config", "Device")
BatchCommand = load_model("connection", "BatchCommand")


class ValidatedDeviceFieldSerializer(ValidatedModelSerializer):
Expand Down Expand Up @@ -43,6 +44,10 @@ class CommandSerializer(ValidatedDeviceFieldSerializer):
required=False,
pk_field=serializers.UUIDField(format="hex_verbose"),
)
batch_command = serializers.PrimaryKeyRelatedField(
read_only=True,
pk_field=serializers.UUIDField(format="hex_verbose"),
)

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
Expand Down Expand Up @@ -115,3 +120,102 @@ class Meta:
"is_working": {"read_only": True},
}
read_only_fields = ("created", "modified")


class BatchCommandExecuteSerializer(
FilterSerializerByOrgManaged, serializers.ModelSerializer
):
type = serializers.CharField()
input = serializers.JSONField(allow_null=True, required=False)
devices = serializers.PrimaryKeyRelatedField(
many=True,
queryset=Device.objects.all(),
required=False,
allow_empty=True,
pk_field=serializers.UUIDField(format="hex_verbose"),
)
execute_all = serializers.BooleanField(required=False, default=True)

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
request = self.context.get("request")
if request and request.method == "GET":
self.fields["type"].required = False
Comment on lines +142 to +143

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need this?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For POST requests, the type field is mandatory. However, for GET requests we only need the dry-run functionality to retrieve the list of devices, so requiring type for every GET request does not make much sense.
At the moment, the endpoint requires requests like:
http://0.0.0.0:8000/api/v1/controller/batch-command/execute/?type=reboot
even for GET requests where the type value is not actually used.
Creating a separate serializer just for the GET request felt like unnecessary complexity, so I decided to handle it this way for now.

Let me know if there is a cleaner or more appropriate way to handle this.


class Meta:
model = BatchCommand
fields = (
"organization",
"type",
"input",
"devices",
"group",
"location",
"execute_all",
)
extra_kwargs = {
"organization": {"required": False, "allow_null": True},
}

def validate(self, data):
org = data.get("organization")
execute_all = data.get("execute_all", False)
devices = data.get("devices")
group = data.get("group")
location = data.get("location")
if not org and not self.context["request"].user.is_superuser:
raise serializers.ValidationError(
_("Only superusers can execute batch commands without an organization.")
)
Comment on lines +166 to +169

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you check if we have existing mixins in openwisp-users which can perform this operation?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No existing DRF serializer-level mixin in openwisp-users performs this check.

if not execute_all and not org and not devices and not group and not location:
raise serializers.ValidationError(
_(
"Specify at least one targeting option "
"or set execute_all to true."
)
)
if devices:
for device in devices:
if org and device.organization_id != org.id:
raise serializers.ValidationError(
{
"devices": _(
"All devices must belong to the same organization."
)
}
)
return data
Comment on lines +177 to +187

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see the Model also performs this validation. And, it does so more elegantly.



class BatchCommandSerializer(BaseSerializer):
device_count = serializers.IntegerField(source="devices.count", read_only=True)

class Meta:
model = BatchCommand
fields = (
"id",
"organization",
"status",
"type",
"input",
"group",
"location",
"device_count",
"created",
"modified",
)
read_only_fields = (
"created",
"modified",
)


class BatchCommandDetailSerializer(BatchCommandSerializer):
devices = serializers.PrimaryKeyRelatedField(
many=True,
read_only=True,
pk_field=serializers.UUIDField(format="hex_verbose"),
)

class Meta(BatchCommandSerializer.Meta):
fields = BatchCommandSerializer.Meta.fields + ("devices",)
15 changes: 15 additions & 0 deletions openwisp_controller/connection/api/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,21 @@ def get_api_urls(api_views):
api_views.deviceconnection_detail_view,
name="deviceconnection_detail",
),
path(
"api/v1/controller/batch-command/",
api_views.batch_command_list_view,
name="batch_command_list",
),
path(
"api/v1/controller/batch-command/<uuid:pk>/",
api_views.batch_command_detail_view,
name="batch_command_detail",
),
path(
"api/v1/controller/batch-command/execute/",
api_views.batch_command_execute_view,
name="batch_command_execute",
),
]


Expand Down
56 changes: 56 additions & 0 deletions openwisp_controller/connection/api/views.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
from django.core.exceptions import ValidationError
from django.utils.translation import gettext_lazy as _
from drf_yasg import openapi
from drf_yasg.utils import swagger_auto_schema
from rest_framework import status
from rest_framework.generics import (
GenericAPIView,
ListAPIView,
ListCreateAPIView,
RetrieveAPIView,
RetrieveUpdateDestroyAPIView,
get_object_or_404,
)
from rest_framework.response import Response
from swapper import load_model

from openwisp_utils.api.pagination import OpenWispPagination
Expand All @@ -17,6 +22,9 @@
RelatedDeviceProtectedAPIMixin,
)
from .serializers import (
BatchCommandDetailSerializer,
BatchCommandExecuteSerializer,
BatchCommandSerializer,
CommandSerializer,
CredentialSerializer,
DeviceConnectionSerializer,
Expand All @@ -26,6 +34,7 @@
Device = load_model("config", "Device")
Credentials = load_model("connection", "Credentials")
DeviceConnection = load_model("connection", "DeviceConnection")
BatchCommand = load_model("connection", "BatchCommand")


class BaseCommandView(RelatedDeviceProtectedAPIMixin):
Expand Down Expand Up @@ -138,6 +147,50 @@ class DeviceConnectionListCreateView(BaseDeviceConnection, ListCreateAPIView):
DeviceConnenctionListCreateView = DeviceConnectionListCreateView


class BatchCommandExecuteView(ProtectedAPIMixin, GenericAPIView):
Comment thread
dee077 marked this conversation as resolved.
model = BatchCommand
queryset = BatchCommand.objects.all()
serializer_class = BatchCommandExecuteSerializer

def post(self, request):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
serializer.validated_data.pop("execute_all", None)
try:
batch = BatchCommand.execute(**serializer.validated_data)
except ValidationError as e:
return Response(
getattr(e, "message_dict", e.messages),
status=status.HTTP_400_BAD_REQUEST,
)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
return Response({"batch": str(batch.pk)}, status=201)

def get(self, request):
serializer = self.get_serializer(data=request.query_params)
serializer.is_valid(raise_exception=True)
serializer.validated_data.pop("execute_all", None)
try:
data = BatchCommand.dry_run(**serializer.validated_data)
except ValidationError as e:
return Response(
getattr(e, "message_dict", e.messages),
status=status.HTTP_400_BAD_REQUEST,
)
data["devices"] = [str(d.pk) for d in data["devices"]]
return Response(data)
Comment thread
coderabbitai[bot] marked this conversation as resolved.


class BatchCommandListView(ProtectedAPIMixin, ListAPIView):
queryset = BatchCommand.objects.all().order_by("-created")
serializer_class = BatchCommandSerializer
pagination_class = OpenWispPagination


class BatchCommandDetailView(ProtectedAPIMixin, RetrieveAPIView):
queryset = BatchCommand.objects.all()
serializer_class = BatchCommandDetailSerializer


class DeviceConnectionDetailView(BaseDeviceConnection, RetrieveUpdateDestroyAPIView):
def get_object(self):
queryset = self.filter_queryset(self.get_queryset())
Expand All @@ -158,3 +211,6 @@ def get_object(self):

# TODO: remove in version 1.4
deviceconnection_details_view = deviceconnection_detail_view
batch_command_execute_view = BatchCommandExecuteView.as_view()
batch_command_list_view = BatchCommandListView.as_view()
batch_command_detail_view = BatchCommandDetailView.as_view()
Loading
Loading