1
# Python
2 3
import re
3 3
import cgi
4 3
import dateutil
5 3
import dateutil.relativedelta
6 3
import datetime
7 3
import logging
8 3
import tzcron
9 3
import pytz
10 3
from base64 import b64encode
11 3
from collections import OrderedDict
12

13
# Django
14 3
from django.conf import settings as dsettings
15 3
from django.core.exceptions import FieldError
16 3
from django.db import IntegrityError, transaction
17 3
from django.utils.safestring import mark_safe
18 3
from django.utils.timezone import now
19 3
from django.views.decorators.csrf import csrf_exempt
20 3
from django.template.loader import render_to_string
21 3
from django.http import HttpResponse
22 3
from django.utils.translation import ugettext_lazy as _
23

24
# Django REST Framework
25 3
from rest_framework.exceptions import PermissionDenied, ParseError
26 3
from rest_framework.permissions import AllowAny, IsAuthenticated
27 3
from rest_framework.response import Response
28 3
from rest_framework.settings import api_settings
29 3
from rest_framework.views import exception_handler
30 3
from rest_framework import status, renderers
31

32
# CyBorgBackup
33 3
from rest_framework_simplejwt.views import TokenObtainPairView
34

35 3
from cyborgbackup.api.generics import (APIView, GenericAPIView, ListAPIView,
36
                                       ListCreateAPIView, SubListAPIView, RetrieveAPIView,
37
                                       RetrieveUpdateAPIView, RetrieveUpdateDestroyAPIView,
38
                                       get_view_name)
39 3
from cyborgbackup.main.models import reverse
40 3
from cyborgbackup.main.models.events import JobEvent
41 3
from cyborgbackup.main.models.catalogs import Catalog
42 3
from cyborgbackup.main.models.clients import Client
43 3
from cyborgbackup.main.models.schedules import Schedule
44 3
from cyborgbackup.main.models.repositories import Repository
45 3
from cyborgbackup.main.models.policies import Policy
46 3
from cyborgbackup.main.models.jobs import Job
47 3
from cyborgbackup.main.models.users import User
48 3
from cyborgbackup.main.models.settings import Setting
49 3
from cyborgbackup.main.utils.common import get_module_provider, camelcase_to_underscore, get_cyborgbackup_version
50 3
from cyborgbackup.main.utils.callbacks import CallbackQueueDispatcher
51 3
from cyborgbackup.main.modules import Querier
52 3
from cyborgbackup.api.renderers import (BrowsableAPIRenderer, PlainTextRenderer,
53
                                        DownloadTextRenderer, AnsiDownloadRenderer, AnsiTextRenderer)
54 3
from cyborgbackup.api.serializers import (EmptySerializer, UserSerializer,
55
                                          JobSerializer, JobStdoutSerializer, JobCancelSerializer,
56
                                          JobRelaunchSerializer, JobListSerializer, JobEventSerializer,
57
                                          SettingSerializer, SettingListSerializer,
58
                                          ClientSerializer, ClientListSerializer, ScheduleSerializer,
59
                                          ScheduleListSerializer, RepositorySerializer, RepositoryListSerializer,
60
                                          PolicySerializer, PolicyLaunchSerializer, PolicyModuleSerializer,
61
                                          PolicyCalendarSerializer, PolicyVMModuleSerializer,
62
                                          CatalogSerializer, CatalogListSerializer, StatsSerializer,
63
                                          CyborgTokenObtainPairSerializer, RestoreLaunchSerializer)
64 3
from cyborgbackup.main.constants import ACTIVE_STATES
65 3
from cyborgbackup.api.permissions import UserPermission
66

67 3
import ansiconv
68 3
from wsgiref.util import FileWrapper
69

70 3
logger = logging.getLogger('cyborgbackup.api.views')
71

72

73 3
def api_exception_handler(exc, context):
74
    '''
75
    Override default API exception handler to catch IntegrityError exceptions.
76
    '''
77 3
    if isinstance(exc, IntegrityError):
78 0
        exc = ParseError(exc.args[0])
79 3
    if isinstance(exc, FieldError):
80 0
        exc = ParseError(exc.args[0])
81 3
    return exception_handler(exc, context)
82

83

84 3
class JobDeletionMixin(object):
85
    '''
86
    Special handling when deleting a running job object.
87
    '''
88 3
    def destroy(self, request, *args, **kwargs):
