Skip to content
Draft
Show file tree
Hide file tree
Changes from all 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
6 changes: 3 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@ on:
push:
branches:
- master
- "gsoc26-*"
pull_request:
branches:
- master
- "1.1"
- "1.2"
- "gsoc26-x509-certificate-generator-templates"
- "gsoc26-mass-commands"
- "gsoc26-*"

jobs:
build:
Expand Down Expand Up @@ -100,7 +100,7 @@ jobs:
with:
parallel: true
format: cobertura
flag-name: python-${{ matrix.env.env }}
flag-name: python-${{ matrix.python-version }}-${{ matrix.django-version }}
github-token: ${{ secrets.GITHUB_TOKEN }}
fail-on-error: false

Expand Down
72 changes: 72 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,70 @@ class Meta:
"is_working": {"read_only": True},
}
read_only_fields = ("created", "modified")


class BatchCommandExecuteSerializer(
FilterSerializerByOrgManaged, serializers.ModelSerializer
):
type = serializers.CharField(source="command_type")
input = serializers.JSONField(
source="command_input", 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 +144 to +145

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?


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 +168 to +171

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?

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 +179 to +189

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.

5 changes: 5 additions & 0 deletions openwisp_controller/connection/api/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,11 @@ def get_api_urls(api_views):
api_views.deviceconnection_detail_view,
name="deviceconnection_detail",
),
path(
"api/v1/controller/batch-command/execute/",
api_views.batch_command_execute_view,
name="batch_command_execute",
),
]


Expand Down
39 changes: 39 additions & 0 deletions openwisp_controller/connection/api/views.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
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,
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 +21,7 @@
RelatedDeviceProtectedAPIMixin,
)
from .serializers import (
BatchCommandExecuteSerializer,
CommandSerializer,
CredentialSerializer,
DeviceConnectionSerializer,
Expand All @@ -26,6 +31,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 +144,37 @@ class DeviceConnectionListCreateView(BaseDeviceConnection, ListCreateAPIView):
DeviceConnenctionListCreateView = DeviceConnectionListCreateView


class BatchCommandExecuteView(ProtectedAPIMixin, GenericAPIView):

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.

We're lacking a List endpoint.

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)
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)
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 DeviceConnectionDetailView(BaseDeviceConnection, RetrieveUpdateDestroyAPIView):
def get_object(self):
queryset = self.filter_queryset(self.get_queryset())
Expand All @@ -158,3 +195,5 @@ def get_object(self):

# TODO: remove in version 1.4
deviceconnection_details_view = deviceconnection_detail_view

batch_command_execute_view = BatchCommandExecuteView.as_view()
Loading
Loading