1
|
|
# Python
|
2
|
6
|
import copy
|
3
|
6
|
import logging
|
4
|
6
|
from collections import OrderedDict
|
5
|
|
|
6
|
|
# Django
|
7
|
6
|
from django.core.exceptions import ObjectDoesNotExist, ValidationError as DjangoValidationError
|
8
|
6
|
from django.db import models
|
9
|
6
|
from django.utils.translation import ugettext_lazy as _
|
10
|
6
|
from django.utils.encoding import force_text
|
11
|
6
|
from django.utils.text import capfirst
|
12
|
6
|
from django.utils.timezone import now
|
13
|
|
|
14
|
|
# Django REST Framework
|
15
|
6
|
from rest_framework.exceptions import ValidationError
|
16
|
6
|
from rest_framework import fields
|
17
|
6
|
from rest_framework import serializers
|
18
|
6
|
from rest_framework import validators
|
19
|
|
|
20
|
|
# Django-Polymorphic
|
21
|
6
|
from polymorphic.models import PolymorphicModel
|
22
|
|
|
23
|
|
# cyborgbackup
|
24
|
6
|
from rest_framework_simplejwt.serializers import TokenObtainPairSerializer
|
25
|
|
|
26
|
6
|
from cyborgbackup.main.constants import ANSI_SGR_PATTERN
|
27
|
6
|
from cyborgbackup.main.models.events import JobEvent
|
28
|
6
|
from cyborgbackup.main.models.clients import Client
|
29
|
6
|
from cyborgbackup.main.models.catalogs import Catalog
|
30
|
6
|
from cyborgbackup.main.models.schedules import Schedule
|
31
|
6
|
from cyborgbackup.main.models.repositories import Repository
|
32
|
6
|
from cyborgbackup.main.models.policies import Policy
|
33
|
6
|
from cyborgbackup.main.models.jobs import Job
|
34
|
6
|
from cyborgbackup.main.models.users import User
|
35
|
6
|
from cyborgbackup.main.models.settings import Setting
|
36
|
6
|
from cyborgbackup.main.constants import ACTIVE_STATES
|
37
|
6
|
from cyborgbackup.main.utils.common import (
|
38
|
|
get_type_for_model, get_model_for_type, camelcase_to_underscore,
|
39
|
|
has_model_field_prefetched, prefetch_page_capabilities)
|
40
|
6
|
from cyborgbackup.main.validators import vars_validate_or_raise
|
41
|
6
|
from cyborgbackup.api.versioning import reverse, get_request_version
|
42
|
6
|
from cyborgbackup.api.fields import BooleanNullField, CharNullField, ChoiceNullField, VerbatimField
|
43
|
|
|
44
|
6
|
logger = logging.getLogger('cyborgbackup.api.serializers')
|
45
|
|
|
46
|
6
|
DEPRECATED = 'This resource has been deprecated and will be removed in a future release'
|
47
|
|
|
48
|
|
|
49
|
|
# Fields that should be summarized regardless of object type.
|
50
|
6
|
DEFAULT_SUMMARY_FIELDS = ('id', 'name', 'created_by', 'modified_by')
|
51
|
|
|
52
|
|
|
53
|
|
# Keys are fields (foreign keys) where, if found on an instance, summary info
|
54
|
|
# should be added to the serialized data. Values are a tuple of field names on
|
55
|
|
# the related object to include in the summary data (if the field is present on
|
56
|
|
# the related object).
|
57
|
6
|
SUMMARIZABLE_FK_FIELDS = {
|
58
|
|
'user': ('id', 'email', 'first_name', 'last_name'),
|
59
|
|
'application': ('id', 'name', 'client_id'),
|
60
|
|
'job': ('id', 'name', 'status', 'failed', 'elapsed'),
|
61
|
|
'policy': ('id', 'name', 'policy_type'),
|
62
|
|
'client': ('id', 'hostname', 'bandwidth_limit'),
|
63
|
|
'repository': ('id', 'name', 'path', 'enabled'),
|
64
|
|
'schedule': ('id', 'name', 'crontab', 'enabled')
|
65
|
|
}
|
66
|
|
|
67
|
|
|
68
|
6
|
def reverse_gfk(content_object, request):
|
69
|
|
'''
|
70
|
|
Computes a reverse for a GenericForeignKey field.
|
71
|
|
|
72
|
|
Returns a dictionary of the form
|
73
|
|
{ '<type>': reverse(<type detail>) }
|
74
|
|
for example
|
75
|
|
{ 'organization': '/api/v1/organizations/1/' }
|
76
|
|
'''
|
77
|
0
|
if content_object is None or not hasattr(content_object, 'get_absolute_url'):
|
78
|
0
|
return {}
|
79
|
|
|
80
|
0
|
return {
|
81
|
|
camelcase_to_underscore(content_object.__class__.__name__): content_object.get_absolute_url(request=request)
|
82
|
|
}
|
83
|
|
|
84
|
|
|
85
|
6
|
class DynamicFieldsSerializerMixin(object):
|
86
|
|
"""
|
87
|
|
A serializer mixin that takes an additional `fields` argument that controls
|
88
|
|
which fields should be displayed.
|
89
|
|
"""
|
90
|
|
|
91
|
6
|
def __init__(self, *args, **kwargs):
|
92
|
|
# Don't pass the 'fields' arg up to the superclass
|
93
|
6
|
fields = kwargs.pop('fields', None)
|
94
|
|
|
95
|
|
# Instantiate the superclass normally
|
96
|
6
|
super(DynamicFieldsSerializerMixin, self).__init__(*args, **kwargs)
|
97
|
|
|
98
|
6
|
if fields is not None:
|
99
|
|
# Drop any fields that are not specified in the `fields` argument.
|
100
|
0
|
allowed = set(fields)
|
101
|
0
|
existing = set(self.fields.keys())
|
102
|
0
|
for field_name in existing - allowed:
|
103
|
0
|
self.fields.pop(field_name)
|
104
|
|
|
105
|
|
|
106
|
6
|
class BaseSerializerMetaclass(serializers.SerializerMetaclass):
|
107
|
|
'''
|
108
|
|
Custom metaclass to enable attribute inheritance from Meta objects on
|
109
|
|
serializer base classes.
|
110
|
|
|
111
|
|
Also allows for inheriting or updating field lists from base class(es):
|
112
|
|
|
113
|
|
class Meta:
|
114
|
|
|
115
|
|
# Inherit all fields from base class.
|
116
|
|
fields = ('*',)
|
117
|
|
|
118
|
|
# Inherit all fields from base class and add 'foo'.
|
119
|
|
fields = ('*', 'foo')
|
120
|
|
|
121
|
|
# Inherit all fields from base class except 'bar'.
|
122
|
|
fields = ('*', '-bar')
|
123
|
|
|
124
|
|
# Define fields as 'foo' and 'bar'; ignore base class fields.
|
125
|
|
fields = ('foo', 'bar')
|
126
|
|
|
127
|
|
# Extra field kwargs dicts are also merged from base classes.
|
128
|
|
extra_kwargs = {
|
129
|
|
'foo': {'required': True},
|
130
|
|
'bar': {'read_only': True},
|
131
|
|
}
|
132
|
|
|
133
|
|
# If a subclass were to define extra_kwargs as:
|
134
|
|
extra_kwargs = {
|
135
|
|
'foo': {'required': False, 'default': ''},
|
136
|
|
'bar': {'label': 'New Label for Bar'},
|
137
|
|
}
|
138
|
|
|
139
|
|
# The resulting value of extra_kwargs would be:
|
140
|
|
extra_kwargs = {
|
141
|
|
'foo': {'required': False, 'default': ''},
|
142
|
|
'bar': {'read_only': True, 'label': 'New Label for Bar'},
|
143
|
|
}
|
144
|
|
|
145
|
|
# Extra field kwargs cannot be removed in subclasses, only replaced.
|
146
|
|
|
147
|
|
'''
|
148
|
|
|
149
|
6
|
@staticmethod
|
150
|
|
def _is_list_of_strings(x):
|
151
|
6
|
return isinstance(x, (list, tuple)) and all([isinstance(y, str) for y in x])
|
152
|
|
|
153
|
6
|
@staticmethod
|
154
|
|
def _is_extra_kwargs(x):
|
155
|
6
|
return isinstance(x, dict) and all([isinstance(k, str) and isinstance(v, dict) for k, v in x.items()])
|
156
|
|
|
157
|
6
|
@classmethod
|
158
|
6
|
def _update_meta(cls, base, meta, other=None):
|
159
|
6
|
for attr in dir(other):
|
160
|
6
|
if attr.startswith('_'):
|
161
|
6
|
continue
|
162
|
6
|
val = getattr(other, attr)
|
163
|
6
|
meta_val = getattr(meta, attr, None)
|
164
|
|
# Special handling for lists/tuples of strings (field names).
|
165
|
6
|
if cls._is_list_of_strings(val) and cls._is_list_of_strings(meta_val or []):
|
166
|
6
|
meta_val = meta_val or []
|
167
|
6
|
new_vals = []
|
168
|
6
|
except_vals = []
|
169
|
6
|
if base:
|
170
|
6
|
new_vals.extend([x for x in meta_val])
|
171
|
6
|
for v in val:
|
172
|
6
|
if not base and v == '*':
|
173
|
6
|
new_vals.extend([x for x in meta_val])
|
174
|
6
|
elif not base and v.startswith('-'):
|
175
|
6
|
except_vals.append(v[1:])
|
176
|
|
else:
|
177
|
6
|
new_vals.append(v)
|
178
|
6
|
val = []
|
179
|
6
|
for v in new_vals:
|
180
|
6
|
if v not in except_vals and v not in val:
|
181
|
6
|
val.append(v)
|
182
|
6
|
val = tuple(val)
|
183
|
|
# Merge extra_kwargs dicts from base classes.
|
184
|
6
|
elif cls._is_extra_kwargs(val) and cls._is_extra_kwargs(meta_val or {}):
|
185
|
0
|
meta_val = meta_val or {}
|
186
|
0
|
new_val = {}
|
187
|
0
|
if base:
|
188
|
0
|
for k, v in meta_val.items():
|
189
|
0
|
new_val[k] = copy.deepcopy(v)
|
190
|
0
|
for k, v in val.items():
|
191
|
0
|
new_val.setdefault(k, {}).update(copy.deepcopy(v))
|
192
|
0
|
val = new_val
|
193
|
|
# Any other values are copied in case they are mutable objects.
|
194
|
|
else:
|
195
|
6
|
val = copy.deepcopy(val)
|
196
|
6
|
setattr(meta, attr, val)
|
197
|
|
|
198
|
6
|
def __new__(cls, name, bases, attrs):
|
199
|
6
|
meta = type('Meta', (object,), {})
|
200
|
6
|
for base in bases[::-1]:
|
201
|
6
|
cls._update_meta(base, meta, getattr(base, 'Meta', None))
|
202
|
6
|
cls._update_meta(None, meta, attrs.get('Meta', meta))
|
203
|
6
|
attrs['Meta'] = meta
|
204
|
6
|
return super(BaseSerializerMetaclass, cls).__new__(cls, name, bases, attrs)
|
205
|
|
|
206
|
|
|
207
|
6
|
class BaseSerializer(serializers.ModelSerializer, metaclass=BaseSerializerMetaclass):
|
208
|
|
|
209
|
6
|
class Meta:
|
210
|
6
|
ordering = ('id',)
|
211
|
6
|
fields = ('id', 'type', 'url', 'related', 'summary_fields', 'created',
|
212
|
|
'modified', 'name', 'created_by', 'modified_by')
|
213
|
6
|
summary_fields = ()
|
214
|
6
|
summarizable_fields = ()
|
215
|
|
|
216
|
|
# add the URL and related resources
|
217
|
6
|
type = serializers.SerializerMethodField()
|
218
|
6
|
url = serializers.SerializerMethodField()
|
219
|
6
|
related = serializers.SerializerMethodField('_get_related')
|
220
|
6
|
summary_fields = serializers.SerializerMethodField('_get_summary_fields')
|
221
|
|
|
222
|
|
# make certain fields read only
|
223
|
6
|
created = serializers.SerializerMethodField()
|
224
|
6
|
modified = serializers.SerializerMethodField()
|
225
|
|
|
226
|
6
|
@property
|
227
|
|
def version(self):
|
228
|
|
"""
|
229
|
|
The request version component of the URL as an integer i.e., 1 or 2
|
230
|
|
"""
|
231
|
6
|
return get_request_version(self.context.get('request'))
|
232
|
|
|
233
|
6
|
def get_type(self, obj):
|
234
|
6
|
return get_type_for_model(self.Meta.model)
|
235
|
|
|
236
|
6
|
def get_types(self):
|
237
|
0
|
return [self.get_type(None)]
|
238
|
|
|
239
|
6
|
def get_type_choices(self):
|
240
|
0
|
type_name_map = {
|
241
|
|
'job': 'Job',
|
242
|
|
'image': 'Image',
|
243
|
|
'client': 'Client',
|
244
|
|
'schedule': 'Schedule',
|
245
|
|
'repository': 'Repository',
|
246
|
|
'user': 'User'
|
247
|
|
}
|
248
|
0
|
choices = []
|
249
|
0
|
for t in self.get_types():
|
250
|
0
|
name = type_name_map.get(t, force_text(get_model_for_type(t)._meta.verbose_name).title())
|
251
|
0
|
choices.append((t, name))
|
252
|
0
|
return choices
|
253
|
|
|
254
|
6
|
def get_url(self, obj):
|
255
|
6
|
if isinstance(obj, User):
|
256
|
6
|
return self.reverse('api:user_detail', kwargs={'pk': obj.pk})
|
257
|
6
|
elif obj is None or not hasattr(obj, 'get_absolute_url'):
|
258
|
0
|
return ''
|
259
|
|
else:
|
260
|
6
|
return obj.get_absolute_url(request=self.context.get('request'))
|
261
|
|
|
262
|
6
|
def filter_field_metadata(self, fields, method):
|
263
|
|
"""
|
264
|
|
Filter field metadata based on the request method.
|
265
|
|
This it intended to be extended by subclasses.
|
266
|
|
"""
|
267
|
0
|
return fields
|
268
|
|
|
269
|
6
|
def _get_related(self, obj):
|
270
|
6
|
return {} if obj is None else self.get_related(obj)
|
271
|
|
|
272
|
6
|
def _generate_named_url(self, url_path, obj, node):
|
273
|
0
|
url_units = url_path.split('/')
|
274
|
0
|
named_url = node.generate_named_url(obj)
|
275
|
0
|
url_units[4] = named_url
|
276
|
0
|
return '/'.join(url_units)
|
277
|
|
|
278
|
6
|
def get_related(self, obj):
|
279
|
6
|
res = OrderedDict()
|
280
|
6
|
if getattr(obj, 'created_by', None):
|
281
|
6
|
res['created_by'] = self.reverse('api:user_detail', kwargs={'pk': obj.created_by.pk})
|
282
|
6
|
if getattr(obj, 'modified_by', None):
|
283
|
6
|
res['modified_by'] = self.reverse('api:user_detail', kwargs={'pk': obj.modified_by.pk})
|
284
|
6
|
return res
|
285
|
|
|
286
|
6
|
def _get_summary_fields(self, obj):
|
287
|
6
|
return {} if obj is None else self.get_summary_fields(obj)
|
288
|
|
|
289
|
6
|
def get_summary_fields(self, obj):
|
290
|
|
# Return values for certain fields on related objects, to simplify
|
291
|
|
# displaying lists of items without additional API requests.
|
292
|
6
|
summary_fields = OrderedDict()
|
293
|
6
|
for fk, related_fields in SUMMARIZABLE_FK_FIELDS.items():
|
294
|
6
|
try:
|
295
|
6
|
fkval = getattr(obj, fk, None)
|
296
|
6
|
if fkval is None:
|
297
|
6
|
continue
|
298
|
6
|
if fkval == obj:
|
299
|
0
|
continue
|
300
|
6
|
summary_fields[fk] = OrderedDict()
|
301
|
6
|
for field in related_fields:
|
302
|
6
|
if (
|
303
|
|
self.version < 2 and field == 'credential_type_id' and
|
304
|
|
fk in ['credential', 'vault_credential']):
|
305
|
0
|
continue
|
306
|
|
|
307
|
6
|
fval = getattr(fkval, field, None)
|
308
|
|
|
309
|
6
|
if fval is None and field == 'type':
|
310
|
0
|
if isinstance(fkval, PolymorphicModel):
|
311
|
0
|
fkval = fkval.get_real_instance()
|
312
|
0
|
fval = get_type_for_model(fkval)
|
313
|
6
|
if fval is not None:
|
314
|
6
|
summary_fields[fk][field] = fval
|
315
|
|
# Can be raised by the reverse accessor for a OneToOneField.
|
316
|
0
|
except ObjectDoesNotExist:
|
317
|
0
|
pass
|
318
|
6
|
if getattr(obj, 'created_by', None):
|
319
|
6
|
summary_fields['created_by'] = OrderedDict()
|
320
|
6
|
for field in SUMMARIZABLE_FK_FIELDS['user']:
|
321
|
6
|
summary_fields['created_by'][field] = getattr(obj.created_by, field)
|
322
|
6
|
if getattr(obj, 'modified_by', None):
|
323
|
6
|
summary_fields['modified_by'] = OrderedDict()
|
324
|
6
|
for field in SUMMARIZABLE_FK_FIELDS['user']:
|
325
|
6
|
summary_fields['modified_by'][field] = getattr(obj.modified_by, field)
|
326
|
|
|
327
|
6
|
return summary_fields
|
328
|
|
|
329
|
6
|
def _obj_capability_dict(self, obj):
|
330
|
|
"""
|
331
|
|
Returns the user_capabilities dictionary for a single item
|
332
|
|
If inside of a list view, it runs the prefetching algorithm for
|
333
|
|
the entire current page, saves it into context
|
334
|
|
"""
|
335
|
0
|
view = self.context.get('view', None)
|
336
|
0
|
parent_obj = None
|
337
|
0
|
if view and hasattr(view, 'parent_model') and hasattr(view, 'get_parent_object'):
|
338
|
0
|
parent_obj = view.get_parent_object()
|
339
|
0
|
if view and view.request and view.request.user:
|
340
|
0
|
capabilities_cache = {}
|
341
|
|
# if serializer has parent, it is ListView, apply page capabilities prefetch
|
342
|
0
|
if self.parent and hasattr(self, 'capabilities_prefetch') and self.capabilities_prefetch:
|
343
|
0
|
qs = self.parent.instance
|
344
|
0
|
if 'capability_map' not in self.context:
|
345
|
0
|
model = self.Meta.model
|
346
|
0
|
prefetch_list = self.capabilities_prefetch
|
347
|
0
|
self.context['capability_map'] = prefetch_page_capabilities(
|
348
|
|
model, qs, prefetch_list, view.request.user
|
349
|
|
)
|
350
|
0
|
if obj.id in self.context['capability_map']:
|
351
|
0
|
capabilities_cache = self.context['capability_map'][obj.id]
|
352
|
0
|
return {parent_obj, capabilities_cache}
|
353
|
|
# return get_user_capabilities(
|
354
|
|
# view.request.user, obj, method_list=self.show_capabilities, parent_obj=parent_obj,
|
355
|
|
# capabilities_cache=capabilities_cache
|
356
|
|
# )
|
357
|
|
else:
|
358
|
|
# Contextual information to produce user_capabilities doesn't exist
|
359
|
0
|
return {}
|
360
|
|
|
361
|
6
|
def get_created(self, obj):
|
362
|
6
|
if obj is None:
|
363
|
0
|
return None
|
364
|
6
|
elif isinstance(obj, User):
|
365
|
6
|
return obj.date_joined
|
366
|
6
|
elif hasattr(obj, 'created'):
|
367
|
6
|
return obj.created
|
368
|
0
|
return None
|
369
|
|
|
370
|
6
|
def get_modified(self, obj):
|
371
|
6
|
if obj is None:
|
372
|
0
|
return None
|
373
|
6
|
elif isinstance(obj, User):
|
374
|
0
|
return obj.last_login
|
375
|
6
|
elif hasattr(obj, 'modified'):
|
376
|
6
|
return obj.modified
|
377
|
0
|
return None
|
378
|
|
|
379
|
6
|
def get_extra_kwargs(self):
|
380
|
6
|
extra_kwargs = super(BaseSerializer, self).get_extra_kwargs()
|
381
|
6
|
if self.instance:
|
382
|
6
|
read_only_on_update_fields = getattr(self.Meta, 'read_only_on_update_fields', tuple())
|
383
|
6
|
for field_name in read_only_on_update_fields:
|
384
|
0
|
kwargs = extra_kwargs.get(field_name, {})
|
385
|
0
|
kwargs['read_only'] = True
|
386
|
0
|
extra_kwargs[field_name] = kwargs
|
387
|
6
|
return extra_kwargs
|
388
|
|
|
389
|
6
|
def build_standard_field(self, field_name, model_field):
|
390
|
|
# DRF 3.3 serializers.py::build_standard_field() -> utils/field_mapping.py::get_field_kwargs() short circuits
|
391
|
|
# when a Model's editable field is set to False. The short circuit skips choice rendering.
|
392
|
|
#
|
393
|
|
# This logic is to force rendering choice's on an uneditable field.
|
394
|
|
# Note: Consider expanding this rendering for more than just choices fields
|
395
|
|
# Note: This logic works in conjuction with
|
396
|
6
|
if hasattr(model_field, 'choices') and model_field.choices:
|
397
|
6
|
was_editable = model_field.editable
|
398
|
6
|
model_field.editable = True
|
399
|
|
|
400
|
6
|
field_class, field_kwargs = super(BaseSerializer, self).build_standard_field(field_name, model_field)
|
401
|
6
|
if hasattr(model_field, 'choices') and model_field.choices:
|
402
|
6
|
model_field.editable = was_editable
|
403
|
6
|
if was_editable is False:
|
404
|
6
|
field_kwargs['read_only'] = True
|
405
|
|
|
406
|
|
# Pass model field default onto the serializer field if field is not read-only.
|
407
|
6
|
if model_field.has_default() and not field_kwargs.get('read_only', False):
|
408
|
6
|
field_kwargs['default'] = field_kwargs['initial'] = model_field.get_default()
|
409
|
|
|
410
|
|
# Enforce minimum value of 0 for PositiveIntegerFields.
|
411
|
6
|
if isinstance(model_field, (models.PositiveIntegerField, models.PositiveSmallIntegerField)) \
|
412
|
|
and 'choices' not in field_kwargs:
|
413
|
6
|
field_kwargs['min_value'] = 0
|
414
|
|
|
415
|
|
# Use custom boolean field that allows null and empty string as False values.
|
416
|
6
|
if isinstance(model_field, models.BooleanField) and not field_kwargs.get('read_only', False):
|
417
|
6
|
field_class = BooleanNullField
|
418
|
|
|
419
|
|
# Use custom char or choice field that coerces null to an empty string.
|
420
|
6
|
if isinstance(model_field, (models.CharField, models.TextField)) and not field_kwargs.get('read_only', False):
|
421
|
6
|
if 'choices' in field_kwargs:
|
422
|
6
|
field_class = ChoiceNullField
|
423
|
|
else:
|
424
|
6
|
field_class = CharNullField
|
425
|
|
|
426
|
|
# Update the message used for the unique validator to use capitalized
|
427
|
|
# verbose name; keeps unique message the same as with DRF 2.x.
|
428
|
6
|
opts = self.Meta.model._meta.concrete_model._meta
|
429
|
6
|
for validator in field_kwargs.get('validators', []):
|
430
|
6
|
if isinstance(validator, validators.UniqueValidator):
|
431
|
6
|
unique_error_message = model_field.error_messages.get('unique', None)
|
432
|
6
|
if unique_error_message:
|
433
|
6
|
unique_error_message = unique_error_message % {
|
434
|
|
'model_name': capfirst(opts.verbose_name),
|
435
|
|
'field_label': capfirst(model_field.verbose_name),
|
436
|
|
}
|
437
|
6
|
validator.message = unique_error_message
|
438
|
|
|
439
|
6
|
return field_class, field_kwargs
|
440
|
|
|
441
|
6
|
def build_relational_field(self, field_name, relation_info):
|
442
|
6
|
field_class, field_kwargs = super(BaseSerializer, self).build_relational_field(field_name, relation_info)
|
443
|
|
# Don't include choices for foreign key fields.
|
444
|
6
|
field_kwargs.pop('choices', None)
|
445
|
6
|
return field_class, field_kwargs
|
446
|
|
|
447
|
6
|
def get_unique_together_validators(self):
|
448
|
|
# Allow the model's full_clean method to handle the unique together validation.
|
449
|
6
|
return []
|
450
|
|
|
451
|
6
|
def run_validation(self, data=fields.empty):
|
452
|
6
|
try:
|
453
|
6
|
return super(BaseSerializer, self).run_validation(data)
|
454
|
0
|
except ValidationError as exc:
|
455
|
|
# Avoid bug? in DRF if exc.detail happens to be a list instead of a dict.
|
456
|
0
|
raise ValidationError(detail=serializers.as_serializer_error(exc))
|
457
|
|
|
458
|
6
|
def get_validation_exclusions(self, obj=None):
|
459
|
|
# Borrowed from DRF 2.x - return model fields that should be excluded
|
460
|
|
# from model validation.
|
461
|
6
|
cls = self.Meta.model
|
462
|
6
|
opts = cls._meta.concrete_model._meta
|
463
|
6
|
exclusions = [field.name for field in opts.fields]
|
464
|
6
|
for field_name, field in self.fields.items():
|
465
|
6
|
field_name = field.source or field_name
|
466
|
6
|
if field_name not in exclusions:
|
467
|
6
|
continue
|
468
|
6
|
if field.read_only:
|
469
|
6
|
continue
|
470
|
6
|
if isinstance(field, serializers.Serializer):
|
471
|
0
|
continue
|
472
|
6
|
exclusions.remove(field_name)
|
473
|
|
# The clean_ methods cannot be ran on many-to-many models
|
474
|
6
|
exclusions.extend([field.name for field in opts.many_to_many])
|
475
|
6
|
return exclusions
|
476
|
|
|
477
|
6
|
def validate(self, attrs):
|
478
|
6
|
attrs = super(BaseSerializer, self).validate(attrs)
|
479
|
6
|
try:
|
480
|
|
# Create/update a model instance and run it's full_clean() method to
|
481
|
|
# do any validation implemented on the model class.
|
482
|
6
|
exclusions = self.get_validation_exclusions(self.instance)
|
483
|
6
|
obj = self.instance or self.Meta.model()
|
484
|
6
|
for k, v in attrs.items():
|
485
|
6
|
if k not in exclusions:
|
486
|
6
|
setattr(obj, k, v)
|
487
|
6
|
obj.full_clean(exclude=exclusions)
|
488
|
|
# full_clean may modify values on the instance; copy those changes
|
489
|
|
# back to attrs so they are saved.
|
490
|
6
|
for k in attrs.keys():
|
491
|
6
|
if k not in exclusions:
|
492
|
6
|
attrs[k] = getattr(obj, k)
|
493
|
0
|
except DjangoValidationError as exc:
|
494
|
|
# DjangoValidationError may contain a list or dict; normalize into a
|
495
|
|
# dict where the keys are the field name and the values are a list
|
496
|
|
# of error messages, then raise as a DRF ValidationError. DRF would
|
497
|
|
# normally convert any DjangoValidationError to a non-field specific
|
498
|
|
# error message; here we preserve field-specific errors raised from
|
499
|
|
# the model's full_clean method.
|
500
|
0
|
d = exc.update_error_dict({})
|
501
|
0
|
for k, v in d.items():
|
502
|
0
|
v = v if isinstance(v, list) else [v]
|
503
|
0
|
v2 = []
|
504
|
0
|
for e in v:
|
505
|
0
|
if isinstance(e, DjangoValidationError):
|
506
|
0
|
v2.extend(list(e))
|
507
|
0
|
elif isinstance(e, list):
|
508
|
0
|
v2.extend(e)
|
509
|
|
else:
|
510
|
0
|
v2.append(e)
|
511
|
0
|
d[k] = map(force_text, v2)
|
512
|
0
|
raise ValidationError(d)
|
513
|
6
|
return attrs
|
514
|
|
|
515
|
6
|
def reverse(self, *args, **kwargs):
|
516
|
6
|
kwargs['request'] = self.context.get('request')
|
517
|
6
|
return reverse(*args, **kwargs)
|
518
|
|
|
519
|
6
|
@property
|
520
|
|
def is_detail_view(self):
|
521
|
0
|
if 'view' in self.context:
|
522
|
0
|
if 'pk' in self.context['view'].kwargs:
|
523
|
0
|
return True
|
524
|
0
|
return False
|
525
|
|
|
526
|
|
|
527
|
6
|
class EmptySerializer(serializers.Serializer):
|
528
|
6
|
pass
|
529
|
|
|
530
|
|
|
531
|
6
|
class UserSerializer(BaseSerializer):
|
532
|
|
|
533
|
6
|
password = serializers.CharField(required=False, default='', write_only=True,
|
534
|
|
help_text=_('Write-only field used to change the password.'))
|
535
|
6
|
show_capabilities = ['edit', 'delete']
|
536
|
|
|
537
|
6
|
class Meta:
|
538
|
6
|
model = User
|
539
|
6
|
fields = ('*', '-name', '-description', '-modified', '-username',
|
540
|
|
'first_name', 'last_name', 'email', 'is_superuser', 'password',
|
541
|
|
'-created_by', '-modified_by', 'notify_backup_daily',
|
542
|
|
'notify_backup_weekly', 'notify_backup_monthly',
|
543
|
|
'notify_backup_success', 'notify_backup_failed',
|
544
|
|
'notify_backup_summary')
|
545
|
|
|
546
|
6
|
def to_representation(self, obj):
|
547
|
6
|
ret = super(UserSerializer, self).to_representation(obj)
|
548
|
6
|
ret.pop('password', None)
|
549
|
6
|
return ret
|
550
|
|
|
551
|
6
|
def get_validation_exclusions(self, obj=None):
|
552
|
0
|
ret = super(UserSerializer, self).get_validation_exclusions(obj)
|
553
|
0
|
ret.append('password')
|
554
|
0
|
return ret
|
555
|
|
|
556
|
6
|
def validate_password(self, value):
|
557
|
0
|
if not self.instance and value in (None, ''):
|
558
|
0
|
raise serializers.ValidationError(_('Password required for new User.'))
|
559
|
0
|
return value
|
560
|
|
|
561
|
6
|
def _update_password(self, obj, new_password):
|
562
|
|
# For now we're not raising an error, just not saving password for
|
563
|
|
# users managed by LDAP who already have an unusable password set.
|
564
|
0
|
if new_password:
|
565
|
0
|
obj.set_password(new_password)
|
566
|
0
|
obj.save(update_fields=['password'])
|
567
|
0
|
elif not obj.password:
|
568
|
0
|
obj.set_unusable_password()
|
569
|
0
|
obj.save(update_fields=['password'])
|
570
|
|
|
571
|
6
|
def create(self, validated_data):
|
572
|
0
|
new_password = validated_data.pop('password', None)
|
573
|
0
|
obj = super(UserSerializer, self).create(validated_data)
|
574
|
0
|
self._update_password(obj, new_password)
|
575
|
0
|
return obj
|
576
|
|
|
577
|
6
|
def update(self, obj, validated_data):
|
578
|
0
|
new_password = validated_data.pop('password', None)
|
579
|
0
|
obj = super(UserSerializer, self).update(obj, validated_data)
|
580
|
0
|
self._update_password(obj, new_password)
|
581
|
0
|
return obj
|
582
|
|
|
583
|
|
|
584
|
6
|
class BaseSerializerWithVariables(BaseSerializer):
|
585
|
|
|
586
|
6
|
def validate_variables(self, value):
|
587
|
0
|
return vars_validate_or_raise(value)
|
588
|
|
|
589
|
|
|
590
|
6
|
class LabelsListMixin(object):
|
591
|
|
|
592
|
6
|
def _summary_field_labels(self, obj):
|
593
|
0
|
label_list = [{'id': x.id, 'name': x.name} for x in obj.labels.all()[:10]]
|
594
|
0
|
if has_model_field_prefetched(obj, 'labels'):
|
595
|
0
|
label_ct = len(obj.labels.all())
|
596
|
|
else:
|
597
|
0
|
if len(label_list) < 10:
|
598
|
0
|
label_ct = len(label_list)
|
599
|
|
else:
|
600
|
0
|
label_ct = obj.labels.count()
|
601
|
0
|
return {'count': label_ct, 'results': label_list}
|
602
|
|
|
603
|
6
|
def get_summary_fields(self, obj):
|
604
|
0
|
res = super(LabelsListMixin, self).get_summary_fields(obj)
|
605
|
0
|
res['labels'] = self._summary_field_labels(obj)
|
606
|
0
|
return res
|
607
|
|
|
608
|
|
|
609
|
6
|
class JobSerializer(BaseSerializer):
|
610
|
|
|
611
|
6
|
show_capabilities = ['start', 'delete']
|
612
|
6
|
event_processing_finished = serializers.BooleanField(
|
613
|
|
help_text=_('Indicates whether all of the events generated by this '
|
614
|
|
'unified job have been saved to the database.'),
|
615
|
|
read_only=True
|
616
|
|
)
|
617
|
|
|
618
|
6
|
class Meta:
|
619
|
6
|
model = Job
|
620
|
6
|
fields = ('*', 'launch_type', 'status', 'policy',
|
621
|
|
'failed', 'started', 'finished', 'elapsed', 'job_args',
|
622
|
|
'original_size', 'compressed_size', 'deduplicated_size', 'archive_name',
|
623
|
|
'job_cwd', 'job_env', 'job_explanation', 'client', 'repository',
|
624
|
|
'dependent_jobs', 'result_traceback', 'event_processing_finished', 'job_type')
|
625
|
|
|
626
|
6
|
def get_types(self):
|
627
|
0
|
if type(self) is JobSerializer:
|
628
|
0
|
return ['job', ]
|
629
|
|
else:
|
630
|
0
|
return super(JobSerializer, self).get_types()
|
631
|
|
|
632
|
6
|
def get_summary_fields(self, obj):
|
633
|
0
|
summary_dict = super(JobSerializer, self).get_summary_fields(obj)
|
634
|
0
|
if obj.policy and obj.policy.repository_id:
|
635
|
0
|
summary_dict['repository'] = {
|
636
|
|
'id': obj.policy.repository_id,
|
637
|
|
'name': obj.policy.repository.name
|
638
|
|
}
|
639
|
|
|
640
|
0
|
if obj.policy and obj.policy.schedule_id:
|
641
|
0
|
summary_dict['schedule'] = {
|
642
|
|
'id': obj.policy.schedule_id,
|
643
|
|
'name': obj.policy.schedule.name
|
644
|
|
}
|
645
|
|
|
646
|
0
|
return summary_dict
|
647
|
|
|
648
|
6
|
def get_related(self, obj):
|
649
|
0
|
res = super(JobSerializer, self).get_related(obj)
|
650
|
0
|
res.update(dict(
|
651
|
|
job_events=self.reverse('api:job_job_events_list', kwargs={'pk': obj.pk}),
|
652
|
|
))
|
653
|
0
|
if obj.policy_id:
|
654
|
0
|
res['policy'] = self.reverse('api:policy_detail', kwargs={'pk': obj.policy_id})
|
655
|
0
|
res['stdout'] = self.reverse('api:job_stdout', kwargs={'pk': obj.pk})
|
656
|
0
|
if (obj.can_start or True):
|
657
|
0
|
res['start'] = self.reverse('api:job_start', kwargs={'pk': obj.pk})
|
658
|
0
|
if obj.can_cancel or True:
|
659
|
0
|
res['cancel'] = self.reverse('api:job_cancel', kwargs={'pk': obj.pk})
|
660
|
0
|
res['relaunch'] = self.reverse('api:job_relaunch', kwargs={'pk': obj.pk})
|
661
|
0
|
return res
|
662
|
|
|
663
|
6
|
def get_artifacts(self, obj):
|
664
|
0
|
if obj:
|
665
|
0
|
return obj.display_artifacts()
|
666
|
0
|
return {}
|
667
|
|
|
668
|
6
|
def to_internal_value(self, data):
|
669
|
0
|
return super(JobSerializer, self).to_internal_value(data)
|
670
|
|
|
671
|
6
|
def to_representation(self, obj):
|
672
|
0
|
ret = super(JobSerializer, self).to_representation(obj)
|
673
|
0
|
serializer_class = None
|
674
|
0
|
if serializer_class:
|
675
|
0
|
serializer = serializer_class(instance=obj, context=self.context)
|
676
|
0
|
ret = serializer.to_representation(obj)
|
677
|
|
else:
|
678
|
0
|
ret = super(JobSerializer, self).to_representation(obj)
|
679
|
|
|
680
|
0
|
if 'elapsed' in ret:
|
681
|
0
|
if obj and obj.pk and obj.started and not obj.finished:
|
682
|
0
|
td = now() - obj.started
|
683
|
0
|
ret['elapsed'] = (td.microseconds + (td.seconds + td.days * 24 * 3600) * 10 ** 6) / (10 ** 6 * 1.0)
|
684
|
0
|
ret['elapsed'] = float(ret['elapsed'])
|
685
|
|
|
686
|
0
|
return ret
|
687
|
|
|
688
|
|
|
689
|
6
|
class JobStdoutSerializer(JobSerializer):
|
690
|
|
|
691
|
6
|
result_stdout = serializers.SerializerMethodField()
|
692
|
|
|
693
|
6
|
class Meta:
|
694
|
6
|
fields = ('result_stdout',)
|
695
|
|
|
696
|
6
|
def get_types(self):
|
697
|
0
|
if type(self) is JobStdoutSerializer:
|
698
|
0
|
return ['job']
|
699
|
|
else:
|
700
|
0
|
return super(JobStdoutSerializer, self).get_types()
|
701
|
|
|
702
|
|
|
703
|
6
|
class JobCancelSerializer(JobSerializer):
|
704
|
|
|
705
|
6
|
can_cancel = serializers.BooleanField(read_only=True)
|
706
|
|
|
707
|
6
|
class Meta:
|
708
|
6
|
fields = ('can_cancel',)
|
709
|
|
|
710
|
|
|
711
|
6
|
class JobRelaunchSerializer(BaseSerializer):
|
712
|
|
|
713
|
6
|
retry_counts = serializers.SerializerMethodField()
|
714
|
|
|
715
|
6
|
class Meta:
|
716
|
6
|
model = Job
|
717
|
6
|
fields = ('retry_counts',)
|
718
|
|
|
719
|
6
|
def get_retry_counts(self, obj):
|
720
|
0
|
if obj.status in ACTIVE_STATES:
|
721
|
0
|
return _('Relaunch by host status not available until job finishes running.')
|
722
|
0
|
data = OrderedDict([])
|
723
|
0
|
return data
|
724
|
|
|
725
|
6
|
def get_validation_exclusions(self, *args, **kwargs):
|
726
|
0
|
r = super(JobRelaunchSerializer, self).get_validation_exclusions(*args, **kwargs)
|
727
|
0
|
return r
|
728
|
|
|
729
|
6
|
def validate(self, attrs):
|
730
|
0
|
attrs = super(JobRelaunchSerializer, self).validate(attrs)
|
731
|
0
|
return attrs
|
732
|
|
|
733
|
|
|
734
|
6
|
class JobListSerializer(DynamicFieldsSerializerMixin, JobSerializer):
|
735
|
|
|
736
|
6
|
class Meta:
|
737
|
6
|
fields = ('*', '-job_args', '-job_cwd', '-job_env', '-result_traceback', '-event_processing_finished')
|
738
|
|
|
739
|
6
|
def get_field_names(self, declared_fields, info):
|
740
|
0
|
field_names = super(JobListSerializer, self).get_field_names(declared_fields, info)
|
741
|
|
# Meta multiple inheritance and -field_name options don't seem to be
|
742
|
|
# taking effect above, so remove the undesired fields here.
|
743
|
0
|
return tuple(x for x in field_names if x not in (
|
744
|
|
'job_args', 'job_cwd',
|
745
|
|
'job_env', 'result_traceback', 'event_processing_finished'
|
746
|
|
))
|
747
|
|
|
748
|
6
|
def get_types(self):
|
749
|
0
|
if type(self) is JobListSerializer:
|
750
|
0
|
return ['job', ]
|
751
|
|
else:
|
752
|
0
|
return super(JobListSerializer, self).get_types()
|
753
|
|
|
754
|
6
|
def to_representation(self, obj):
|
755
|
0
|
serializer_class = None
|
756
|
0
|
if type(self) is JobListSerializer:
|
757
|
0
|
if isinstance(obj, Job):
|
758
|
0
|
serializer_class = JobSerializer
|
759
|
0
|
if serializer_class:
|
760
|
0
|
serializer = serializer_class(instance=obj, context=self.context)
|
761
|
0
|
ret = serializer.to_representation(obj)
|
762
|
|
else:
|
763
|
0
|
ret = super(JobListSerializer, self).to_representation(obj)
|
764
|
0
|
if 'elapsed' in ret:
|
765
|
0
|
ret['elapsed'] = float(ret['elapsed'])
|
766
|
0
|
return ret
|
767
|
|
|
768
|
|
|
769
|
6
|
class JobEventSerializer(BaseSerializer):
|
770
|
|
|
771
|
6
|
event_display = serializers.CharField(source='get_event_display2', read_only=True)
|
772
|
6
|
event_level = serializers.IntegerField(read_only=True)
|
773
|
|
|
774
|
6
|
class Meta:
|
775
|
6
|
model = JobEvent
|
776
|
6
|
fields = ('*', '-name', '-description', 'job', 'event', 'counter',
|
777
|
|
'event_display', 'event_data', 'event_level', 'failed',
|
778
|
|
'changed', 'uuid', 'task', 'stdout', 'start_line', 'end_line',
|
779
|
|
'verbosity', '-created_by', '-modified_by')
|
780
|
|
|
781
|
6
|
def get_related(self, obj):
|
782
|
0
|
res = super(JobEventSerializer, self).get_related(obj)
|
783
|
0
|
res.update(dict(
|
784
|
|
job=self.reverse('api:job_detail', kwargs={'pk': obj.job_id}),
|
785
|
|
))
|
786
|
0
|
return res
|
787
|
|
|
788
|
6
|
def get_summary_fields(self, obj):
|
789
|
0
|
d = super(JobEventSerializer, self).get_summary_fields(obj)
|
790
|
0
|
return d
|
791
|
|
|
792
|
6
|
def to_representation(self, obj):
|
793
|
0
|
ret = super(JobEventSerializer, self).to_representation(obj)
|
794
|
|
# Show full stdout for event detail view, truncate only for list view.
|
795
|
0
|
if hasattr(self.context.get('view', None), 'retrieve'):
|
796
|
0
|
return ret
|
797
|
|
# Show full stdout for playbook_on_* events.
|
798
|
0
|
if obj and obj.event.startswith('playbook_on'):
|
799
|
0
|
return ret
|
800
|
0
|
max_bytes = 1024
|
801
|
0
|
if max_bytes > 0 and 'stdout' in ret and len(ret['stdout']) >= max_bytes:
|
802
|
0
|
ret['stdout'] = ret['stdout'][:(max_bytes - 1)] + u'\u2026'
|
803
|
0
|
set_count = 0
|
804
|
0
|
reset_count = 0
|
805
|
0
|
for m in ANSI_SGR_PATTERN.finditer(ret['stdout']):
|
806
|
0
|
if m.string[m.start():m.end()] == u'\u001b[0m':
|
807
|
0
|
reset_count += 1
|
808
|
|
else:
|
809
|
0
|
set_count += 1
|
810
|
0
|
ret['stdout'] += u'\u001b[0m' * (set_count - reset_count)
|
811
|
0
|
return ret
|
812
|
|
|
813
|
|
|
814
|
6
|
class JobEventWebSocketSerializer(JobEventSerializer):
|
815
|
6
|
created = serializers.SerializerMethodField()
|
816
|
6
|
modified = serializers.SerializerMethodField()
|
817
|
6
|
event_name = serializers.CharField(source='event')
|
818
|
6
|
group_name = serializers.SerializerMethodField()
|
819
|
|
|
820
|
6
|
class Meta:
|
821
|
6
|
model = JobEvent
|
822
|
6
|
fields = ('*', 'event_name', 'group_name',)
|
823
|
|
|
824
|
6
|
def get_created(self, obj):
|
825
|
0
|
return obj.created.isoformat()
|
826
|
|
|
827
|
6
|
def get_modified(self, obj):
|
828
|
0
|
return obj.modified.isoformat()
|
829
|
|
|
830
|
6
|
def get_group_name(self, obj):
|
831
|
0
|
return 'job_events'
|
832
|
|
|
833
|
|
|
834
|
6
|
class SettingSerializer(BaseSerializer):
|
835
|
|
"""Read-only serializer for activity stream."""
|
836
|
|
|
837
|
6
|
value = VerbatimField(allow_null=True)
|
838
|
|
|
839
|
6
|
class Meta:
|
840
|
6
|
model = Setting
|
841
|
6
|
fields = ('id', 'url', 'key', 'type', 'setting_type', 'value', 'created', 'modified')
|
842
|
|
|
843
|
6
|
def update(self, obj, validated_data):
|
844
|
0
|
validated_data.pop('key', None)
|
845
|
0
|
obj = super(SettingSerializer, self).update(obj, validated_data)
|
846
|
0
|
return obj
|
847
|
|
|
848
|
6
|
def validate(self, attrs):
|
849
|
0
|
attrs.pop('key', None)
|
850
|
0
|
return attrs
|
851
|
|
|
852
|
|
|
853
|
6
|
class SettingListSerializer(SettingSerializer):
|
854
|
|
|
855
|
6
|
class Meta:
|
856
|
6
|
fields = ('*',)
|
857
|
|
|
858
|
6
|
def get_field_names(self, declared_fields, info):
|
859
|
6
|
field_names = super(SettingListSerializer, self).get_field_names(declared_fields, info)
|
860
|
|
# Meta multiple inheritance and -field_name options don't seem to be
|
861
|
|
# taking effect above, so remove the undesired fields here.
|
862
|
6
|
return tuple(x for x in field_names)
|
863
|
|
|
864
|
6
|
def get_types(self):
|
865
|
0
|
if type(self) is SettingListSerializer:
|
866
|
0
|
return ['setting']
|
867
|
|
else:
|
868
|
0
|
return super(SettingListSerializer, self).get_types()
|
869
|
|
|
870
|
|
|
871
|
6
|
class ClientSerializer(BaseSerializer):
|
872
|
6
|
show_capabilities = ['edit', 'delete']
|
873
|
|
|
874
|
6
|
class Meta:
|
875
|
6
|
model = Client
|
876
|
6
|
fields = ('*', '-name', '-description', 'hostname', 'ip', 'bandwidth_limit',
|
877
|
|
'version', 'ready', 'hypervisor_ready', 'hypervisor_name', 'enabled', 'uuid')
|
878
|
|
|
879
|
6
|
def get_summary_fields(self, obj):
|
880
|
6
|
summary_dict = super(ClientSerializer, self).get_summary_fields(obj)
|
881
|
6
|
relPolicies = Policy.objects.filter(clients__id=obj.pk)
|
882
|
6
|
if relPolicies.exists():
|
883
|
0
|
summary_dict['policies'] = []
|
884
|
0
|
for pol in relPolicies:
|
885
|
0
|
summary_dict['policies'].append({'id': pol.id, 'name': pol.name})
|
886
|
|
|
887
|
6
|
return summary_dict
|
888
|
|
|
889
|
|
|
890
|
6
|
class ClientListSerializer(ClientSerializer):
|
891
|
|
|
892
|
6
|
class Meta:
|
893
|
6
|
fields = ('*',)
|
894
|
|
|
895
|
6
|
def get_field_names(self, declared_fields, info):
|
896
|
6
|
field_names = super(ClientListSerializer, self).get_field_names(declared_fields, info)
|
897
|
6
|
return tuple(x for x in field_names)
|
898
|
|
|
899
|
6
|
def get_types(self):
|
900
|
0
|
if type(self) is ClientListSerializer:
|
901
|
0
|
return ['client']
|
902
|
|
else:
|
903
|
0
|
return super(ClientListSerializer, self).get_types()
|
904
|
|
|
905
|
|
|
906
|
6
|
class ScheduleSerializer(BaseSerializer):
|
907
|
|
"""Read-only serializer for activity stream."""
|
908
|
|
|
909
|
6
|
class Meta:
|
910
|
6
|
model = Schedule
|
911
|
6
|
fields = ('id', 'uuid', 'url', 'name', 'crontab', 'enabled', 'created', 'modified')
|
912
|
|
|
913
|
6
|
def update(self, obj, validated_data):
|
914
|
6
|
obj = super(ScheduleSerializer, self).update(obj, validated_data)
|
915
|
6
|
return obj
|
916
|
|
|
917
|
6
|
def validate(self, attrs):
|
918
|
6
|
return attrs
|
919
|
|
|
920
|
|
|
921
|
6
|
class ScheduleListSerializer(ScheduleSerializer):
|
922
|
|
|
923
|
6
|
class Meta:
|
924
|
6
|
fields = ('*',)
|
925
|
|
|
926
|
6
|
def get_field_names(self, declared_fields, info):
|
927
|
6
|
field_names = super(ScheduleListSerializer, self).get_field_names(declared_fields, info)
|
928
|
|
# Meta multiple inheritance and -field_name options don't seem to be
|
929
|
|
# taking effect above, so remove the undesired fields here.
|
930
|
6
|
return tuple(x for x in field_names)
|
931
|
|
|
932
|
6
|
def get_types(self):
|
933
|
0
|
if type(self) is ScheduleListSerializer:
|
934
|
0
|
return ['schedule']
|
935
|
|
else:
|
936
|
0
|
return super(ScheduleListSerializer, self).get_types()
|
937
|
|
|
938
|
|
|
939
|
6
|
class RepositorySerializer(BaseSerializer):
|
940
|
|
"""Read-only serializer for activity stream."""
|
941
|
|
|
942
|
6
|
class Meta:
|
943
|
6
|
model = Repository
|
944
|
6
|
fields = ('id', 'uuid', 'url', 'name', 'path', 'repository_key',
|
945
|
|
'original_size', 'compressed_size', 'deduplicated_size', 'ready', 'enabled', 'created', 'modified')
|
946
|
|
|
947
|
6
|
def update(self, obj, validated_data):
|
948
|
6
|
obj = super(RepositorySerializer, self).update(obj, validated_data)
|
949
|
6
|
return obj
|
950
|
|
|
951
|
6
|
def validate(self, attrs):
|
952
|
6
|
return attrs
|
953
|
|
|
954
|
|
|
955
|
6
|
class RepositoryListSerializer(RepositorySerializer):
|
956
|
|
|
957
|
6
|
class Meta:
|
958
|
6
|
fields = ('*',)
|
959
|
|
|
960
|
6
|
def get_field_names(self, declared_fields, info):
|
961
|
6
|
field_names = super(RepositoryListSerializer, self).get_field_names(declared_fields, info)
|
962
|
|
# Meta multiple inheritance and -field_name options don't seem to be
|
963
|
|
# taking effect above, so remove the undesired fields here.
|
964
|
6
|
return tuple(x for x in field_names)
|
965
|
|
|
966
|
6
|
def get_types(self):
|
967
|
0
|
if type(self) is RepositoryListSerializer:
|
968
|
0
|
return ['repository']
|
969
|
|
else:
|
970
|
0
|
return super(RepositoryListSerializer, self).get_types()
|
971
|
|
|
972
|
|
|
973
|
6
|
class PolicySerializer(BaseSerializer):
|
974
|
|
|
975
|
6
|
class Meta:
|
976
|
6
|
model = Policy
|
977
|
6
|
fields = ('*', 'id', 'uuid', 'url', 'name', 'extra_vars',
|
978
|
|
'clients', 'repository', 'schedule', 'policy_type', 'keep_hourly',
|
979
|
|
'keep_yearly', 'keep_daily', 'keep_weekly', 'keep_monthly',
|
980
|
|
'vmprovider', 'next_run', 'mode_pull', 'enabled', 'created', 'modified',
|
981
|
|
'prehook', 'posthook')
|
982
|
|
|
983
|
6
|
def get_related(self, obj):
|
984
|
6
|
res = super(PolicySerializer, self).get_related(obj)
|
985
|
6
|
res['launch'] = self.reverse('api:policy_launch', kwargs={'pk': obj.pk})
|
986
|
6
|
res['calendar'] = self.reverse('api:policy_calendar', kwargs={'pk': obj.pk})
|
987
|
6
|
if obj.schedule:
|
988
|
6
|
res['schedule'] = self.reverse('api:schedule_detail', kwargs={'pk': obj.schedule.pk})
|
989
|
6
|
if obj.repository:
|
990
|
6
|
res['repository'] = self.reverse('api:repository_detail', kwargs={'pk': obj.repository.pk})
|
991
|
6
|
return res
|
992
|
|
|
993
|
6
|
def to_representation(self, obj):
|
994
|
6
|
ret = super(PolicySerializer, self).to_representation(obj)
|
995
|
6
|
if obj is not None and 'schedule' in ret and not obj.schedule:
|
996
|
0
|
ret['schedule'] = None
|
997
|
6
|
if obj is not None and 'repository' in ret and not obj.repository:
|
998
|
0
|
ret['repository'] = None
|
999
|
6
|
return ret
|
1000
|
|
|
1001
|
|
|
1002
|
6
|
class PolicyListSerializer(PolicySerializer):
|
1003
|
|
|
1004
|
6
|
class Meta:
|
1005
|
6
|
fields = ('*',)
|
1006
|
|
|
1007
|
6
|
def get_field_names(self, declared_fields, info):
|
1008
|
0
|
field_names = super(PolicyListSerializer, self).get_field_names(declared_fields, info)
|
1009
|
|
# Meta multiple inheritance and -field_name options don't seem to be
|
1010
|
|
# taking effect above, so remove the undesired fields here.
|
1011
|
0
|
return tuple(x for x in field_names)
|
1012
|
|
|
1013
|
6
|
def get_types(self):
|
1014
|
0
|
if type(self) is PolicyListSerializer:
|
1015
|
0
|
return ['policy']
|
1016
|
|
else:
|
1017
|
0
|
return super(PolicyListSerializer, self).get_types()
|
1018
|
|
|
1019
|
|
|
1020
|
6
|
class PolicyLaunchSerializer(BaseSerializer):
|
1021
|
6
|
defaults = serializers.SerializerMethodField()
|
1022
|
6
|
extra_vars = serializers.JSONField(required=False, write_only=True)
|
1023
|
6
|
verbosity = serializers.IntegerField(required=False, initial=0, min_value=0, max_value=4, write_only=True)
|
1024
|
|
|
1025
|
6
|
class Meta:
|
1026
|
6
|
model = Policy
|
1027
|
6
|
fields = ('defaults', 'extra_vars', 'verbosity')
|
1028
|
|
|
1029
|
6
|
def get_defaults(self, obj):
|
1030
|
0
|
defaults_dict = {'verbosity': 0, 'extra_vars': obj.extra_vars}
|
1031
|
0
|
return defaults_dict
|
1032
|
|
|
1033
|
6
|
def get_job_template_data(self, obj):
|
1034
|
0
|
return dict(name=obj.name, id=obj.id, description=obj.description)
|
1035
|
|
|
1036
|
6
|
def validate_extra_vars(self, value):
|
1037
|
0
|
return vars_validate_or_raise(value)
|
1038
|
|
|
1039
|
|
|
1040
|
6
|
class PolicyCalendarSerializer(EmptySerializer):
|
1041
|
6
|
events = serializers.ListField(child=serializers.DateTimeField())
|
1042
|
|
|
1043
|
|
|
1044
|
6
|
class PolicyVMModuleSerializer(EmptySerializer):
|
1045
|
6
|
modules = serializers.SerializerMethodField()
|
1046
|
|
|
1047
|
|
|
1048
|
6
|
class PolicyModuleSerializer(EmptySerializer):
|
1049
|
6
|
modules = serializers.SerializerMethodField()
|
1050
|
|
|
1051
|
|
|
1052
|
6
|
class RestoreLaunchSerializer(BaseSerializer):
|
1053
|
6
|
defaults = serializers.SerializerMethodField()
|
1054
|
6
|
archive_name = serializers.CharField(required=True, write_only=True)
|
1055
|
6
|
destination = serializers.CharField(required=True, write_only=True)
|
1056
|
6
|
dest_folder = serializers.CharField(required=True, write_only=True)
|
1057
|
6
|
dry_run = serializers.BooleanField(required=False, initial=False, write_only=True)
|
1058
|
6
|
item = serializers.CharField(required=False, write_only=True)
|
1059
|
6
|
verbosity = serializers.IntegerField(required=False, initial=0, min_value=0, max_value=4, write_only=True)
|
1060
|
|
|
1061
|
6
|
class Meta:
|
1062
|
6
|
model = Job
|
1063
|
6
|
fields = ('defaults', 'archive_name', 'destination', 'dest_folder', 'dry_run', 'item', 'verbosity')
|
1064
|
|
|
1065
|
6
|
def get_defaults(self, obj):
|
1066
|
0
|
defaults_dict = {'verbosity': 0, 'archive_name': '', 'destination': '', 'dest_folder': '', 'dry_run': False, 'item': ''}
|
1067
|
0
|
return defaults_dict
|
1068
|
|
|
1069
|
6
|
def get_job_template_data(self, obj):
|
1070
|
0
|
return dict(name=obj.name, id=obj.id, description=obj.description)
|
1071
|
|
|
1072
|
6
|
def validate_extra_vars(self, value):
|
1073
|
0
|
return vars_validate_or_raise(value)
|
1074
|
|
|
1075
|
|
|
1076
|
6
|
class CatalogSerializer(BaseSerializer):
|
1077
|
|
|
1078
|
6
|
class Meta:
|
1079
|
6
|
model = Catalog
|
1080
|
6
|
fields = ('id', 'url', 'archive_name', 'path', 'job', 'mode', 'mtime', 'owner', 'group', 'size', 'healthy')
|
1081
|
|
|
1082
|
6
|
def get_related(self, obj):
|
1083
|
0
|
res = super(CatalogSerializer, self).get_related(obj)
|
1084
|
0
|
if obj.job:
|
1085
|
0
|
res['job'] = self.reverse('api:job_detail', kwargs={'pk': obj.job.pk})
|
1086
|
0
|
return res
|
1087
|
|
|
1088
|
6
|
def to_representation(self, obj):
|
1089
|
0
|
ret = super(CatalogSerializer, self).to_representation(obj)
|
1090
|
0
|
if obj is not None and 'job' in ret and not obj.job:
|
1091
|
0
|
ret['job'] = None
|
1092
|
0
|
return ret
|
1093
|
|
|
1094
|
|
|
1095
|
6
|
class CatalogListSerializer(DynamicFieldsSerializerMixin, CatalogSerializer):
|
1096
|
|
|
1097
|
6
|
class Meta:
|
1098
|
6
|
model = Catalog
|
1099
|
6
|
fields = ('id', 'url', 'archive_name', 'path', 'job', 'mode', 'mtime', 'owner', 'group', 'size', 'healthy')
|
1100
|
|
|
1101
|
|
|
1102
|
6
|
class StatsSerializer(EmptySerializer):
|
1103
|
6
|
stats = serializers.ListField(serializers.SerializerMethodField())
|
1104
|
|
|
1105
|
|
|
1106
|
6
|
class CyborgTokenObtainPairSerializer(TokenObtainPairSerializer):
|
1107
|
6
|
@classmethod
|
1108
|
|
def get_token(cls, user):
|
1109
|
0
|
token = super().get_token(user)
|
1110
|
|
# Add custom claims
|
1111
|
0
|
token['email'] = user.email
|
1112
|
0
|
token['first_name'] = user.first_name
|
1113
|
0
|
token['last_name'] = user.last_name
|
1114
|
|
|
1115
|
0
|
return token
|