89 0
        obj = self.get_object()
90 0
        try:
91 0
            if obj.job_node.workflow_job.status in ACTIVE_STATES:
92 0
                raise PermissionDenied(detail=_('Cannot delete job resource when associated workflow job is running.'))
93 0
        except self.model.job_node.RelatedObjectDoesNotExist:
94 0
            pass
95
        # Still allow deletion of new status, because these can be manually created
96 0
        if obj.status in ACTIVE_STATES and obj.status != 'new':
97 0
            raise PermissionDenied(detail=_("Cannot delete running job resource."))
98 0
        elif not obj.event_processing_finished:
99
            # Prohibit deletion if job events are still coming in
100 0
            if obj.finished and now() < obj.finished + dateutil.relativedelta.relativedelta(minutes=1):
101
                # less than 1 minute has passed since job finished and events are not in
102 0
                return Response({"error": _("Job has not finished processing events.")},
103
                                status=status.HTTP_400_BAD_REQUEST)
104
            else:
105
                # if it has been > 1 minute, events are probably lost
106 0
                logger.warning('Allowing deletion of {} through the API without all events '
107
                               'processed.'.format(obj.log_format))
108 0
        obj.delete()
109 0
        return Response(status=status.HTTP_204_NO_CONTENT)
110

111

112 3
class ApiRootView(APIView):
113

114 3
    permission_classes = (AllowAny,)
115 3
    view_name = _('REST API')
116 3
    versioning_class = None
117 3
    swagger_topic = 'Versioning'
118

119 3
    def get(self, request, format=None):
120
        ''' List supported API versions '''
121

122 3
        v1 = reverse('api:api_v1_root_view', kwargs={'version': 'v1'})
123 3
        data = dict(
124
            description=_('CyBorgBackup API Rest'),
125
            current_version=v1,
126
            available_versions=dict(v1=v1),
127
        )
128 3
        return Response(data)
129

130

131 3
class ApiVersionRootView(APIView):
132

133 3
    permission_classes = (AllowAny,)
134 3
    swagger_topic = 'Versioning'
135

136 3
    def get(self, request, format=None):
137
        ''' List top level resources '''
138 3
        data = OrderedDict()
139 3
        data['ping'] = reverse('api:api_v1_ping_view', request=request)
140 3
        data['config'] = reverse('api:api_v1_config_view', request=request)
141 3
        data['me'] = reverse('api:user_me_list', request=request)
142 3
        data['users'] = reverse('api:user_list', request=request)
143 3
        data['jobs'] = reverse('api:job_list', request=request)
144 3
        data['job_events'] = reverse('api:job_event_list', request=request)
145 3
        data['settings'] = reverse('api:setting_list', request=request)
146 3
        data['clients'] = reverse('api:client_list', request=request)
147 3
        data['schedules'] = reverse('api:schedule_list', request=request)
148 3
        data['repositories'] = reverse('api:repository_list', request=request)
149 3
        data['policies'] = reverse('api:policy_list', request=request)
150 3
        data['restore'] = reverse('api:restore_launch', request=request)
151 3
        data['catalogs'] = reverse('api:catalog_list', request=request)
152 3
        data['stats'] = reverse('api:stats', request=request)
153 3
        data['escatalogs'] = reverse('api:escatalog_list', request=request)
154 3
        return Response(data)
155

156

157 3
class ApiV1RootView(ApiVersionRootView):
158 3
    view_name = _('Version 1')
159

160

161 3
class ApiV1PingView(APIView):
162
    """A simple view that reports very basic information about this
163
    instance, which is acceptable to be public information.
164
    """
165 3
    permission_classes = (AllowAny,)
166 3
    authentication_classes = ()
167 3
    view_name = _('Ping')
168 3
    swagger_topic = 'System Configuration'
169

170 3
    def get(self, request, format=None):
171
        """Return some basic information about this instance
172

173
        Everything returned here should be considered public / insecure, as
174
        this requires no auth and is intended for use by the installer process.
175
        """
176 3
        response = {
177
            'version': get_cyborgbackup_version(),
178
        }
179

180 3
        response['ping'] = "pong"
181 3
        return Response(response)
182

183

184 3
class ApiV1ConfigView(APIView):
185

186 3
    permission_classes = (IsAuthenticated,)
187 3
    view_name = _('Configuration')
188 3
    swagger_topic = 'System Configuration'
189

190 3
    def check_permissions(self, request):
191 3
        super(ApiV1ConfigView, self).check_permissions(request)
192 3
        if not request.user.is_superuser and request.method.lower() not in {'options', 'head', 'get'}:
193 0
            self.permission_denied(request)  # Raises PermissionDenied exception.
194

195 3
    def get(self, request, format=None):
196
        '''Return various sitewide configuration settings'''
197

198 3
        data = dict(
199
            time_zone=dsettings.TIME_ZONE,
200
            debug=dsettings.DEBUG,
201
            sql_debug=dsettings.SQL_DEBUG,
202
            allowed_hosts=dsettings.ALLOWED_HOSTS,
203
            # beat_schedule=dsettings.CELERY_BEAT_SCHEDULE,
204
            version=get_cyborgbackup_version(),
205
        )
206

207 3
        return Response(data)
208

209

210 3
class AuthView(APIView):
211
    ''' List enabled single-sign-on endpoints '''
212

213 3
    authentication_classes = []
214 3
    permission_classes = (AllowAny,)
215 3
    swagger_topic = 'System Configuration'
216

217 3
    def get(self, request):
218 0
        data = OrderedDict()
219 0
        err_backend, err_message = request.session.get('social_auth_error', (None, None))
220 0
        return Response(data)
221

222

223 3
class UserList(ListCreateAPIView):
224

225 3
    model = User
226 3
    serializer_class = UserSerializer
227 3
    permission_classes = (UserPermission,)
228

229 3
    def post(self, request, *args, **kwargs):
230 0
        ret = super(UserList, self).post(request, *args, **kwargs)
231 0
        return ret
232

233

234 3
class UserMeList(ListAPIView):
235

236 3
    model = User
237 3
    serializer_class = UserSerializer
238 3
    view_name = _('Me')
239

240 3
    def get_queryset(self):
241 3
        return self.model.objects.filter(pk=self.request.user.pk)
242

243

244 3
class UserDetail(RetrieveUpdateDestroyAPIView):
245

246 3
    model = User
247 3
    serializer_class = UserSerializer
248

249 3
    def update_filter(self, request, *args, **kwargs):
250
        ''' make sure non-read-only fields that can only be edited by admins, are only edited by admins '''
251 0
        obj = self.get_object()
252

253 0
        su_only_edit_fields = ('is_superuser')
254
        # admin_only_edit_fields = ('username', 'is_active')
255

256 0
        fields_to_check = ()
257 0
        if not request.user.is_superuser:
258 0
            fields_to_check += su_only_edit_fields
259

260 0
        bad_changes = {}
261 0
        for field in fields_to_check:
262 0
            left = getattr(obj, field, None)
263 0
            right = request.data.get(field, None)
264 0
            if left is not None and right is not None and left != right:
265 0
                bad_changes[field] = (left, right)
266 0
        if bad_changes:
267 0
            raise PermissionDenied(_('Cannot change %s.') % ', '.join(bad_changes.keys()))
268

269

270 3
class StdoutANSIFilter(object):
271

272 3
    def __init__(self, fileobj):
273 0
        self.fileobj = fileobj
274 0
        self.extra_data = ''
275 0
        if hasattr(fileobj, 'close'):
276 0
            self.close = fileobj.close
277

278 3
    def read(self, size=-1):
279 0
        data = self.extra_data
280 0
        while size > 0 and len(data) < size:
281 0
            line = self.fileobj.readline(size)
282 0
            if not line:
283 0
                break
284
            # Remove ANSI escape sequences used to embed event data.
285 0
            line = re.sub(r'\x1b\[K(?:[A-Za-z0-9+/=]+\x1b\[\d+D)+\x1b\[K', '', line)
286
            # Remove ANSI color escape sequences.
287 0
            line = re.sub(r'\x1b[^m]*m', '', line)
288 0
            data += line
289 0
        if size > 0 and len(data) > size:
290 0
            self.extra_data = data[size:]
291 0
            data = data[:size]
292
        else:
293 0
            self.extra_data = ''
294 0
        return data
295

296

297 3
class JobList(ListCreateAPIView):
298

299 3
    model = Job
300 3
    serializer_class = JobListSerializer
301

302 3
    @property
303
    def allowed_methods(self):
304 3
        methods = super(JobList, self).allowed_methods
305 3
        return methods
306

307
    # NOTE: Remove in 3.3, switch ListCreateAPIView to ListAPIView
308 3
    def post(self, request, *args, **kwargs):
309 0
        return super(JobList, self).post(request, *args, **kwargs)
310

311

312 3
class JobDetail(JobDeletionMixin, RetrieveUpdateDestroyAPIView):
313

314 3
    model = Job
315 3
    serializer_class = JobSerializer
316

317 3
    def update(self, request, *args, **kwargs):
318 0
        obj = self.get_object()
319
        # Only allow changes (PUT/PATCH) when job status is "new".
320 0
        if obj.status != 'new':
321 0
            return self.http_method_not_allowed(request, *args, **kwargs)
322 0
        return super(JobDetail, self).update(request, *args, **kwargs)
323

324

325 3
class StdoutMaxBytesExceeded(Exception):
326

327 3
    def __init__(self, total, supported):
328 0
        self.total = total
329 0
        self.supported = supported
330

331

332 3
class JobStdout(RetrieveAPIView):
333

334 3
    model = Job
335 3
    authentication_classes = api_settings.DEFAULT_AUTHENTICATION_CLASSES
336 3
    serializer_class = JobStdoutSerializer
337 3
    renderer_classes = [BrowsableAPIRenderer, renderers.StaticHTMLRenderer,
338
                        PlainTextRenderer, AnsiTextRenderer,
339
                        renderers.JSONRenderer, DownloadTextRenderer, AnsiDownloadRenderer]
340 3
    filter_backends = ()
341

342 3
    def retrieve(self, request, *args, **kwargs):
343 0
        job = self.get_object()
344 0
        try:
345 0
            target_format = request.accepted_renderer.format
346 0
            if target_format in ('html', 'api', 'json'):
347 0
                content_format = request.query_params.get('content_format', 'html')
348 0
                content_encoding = request.query_params.get('content_encoding', None)
349 0
                start_line = request.query_params.get('start_line', 0)
350 0
                end_line = request.query_params.get('end_line', None)
351 0
                dark_val = request.query_params.get('dark', '')
352 0
                dark = bool(dark_val and dark_val[0].lower() in ('1', 't', 'y'))
353 0
                content_only = bool(target_format in ('api', 'json'))
354 0
                dark_bg = (content_only and dark) or (not content_only and (dark or not dark_val))
355 0
                content, start, end, absolute_end = job.result_stdout_raw_limited(start_line, end_line)
356

357
                # Remove any ANSI escape sequences containing job event data.
358 0
                content = re.sub(r'\x1b\[K(?:[A-Za-z0-9+/=]+\x1b\[\d+D)+\x1b\[K', '', content)
359

360 0
                body = ansiconv.to_html(cgi.escape(content))
361

362 0
                context = {
363
                    'title': get_view_name(self.__class__),
364
                    'body': mark_safe(body),
365
                    'dark': dark_bg,
366
                    'content_only': content_only,
367
                }
368 0
                data = render_to_string('api/stdout.html', context).strip()
369

370 0
                if target_format == 'api':
371 0
                    return Response(mark_safe(data))
372 0
                if target_format == 'json':
373 0
                    if content_encoding == 'base64' and content_format == 'ansi':
374 0
                        return Response({'range': {'start': start, 'end': end, 'absolute_end': absolute_end},
375
                                         'content': b64encode(content.encode('utf-8'))})
376 0
                    elif content_format == 'html':
377 0
                        return Response({'range': {'start': start, 'end': end, 'absolute_end': absolute_end},
378
                                         'content': body})
379 0
                return Response(data)
380 0
            elif target_format == 'txt':
381 0
                return Response(job.result_stdout)
382 0
            elif target_format == 'ansi':
383 0
                return Response(job.result_stdout_raw)
384 0
            elif target_format in {'txt_download', 'ansi_download'}:
385 0
                filename = '{type}_{pk}{suffix}.txt'.format(
386
                    type=camelcase_to_underscore(job.__class__.__name__),
387
                    pk=job.id,
388
                    suffix='.ansi' if target_format == 'ansi_download' else ''
389
                )
390 0
                content_fd = job.result_stdout_raw_handle(enforce_max_bytes=False)
391 0
                if target_format == 'txt_download':
392 0
                    content_fd = StdoutANSIFilter(content_fd)
393 0
                response = HttpResponse(FileWrapper(content_fd), content_type='text/plain')
394 0
                response["Content-Disposition"] = 'attachment; filename="{}"'.format(filename)
395 0
                return response
396
            else:
397 0
                return super(JobStdout, self).retrieve(request, *args, **kwargs)
398 0
        except StdoutMaxBytesExceeded as e:
399 0
            response_message = _(
400
                "Standard Output too large to display ({text_size} bytes), "
401
                "only download supported for sizes over {supported_size} bytes.").format(
402
                    text_size=e.total, supported_size=e.supported
403
                )
404 0
            if request.accepted_renderer.format == 'json':
405 0
                return Response({'range': {'start': 0, 'end': 1, 'absolute_end': 1}, 'content': response_message})
406
            else:
407 0
                return Response(response_message)
408

409

410 3
class JobStart(GenericAPIView):
411

412 3
    model = Job
413 3
    obj_permission_type = 'start'
414 3
    serializer_class = EmptySerializer
415 3
    deprecated = True
416

417 3
    def get(self, request, *args, **kwargs):
418 0
        obj = self.get_object()
419 0
        data = dict(
420
            can_start=obj.can_start,
421
        )
422 0
        return Response(data)
423

424 3
    def post(self, request, *args, **kwargs):
425 0
        obj = self.get_object()
426 0
        if obj.can_start:
427 0
            result = obj.signal_start(**request.data)
428 0
            if not result:
429 0
                return Response(request.data, status=status.HTTP_400_BAD_REQUEST)
430
            else:
431 0
                return Response(status=status.HTTP_202_ACCEPTED)
432
        else:
433 0
            return self.http_method_not_allowed(request, *args, **kwargs)
434

435

436 3
class JobCancel(RetrieveAPIView):
437

438 3
    model = Job
439 3
    obj_permission_type = 'cancel'
440 3
    serializer_class = JobCancelSerializer
441

442 3
    def post(self, request, *args, **kwargs):
443 0
        obj = self.get_object()
444 0
        if obj.can_cancel:
445 0
            obj.cancel()
446 0
            return Response(status=status.HTTP_202_ACCEPTED)
447
        else:
448 0
            return self.http_method_not_allowed(request, *args, **kwargs)
449

450

451 3
class JobRelaunch(RetrieveAPIView):
452

453 3
    model = Job
454 3
    obj_permission_type = 'start'
455 3
    serializer_class = JobRelaunchSerializer
456

457 3
    def update_raw_data(self, data):
458 0
        data = super(JobRelaunch, self).update_raw_data(data)
459 0
        try:
460 0
            self.get_object()
461 0
        except PermissionDenied:
462 0
            return data
463 0
        return data
464

465 3
    @csrf_exempt
466 3
    @transaction.non_atomic_requests
467
    def dispatch(self, *args, **kwargs):
468 0
        return super(JobRelaunch, self).dispatch(*args, **kwargs)
469

470 3
    def check_object_permissions(self, request, obj):
471 0
        return super(JobRelaunch, self).check_object_permissions(request, obj)
472

473 3
    def post(self, request, *args, **kwargs):
474 0
        obj = self.get_object()
475 0
        context = self.get_serializer_context()
476

477 0
        modified_data = request.data.copy()
478 0
        serializer = self.serializer_class(data=modified_data, context=context, instance=obj)
479 0
        if not serializer.is_valid():
480 0
            return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
481

482 0
        jobs = Job.objects.filter(client=obj.client_id)
483 0
        if jobs.exists():
484 0
            for job in jobs:
485 0
                if job.status in ['waiting', 'pending', 'running']:
486 0
                    return Response({'detail': 'Backup job already running for this client.'},
487
                                    status=status.HTTP_400_BAD_REQUEST)
488

489 0
        copy_kwargs = {}
490

491 0
        new_job = obj.copy_job(**copy_kwargs)
492 0
        result = new_job.signal_start()
493 0
        if not result:
494 0
            data = dict(msg=_('Error starting job!'))
495 0
            new_job.delete()
496 0
            return Response(data, status=status.HTTP_400_BAD_REQUEST)
497
        else:
498 0
            data = JobSerializer(new_job, context=context).data
499
            # Add job key to match what old relaunch returned.
500 0
            data['job'] = new_job.id
501 0
            headers = {'Location': new_job.get_absolute_url(request=request)}
502 0
            return Response(data, status=status.HTTP_201_CREATED, headers=headers)
503

504

505 3
class JobEventList(ListAPIView):
506

507 3
    model = JobEvent
508 3
    serializer_class = JobEventSerializer
509

510

511 3
class JobEventDetail(RetrieveAPIView):
512

513 3
    model = JobEvent
514 3
    serializer_class = JobEventSerializer
515

516

517 3
class BaseJobEventsList(SubListAPIView):
518

519 3
    model = JobEvent
520 3
    serializer_class = JobEventSerializer
521 3
    parent_model = None  # Subclasses must define this attribute.
522 3
    relationship = 'job_events'
523 3
    view_name = _('Job Events List')
524 3
    search_fields = ('stdout',)
525

526 3
    def finalize_response(self, request, response, *args, **kwargs):
527 0
        response['X-UI-Max-Events'] = 4000
528 0
        return super(BaseJobEventsList, self).finalize_response(request, response, *args, **kwargs)
529

530

531 3
class JobJobEventsList(BaseJobEventsList):
532

533 3
    parent_model = Job
534

535 3
    def get_queryset(self):
536 0
        job = self.get_parent_object()
537 0
        self.check_parent_access(job)
538 0
        qs = job.job_events
539 0
        return qs.all()
540

541

542 3
class SettingList(ListAPIView):
543

544 3
    model = Setting
545 3
    serializer_class = SettingListSerializer
546

547 3
    @property
548
    def allowed_methods(self):
549 3
        methods = super(SettingList, self).allowed_methods
550 3
        return methods
551

552

553 3
class SettingDetail(RetrieveUpdateAPIView):
554

555 3
    model = Setting
556 3
    serializer_class = SettingSerializer
557

558

559 3
class ClientList(ListCreateAPIView):
560

561 3
    model = Client
562 3
    serializer_class = ClientListSerializer
563

564 3
    @property
565
    def allowed_methods(self):
566 3
        methods = super(ClientList, self).allowed_methods
567 3
        return methods
568

569

570 3
class ClientDetail(RetrieveUpdateDestroyAPIView):
571

572 3
    model = Client
573 3
    serializer_class = ClientSerializer
574

575 3
    def patch(self, request, *args, **kwargs):
576 3
        obj = self.get_object()
577 3
        logger.debug(request.data)
578

579 3
        serializer = self.serializer_class(obj, data=request.data, partial=True)
580 3
        if not serializer.is_valid():
581 0
            return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
582

583 3
        policies = Policy.objects.all()
584 3
        if policies.exists() and 'policies' in request.data.keys():
585 0
            for pol in policies:
586 0
                if pol.id in request.data['policies'] and len([x for x in pol.clients.all() if x.id == obj.id]) == 0:
587 0
                    logger.debug('Add client to policy {}'.format(pol.name))
588 0
                    pol.clients
589 0
                if len([x for x in pol.clients.all() if x.id == obj.id]) > 0 and pol.id not in request.data['policies']:
590 0
                    logger.debug('Remove client from policy {}'.format(pol.name))
591

592 3
        return super(ClientDetail, self).patch(request, *args, **kwargs)
593

594

595 3
class ScheduleList(ListCreateAPIView):
596

597 3
    model = Schedule
598 3
    serializer_class = ScheduleListSerializer
599

600 3
    @property
601
    def allowed_methods(self):
602 3
        methods = super(ScheduleList, self).allowed_methods
603 3
        return methods
604

605

606 3
class ScheduleDetail(RetrieveUpdateDestroyAPIView):
607

608 3
    model = Schedule
609 3
    serializer_class = ScheduleSerializer
610

611

612 3
class RepositoryList(ListCreateAPIView):
613

614 3
    model = Repository
615 3
    serializer_class = RepositoryListSerializer
616

617 3
    @property
618
    def allowed_methods(self):
619 3
        methods = super(RepositoryList, self).allowed_methods
620 3
        return methods
621

622

623 3
class RepositoryDetail(RetrieveUpdateDestroyAPIView):
624

625 3
    model = Repository
626 3
    serializer_class = RepositorySerializer
627

628

629 3
class PolicyList(ListCreateAPIView):
630

631 3
    model = Policy
632 3
    serializer_class = PolicySerializer
633

634

635 3
class PolicyDetail(RetrieveUpdateDestroyAPIView):
636

637 3
    model = Policy
638 3
    serializer_class = PolicySerializer
639

640

641 3
class PolicyVMModule(ListAPIView):
642

643 3
    model = Policy
644 3
    serializer_class = PolicyVMModuleSerializer
645

646 3
    def list(self, request, *args, **kwargs):
647 3
        data = get_module_provider()
648 3
        return Response(data)
649

650

651 3
class PolicyModule(ListCreateAPIView):
652

653 3
    model = Policy
654 3
    serializer_class = PolicyModuleSerializer
655

656 3
    def callModule(self, request, args, kwargs):
657 0
        module = kwargs['module']
658 0
        client_id = kwargs['client']
659 0
        data = {}
660 0
        if module == 'vm':
661 0
            data = get_module_provider()
662
        else:
663 0
            client = Client.objects.get(pk=client_id)
664 0
            if client:
665 0
                q = Querier()
666 0
                params = {**request.query_params.dict(), **request.data}
667 0
                data = q.querier(module, client, params)
668 0
                if data == -1:
669 0
                    return Response({}, status=status.HTTP_204_NO_CONTENT)
670 0
        return Response(data)
671

672 3
    def list(self, request, *args, **kwargs):
673 0
        return self.callModule(request, args, kwargs)
674

675 3
    def post(self, request, *args, **kwargs):
676 0
        return self.callModule(request, args, kwargs)
677

678

679 3
class PolicyCalendar(ListAPIView):
680

681 3
    model = Policy
682 3
    serializer_class = PolicyCalendarSerializer
683

684 3
    def list(self, request, *args, **kwargs):
685 3
        obj = self.get_object()
686 3
        now = datetime.datetime.now(pytz.utc)
687 3
        start_month = now.replace(day=1, hour=0, minute=0, second=0, microsecond=0)
688 3
        year = now.year
689 3
        if start_month.month == 12:
690 3
            year += 1
691 3
        relative_month = dateutil.relativedelta.relativedelta(months=1)
692 3
        end_month = datetime.datetime(year, (start_month + relative_month).month, 1) - datetime.timedelta(days=1)
693 3
        end_month = end_month.replace(hour=23, minute=59, second=50, tzinfo=pytz.utc)
694 3
        schedule = tzcron.Schedule(obj.schedule.crontab, pytz.utc, start_month, end_month)
695 3
        return Response([s.isoformat() for s in schedule])
696

697

698 3
class PolicyLaunch(RetrieveAPIView):
699

700 3
    model = Policy
701 3
    serializer_class = PolicyLaunchSerializer
702

703 3
    def update_raw_data(self, data):
704 0
        obj = self.get_object()
705 0
        extra_vars = data.pop('extra_vars', None) or {}
706 0
        if obj:
707 0
            data['extra_vars'] = extra_vars
708 0
        return data
709

710 3
    def post(self, request, *args, **kwargs):
711 0
        obj = self.get_object()
712

713 0
        serializer = self.serializer_class(data=request.data, context={'job': obj})
714 0
        if not serializer.is_valid():
715 0
            return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
716

717 0
        for client in obj.clients.all():
718 0
            jobs = Job.objects.filter(client=client.pk)
719 0
            if jobs.exists():
720 0
                for job in jobs:
721 0
                    if job.status in ['waiting', 'pending', 'running']:
722 0
                        return Response({'detail': 'Backup job already running for theses clients.'},
723
                                        status=status.HTTP_400_BAD_REQUEST)
724

725 0
        new_job = obj.create_job(**serializer.validated_data)
726 0
        result = new_job.signal_start()
727

728 0
        if not result:
729 0
            data = OrderedDict()
730 0
            new_job.delete()
731 0
            return Response(data, status=status.HTTP_400_BAD_REQUEST)
732
        else:
733 0
            data = OrderedDict()
734 0
            data['job'] = new_job.id
735 0
            data.update(JobSerializer(new_job, context=self.get_serializer_context()).to_representation(new_job))
736 0
            return Response(data, status=status.HTTP_201_CREATED)
737

738 3
    def sanitize_for_response(self, data):
739
        '''
740
        Model objects cannot be serialized by DRF,
741
        this replaces objects with their ids for inclusion in response
742
        '''
743

744 0
        def display_value(val):
745 0
            if hasattr(val, 'id'):
746 0
                return val.id
747
            else:
748 0
                return val
749

750 0
        sanitized_data = {}
751 0
        for field_name, value in data.items():
752 0
            if isinstance(value, (set, list)):
753 0
                sanitized_data[field_name] = []
754 0
                for sub_value in value:
755 0
                    sanitized_data[field_name].append(display_value(sub_value))
756
            else:
757 0
                sanitized_data[field_name] = display_value(value)
758

759 0
        return sanitized_data
760

761

762 3
class RestoreLaunch(ListCreateAPIView):
763

764 3
    model = Job
765 3
    serializer_class = RestoreLaunchSerializer
766

767 3
    def list(self, request, *args, **kwargs):
768 0
        data = []
769 0
        return Response(data)
770

771 3
    def create(self, request, *args, **kwargs):
772 0
        result = None
773 0
        serializer = self.serializer_class(data=request.data)
774 0
        if not serializer.is_valid():
775 0
            return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
776

777 0
        obj = serializer.validated_data
778 0
        jobs = Job.objects.filter(archive_name=obj['archive_name'])
779 0
        if jobs.exists():
780 0
            job = jobs[0]
781 0
            client = job.client
782

783 0
            jobs_client = Job.objects.filter(client=client.pk)
784 0
            if jobs_client.exists():
785 0
                for job_client in jobs_client:
786 0
                    if job_client.status in ['waiting', 'pending', 'running']:
787 0
                        return Response({'detail': 'Backup job already running for this client.'},
788
                                        status=status.HTTP_400_BAD_REQUEST)
789

790 0
            extra_vars = {
791
                'item': serializer.validated_data['item'],
792
                'dest': serializer.validated_data['destination'],
793
                'dry_run': serializer.validated_data['dry_run'],
794
                'dest_folder': serializer.validated_data['dest_folder']
795
            }
796

797 0
            new_job = job.policy.create_restore_job(source_job=job, extra_vars=extra_vars)
798

799 0
            result = new_job.signal_start()
800

801 0
        if not result:
802 0
            data = OrderedDict()
803 0
            new_job.delete()
804 0
            return Response(data, status=status.HTTP_400_BAD_REQUEST)
805
        else:
806 0
            data = OrderedDict()
807 0
            data['job'] = new_job.id
808 0
            data.update(JobSerializer(new_job, context=self.get_serializer_context()).to_representation(new_job))
809 0
            return Response(data, status=status.HTTP_201_CREATED)
810

811

812 3
class CatalogList(ListCreateAPIView):
813

814 3
    model = Catalog
815 3
    serializer_class = CatalogListSerializer
816

817 3
    def create(self, request, *args, **kwargs):
818 0
        data = request.data
819 0
        if set(data.keys()).intersection(['archive_name', 'job', 'event', 'catalog']):
820 0
            callback = CallbackQueueDispatcher()
821 0
            callback.dispatch(data)
822 0
            return Response(OrderedDict(), status=status.HTTP_201_CREATED)
823

824 0
        return Response(OrderedDict(), status=status.HTTP_400_BAD_REQUEST)
825

826

827 3
class CatalogDetail(RetrieveUpdateDestroyAPIView):
828

829 3
    model = Catalog
830 3
    serializer_class = CatalogSerializer
831

832

833 3
class Stats(ListAPIView):
834

835 3
    model = Job
836 3
    serializer_class = StatsSerializer
837

838 3
    def list(self, request, *args, **kwargs):
839 3
        data = []
840 3
        now = datetime.datetime.now(pytz.utc)
841 3
        last_30_days = now - datetime.timedelta(days=30)
842 3
        jobs = Job.objects.filter(job_type='job', started__gte=last_30_days).order_by('started')
843 3
        if jobs.exists():
844 0
            for job in jobs:
845 0
                stats_dates = [stat['date'] for stat in data]
846 0
                if job.started.date() not in stats_dates:
847 0
                    data.append({'date': job.started.date(), 'size': 0, 'dedup': 0, 'success': 0, 'failed': 0})
848 0
                for stat in data:
849 0
                    if stat['date'] == job.started.date():
850 0
                        stat['size'] += job.original_size
851 0
                        stat['dedup'] += job.deduplicated_size
852 0
                        if job.status == 'successful':
853 0
                            stat['success'] += 1
854 0
                        if job.status == 'failed':
855 0
                            stat['failed'] += 1
856 3
        return Response(data)
857

858

859 3
class CyborgTokenObtainPairView(TokenObtainPairView):
860 3
    serializer_class = CyborgTokenObtainPairSerializer

Read our documentation on viewing source code .

Loading