Merge lp:~fo0bar/turku/turku-api-cleanup into lp:turku/turku-api

Proposed by Ryan Finnie
Status: Merged
Approved by: Barry Price
Approved revision: 65
Merged at revision: 65
Proposed branch: lp:~fo0bar/turku/turku-api-cleanup
Merge into: lp:turku/turku-api
Diff against target: 2291 lines (+901/-541)
12 files modified
.bzrignore (+61/-1)
MANIFEST.in (+9/-0)
Makefile (+28/-0)
setup.py (+28/-0)
tests/test_stub.py (+8/-0)
tox.ini (+38/-0)
turku_api/admin.py (+109/-82)
turku_api/models.py (+167/-162)
turku_api/settings.py (+37/-35)
turku_api/urls.py (+31/-13)
turku_api/views.py (+382/-247)
turku_api/wsgi.py (+3/-1)
To merge this branch: bzr merge lp:~fo0bar/turku/turku-api-cleanup
Reviewer Review Type Date Requested Status
Stuart Bishop (community) Approve
Review via email: mp+386143@code.launchpad.net

Commit message

Mega-noop cleanup

Description of the change

This is the minimum required for:
- tox test suite with all passing tests
- black-managed formatting
- Shippable sdist module

It is intended as a base for the other MPs, so they don't have to e.g. establish tests/*, or worry about about existing failing flake8, or worry about how to add additional optional modules.

To post a comment you must log in.
Revision history for this message
🤖 Canonical IS Merge Bot (canonical-is-mergebot) wrote :

This merge proposal is being monitored by mergebot. Change the status to Approved to merge.

lp:~fo0bar/turku/turku-api-cleanup updated
65. By Ryan Finnie

Mega-noop cleanup

- Add setup.py
  - This is a Django application, but treating it as a full Python
    module helps with tox testing
- Sort imports
- Create MANIFEST.in so `setup.py sdist` produces usable tarballs
- Create stub tests
- Add tox.ini
- Add blank requirements.txt
- Add Makefile
- make black
- Update .bzrignore
- Remove blank turku_api/tests.py
- Clean up flake8:
  - Ignore local_settings/local_urls import * F401/F403
  - Ignore wsgi.py import E402
  - Fix urls.py 'django.conf.urls.include' imported but unused

Revision history for this message
Stuart Bishop (stub) wrote :

Yup, same deal as the other two turku cleanup branches.

review: Approve
Revision history for this message
🤖 Canonical IS Merge Bot (canonical-is-mergebot) wrote :

Change successfully merged at revision 65

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file '.bzrignore'
--- .bzrignore 2015-03-26 05:04:41 +0000
+++ .bzrignore 2020-06-21 23:58:30 +0000
@@ -1,4 +1,64 @@
1*.pyc
2db.sqlite31db.sqlite3
3turku_api/local_settings.py2turku_api/local_settings.py
4turku_api/local_urls.py3turku_api/local_urls.py
4MANIFEST
5.pybuild/
6.pytest_cache/
7
8# Byte-compiled / optimized / DLL files
9__pycache__/
10*.py[cod]
11
12# C extensions
13*.so
14
15# Distribution / packaging
16.Python
17env/
18build/
19develop-eggs/
20dist/
21downloads/
22eggs/
23.eggs/
24lib/
25lib64/
26parts/
27sdist/
28var/
29*.egg-info/
30.installed.cfg
31*.egg
32
33# PyInstaller
34# Usually these files are written by a python script from a template
35# before PyInstaller builds the exe, so as to inject date/other infos into it.
36*.manifest
37*.spec
38
39# Installer logs
40pip-log.txt
41pip-delete-this-directory.txt
42
43# Unit test / coverage reports
44htmlcov/
45.tox/
46.coverage
47.coverage.*
48.cache
49nosetests.xml
50coverage.xml
51*,cover
52
53# Translations
54*.mo
55*.pot
56
57# Django stuff:
58*.log
59
60# Sphinx documentation
61docs/_build/
62
63# PyBuilder
64target/
565
=== added file 'MANIFEST.in'
--- MANIFEST.in 1970-01-01 00:00:00 +0000
+++ MANIFEST.in 2020-06-21 23:58:30 +0000
@@ -0,0 +1,9 @@
1include Makefile
2include manage.py
3include MANIFEST.in
4include README.md
5include requirements.txt
6include scripts/turku_health
7include tests/*.py
8include tox.ini
9include turku_api/templates/admin/*.html
010
=== added file 'Makefile'
--- Makefile 1970-01-01 00:00:00 +0000
+++ Makefile 2020-06-21 23:58:30 +0000
@@ -0,0 +1,28 @@
1PYTHON := python3
2
3all: build
4
5build:
6 $(PYTHON) setup.py build
7
8lint:
9 $(PYTHON) -mtox -e flake8
10
11test:
12 $(PYTHON) -mtox
13
14test-quick:
15 $(PYTHON) -mtox -e black,flake8,pytest-quick
16
17black-check:
18 $(PYTHON) -mtox -e black
19
20black:
21 $(PYTHON) -mblack $(CURDIR)
22
23install: build
24 $(PYTHON) setup.py install
25
26clean:
27 $(PYTHON) setup.py clean
28 $(RM) -r build MANIFEST
029
=== added file 'requirements.txt'
=== added file 'setup.py'
--- setup.py 1970-01-01 00:00:00 +0000
+++ setup.py 2020-06-21 23:58:30 +0000
@@ -0,0 +1,28 @@
1#!/usr/bin/env python3
2
3# Turku backups - API application
4# Copyright 2015-2020 Canonical Ltd.
5#
6# This program is free software: you can redistribute it and/or modify it
7# under the terms of the GNU General Public License version 3, as published by
8# the Free Software Foundation.
9#
10# This program is distributed in the hope that it will be useful, but WITHOUT
11# ANY WARRANTY; without even the implied warranties of MERCHANTABILITY,
12# SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13# General Public License for more details.
14#
15# You should have received a copy of the GNU General Public License along with
16# this program. If not, see <http://www.gnu.org/licenses/>.
17
18from setuptools import setup
19
20
21setup(
22 name="turku_api",
23 description="Turku backups - API application",
24 author="Ryan Finnie",
25 url="https://launchpad.net/turku",
26 python_requires="~=3.4",
27 packages=["turku_api"],
28)
029
=== added directory 'tests'
=== added file 'tests/__init__.py'
=== added file 'tests/test_stub.py'
--- tests/test_stub.py 1970-01-01 00:00:00 +0000
+++ tests/test_stub.py 2020-06-21 23:58:30 +0000
@@ -0,0 +1,8 @@
1import unittest
2import warnings
3
4
5class TestStub(unittest.TestCase):
6 def test_stub(self):
7 # pytest doesn't like a tests/ with no tests
8 warnings.warn("Remove this file once unit tests are added")
09
=== added file 'tox.ini'
--- tox.ini 1970-01-01 00:00:00 +0000
+++ tox.ini 2020-06-21 23:58:30 +0000
@@ -0,0 +1,38 @@
1[tox]
2envlist = black, flake8, pytest
3
4[testenv]
5basepython = python
6
7[testenv:black]
8commands = python -mblack --check .
9deps = black
10
11[testenv:flake8]
12commands = python -mflake8
13deps = flake8
14
15[testenv:pytest]
16commands = python -mpytest --cov=turku_api --cov-report=term-missing
17deps = pytest
18 pytest-cov
19 -r{toxinidir}/requirements.txt
20
21[testenv:pytest-quick]
22commands = python -mpytest -m "not slow"
23deps = pytest
24 -r{toxinidir}/requirements.txt
25
26[flake8]
27exclude =
28 .git,
29 __pycache__,
30 .tox,
31# TODO: remove C901 once complexity is reduced
32ignore = C901,E203,E231,W503
33max-line-length = 120
34max-complexity = 10
35
36[pytest]
37markers =
38 slow
039
=== modified file 'turku_api/admin.py'
--- turku_api/admin.py 2020-05-06 02:41:37 +0000
+++ turku_api/admin.py 2020-06-21 23:58:30 +0000
@@ -14,38 +14,39 @@
14# License along with this program. If not, see14# License along with this program. If not, see
15# <http://www.gnu.org/licenses/>.15# <http://www.gnu.org/licenses/>.
1616
17import datetime
18
17from django import forms19from django import forms
18from django.contrib import admin20from django.contrib import admin
19from turku_api.models import Machine, Source, Auth, Storage, BackupLog, FilterSet21from django.contrib.humanize.templatetags.humanize import naturaltime
22from django.utils import timezone
20from django.utils.html import format_html23from django.utils.html import format_html
21from django.utils import timezone24
22from django.contrib.humanize.templatetags.humanize import naturaltime
23try:25try:
24 from django.urls import reverse # 1.10+26 from django.urls import reverse # 1.10+
25except ModuleNotFoundError:27except ModuleNotFoundError:
26 from django.core.urlresolvers import reverse # pre-1.1028 from django.core.urlresolvers import reverse # pre-1.10
27import datetime29
30from turku_api.models import Auth, BackupLog, FilterSet, Machine, Source, Storage
2831
2932
30def get_admin_change_link(obj, name=None):33def get_admin_change_link(obj, name=None):
31 url = reverse(34 url = reverse(
32 'admin:%s_%s_change' % (obj._meta.app_label, obj._meta.model_name),35 "admin:%s_%s_change" % (obj._meta.app_label, obj._meta.model_name),
33 args=(obj.id,)36 args=(obj.id,),
34 )37 )
35 if not name:38 if not name:
36 name = obj39 name = obj
37 return format_html(40 return format_html('<a href="{}">{}</a>'.format(url, name))
38 '<a href="{}">{}</a>'.format(url, name)
39 )
4041
4142
42def human_si(v, begin=0):43def human_si(v, begin=0):
43 p = ('', 'Ki', 'Mi', 'Gi', 'Ti', 'Pi', 'Ei', 'Zi', 'Yi')44 p = ("", "Ki", "Mi", "Gi", "Ti", "Pi", "Ei", "Zi", "Yi")
44 i = begin45 i = begin
45 while v >= 1024.0:46 while v >= 1024.0:
46 v = int(v / 10.24) / 100.047 v = int(v / 10.24) / 100.0
47 i += 148 i += 1
48 return '%g %sB' % (v, p[i])49 return "%g %sB" % (v, p[i])
4950
5051
51def human_time(t):52def human_time(t):
@@ -58,58 +59,62 @@
5859
5960
60class CustomModelAdmin(admin.ModelAdmin):61class CustomModelAdmin(admin.ModelAdmin):
61 change_form_template = 'admin/custom_change_form.html'62 change_form_template = "admin/custom_change_form.html"
6263
63 def render_change_form(self, request, context, *args, **kwargs):64 def render_change_form(self, request, context, *args, **kwargs):
64 # Build a list of related children objects and their counts65 # Build a list of related children objects and their counts
65 # so they may be linked to in the admin interface66 # so they may be linked to in the admin interface
66 related_links = []67 related_links = []
67 if 'object_id' in context and hasattr(self.model._meta, 'get_fields'):68 if "object_id" in context and hasattr(self.model._meta, "get_fields"):
68 related_objs = [69 related_objs = [
69 f for f in self.model._meta.get_fields()70 f
70 if (f.one_to_many or f.one_to_one)71 for f in self.model._meta.get_fields()
71 and f.auto_created and not f.concrete72 if (f.one_to_many or f.one_to_one) and f.auto_created and not f.concrete
72 ]73 ]
73 for obj in related_objs:74 for obj in related_objs:
74 count = obj.related_model.objects.filter(**{obj.field.name: context['object_id']}).count()75 count = obj.related_model.objects.filter(
76 **{obj.field.name: context["object_id"]}
77 ).count()
75 if count > 0:78 if count > 0:
76 related_links.append((obj, obj.related_model._meta, count))79 related_links.append((obj, obj.related_model._meta, count))
77 context.update({'related_links': related_links})80 context.update({"related_links": related_links})
7881
79 return super(CustomModelAdmin, self).render_change_form(request, context, *args, **kwargs)82 return super(CustomModelAdmin, self).render_change_form(
83 request, context, *args, **kwargs
84 )
8085
8186
82class MachineAdminForm(forms.ModelForm):87class MachineAdminForm(forms.ModelForm):
83 class Meta:88 class Meta:
84 model = Machine89 model = Machine
85 fields = '__all__'90 fields = "__all__"
8691
87 def __init__(self, *args, **kwargs):92 def __init__(self, *args, **kwargs):
88 super(MachineAdminForm, self).__init__(*args, **kwargs)93 super(MachineAdminForm, self).__init__(*args, **kwargs)
89 self.fields['auth'].queryset = Auth.objects.filter(secret_type='machine_reg')94 self.fields["auth"].queryset = Auth.objects.filter(secret_type="machine_reg")
9095
9196
92class StorageAdminForm(forms.ModelForm):97class StorageAdminForm(forms.ModelForm):
93 class Meta:98 class Meta:
94 model = Storage99 model = Storage
95 fields = '__all__'100 fields = "__all__"
96101
97 def __init__(self, *args, **kwargs):102 def __init__(self, *args, **kwargs):
98 super(StorageAdminForm, self).__init__(*args, **kwargs)103 super(StorageAdminForm, self).__init__(*args, **kwargs)
99 self.fields['auth'].queryset = Auth.objects.filter(secret_type='storage_reg')104 self.fields["auth"].queryset = Auth.objects.filter(secret_type="storage_reg")
100105
101106
102class AuthAdmin(CustomModelAdmin):107class AuthAdmin(CustomModelAdmin):
103 list_display = ('name', 'secret_type', 'date_added', 'active')108 list_display = ("name", "secret_type", "date_added", "active")
104 ordering = ('name',)109 ordering = ("name",)
105 search_fields = ('name', 'comment',)110 search_fields = ("name", "comment")
106111
107112
108class ExcludeListFilter(admin.SimpleListFilter):113class ExcludeListFilter(admin.SimpleListFilter):
109 def __init__(self, *args, **kwargs):114 def __init__(self, *args, **kwargs):
110 if not self.title:115 if not self.title:
111 self.title = self.parameter_name116 self.title = self.parameter_name
112 self.parameter_name += '__exclude'117 self.parameter_name += "__exclude"
113 super(ExcludeListFilter, self).__init__(*args, **kwargs)118 super(ExcludeListFilter, self).__init__(*args, **kwargs)
114119
115 def has_output(self):120 def has_output(self):
@@ -125,7 +130,7 @@
125130
126131
127class NameExcludeListFilter(ExcludeListFilter):132class NameExcludeListFilter(ExcludeListFilter):
128 parameter_name = 'name'133 parameter_name = "name"
129134
130135
131class MachineAdmin(CustomModelAdmin):136class MachineAdmin(CustomModelAdmin):
@@ -133,61 +138,73 @@
133 return get_admin_change_link(obj.storage)138 return get_admin_change_link(obj.storage)
134139
135 storage_link.allow_tags = True140 storage_link.allow_tags = True
136 storage_link.admin_order_field = 'storage__name'141 storage_link.admin_order_field = "storage__name"
137 storage_link.short_description = 'storage'142 storage_link.short_description = "storage"
138143
139 def date_checked_in_human(self, obj):144 def date_checked_in_human(self, obj):
140 return human_time(obj.date_checked_in)145 return human_time(obj.date_checked_in)
141146
142 date_checked_in_human.admin_order_field = 'date_checked_in'147 date_checked_in_human.admin_order_field = "date_checked_in"
143 date_checked_in_human.short_description = 'date checked in'148 date_checked_in_human.short_description = "date checked in"
144149
145 form = MachineAdminForm150 form = MachineAdminForm
146 list_display = (151 list_display = (
147 'unit_name', 'uuid', 'storage_link', 'environment_name',152 "unit_name",
148 'service_name', 'date_checked_in_human', 'published', 'active',153 "uuid",
149 'healthy',154 "storage_link",
150 )155 "environment_name",
151 list_display_links = ('unit_name',)156 "service_name",
152 list_filter = ('date_checked_in', 'storage', 'active', 'published')157 "date_checked_in_human",
153 ordering = ('unit_name',)158 "published",
154 search_fields = (159 "active",
155 'unit_name', 'uuid', 'environment_name', 'service_name',160 "healthy",
156 'comment',161 )
157 )162 list_display_links = ("unit_name",)
163 list_filter = ("date_checked_in", "storage", "active", "published")
164 ordering = ("unit_name",)
165 search_fields = ("unit_name", "uuid", "environment_name", "service_name", "comment")
158166
159167
160class SourceAdmin(CustomModelAdmin):168class SourceAdmin(CustomModelAdmin):
161 def date_last_backed_up_human(self, obj):169 def date_last_backed_up_human(self, obj):
162 return human_time(obj.date_last_backed_up)170 return human_time(obj.date_last_backed_up)
163171
164 date_last_backed_up_human.admin_order_field = 'date_last_backed_up'172 date_last_backed_up_human.admin_order_field = "date_last_backed_up"
165 date_last_backed_up_human.short_description = 'date last backed up'173 date_last_backed_up_human.short_description = "date last backed up"
166174
167 def date_next_backup_human(self, obj):175 def date_next_backup_human(self, obj):
168 return human_time(obj.date_next_backup)176 return human_time(obj.date_next_backup)
169177
170 date_next_backup_human.admin_order_field = 'date_next_backup'178 date_next_backup_human.admin_order_field = "date_next_backup"
171 date_next_backup_human.short_description = 'date next backup'179 date_next_backup_human.short_description = "date next backup"
172180
173 def machine_link(self, obj):181 def machine_link(self, obj):
174 return get_admin_change_link(obj.machine)182 return get_admin_change_link(obj.machine)
175183
176 machine_link.allow_tags = True184 machine_link.allow_tags = True
177 machine_link.admin_order_field = 'machine__unit_name'185 machine_link.admin_order_field = "machine__unit_name"
178 machine_link.short_description = 'machine'186 machine_link.short_description = "machine"
179187
180 list_display = (188 list_display = (
181 'name', 'machine_link', 'path', 'date_last_backed_up_human',189 "name",
182 'date_next_backup_human', 'published', 'active', 'healthy',190 "machine_link",
191 "path",
192 "date_last_backed_up_human",
193 "date_next_backup_human",
194 "published",
195 "active",
196 "healthy",
183 )197 )
184 list_display_links = ('name',)198 list_display_links = ("name",)
185 list_filter = (199 list_filter = (
186 'date_last_backed_up', 'date_next_backup', 'active', 'published',200 "date_last_backed_up",
201 "date_next_backup",
202 "active",
203 "published",
187 NameExcludeListFilter,204 NameExcludeListFilter,
188 )205 )
189 ordering = ('machine__unit_name', 'name')206 ordering = ("machine__unit_name", "name")
190 search_fields = ('name', 'comment', 'path',)207 search_fields = ("name", "comment", "path")
191208
192209
193class BackupLogAdmin(CustomModelAdmin):210class BackupLogAdmin(CustomModelAdmin):
@@ -195,8 +212,8 @@
195 return get_admin_change_link(obj.source)212 return get_admin_change_link(obj.source)
196213
197 source_link.allow_tags = True214 source_link.allow_tags = True
198 source_link.admin_order_field = 'source__name'215 source_link.admin_order_field = "source__name"
199 source_link.short_description = 'source'216 source_link.short_description = "source"
200217
201 def duration(self, obj):218 def duration(self, obj):
202 if not (obj.date_end and obj.date_begin):219 if not (obj.date_end and obj.date_begin):
@@ -204,8 +221,8 @@
204 d = obj.date_end - obj.date_begin221 d = obj.date_end - obj.date_begin
205 return d - datetime.timedelta(microseconds=d.microseconds)222 return d - datetime.timedelta(microseconds=d.microseconds)
206223
207 duration.admin_order_field = 'date_end'224 duration.admin_order_field = "date_end"
208 duration.short_description = 'duration'225 duration.short_description = "duration"
209226
210 def storage_link(self, obj):227 def storage_link(self, obj):
211 if not obj.storage:228 if not obj.storage:
@@ -213,57 +230,67 @@
213 return get_admin_change_link(obj.storage)230 return get_admin_change_link(obj.storage)
214231
215 storage_link.allow_tags = True232 storage_link.allow_tags = True
216 storage_link.admin_order_field = 'storage__name'233 storage_link.admin_order_field = "storage__name"
217 storage_link.short_description = 'storage'234 storage_link.short_description = "storage"
218235
219 def date_human(self, obj):236 def date_human(self, obj):
220 return human_time(obj.date)237 return human_time(obj.date)
221238
222 date_human.admin_order_field = 'date'239 date_human.admin_order_field = "date"
223 date_human.short_description = 'date'240 date_human.short_description = "date"
224241
225 list_display = (242 list_display = (
226 'date_human', 'source_link', 'success', 'snapshot', 'storage_link',243 "date_human",
227 'duration',244 "source_link",
245 "success",
246 "snapshot",
247 "storage_link",
248 "duration",
228 )249 )
229 list_display_links = ('date_human',)250 list_display_links = ("date_human",)
230 list_filter = ('date', 'success')251 list_filter = ("date", "success")
231 ordering = ('-date',)252 ordering = ("-date",)
232253
233254
234class FilterSetAdmin(CustomModelAdmin):255class FilterSetAdmin(CustomModelAdmin):
235 list_display = ('name', 'date_added', 'active')256 list_display = ("name", "date_added", "active")
236 ordering = ('name',)257 ordering = ("name",)
237 search_fields = ('name', 'comment',)258 search_fields = ("name", "comment")
238259
239260
240class StorageAdmin(CustomModelAdmin):261class StorageAdmin(CustomModelAdmin):
241 def space_total_human(self, obj):262 def space_total_human(self, obj):
242 return human_si(obj.space_total, 2)263 return human_si(obj.space_total, 2)
243264
244 space_total_human.admin_order_field = 'space_total'265 space_total_human.admin_order_field = "space_total"
245 space_total_human.short_description = 'space total'266 space_total_human.short_description = "space total"
246267
247 def space_available_human(self, obj):268 def space_available_human(self, obj):
248 return human_si(obj.space_available, 2)269 return human_si(obj.space_available, 2)
249270
250 space_available_human.admin_order_field = 'space_available'271 space_available_human.admin_order_field = "space_available"
251 space_available_human.short_description = 'space available'272 space_available_human.short_description = "space available"
252273
253 def date_checked_in_human(self, obj):274 def date_checked_in_human(self, obj):
254 return human_time(obj.date_checked_in)275 return human_time(obj.date_checked_in)
255276
256 date_checked_in_human.admin_order_field = 'date_checked_in'277 date_checked_in_human.admin_order_field = "date_checked_in"
257 date_checked_in_human.short_description = 'date checked in'278 date_checked_in_human.short_description = "date checked in"
258279
259 form = StorageAdminForm280 form = StorageAdminForm
260 list_display = (281 list_display = (
261 'name', 'ssh_ping_host', 'ssh_ping_user', 'date_checked_in_human',282 "name",
262 'space_total_human', 'space_available_human', 'published', 'active',283 "ssh_ping_host",
263 'healthy',284 "ssh_ping_user",
285 "date_checked_in_human",
286 "space_total_human",
287 "space_available_human",
288 "published",
289 "active",
290 "healthy",
264 )291 )
265 ordering = ('name',)292 ordering = ("name",)
266 search_fields = ('name', 'comment', 'ssh_ping_host',)293 search_fields = ("name", "comment", "ssh_ping_host")
267294
268295
269admin.site.register(Auth, AuthAdmin)296admin.site.register(Auth, AuthAdmin)
270297
=== modified file 'turku_api/models.py'
--- turku_api/models.py 2020-04-11 21:20:31 +0000
+++ turku_api/models.py 2020-06-21 23:58:30 +0000
@@ -14,14 +14,15 @@
14# License along with this program. If not, see14# License along with this program. If not, see
15# <http://www.gnu.org/licenses/>.15# <http://www.gnu.org/licenses/>.
1616
17from datetime import timedelta
18import json
19import uuid
20
17from django.db import models21from django.db import models
22from django.contrib.auth.hashers import is_password_usable
18from django.core.exceptions import ValidationError23from django.core.exceptions import ValidationError
19from django.core.validators import MaxValueValidator, MinValueValidator24from django.core.validators import MaxValueValidator, MinValueValidator
20from django.contrib.auth.hashers import is_password_usable
21from django.utils import timezone25from django.utils import timezone
22from datetime import timedelta
23import json
24import uuid
2526
2627
27def new_uuid():28def new_uuid():
@@ -32,85 +33,83 @@
32 try:33 try:
33 str(uuid.UUID(value))34 str(uuid.UUID(value))
34 except ValueError:35 except ValueError:
35 raise ValidationError('Invalid UUID format')36 raise ValidationError("Invalid UUID format")
3637
3738
38def validate_hashed_password(value):39def validate_hashed_password(value):
39 if not is_password_usable(value):40 if not is_password_usable(value):
40 raise ValidationError('Invalid hashed password')41 raise ValidationError("Invalid hashed password")
4142
4243
43def validate_json_string_list(value):44def validate_json_string_list(value):
44 try:45 try:
45 decoded_json = json.loads(value)46 decoded_json = json.loads(value)
46 except ValueError:47 except ValueError:
47 raise ValidationError('Must be a valid JSON string list')48 raise ValidationError("Must be a valid JSON string list")
48 if not isinstance(decoded_json, (list, tuple, set)):49 if not isinstance(decoded_json, (list, tuple, set)):
49 raise ValidationError('Must be a valid JSON string list')50 raise ValidationError("Must be a valid JSON string list")
50 for i in decoded_json:51 for i in decoded_json:
51 if not isinstance(i, str):52 if not isinstance(i, str):
52 raise ValidationError('Must be a valid JSON string list')53 raise ValidationError("Must be a valid JSON string list")
5354
5455
55def validate_storage_auth(value):56def validate_storage_auth(value):
56 try:57 try:
57 a = Auth.objects.get(id=value)58 a = Auth.objects.get(id=value)
58 except Auth.DoesNotExist:59 except Auth.DoesNotExist:
59 raise ValidationError('Auth %s does not exist' % value)60 raise ValidationError("Auth %s does not exist" % value)
60 if a.secret_type != 'storage_reg':61 if a.secret_type != "storage_reg":
61 raise ValidationError('Must be a Storage registration')62 raise ValidationError("Must be a Storage registration")
6263
6364
64def validate_machine_auth(value):65def validate_machine_auth(value):
65 try:66 try:
66 a = Auth.objects.get(id=value)67 a = Auth.objects.get(id=value)
67 except Auth.DoesNotExist:68 except Auth.DoesNotExist:
68 raise ValidationError('Auth %s does not exist' % value)69 raise ValidationError("Auth %s does not exist" % value)
69 if a.secret_type != 'machine_reg':70 if a.secret_type != "machine_reg":
70 raise ValidationError('Must be a Machine registration')71 raise ValidationError("Must be a Machine registration")
7172
7273
73class UuidPrimaryKeyField(models.CharField):74class UuidPrimaryKeyField(models.CharField):
74 def __init__(self, *args, **kwargs):75 def __init__(self, *args, **kwargs):
75 kwargs['blank'] = True76 kwargs["blank"] = True
76 kwargs['default'] = new_uuid77 kwargs["default"] = new_uuid
77 kwargs['editable'] = False78 kwargs["editable"] = False
78 kwargs['max_length'] = 3679 kwargs["max_length"] = 36
79 kwargs['primary_key'] = True80 kwargs["primary_key"] = True
80 super(UuidPrimaryKeyField, self).__init__(*args, **kwargs)81 super(UuidPrimaryKeyField, self).__init__(*args, **kwargs)
8182
8283
83class Auth(models.Model):84class Auth(models.Model):
84 SECRET_TYPES = (85 SECRET_TYPES = (
85 ('machine_reg', 'Machine registration'),86 ("machine_reg", "Machine registration"),
86 ('storage_reg', 'Storage registration'),87 ("storage_reg", "Storage registration"),
87 )88 )
88 id = UuidPrimaryKeyField()89 id = UuidPrimaryKeyField()
89 name = models.CharField(90 name = models.CharField(
90 max_length=200, unique=True,91 max_length=200, unique=True, help_text="Human-readable name of this auth."
91 help_text='Human-readable name of this auth.',
92 )92 )
93 secret_hash = models.CharField(93 secret_hash = models.CharField(
94 max_length=200,94 max_length=200,
95 validators=[validate_hashed_password],95 validators=[validate_hashed_password],
96 help_text='Hashed secret (password) of this auth.',96 help_text="Hashed secret (password) of this auth.",
97 )97 )
98 secret_type = models.CharField(98 secret_type = models.CharField(
99 max_length=200, choices=SECRET_TYPES,99 max_length=200,
100 help_text='Auth secret type (machine/storage).',100 choices=SECRET_TYPES,
101 help_text="Auth secret type (machine/storage).",
101 )102 )
102 comment = models.CharField(103 comment = models.CharField(
103 max_length=200, blank=True, null=True,104 max_length=200, blank=True, null=True, help_text="Human-readable comment."
104 help_text='Human-readable comment.',
105 )105 )
106 active = models.BooleanField(106 active = models.BooleanField(
107 default=True,107 default=True,
108 help_text='Whether this auth is enabled. Disabling prevents new registrations using its key, and prevents ' +108 help_text="Whether this auth is enabled. Disabling prevents new registrations using its key, and prevents "
109 'existing machines using its key from updating their configs.',109 + "existing machines using its key from updating their configs.",
110 )110 )
111 date_added = models.DateTimeField(111 date_added = models.DateTimeField(
112 default=timezone.now,112 default=timezone.now, help_text="Date/time this auth was added."
113 help_text='Date/time this auth was added.',
114 )113 )
115114
116 def __str__(self):115 def __str__(self):
@@ -125,77 +124,78 @@
125 return True124 return True
126 if not self.date_checked_in:125 if not self.date_checked_in:
127 return False126 return False
128 return (now <= (self.date_checked_in + timedelta(minutes=30)))127 return now <= (self.date_checked_in + timedelta(minutes=30))
128
129 healthy.boolean = True129 healthy.boolean = True
130130
131 id = UuidPrimaryKeyField()131 id = UuidPrimaryKeyField()
132 name = models.CharField(132 name = models.CharField(
133 max_length=200, unique=True,133 max_length=200,
134 help_text='Name of this storage unit. This is used as its login ID and must be unique.',134 unique=True,
135 help_text="Name of this storage unit. This is used as its login ID and must be unique.",
135 )136 )
136 secret_hash = models.CharField(137 secret_hash = models.CharField(
137 max_length=200,138 max_length=200,
138 validators=[validate_hashed_password],139 validators=[validate_hashed_password],
139 help_text='Hashed secret (password) of this storage unit.',140 help_text="Hashed secret (password) of this storage unit.",
140 )141 )
141 comment = models.CharField(142 comment = models.CharField(
142 max_length=200, blank=True, null=True,143 max_length=200, blank=True, null=True, help_text="Human-readable comment."
143 help_text='Human-readable comment.',
144 )144 )
145 ssh_ping_host = models.CharField(145 ssh_ping_host = models.CharField(
146 max_length=200,146 max_length=200,
147 verbose_name='SSH ping host',147 verbose_name="SSH ping host",
148 help_text='Hostname/IP address of this storage unit\'s SSH server.',148 help_text="Hostname/IP address of this storage unit's SSH server.",
149 )149 )
150 ssh_ping_host_keys = models.CharField(150 ssh_ping_host_keys = models.CharField(
151 max_length=65536, default='[]',151 max_length=65536,
152 default="[]",
152 validators=[validate_json_string_list],153 validators=[validate_json_string_list],
153 verbose_name='SSH ping host keys',154 verbose_name="SSH ping host keys",
154 help_text='JSON list of this storage unit\'s SSH host keys.',155 help_text="JSON list of this storage unit's SSH host keys.",
155 )156 )
156 ssh_ping_port = models.PositiveIntegerField(157 ssh_ping_port = models.PositiveIntegerField(
157 validators=[MinValueValidator(1), MaxValueValidator(65535)],158 validators=[MinValueValidator(1), MaxValueValidator(65535)],
158 verbose_name='SSH ping port',159 verbose_name="SSH ping port",
159 help_text='Port number of this storage unit\'s SSH server.',160 help_text="Port number of this storage unit's SSH server.",
160 )161 )
161 ssh_ping_user = models.CharField(162 ssh_ping_user = models.CharField(
162 max_length=200,163 max_length=200,
163 verbose_name='SSH ping user',164 verbose_name="SSH ping user",
164 help_text='Username of this storage unit\'s SSH server.',165 help_text="Username of this storage unit's SSH server.",
165 )166 )
166 space_total = models.PositiveIntegerField(167 space_total = models.PositiveIntegerField(
167 default=0,168 default=0,
168 help_text='Total disk space of this storage unit\'s storage directories, in MiB.',169 help_text="Total disk space of this storage unit's storage directories, in MiB.",
169 )170 )
170 space_available = models.PositiveIntegerField(171 space_available = models.PositiveIntegerField(
171 default=0,172 default=0,
172 help_text='Available disk space of this storage unit\'s storage directories, in MiB.',173 help_text="Available disk space of this storage unit's storage directories, in MiB.",
173 )174 )
174 auth = models.ForeignKey(175 auth = models.ForeignKey(
175 Auth, validators=[validate_storage_auth], on_delete=models.CASCADE,176 Auth,
176 help_text='Storage auth used to register this storage unit.',177 validators=[validate_storage_auth],
178 on_delete=models.CASCADE,
179 help_text="Storage auth used to register this storage unit.",
177 )180 )
178 active = models.BooleanField(181 active = models.BooleanField(
179 default=True,182 default=True,
180 help_text='Whether this storage unit is enabled. Disabling prevents this storage unit from checking in or ' +183 help_text="Whether this storage unit is enabled. Disabling prevents this storage unit from checking in or "
181 'being assigned to new machines. Existing machines which ping this storage unit will get errors ' +184 + "being assigned to new machines. Existing machines which ping this storage unit will get errors "
182 'because this storage unit can no longer query the API server.',185 + "because this storage unit can no longer query the API server.",
183 )186 )
184 published = models.BooleanField(187 published = models.BooleanField(
185 default=True,188 default=True, help_text="Whether this storage unit has been enabled by itself."
186 help_text='Whether this storage unit has been enabled by itself.',
187 )189 )
188 date_registered = models.DateTimeField(190 date_registered = models.DateTimeField(
189 default=timezone.now,191 default=timezone.now, help_text="Date/time this storage unit was registered."
190 help_text='Date/time this storage unit was registered.',
191 )192 )
192 date_updated = models.DateTimeField(193 date_updated = models.DateTimeField(
193 default=timezone.now,194 default=timezone.now,
194 help_text='Date/time this storage unit presented a modified config.',195 help_text="Date/time this storage unit presented a modified config.",
195 )196 )
196 date_checked_in = models.DateTimeField(197 date_checked_in = models.DateTimeField(
197 blank=True, null=True,198 blank=True, null=True, help_text="Date/time this storage unit last checked in."
198 help_text='Date/time this storage unit last checked in.',
199 )199 )
200200
201 def __str__(self):201 def __str__(self):
@@ -210,75 +210,82 @@
210 return True210 return True
211 if not self.date_checked_in:211 if not self.date_checked_in:
212 return False212 return False
213 return (now <= (self.date_checked_in + timedelta(hours=10)))213 return now <= (self.date_checked_in + timedelta(hours=10))
214
214 healthy.boolean = True215 healthy.boolean = True
215216
216 id = UuidPrimaryKeyField()217 id = UuidPrimaryKeyField()
217 uuid = models.CharField(218 uuid = models.CharField(
218 max_length=36, unique=True, validators=[validate_uuid],219 max_length=36,
219 verbose_name='UUID',220 unique=True,
220 help_text='UUID of this machine. This UUID is set by the machine and must be globally unique.',221 validators=[validate_uuid],
222 verbose_name="UUID",
223 help_text="UUID of this machine. This UUID is set by the machine and must be globally unique.",
221 )224 )
222 secret_hash = models.CharField(225 secret_hash = models.CharField(
223 max_length=200,226 max_length=200,
224 validators=[validate_hashed_password],227 validators=[validate_hashed_password],
225 help_text='Hashed secret (password) of this machine.',228 help_text="Hashed secret (password) of this machine.",
226 )229 )
227 environment_name = models.CharField(230 environment_name = models.CharField(
228 max_length=200, blank=True, null=True,231 max_length=200,
229 help_text='Environment this machine is part of.',232 blank=True,
233 null=True,
234 help_text="Environment this machine is part of.",
230 )235 )
231 service_name = models.CharField(236 service_name = models.CharField(
232 max_length=200, blank=True, null=True,237 max_length=200,
233 help_text='Service this machine is part of. For Juju units, this is the first part of the unit name ' +238 blank=True,
234 '(before the slash).',239 null=True,
240 help_text="Service this machine is part of. For Juju units, this is the first part of the unit name "
241 + "(before the slash).",
235 )242 )
236 unit_name = models.CharField(243 unit_name = models.CharField(
237 max_length=200,244 max_length=200,
238 help_text='Unit name of this machine. For Juju units, this is the full unit name (e.g. "service-name/0"). ' +245 help_text='Unit name of this machine. For Juju units, this is the full unit name (e.g. "service-name/0"). '
239 'Otherwise, this should be the machine\'s hostname.',246 + "Otherwise, this should be the machine's hostname.",
240 )247 )
241 comment = models.CharField(248 comment = models.CharField(
242 max_length=200, blank=True, null=True,249 max_length=200, blank=True, null=True, help_text="Human-readable comment."
243 help_text='Human-readable comment.',
244 )250 )
245 ssh_public_key = models.CharField(251 ssh_public_key = models.CharField(
246 max_length=2048,252 max_length=2048,
247 verbose_name='SSH public key',253 verbose_name="SSH public key",
248 help_text='SSH public key of this machine\'s agent.',254 help_text="SSH public key of this machine's agent.",
249 )255 )
250 auth = models.ForeignKey(256 auth = models.ForeignKey(
251 Auth, validators=[validate_machine_auth], on_delete=models.CASCADE,257 Auth,
252 help_text='Machine auth used to register this machine.',258 validators=[validate_machine_auth],
259 on_delete=models.CASCADE,
260 help_text="Machine auth used to register this machine.",
253 )261 )
254 storage = models.ForeignKey(262 storage = models.ForeignKey(
255 Storage, on_delete=models.CASCADE,263 Storage,
256 help_text='Storage unit this machine is assigned to.',264 on_delete=models.CASCADE,
265 help_text="Storage unit this machine is assigned to.",
257 )266 )
258 active = models.BooleanField(267 active = models.BooleanField(
259 default=True,268 default=True,
260 help_text='Whether this machine is enabled. Disabling removes its key from its storage unit, stops this ' +269 help_text="Whether this machine is enabled. Disabling removes its key from its storage unit, stops this "
261 'machine from updating its registration, etc.',270 + "machine from updating its registration, etc.",
262 )271 )
263 published = models.BooleanField(272 published = models.BooleanField(
264 default=True,273 default=True,
265 help_text='Whether this machine has been enabled by the machine agent.',274 help_text="Whether this machine has been enabled by the machine agent.",
266 )275 )
267 date_registered = models.DateTimeField(276 date_registered = models.DateTimeField(
268 default=timezone.now,277 default=timezone.now, help_text="Date/time this machine was registered."
269 help_text='Date/time this machine was registered.',
270 )278 )
271 date_updated = models.DateTimeField(279 date_updated = models.DateTimeField(
272 default=timezone.now,280 default=timezone.now,
273 help_text='Date/time this machine presented a modified config.',281 help_text="Date/time this machine presented a modified config.",
274 )282 )
275 date_checked_in = models.DateTimeField(283 date_checked_in = models.DateTimeField(
276 blank=True, null=True,284 blank=True, null=True, help_text="Date/time this machine last checked in."
277 help_text='Date/time this machine last checked in.',
278 )285 )
279286
280 def __str__(self):287 def __str__(self):
281 return '%s (%s)' % (self.unit_name, self.uuid[0:8])288 return "%s (%s)" % (self.unit_name, self.uuid[0:8])
282289
283290
284class Source(models.Model):291class Source(models.Model):
@@ -289,174 +296,172 @@
289 return True296 return True
290 if not self.success:297 if not self.success:
291 return False298 return False
292 return (now <= (self.date_next_backup + timedelta(hours=10)))299 return now <= (self.date_next_backup + timedelta(hours=10))
300
293 healthy.boolean = True301 healthy.boolean = True
294302
295 SNAPSHOT_MODES = (303 SNAPSHOT_MODES = (
296 ('none', 'No snapshotting'),304 ("none", "No snapshotting"),
297 ('attic', 'Attic'),305 ("attic", "Attic"),
298 ('link-dest', 'Hardlink trees (rsync --link-dest)'),306 ("link-dest", "Hardlink trees (rsync --link-dest)"),
299 )307 )
300 id = UuidPrimaryKeyField()308 id = UuidPrimaryKeyField()
301 name = models.CharField(309 name = models.CharField(
302 max_length=200,310 max_length=200, help_text="Computer-readable source name identifier."
303 help_text='Computer-readable source name identifier.',
304 )311 )
305 machine = models.ForeignKey(312 machine = models.ForeignKey(
306 Machine, on_delete=models.CASCADE,313 Machine, on_delete=models.CASCADE, help_text="Machine this source belongs to."
307 help_text='Machine this source belongs to.',
308 )314 )
309 comment = models.CharField(315 comment = models.CharField(
310 max_length=200, blank=True, null=True,316 max_length=200, blank=True, null=True, help_text="Human-readable comment."
311 help_text='Human-readable comment.',
312 )317 )
313 path = models.CharField(318 path = models.CharField(
314 max_length=200,319 max_length=200, help_text="Full filesystem path of this source."
315 help_text='Full filesystem path of this source.',
316 )320 )
317 filter = models.CharField(321 filter = models.CharField(
318 max_length=2048, default='[]', validators=[validate_json_string_list],322 max_length=2048,
319 help_text='JSON list of rsync-compatible --filter options.',323 default="[]",
324 validators=[validate_json_string_list],
325 help_text="JSON list of rsync-compatible --filter options.",
320 )326 )
321 exclude = models.CharField(327 exclude = models.CharField(
322 max_length=2048, default='[]', validators=[validate_json_string_list],328 max_length=2048,
323 help_text='JSON list of rsync-compatible --exclude options.',329 default="[]",
330 validators=[validate_json_string_list],
331 help_text="JSON list of rsync-compatible --exclude options.",
324 )332 )
325 frequency = models.CharField(333 frequency = models.CharField(
326 max_length=200, default='daily',334 max_length=200, default="daily", help_text="How often to back up this source."
327 help_text='How often to back up this source.',
328 )335 )
329 retention = models.CharField(336 retention = models.CharField(
330 max_length=200, default='last 5 days, earliest of month',337 max_length=200,
331 help_text='Retention schedule, describing when to preserve snapshots.',338 default="last 5 days, earliest of month",
339 help_text="Retention schedule, describing when to preserve snapshots.",
332 )340 )
333 bwlimit = models.CharField(341 bwlimit = models.CharField(
334 max_length=200,342 max_length=200,
335 blank=True, null=True,343 blank=True,
336 verbose_name='bandwidth limit',344 null=True,
337 help_text='Bandwith limit for remote transfer, using the rsync --bwlimit format.',345 verbose_name="bandwidth limit",
346 help_text="Bandwith limit for remote transfer, using the rsync --bwlimit format.",
338 )347 )
339 snapshot_mode = models.CharField(348 snapshot_mode = models.CharField(
340 blank=True, null=True,349 blank=True,
341 max_length=200, choices=SNAPSHOT_MODES,350 null=True,
342 help_text='Override the storage unit\'s snapshot logic and use an explicit snapshot mode for this source.',351 max_length=200,
352 choices=SNAPSHOT_MODES,
353 help_text="Override the storage unit's snapshot logic and use an explicit snapshot mode for this source.",
343 )354 )
344 preserve_hard_links = models.BooleanField(355 preserve_hard_links = models.BooleanField(
345 default=False,356 default=False,
346 help_text='Whether to preserve hard links when backing up this source.',357 help_text="Whether to preserve hard links when backing up this source.",
347 )358 )
348 shared_service = models.BooleanField(359 shared_service = models.BooleanField(
349 default=False,360 default=False,
350 help_text='Whether this source is part of a shared service of multiple machines to be backed up.',361 help_text="Whether this source is part of a shared service of multiple machines to be backed up.",
351 )362 )
352 large_rotating_files = models.BooleanField(363 large_rotating_files = models.BooleanField(
353 default=False,364 default=False,
354 help_text='Whether this source contains a number of large files which rotate through filenames, e.g. ' +365 help_text="Whether this source contains a number of large files which rotate through filenames, e.g. "
355 '"postgresql.1.dump.gz" becomes "postgresql.2.dump.gz".',366 + '"postgresql.1.dump.gz" becomes "postgresql.2.dump.gz".',
356 )367 )
357 large_modifying_files = models.BooleanField(368 large_modifying_files = models.BooleanField(
358 default=False,369 default=False,
359 help_text='Whether this source contains a number of large files which grow or are otherwise modified, ' +370 help_text="Whether this source contains a number of large files which grow or are otherwise modified, "
360 'e.g. log files or filesystem images.',371 + "e.g. log files or filesystem images.",
361 )372 )
362 active = models.BooleanField(373 active = models.BooleanField(
363 default=True,374 default=True,
364 help_text='Whether this source is enabled. Disabling means the API server no longer gives it to the ' +375 help_text="Whether this source is enabled. Disabling means the API server no longer gives it to the "
365 'storage unit, even if it\'s time for a backup.',376 + "storage unit, even if it's time for a backup.",
366 )377 )
367 success = models.BooleanField(378 success = models.BooleanField(
368 default=True,379 default=True, help_text="Whether this source's last backup was successful."
369 help_text='Whether this source\'s last backup was successful.',
370 )380 )
371 published = models.BooleanField(381 published = models.BooleanField(
372 default=True,382 default=True,
373 help_text='Whether this source is actively being published by the machine agent.',383 help_text="Whether this source is actively being published by the machine agent.",
374 )384 )
375 date_added = models.DateTimeField(385 date_added = models.DateTimeField(
376 default=timezone.now,386 default=timezone.now,
377 help_text='Date/time this source was first added by the machine agent.',387 help_text="Date/time this source was first added by the machine agent.",
378 )388 )
379 date_updated = models.DateTimeField(389 date_updated = models.DateTimeField(
380 default=timezone.now,390 default=timezone.now,
381 help_text='Date/time the machine presented a modified config of this source.',391 help_text="Date/time the machine presented a modified config of this source.",
382 )392 )
383 date_last_backed_up = models.DateTimeField(393 date_last_backed_up = models.DateTimeField(
384 blank=True, null=True,394 blank=True,
385 help_text='Date/time this source was last successfully backed up.',395 null=True,
396 help_text="Date/time this source was last successfully backed up.",
386 )397 )
387 date_next_backup = models.DateTimeField(398 date_next_backup = models.DateTimeField(
388 default=timezone.now,399 default=timezone.now,
389 help_text='Date/time this source is next scheduled to be backed up. Set to now (or in the past) to ' +400 help_text="Date/time this source is next scheduled to be backed up. Set to now (or in the past) to "
390 'trigger a backup as soon as possible.',401 + "trigger a backup as soon as possible.",
391 )402 )
392403
393 class Meta:404 class Meta:
394 unique_together = (('machine', 'name'),)405 unique_together = (("machine", "name"),)
395406
396 def __str__(self):407 def __str__(self):
397 return '%s %s' % (self.machine.unit_name, self.name)408 return "%s %s" % (self.machine.unit_name, self.name)
398409
399410
400class BackupLog(models.Model):411class BackupLog(models.Model):
401 id = UuidPrimaryKeyField()412 id = UuidPrimaryKeyField()
402 source = models.ForeignKey(413 source = models.ForeignKey(
403 Source, on_delete=models.CASCADE,414 Source, on_delete=models.CASCADE, help_text="Source this log entry belongs to."
404 help_text='Source this log entry belongs to.',
405 )415 )
406 date = models.DateTimeField(416 date = models.DateTimeField(
407 default=timezone.now,417 default=timezone.now,
408 help_text='Date/time this log entry was received/processed.',418 help_text="Date/time this log entry was received/processed.",
409 )419 )
410 storage = models.ForeignKey(420 storage = models.ForeignKey(
411 Storage, blank=True, null=True, on_delete=models.CASCADE,421 Storage,
412 help_text='Storage unit this backup occurred on.',422 blank=True,
423 null=True,
424 on_delete=models.CASCADE,
425 help_text="Storage unit this backup occurred on.",
413 )426 )
414 success = models.BooleanField(427 success = models.BooleanField(
415 default=False,428 default=False, help_text="Whether this backup succeeded."
416 help_text='Whether this backup succeeded.',
417 )429 )
418 date_begin = models.DateTimeField(430 date_begin = models.DateTimeField(
419 blank=True, null=True,431 blank=True, null=True, help_text="Date/time this backup began."
420 help_text='Date/time this backup began.',
421 )432 )
422 date_end = models.DateTimeField(433 date_end = models.DateTimeField(
423 blank=True, null=True,434 blank=True, null=True, help_text="Date/time this backup ended."
424 help_text='Date/time this backup ended.',
425 )435 )
426 snapshot = models.CharField(436 snapshot = models.CharField(
427 max_length=200, blank=True, null=True,437 max_length=200, blank=True, null=True, help_text="Name of the created snapshot."
428 help_text='Name of the created snapshot.',
429 )438 )
430 summary = models.TextField(439 summary = models.TextField(
431 blank=True, null=True,440 blank=True, null=True, help_text="Summary of the backup's events."
432 help_text='Summary of the backup\'s events.',
433 )441 )
434442
435 def __str__(self):443 def __str__(self):
436 return '%s %s' % (str(self.source), self.date.strftime('%Y-%m-%d %H:%M:%S'))444 return "%s %s" % (str(self.source), self.date.strftime("%Y-%m-%d %H:%M:%S"))
437445
438446
439class FilterSet(models.Model):447class FilterSet(models.Model):
440 id = UuidPrimaryKeyField()448 id = UuidPrimaryKeyField()
441 name = models.CharField(449 name = models.CharField(
442 max_length=200, unique=True,450 max_length=200, unique=True, help_text="Name of this filter set."
443 help_text='Name of this filter set.',
444 )451 )
445 filters = models.TextField(452 filters = models.TextField(
446 default='[]', validators=[validate_json_string_list],453 default="[]",
447 help_text='JSON list of this filter set\'s filter rules.',454 validators=[validate_json_string_list],
455 help_text="JSON list of this filter set's filter rules.",
448 )456 )
449 comment = models.CharField(457 comment = models.CharField(
450 max_length=200, blank=True, null=True,458 max_length=200, blank=True, null=True, help_text="Human-readable comment."
451 help_text='Human-readable comment.',
452 )459 )
453 active = models.BooleanField(460 active = models.BooleanField(
454 default=True,461 default=True, help_text="Whether this filter set is enabled."
455 help_text='Whether this filter set is enabled.',
456 )462 )
457 date_added = models.DateTimeField(463 date_added = models.DateTimeField(
458 default=timezone.now,464 default=timezone.now, help_text="Date/time this filter set was added."
459 help_text='Date/time this filter set was added.',
460 )465 )
461466
462 def __str__(self):467 def __str__(self):
463468
=== modified file 'turku_api/settings.py'
--- turku_api/settings.py 2020-04-11 21:20:31 +0000
+++ turku_api/settings.py 2020-06-21 23:58:30 +0000
@@ -21,57 +21,59 @@
21BASE_DIR = os.path.dirname(os.path.dirname(__file__))21BASE_DIR = os.path.dirname(os.path.dirname(__file__))
22DEBUG = False22DEBUG = False
23TEMPLATE_DEBUG = False23TEMPLATE_DEBUG = False
24ALLOWED_HOSTS = ('*',)24ALLOWED_HOSTS = ("*",)
25INSTALLED_APPS = (25INSTALLED_APPS = (
26 'django.contrib.admin',26 "django.contrib.admin",
27 'django.contrib.auth',27 "django.contrib.auth",
28 'django.contrib.contenttypes',28 "django.contrib.contenttypes",
29 'django.contrib.sessions',29 "django.contrib.sessions",
30 'django.contrib.messages',30 "django.contrib.messages",
31 'django.contrib.staticfiles',31 "django.contrib.staticfiles",
32 'turku_api',32 "turku_api",
33)33)
34MIDDLEWARE = (34MIDDLEWARE = (
35 'django.contrib.sessions.middleware.SessionMiddleware',35 "django.contrib.sessions.middleware.SessionMiddleware",
36 'django.middleware.common.CommonMiddleware',36 "django.middleware.common.CommonMiddleware",
37 'django.middleware.csrf.CsrfViewMiddleware',37 "django.middleware.csrf.CsrfViewMiddleware",
38 'django.contrib.auth.middleware.AuthenticationMiddleware',38 "django.contrib.auth.middleware.AuthenticationMiddleware",
39 'django.contrib.messages.middleware.MessageMiddleware',39 "django.contrib.messages.middleware.MessageMiddleware",
40 'django.middleware.clickjacking.XFrameOptionsMiddleware',40 "django.middleware.clickjacking.XFrameOptionsMiddleware",
41)41)
42MIDDLEWARE_CLASSES = MIDDLEWARE # pre-1.1042MIDDLEWARE_CLASSES = MIDDLEWARE # pre-1.10
43ROOT_URLCONF = 'turku_api.urls'43ROOT_URLCONF = "turku_api.urls"
44WSGI_APPLICATION = 'turku_api.wsgi.application'44WSGI_APPLICATION = "turku_api.wsgi.application"
45LANGUAGE_CODE = 'en-us'45LANGUAGE_CODE = "en-us"
46TIME_ZONE = 'UTC'46TIME_ZONE = "UTC"
47USE_I18N = True47USE_I18N = True
48USE_L10N = True48USE_L10N = True
49USE_TZ = True49USE_TZ = True
50STATIC_URL = '/static/'50STATIC_URL = "/static/"
51TEMPLATES = [51TEMPLATES = [
52 {52 {
53 'BACKEND': 'django.template.backends.django.DjangoTemplates',53 "BACKEND": "django.template.backends.django.DjangoTemplates",
54 'DIRS': [os.path.join(BASE_DIR, 'turku_api/templates')],54 "DIRS": [os.path.join(BASE_DIR, "turku_api/templates")],
55 'APP_DIRS': True,55 "APP_DIRS": True,
56 'OPTIONS': {56 "OPTIONS": {
57 'context_processors': [57 "context_processors": [
58 'django.template.context_processors.debug',58 "django.template.context_processors.debug",
59 'django.template.context_processors.request',59 "django.template.context_processors.request",
60 'django.contrib.auth.context_processors.auth',60 "django.contrib.auth.context_processors.auth",
61 'django.contrib.messages.context_processors.messages',61 "django.contrib.messages.context_processors.messages",
62 ],62 ]
63 },63 },
64 },64 }
65]65]
66DATABASES = {66DATABASES = {
67 'default': {67 "default": {
68 'ENGINE': 'django.db.backends.sqlite3',68 "ENGINE": "django.db.backends.sqlite3",
69 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),69 "NAME": os.path.join(BASE_DIR, "db.sqlite3"),
70 }70 }
71}71}
72SECRET_KEY = ''.join(random.choice(string.ascii_letters + string.digits) for i in range(30))72SECRET_KEY = "".join(
73 random.choice(string.ascii_letters + string.digits) for i in range(30)
74)
7375
74try:76try:
75 from turku_api.local_settings import *77 from turku_api.local_settings import * # noqa: F401,F403
76except ImportError:78except ImportError:
77 pass79 pass
7880
=== removed file 'turku_api/tests.py'
=== modified file 'turku_api/urls.py'
--- turku_api/urls.py 2020-04-11 21:20:31 +0000
+++ turku_api/urls.py 2020-06-21 23:58:30 +0000
@@ -14,31 +14,49 @@
14# License along with this program. If not, see14# License along with this program. If not, see
15# <http://www.gnu.org/licenses/>.15# <http://www.gnu.org/licenses/>.
1616
17from django.conf.urls import include, url17from django.conf.urls import url
18from django.contrib import admin
19from django.views.generic.base import RedirectView
20
18try:21try:
19 from django.urls import reverse_lazy # 1.10+22 from django.urls import reverse_lazy # 1.10+
20except ModuleNotFoundError:23except ModuleNotFoundError:
21 from django.core.urlresolvers import reverse_lazy # pre-1.1024 from django.core.urlresolvers import reverse_lazy # pre-1.10
22from django.views.generic.base import RedirectView25
23from turku_api import views26from turku_api import views
24from django.contrib import admin
2527
2628
27admin.autodiscover()29admin.autodiscover()
2830
29urlpatterns = [31urlpatterns = [
30 url(r'^$', RedirectView.as_view(url=reverse_lazy('admin:index'))),32 url(r"^$", RedirectView.as_view(url=reverse_lazy("admin:index"))),
31 url(r'^v1/health$', views.health, name='health'),33 url(r"^v1/health$", views.health, name="health"),
32 url(r'^v1/update_config$', views.update_config, name='update_config'),34 url(r"^v1/update_config$", views.update_config, name="update_config"),
33 url(r'^v1/agent_ping_checkin$', views.agent_ping_checkin, name='agent_ping_checkin'),35 url(
34 url(r'^v1/agent_ping_restore$', views.agent_ping_restore, name='agent_ping_restore'),36 r"^v1/agent_ping_checkin$", views.agent_ping_checkin, name="agent_ping_checkin"
35 url(r'^v1/storage_ping_checkin$', views.storage_ping_checkin, name='storage_ping_checkin'),37 ),
36 url(r'^v1/storage_ping_source_update$', views.storage_ping_source_update, name='storage_ping_source_update'),38 url(
37 url(r'^v1/storage_update_config$', views.storage_update_config, name='storage_update_config'),39 r"^v1/agent_ping_restore$", views.agent_ping_restore, name="agent_ping_restore"
38 url(r'^admin/', admin.site.urls),40 ),
41 url(
42 r"^v1/storage_ping_checkin$",
43 views.storage_ping_checkin,
44 name="storage_ping_checkin",
45 ),
46 url(
47 r"^v1/storage_ping_source_update$",
48 views.storage_ping_source_update,
49 name="storage_ping_source_update",
50 ),
51 url(
52 r"^v1/storage_update_config$",
53 views.storage_update_config,
54 name="storage_update_config",
55 ),
56 url(r"^admin/", admin.site.urls),
39]57]
4058
41try:59try:
42 from local_urls import *60 from local_urls import * # noqa: F401,F403
43except ImportError:61except ImportError:
44 pass62 pass
4563
=== modified file 'turku_api/views.py'
--- turku_api/views.py 2020-03-24 23:07:22 +0000
+++ turku_api/views.py 2020-06-21 23:58:30 +0000
@@ -14,87 +14,107 @@
14# License along with this program. If not, see14# License along with this program. If not, see
15# <http://www.gnu.org/licenses/>.15# <http://www.gnu.org/licenses/>.
1616
17from datetime import datetime, timedelta
18import json
19import random
20
21from django.contrib.auth import hashers
22from django.core.exceptions import ValidationError
17from django.http import (23from django.http import (
18 HttpResponse, HttpResponseBadRequest, HttpResponseNotAllowed,24 HttpResponse,
19 HttpResponseForbidden, HttpResponseNotFound,25 HttpResponseBadRequest,
26 HttpResponseForbidden,
27 HttpResponseNotAllowed,
28 HttpResponseNotFound,
20)29)
30from django.utils import timezone
21from django.views.decorators.csrf import csrf_exempt31from django.views.decorators.csrf import csrf_exempt
22from django.utils import timezone32
23from django.core.exceptions import ValidationError33from turku_api.models import Auth, BackupLog, FilterSet, Machine, Source, Storage
24
25from turku_api.models import Auth, Machine, Source, Storage, BackupLog, FilterSet
26
27import json
28import random
29from datetime import timedelta, datetime
30from django.contrib.auth import hashers
3134
3235
33def frequency_next_scheduled(frequency, base_time=None):36def frequency_next_scheduled(frequency, base_time=None):
34 if not base_time:37 if not base_time:
35 base_time = timezone.now()38 base_time = timezone.now()
36 f = [x.strip() for x in frequency.split(',')]39 f = [x.strip() for x in frequency.split(",")]
3740
38 if f[0] == 'hourly':41 if f[0] == "hourly":
39 target_time = (42 target_time = base_time.replace(
40 base_time.replace(43 minute=random.randint(0, 59), second=random.randint(0, 59), microsecond=0
41 minute=random.randint(0, 59), second=random.randint(0, 59), microsecond=044 ) + timedelta(hours=1)
42 ) + timedelta(hours=1)
43 )
44 # Push it out 10 minutes if it falls within 10 minutes of now45 # Push it out 10 minutes if it falls within 10 minutes of now
45 if target_time < (base_time + timedelta(minutes=10)):46 if target_time < (base_time + timedelta(minutes=10)):
46 target_time = (target_time + timedelta(minutes=10))47 target_time = target_time + timedelta(minutes=10)
47 return target_time48 return target_time
4849
49 today = base_time.replace(hour=0, minute=0, second=0, microsecond=0)50 today = base_time.replace(hour=0, minute=0, second=0, microsecond=0)
50 if f[0] == 'daily':51 if f[0] == "daily":
51 # Tomorrow52 # Tomorrow
52 target_date = (today + timedelta(days=1))53 target_date = today + timedelta(days=1)
53 elif f[0] == 'weekly':54 elif f[0] == "weekly":
54 # Random day next week55 # Random day next week
55 target_day = random.randint(0, 6)56 target_day = random.randint(0, 6)
56 target_date = (today + timedelta(weeks=1) - timedelta(days=((today.weekday() + 1) % 7)) + timedelta(days=target_day))57 target_date = (
58 today
59 + timedelta(weeks=1)
60 - timedelta(days=((today.weekday() + 1) % 7))
61 + timedelta(days=target_day)
62 )
57 # Push it out 3 days if it falls within 3 days of now63 # Push it out 3 days if it falls within 3 days of now
58 if target_date < (base_time + timedelta(days=3)):64 if target_date < (base_time + timedelta(days=3)):
59 target_date = (target_date + timedelta(days=3))65 target_date = target_date + timedelta(days=3)
60 elif f[0] in ('sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday'):66 elif f[0] in (
67 "sunday",
68 "monday",
69 "tuesday",
70 "wednesday",
71 "thursday",
72 "friday",
73 "saturday",
74 ):
61 # Next Xday75 # Next Xday
62 day_map = {76 day_map = {
63 'sunday': 0,77 "sunday": 0,
64 'monday': 1,78 "monday": 1,
65 'tuesday': 2,79 "tuesday": 2,
66 'wednesday': 3,80 "wednesday": 3,
67 'thursday': 4,81 "thursday": 4,
68 'friday': 5,82 "friday": 5,
69 'saturday': 6,83 "saturday": 6,
70 }84 }
71 target_day = day_map[f[0]]85 target_day = day_map[f[0]]
72 target_date = (today - timedelta(days=((today.weekday() + 1) % 7)) + timedelta(days=target_day))86 target_date = (
87 today
88 - timedelta(days=((today.weekday() + 1) % 7))
89 + timedelta(days=target_day)
90 )
73 if target_date < today:91 if target_date < today:
74 target_date = (target_date + timedelta(weeks=1))92 target_date = target_date + timedelta(weeks=1)
75 elif f[0] == 'monthly':93 elif f[0] == "monthly":
76 next_month = (today.replace(day=1) + timedelta(days=40)).replace(day=1)94 next_month = (today.replace(day=1) + timedelta(days=40)).replace(day=1)
77 month_after = (next_month.replace(day=1) + timedelta(days=40)).replace(day=1)95 month_after = (next_month.replace(day=1) + timedelta(days=40)).replace(day=1)
78 target_date = (next_month + timedelta(days=random.randint(1, (month_after - next_month).days)))96 target_date = next_month + timedelta(
97 days=random.randint(1, (month_after - next_month).days)
98 )
79 # Push it out a week if it falls within a week of now99 # Push it out a week if it falls within a week of now
80 if target_date < (base_time + timedelta(days=7)):100 if target_date < (base_time + timedelta(days=7)):
81 target_date = (target_date + timedelta(days=7))101 target_date = target_date + timedelta(days=7)
82 else:102 else:
83 # Fall back to tomorrow103 # Fall back to tomorrow
84 target_date = (today + timedelta(days=1))104 target_date = today + timedelta(days=1)
85105
86 if len(f) == 1:106 if len(f) == 1:
87 return (target_date + timedelta(seconds=random.randint(0, 86399)))107 return target_date + timedelta(seconds=random.randint(0, 86399))
88 time_range = f[1].split('-')108 time_range = f[1].split("-")
89 start = (int(time_range[0][0:2]) * 60 * 60) + (int(time_range[0][2:4]) * 60)109 start = (int(time_range[0][0:2]) * 60 * 60) + (int(time_range[0][2:4]) * 60)
90 if len(time_range) == 1:110 if len(time_range) == 1:
91 # Not a range111 # Not a range
92 return (target_date + timedelta(seconds=start))112 return target_date + timedelta(seconds=start)
93 end = (int(time_range[1][0:2]) * 60 * 60) + (int(time_range[1][2:4]) * 60)113 end = (int(time_range[1][0:2]) * 60 * 60) + (int(time_range[1][2:4]) * 60)
94 if end < start:114 if end < start:
95 # Day rollover115 # Day rollover
96 end = end + 86400116 end = end + 86400
97 return (target_date + timedelta(seconds=random.randint(start, end)))117 return target_date + timedelta(seconds=random.randint(start, end))
98118
99119
100def random_weighted(m):120def random_weighted(m):
@@ -115,8 +135,9 @@
115135
116def get_repo_revision():136def get_repo_revision():
117 import os137 import os
138
118 base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))139 base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
119 if os.path.isdir(os.path.join(base_dir, '.bzr')):140 if os.path.isdir(os.path.join(base_dir, ".bzr")):
120 try:141 try:
121 import bzrlib.errors142 import bzrlib.errors
122 from bzrlib.branch import Branch143 from bzrlib.branch import Branch
@@ -137,17 +158,22 @@
137 return repr(self.message)158 return repr(self.message)
138159
139160
140class ViewV1():161class ViewV1:
141 def __init__(self, django_request):162 def __init__(self, django_request):
142 self.django_request = django_request163 self.django_request = django_request
143 self._parse_json_post()164 self._parse_json_post()
144165
145 def _parse_json_post(self):166 def _parse_json_post(self):
146 # Require JSON POST167 # Require JSON POST
147 if not self.django_request.method == 'POST':168 if not self.django_request.method == "POST":
148 raise HttpResponseException(HttpResponseNotAllowed(['POST']))169 raise HttpResponseException(HttpResponseNotAllowed(["POST"]))
149 if not (('CONTENT_TYPE' in self.django_request.META) and (self.django_request.META['CONTENT_TYPE'] == 'application/json')):170 if not (
150 raise HttpResponseException(HttpResponseBadRequest('Bad Content-Type (expected application/json)'))171 ("CONTENT_TYPE" in self.django_request.META)
172 and (self.django_request.META["CONTENT_TYPE"] == "application/json")
173 ):
174 raise HttpResponseException(
175 HttpResponseBadRequest("Bad Content-Type (expected application/json)")
176 )
151177
152 # Load the POSTed JSON178 # Load the POSTed JSON
153 try:179 try:
@@ -157,80 +183,103 @@
157183
158 def _storage_authenticate(self):184 def _storage_authenticate(self):
159 # Check for storage auth185 # Check for storage auth
160 if 'storage' not in self.req:186 if "storage" not in self.req:
161 raise HttpResponseException(HttpResponseBadRequest('Missing required option "storage"'))187 raise HttpResponseException(
162 for k in ('name', 'secret'):188 HttpResponseBadRequest('Missing required option "storage"')
163 if k not in self.req['storage']:189 )
164 raise HttpResponseException(HttpResponseForbidden('Bad auth'))190 for k in ("name", "secret"):
191 if k not in self.req["storage"]:
192 raise HttpResponseException(HttpResponseForbidden("Bad auth"))
165 try:193 try:
166 self.storage = Storage.objects.get(name=self.req['storage']['name'], active=True)194 self.storage = Storage.objects.get(
195 name=self.req["storage"]["name"], active=True
196 )
167 except Storage.DoesNotExist:197 except Storage.DoesNotExist:
168 raise HttpResponseException(HttpResponseForbidden('Bad auth'))198 raise HttpResponseException(HttpResponseForbidden("Bad auth"))
169 if not hashers.check_password(self.req['storage']['secret'], self.storage.secret_hash):199 if not hashers.check_password(
170 raise HttpResponseException(HttpResponseForbidden('Bad auth'))200 self.req["storage"]["secret"], self.storage.secret_hash
201 ):
202 raise HttpResponseException(HttpResponseForbidden("Bad auth"))
171203
172 def _storage_get_machine(self):204 def _storage_get_machine(self):
173 # Make sure these exist in the request205 # Make sure these exist in the request
174 if 'machine' not in self.req:206 if "machine" not in self.req:
175 raise HttpResponseException(HttpResponseBadRequest('Missing required option "machine"'))207 raise HttpResponseException(
176 if 'uuid' not in self.req['machine']:208 HttpResponseBadRequest('Missing required option "machine"')
177 raise HttpResponseException(HttpResponseBadRequest('Missing required option "machine.uuid"'))209 )
210 if "uuid" not in self.req["machine"]:
211 raise HttpResponseException(
212 HttpResponseBadRequest('Missing required option "machine.uuid"')
213 )
178214
179 # Create or load the machine215 # Create or load the machine
180 try:216 try:
181 return Machine.objects.get(uuid=self.req['machine']['uuid'], storage=self.storage, active=True, published=True)217 return Machine.objects.get(
218 uuid=self.req["machine"]["uuid"],
219 storage=self.storage,
220 active=True,
221 published=True,
222 )
182 except Machine.DoesNotExist:223 except Machine.DoesNotExist:
183 raise HttpResponseException(HttpResponseNotFound('Machine not found'))224 raise HttpResponseException(HttpResponseNotFound("Machine not found"))
184225
185 def get_registration_auth(self, secret_type):226 def get_registration_auth(self, secret_type):
186 # Check for global auth227 # Check for global auth
187 if 'auth' not in self.req:228 if "auth" not in self.req:
188 raise HttpResponseException(HttpResponseForbidden('Bad auth'))229 raise HttpResponseException(HttpResponseForbidden("Bad auth"))
189 if isinstance(self.req['auth'], dict):230 if isinstance(self.req["auth"], dict):
190 if not (('name' in self.req['auth']) and ('secret' in self.req['auth'])):231 if not (("name" in self.req["auth"]) and ("secret" in self.req["auth"])):
191 raise HttpResponseException(HttpResponseForbidden('Bad auth'))232 raise HttpResponseException(HttpResponseForbidden("Bad auth"))
192 try:233 try:
193 a = Auth.objects.get(name=self.req['auth']['name'], secret_type=secret_type, active=True)234 a = Auth.objects.get(
235 name=self.req["auth"]["name"], secret_type=secret_type, active=True
236 )
194 except Auth.DoesNotExist:237 except Auth.DoesNotExist:
195 raise HttpResponseException(HttpResponseForbidden('Bad auth'))238 raise HttpResponseException(HttpResponseForbidden("Bad auth"))
196 if hashers.check_password(self.req['auth']['secret'], a.secret_hash):239 if hashers.check_password(self.req["auth"]["secret"], a.secret_hash):
197 return a240 return a
198 else:241 else:
199 # XXX inefficient but temporary (legacy)242 # XXX inefficient but temporary (legacy)
200 for a in Auth.objects.filter(secret_type=secret_type, active=True):243 for a in Auth.objects.filter(secret_type=secret_type, active=True):
201 if hashers.check_password(self.req['auth'], a.secret_hash):244 if hashers.check_password(self.req["auth"], a.secret_hash):
202 return a245 return a
203 raise HttpResponseException(HttpResponseForbidden('Bad auth'))246 raise HttpResponseException(HttpResponseForbidden("Bad auth"))
204247
205 def update_config(self):248 def update_config(self):
206 if not (('machine' in self.req) and (isinstance(self.req['machine'], dict))):249 if not (("machine" in self.req) and (isinstance(self.req["machine"], dict))):
207 raise HttpResponseException(HttpResponseBadRequest('"machine" dict required'))250 raise HttpResponseException(
208 req_machine = self.req['machine']251 HttpResponseBadRequest('"machine" dict required')
252 )
253 req_machine = self.req["machine"]
209254
210 # Make sure these exist in the request (validation comes later)255 # Make sure these exist in the request (validation comes later)
211 for k in ('uuid', 'secret'):256 for k in ("uuid", "secret"):
212 if k not in req_machine:257 if k not in req_machine:
213 raise HttpResponseException(HttpResponseBadRequest('Missing required machine option "%s"' % k))258 raise HttpResponseException(
259 HttpResponseBadRequest('Missing required machine option "%s"' % k)
260 )
214261
215 # Create or load the machine262 # Create or load the machine
216 try:263 try:
217 m = Machine.objects.get(uuid=req_machine['uuid'], active=True)264 m = Machine.objects.get(uuid=req_machine["uuid"], active=True)
218 modified = False265 modified = False
219 except Machine.DoesNotExist:266 except Machine.DoesNotExist:
220 m = Machine(uuid=req_machine['uuid'])267 m = Machine(uuid=req_machine["uuid"])
221 m.secret_hash = hashers.make_password(req_machine['secret'])268 m.secret_hash = hashers.make_password(req_machine["secret"])
222 m.auth = self.get_registration_auth('machine_reg')269 m.auth = self.get_registration_auth("machine_reg")
223 modified = True270 modified = True
224271
225 # If the machine existed before, it had a secret. Make sure that272 # If the machine existed before, it had a secret. Make sure that
226 # hasn't changed.273 # hasn't changed.
227 if not hashers.check_password(req_machine['secret'], m.secret_hash):274 if not hashers.check_password(req_machine["secret"], m.secret_hash):
228 raise HttpResponseException(HttpResponseForbidden('Bad secret for existing machine'))275 raise HttpResponseException(
276 HttpResponseForbidden("Bad secret for existing machine")
277 )
229278
230 # Change the machine published status if needed279 # Change the machine published status if needed
231 if ('published' in req_machine):280 if "published" in req_machine:
232 if req_machine['published'] != m.published:281 if req_machine["published"] != m.published:
233 m.published = req_machine['published']282 m.published = req_machine["published"]
234 modified = True283 modified = True
235 else:284 else:
236 # If not present, default to want published285 # If not present, default to want published
@@ -251,11 +300,19 @@
251 m.storage = random_weighted(weights)300 m.storage = random_weighted(weights)
252 modified = True301 modified = True
253 except IndexError:302 except IndexError:
254 raise HttpResponseException(HttpResponseNotFound('No storages are currently available'))303 raise HttpResponseException(
304 HttpResponseNotFound("No storages are currently available")
305 )
255306
256 # If any of these exist in the request, add or update them in the307 # If any of these exist in the request, add or update them in the
257 # machine.308 # machine.
258 for k in ('environment_name', 'service_name', 'unit_name', 'comment', 'ssh_public_key'):309 for k in (
310 "environment_name",
311 "service_name",
312 "unit_name",
313 "comment",
314 "ssh_public_key",
315 ):
259 if (k in req_machine) and (getattr(m, k) != req_machine[k]):316 if (k in req_machine) and (getattr(m, k) != req_machine[k]):
260 setattr(m, k, req_machine[k])317 setattr(m, k, req_machine[k])
261 modified = True318 modified = True
@@ -266,18 +323,24 @@
266 try:323 try:
267 m.full_clean()324 m.full_clean()
268 except ValidationError as e:325 except ValidationError as e:
269 raise HttpResponseException(HttpResponseBadRequest('Validation error: %s' % str(e)))326 raise HttpResponseException(
327 HttpResponseBadRequest("Validation error: %s" % str(e))
328 )
270 m.save()329 m.save()
271330
272 if 'sources' in req_machine:331 if "sources" in req_machine:
273 req_sources = req_machine['sources']332 req_sources = req_machine["sources"]
274 if not isinstance(req_sources, dict):333 if not isinstance(req_sources, dict):
275 raise HttpResponseException(HttpResponseBadRequest('Invalid type for "sources"'))334 raise HttpResponseException(
276 elif 'sources' in self.req:335 HttpResponseBadRequest('Invalid type for "sources"')
336 )
337 elif "sources" in self.req:
277 # XXX legacy338 # XXX legacy
278 req_sources = self.req['sources']339 req_sources = self.req["sources"]
279 if not isinstance(req_sources, dict):340 if not isinstance(req_sources, dict):
280 raise HttpResponseException(HttpResponseBadRequest('Invalid type for "sources"'))341 raise HttpResponseException(
342 HttpResponseBadRequest('Invalid type for "sources"')
343 )
281 else:344 else:
282 req_sources = {}345 req_sources = {}
283346
@@ -291,17 +354,27 @@
291354
292 modified = False355 modified = False
293 for k in (356 for k in (
294 'path', 'frequency', 'retention',357 "path",
295 'comment', 'shared_service', 'large_rotating_files',358 "frequency",
296 'large_modifying_files', 'bwlimit', 'snapshot_mode',359 "retention",
297 'preserve_hard_links',360 "comment",
361 "shared_service",
362 "large_rotating_files",
363 "large_modifying_files",
364 "bwlimit",
365 "snapshot_mode",
366 "preserve_hard_links",
298 ):367 ):
299 if (k in req_sources[s.name]) and (getattr(s, k) != req_sources[s.name][k]):368 if (k in req_sources[s.name]) and (
369 getattr(s, k) != req_sources[s.name][k]
370 ):
300 setattr(s, k, req_sources[s.name][k])371 setattr(s, k, req_sources[s.name][k])
301 if k == 'frequency':372 if k == "frequency":
302 s.date_next_backup = frequency_next_scheduled(req_sources[s.name][k])373 s.date_next_backup = frequency_next_scheduled(
374 req_sources[s.name][k]
375 )
303 modified = True376 modified = True
304 for k in ('filter', 'exclude'):377 for k in ("filter", "exclude"):
305 if k not in req_sources[s.name]:378 if k not in req_sources[s.name]:
306 continue379 continue
307 v = json.dumps(req_sources[s.name][k], sort_keys=True)380 v = json.dumps(req_sources[s.name][k], sort_keys=True)
@@ -315,7 +388,9 @@
315 try:388 try:
316 s.full_clean()389 s.full_clean()
317 except ValidationError as e:390 except ValidationError as e:
318 raise HttpResponseException(HttpResponseBadRequest('Validation error: %s' % str(e)))391 raise HttpResponseException(
392 HttpResponseBadRequest("Validation error: %s" % str(e))
393 )
319 s.save()394 s.save()
320395
321 for name in req_sources:396 for name in req_sources:
@@ -326,15 +401,21 @@
326 s.machine = m401 s.machine = m
327402
328 for k in (403 for k in (
329 'path', 'frequency', 'retention',404 "path",
330 'comment', 'shared_service', 'large_rotating_files',405 "frequency",
331 'large_modifying_files', 'bwlimit', 'snapshot_mode',406 "retention",
332 'preserve_hard_links',407 "comment",
408 "shared_service",
409 "large_rotating_files",
410 "large_modifying_files",
411 "bwlimit",
412 "snapshot_mode",
413 "preserve_hard_links",
333 ):414 ):
334 if k not in req_sources[s.name]:415 if k not in req_sources[s.name]:
335 continue416 continue
336 setattr(s, k, req_sources[s.name][k])417 setattr(s, k, req_sources[s.name][k])
337 for k in ('filter', 'exclude'):418 for k in ("filter", "exclude"):
338 if k not in req_sources[s.name]:419 if k not in req_sources[s.name]:
339 continue420 continue
340 v = json.dumps(req_sources[s.name][k], sort_keys=True)421 v = json.dumps(req_sources[s.name][k], sort_keys=True)
@@ -346,18 +427,20 @@
346 try:427 try:
347 s.full_clean()428 s.full_clean()
348 except ValidationError as e:429 except ValidationError as e:
349 raise HttpResponseException(HttpResponseBadRequest('Validation error: %s' % str(e)))430 raise HttpResponseException(
431 HttpResponseBadRequest("Validation error: %s" % str(e))
432 )
350 s.save()433 s.save()
351434
352 # XXX legacy435 # XXX legacy
353 out = {436 out = {
354 'storage_name': m.storage.name,437 "storage_name": m.storage.name,
355 'ssh_ping_host': m.storage.ssh_ping_host,438 "ssh_ping_host": m.storage.ssh_ping_host,
356 'ssh_ping_host_keys': json.loads(m.storage.ssh_ping_host_keys),439 "ssh_ping_host_keys": json.loads(m.storage.ssh_ping_host_keys),
357 'ssh_ping_port': m.storage.ssh_ping_port,440 "ssh_ping_port": m.storage.ssh_ping_port,
358 'ssh_ping_user': m.storage.ssh_ping_user,441 "ssh_ping_user": m.storage.ssh_ping_user,
359 }442 }
360 return HttpResponse(json.dumps(out), content_type='application/json')443 return HttpResponse(json.dumps(out), content_type="application/json")
361444
362 def build_filters(self, set, loaded_sets=None):445 def build_filters(self, set, loaded_sets=None):
363 if not loaded_sets:446 if not loaded_sets:
@@ -365,10 +448,10 @@
365 out = []448 out = []
366 for f in set:449 for f in set:
367 try:450 try:
368 (verb, subsetname) = f.split(' ', 1)451 (verb, subsetname) = f.split(" ", 1)
369 except ValueError:452 except ValueError:
370 continue453 continue
371 if verb in ('merge', '.'):454 if verb in ("merge", "."):
372 if subsetname in loaded_sets:455 if subsetname in loaded_sets:
373 continue456 continue
374 try:457 try:
@@ -379,10 +462,22 @@
379 out.append(f2)462 out.append(f2)
380 loaded_sets.append(subsetname)463 loaded_sets.append(subsetname)
381 elif verb in (464 elif verb in (
382 'dir-merge', ':', 'clear', '!',465 "dir-merge",
383 'exclude', '-', 'include', '+',466 ":",
384 'hide', 'H', 'show', 'S',467 "clear",
385 'protect', 'P', 'risk', 'R',468 "!",
469 "exclude",
470 "-",
471 "include",
472 "+",
473 "hide",
474 "H",
475 "show",
476 "S",
477 "protect",
478 "P",
479 "risk",
480 "R",
386 ):481 ):
387 out.append(f)482 out.append(f)
388 return out483 return out
@@ -390,109 +485,117 @@
390 def get_checkin_scheduled_sources(self, m):485 def get_checkin_scheduled_sources(self, m):
391 scheduled_sources = {}486 scheduled_sources = {}
392 now = timezone.now()487 now = timezone.now()
393 for s in m.source_set.filter(date_next_backup__lte=now, active=True, published=True):488 for s in m.source_set.filter(
489 date_next_backup__lte=now, active=True, published=True
490 ):
394 scheduled_sources[s.name] = {491 scheduled_sources[s.name] = {
395 'path': s.path,492 "path": s.path,
396 'retention': s.retention,493 "retention": s.retention,
397 'bwlimit': s.bwlimit,494 "bwlimit": s.bwlimit,
398 'filter': self.build_filters(json.loads(s.filter)),495 "filter": self.build_filters(json.loads(s.filter)),
399 'exclude': json.loads(s.exclude),496 "exclude": json.loads(s.exclude),
400 'shared_service': s.shared_service,497 "shared_service": s.shared_service,
401 'large_rotating_files': s.large_rotating_files,498 "large_rotating_files": s.large_rotating_files,
402 'large_modifying_files': s.large_modifying_files,499 "large_modifying_files": s.large_modifying_files,
403 'snapshot_mode': s.snapshot_mode,500 "snapshot_mode": s.snapshot_mode,
404 'preserve_hard_links': s.preserve_hard_links,501 "preserve_hard_links": s.preserve_hard_links,
405 'storage': {502 "storage": {
406 'name': s.machine.storage.name,503 "name": s.machine.storage.name,
407 'ssh_ping_host': s.machine.storage.ssh_ping_host,504 "ssh_ping_host": s.machine.storage.ssh_ping_host,
408 'ssh_ping_host_keys': json.loads(s.machine.storage.ssh_ping_host_keys),505 "ssh_ping_host_keys": json.loads(
409 'ssh_ping_port': s.machine.storage.ssh_ping_port,506 s.machine.storage.ssh_ping_host_keys
410 'ssh_ping_user': s.machine.storage.ssh_ping_user,507 ),
411 }508 "ssh_ping_port": s.machine.storage.ssh_ping_port,
509 "ssh_ping_user": s.machine.storage.ssh_ping_user,
510 },
412 }511 }
413 return scheduled_sources512 return scheduled_sources
414513
415 def agent_ping_checkin(self):514 def agent_ping_checkin(self):
416 if not (('machine' in self.req) and (isinstance(self.req['machine'], dict))):515 if not (("machine" in self.req) and (isinstance(self.req["machine"], dict))):
417 raise HttpResponseException(HttpResponseBadRequest('"machine" dict required'))516 raise HttpResponseException(
418 req_machine = self.req['machine']517 HttpResponseBadRequest('"machine" dict required')
518 )
519 req_machine = self.req["machine"]
419520
420 # Make sure these exist in the request521 # Make sure these exist in the request
421 for k in ('uuid', 'secret'):522 for k in ("uuid", "secret"):
422 if k not in req_machine:523 if k not in req_machine:
423 raise HttpResponseException(HttpResponseBadRequest('Missing required machine option "%s"' % k))524 raise HttpResponseException(
525 HttpResponseBadRequest('Missing required machine option "%s"' % k)
526 )
424527
425 # Load the machine528 # Load the machine
426 try:529 try:
427 m = Machine.objects.get(uuid=req_machine['uuid'], active=True, published=True)530 m = Machine.objects.get(
531 uuid=req_machine["uuid"], active=True, published=True
532 )
428 except Machine.DoesNotExist:533 except Machine.DoesNotExist:
429 raise HttpResponseException(HttpResponseForbidden('Bad auth'))534 raise HttpResponseException(HttpResponseForbidden("Bad auth"))
430 if not hashers.check_password(req_machine['secret'], m.secret_hash):535 if not hashers.check_password(req_machine["secret"], m.secret_hash):
431 raise HttpResponseException(HttpResponseForbidden('Bad auth'))536 raise HttpResponseException(HttpResponseForbidden("Bad auth"))
432537
433 scheduled_sources = self.get_checkin_scheduled_sources(m)538 scheduled_sources = self.get_checkin_scheduled_sources(m)
434 now = timezone.now()539 now = timezone.now()
435540
436 out = {541 out = {"machine": {"scheduled_sources": scheduled_sources}}
437 'machine': {
438 'scheduled_sources': scheduled_sources,
439 },
440 }
441542
442 # XXX legacy543 # XXX legacy
443 out['scheduled_sources'] = scheduled_sources544 out["scheduled_sources"] = scheduled_sources
444545
445 m.date_checked_in = now546 m.date_checked_in = now
446 m.save()547 m.save()
447 return HttpResponse(json.dumps(out), content_type='application/json')548 return HttpResponse(json.dumps(out), content_type="application/json")
448549
449 def agent_ping_restore(self):550 def agent_ping_restore(self):
450 if not (('machine' in self.req) and (isinstance(self.req['machine'], dict))):551 if not (("machine" in self.req) and (isinstance(self.req["machine"], dict))):
451 raise HttpResponseException(HttpResponseBadRequest('"machine" dict required'))552 raise HttpResponseException(
452 req_machine = self.req['machine']553 HttpResponseBadRequest('"machine" dict required')
554 )
555 req_machine = self.req["machine"]
453556
454 # Make sure these exist in the request557 # Make sure these exist in the request
455 for k in ('uuid', 'secret'):558 for k in ("uuid", "secret"):
456 if k not in req_machine:559 if k not in req_machine:
457 raise HttpResponseException(HttpResponseBadRequest('Missing required machine option "%s"' % k))560 raise HttpResponseException(
561 HttpResponseBadRequest('Missing required machine option "%s"' % k)
562 )
458563
459 # Load the machine564 # Load the machine
460 try:565 try:
461 m = Machine.objects.get(uuid=req_machine['uuid'], active=True)566 m = Machine.objects.get(uuid=req_machine["uuid"], active=True)
462 except Machine.DoesNotExist:567 except Machine.DoesNotExist:
463 raise HttpResponseException(HttpResponseForbidden('Bad auth'))568 raise HttpResponseException(HttpResponseForbidden("Bad auth"))
464 if not hashers.check_password(req_machine['secret'], m.secret_hash):569 if not hashers.check_password(req_machine["secret"], m.secret_hash):
465 raise HttpResponseException(HttpResponseForbidden('Bad auth'))570 raise HttpResponseException(HttpResponseForbidden("Bad auth"))
466571
467 sources = {}572 sources = {}
468 for s in m.source_set.filter(active=True):573 for s in m.source_set.filter(active=True):
469 sources[s.name] = {574 sources[s.name] = {
470 'path': s.path,575 "path": s.path,
471 'retention': s.retention,576 "retention": s.retention,
472 'bwlimit': s.bwlimit,577 "bwlimit": s.bwlimit,
473 'filter': self.build_filters(json.loads(s.filter)),578 "filter": self.build_filters(json.loads(s.filter)),
474 'exclude': json.loads(s.exclude),579 "exclude": json.loads(s.exclude),
475 'shared_service': s.shared_service,580 "shared_service": s.shared_service,
476 'large_rotating_files': s.large_rotating_files,581 "large_rotating_files": s.large_rotating_files,
477 'large_modifying_files': s.large_modifying_files,582 "large_modifying_files": s.large_modifying_files,
478 'snapshot_mode': s.snapshot_mode,583 "snapshot_mode": s.snapshot_mode,
479 'preserve_hard_links': s.preserve_hard_links,584 "preserve_hard_links": s.preserve_hard_links,
480 'storage': {585 "storage": {
481 'name': s.machine.storage.name,586 "name": s.machine.storage.name,
482 'ssh_ping_host': s.machine.storage.ssh_ping_host,587 "ssh_ping_host": s.machine.storage.ssh_ping_host,
483 'ssh_ping_host_keys': json.loads(s.machine.storage.ssh_ping_host_keys),588 "ssh_ping_host_keys": json.loads(
484 'ssh_ping_port': s.machine.storage.ssh_ping_port,589 s.machine.storage.ssh_ping_host_keys
485 'ssh_ping_user': s.machine.storage.ssh_ping_user,590 ),
486 }591 "ssh_ping_port": s.machine.storage.ssh_ping_port,
592 "ssh_ping_user": s.machine.storage.ssh_ping_user,
593 },
487 }594 }
488595
489 out = {596 out = {"machine": {"sources": sources}}
490 'machine': {
491 'sources': sources,
492 },
493 }
494597
495 return HttpResponse(json.dumps(out), content_type='application/json')598 return HttpResponse(json.dumps(out), content_type="application/json")
496599
497 def storage_ping_checkin(self):600 def storage_ping_checkin(self):
498 self._storage_authenticate()601 self._storage_authenticate()
@@ -502,32 +605,34 @@
502 now = timezone.now()605 now = timezone.now()
503606
504 out = {607 out = {
505 'machine': {608 "machine": {
506 'uuid': m.uuid,609 "uuid": m.uuid,
507 'environment_name': m.environment_name,610 "environment_name": m.environment_name,
508 'service_name': m.service_name,611 "service_name": m.service_name,
509 'unit_name': m.unit_name,612 "unit_name": m.unit_name,
510 'scheduled_sources': scheduled_sources,613 "scheduled_sources": scheduled_sources,
511 },614 }
512 }615 }
513 m.date_checked_in = now616 m.date_checked_in = now
514 m.save()617 m.save()
515 return HttpResponse(json.dumps(out), content_type='application/json')618 return HttpResponse(json.dumps(out), content_type="application/json")
516619
517 def storage_ping_source_update(self):620 def storage_ping_source_update(self):
518 self._storage_authenticate()621 self._storage_authenticate()
519 m = self._storage_get_machine()622 m = self._storage_get_machine()
520623
521 if 'sources' not in self.req['machine']:624 if "sources" not in self.req["machine"]:
522 raise HttpResponseException(HttpResponseBadRequest('Missing required option "machine.sources"'))625 raise HttpResponseException(
523 for source_name in self.req['machine']['sources']:626 HttpResponseBadRequest('Missing required option "machine.sources"')
524 source_data = self.req['machine']['sources'][source_name]627 )
628 for source_name in self.req["machine"]["sources"]:
629 source_data = self.req["machine"]["sources"][source_name]
525 try:630 try:
526 s = m.source_set.get(name=source_name, active=True, published=True)631 s = m.source_set.get(name=source_name, active=True, published=True)
527 except Source.DoesNotExist:632 except Source.DoesNotExist:
528 raise HttpResponseException(HttpResponseNotFound('Source not found'))633 raise HttpResponseException(HttpResponseNotFound("Source not found"))
529 now = timezone.now()634 now = timezone.now()
530 is_success = ('success' in source_data and source_data['success'])635 is_success = "success" in source_data and source_data["success"]
531 s.success = is_success636 s.success = is_success
532 if is_success:637 if is_success:
533 s.date_last_backed_up = now638 s.date_last_backed_up = now
@@ -538,46 +643,63 @@
538 bl.date = now643 bl.date = now
539 bl.storage = self.storage644 bl.storage = self.storage
540 bl.success = is_success645 bl.success = is_success
541 if 'snapshot' in source_data:646 if "snapshot" in source_data:
542 bl.snapshot = source_data['snapshot']647 bl.snapshot = source_data["snapshot"]
543 if 'summary' in source_data:648 if "summary" in source_data:
544 bl.summary = source_data['summary']649 bl.summary = source_data["summary"]
545 if 'time_begin' in source_data:650 if "time_begin" in source_data:
546 bl.date_begin = timezone.make_aware(datetime.utcfromtimestamp(source_data['time_begin']), timezone.utc)651 bl.date_begin = timezone.make_aware(
547 if 'time_end' in source_data:652 datetime.utcfromtimestamp(source_data["time_begin"]), timezone.utc
548 bl.date_end = timezone.make_aware(datetime.utcfromtimestamp(source_data['time_end']), timezone.utc)653 )
654 if "time_end" in source_data:
655 bl.date_end = timezone.make_aware(
656 datetime.utcfromtimestamp(source_data["time_end"]), timezone.utc
657 )
549 bl.save()658 bl.save()
550 return HttpResponse(json.dumps({}), content_type='application/json')659 return HttpResponse(json.dumps({}), content_type="application/json")
551660
552 def storage_update_config(self):661 def storage_update_config(self):
553 if not (('storage' in self.req) and (isinstance(self.req['storage'], dict))):662 if not (("storage" in self.req) and (isinstance(self.req["storage"], dict))):
554 raise HttpResponseException(HttpResponseBadRequest('"storage" dict required'))663 raise HttpResponseException(
555 req_storage = self.req['storage']664 HttpResponseBadRequest('"storage" dict required')
665 )
666 req_storage = self.req["storage"]
556667
557 # Make sure these exist in the request (validation comes later)668 # Make sure these exist in the request (validation comes later)
558 for k in ('name', 'secret', 'ssh_ping_host', 'ssh_ping_port', 'ssh_ping_user', 'ssh_ping_host_keys'):669 for k in (
670 "name",
671 "secret",
672 "ssh_ping_host",
673 "ssh_ping_port",
674 "ssh_ping_user",
675 "ssh_ping_host_keys",
676 ):
559 if k not in req_storage:677 if k not in req_storage:
560 raise HttpResponseException(HttpResponseBadRequest('Missing required storage option "%s"' % k))678 raise HttpResponseException(
679 HttpResponseBadRequest('Missing required storage option "%s"' % k)
680 )
561681
562 # Create or load the storage682 # Create or load the storage
563 try:683 try:
564 self.storage = Storage.objects.get(name=req_storage['name'], active=True)684 self.storage = Storage.objects.get(name=req_storage["name"], active=True)
565 modified = False685 modified = False
566 except Storage.DoesNotExist:686 except Storage.DoesNotExist:
567 self.storage = Storage(name=req_storage['name'])687 self.storage = Storage(name=req_storage["name"])
568 self.storage.secret_hash = hashers.make_password(req_storage['secret'])688 self.storage.secret_hash = hashers.make_password(req_storage["secret"])
569 self.storage.auth = self.get_registration_auth('storage_reg')689 self.storage.auth = self.get_registration_auth("storage_reg")
570 modified = True690 modified = True
571691
572 # If the storage existed before, it had a secret. Make sure that692 # If the storage existed before, it had a secret. Make sure that
573 # hasn't changed.693 # hasn't changed.
574 if not hashers.check_password(req_storage['secret'], self.storage.secret_hash):694 if not hashers.check_password(req_storage["secret"], self.storage.secret_hash):
575 raise HttpResponseException(HttpResponseForbidden('Bad secret for existing storage'))695 raise HttpResponseException(
696 HttpResponseForbidden("Bad secret for existing storage")
697 )
576698
577 # Change the storage published status if needed699 # Change the storage published status if needed
578 if ('published' in req_storage):700 if "published" in req_storage:
579 if req_storage['published'] != self.storage.published:701 if req_storage["published"] != self.storage.published:
580 self.storage.published = req_storage['published']702 self.storage.published = req_storage["published"]
581 modified = True703 modified = True
582 else:704 else:
583 # If not present, default to want published705 # If not present, default to want published
@@ -587,12 +709,19 @@
587709
588 # If any of these exist in the request, add or update them in the710 # If any of these exist in the request, add or update them in the
589 # self.storage.711 # self.storage.
590 for k in ('comment', 'ssh_ping_host', 'ssh_ping_port', 'ssh_ping_user', 'space_total', 'space_available'):712 for k in (
713 "comment",
714 "ssh_ping_host",
715 "ssh_ping_port",
716 "ssh_ping_user",
717 "space_total",
718 "space_available",
719 ):
591 if (k in req_storage) and (getattr(self.storage, k) != req_storage[k]):720 if (k in req_storage) and (getattr(self.storage, k) != req_storage[k]):
592 setattr(self.storage, k, req_storage[k])721 setattr(self.storage, k, req_storage[k])
593 modified = True722 modified = True
594723
595 for k in ('ssh_ping_host_keys',):724 for k in ("ssh_ping_host_keys",):
596 if k not in req_storage:725 if k not in req_storage:
597 continue726 continue
598 v = json.dumps(req_storage[k], sort_keys=True)727 v = json.dumps(req_storage[k], sort_keys=True)
@@ -606,21 +735,27 @@
606 try:735 try:
607 self.storage.full_clean()736 self.storage.full_clean()
608 except ValidationError as e:737 except ValidationError as e:
609 raise HttpResponseException(HttpResponseBadRequest('Validation error: %s' % str(e)))738 raise HttpResponseException(
739 HttpResponseBadRequest("Validation error: %s" % str(e))
740 )
610741
611 self.storage.date_checked_in = timezone.now()742 self.storage.date_checked_in = timezone.now()
612 self.storage.save()743 self.storage.save()
613744
614 machines = {}745 machines = {}
615 for m in Machine.objects.filter(storage=self.storage, active=True, published=True):746 for m in Machine.objects.filter(
747 storage=self.storage, active=True, published=True
748 ):
616 machines[m.uuid] = {749 machines[m.uuid] = {
617 'environment_name': m.environment_name,750 "environment_name": m.environment_name,
618 'service_name': m.service_name,751 "service_name": m.service_name,
619 'unit_name': m.unit_name,752 "unit_name": m.unit_name,
620 'comment': m.comment,753 "comment": m.comment,
621 'ssh_public_key': m.ssh_public_key,754 "ssh_public_key": m.ssh_public_key,
622 }755 }
623 return HttpResponse(json.dumps({'machines': machines}), content_type='application/json')756 return HttpResponse(
757 json.dumps({"machines": machines}), content_type="application/json"
758 )
624759
625760
626@csrf_exempt761@csrf_exempt
@@ -629,19 +764,19 @@
629 # to connect to its database and serve data). It does not764 # to connect to its database and serve data). It does not
630 # indicate the health of machines, storage units, etc.765 # indicate the health of machines, storage units, etc.
631 out = {766 out = {
632 'healthy': True,767 "healthy": True,
633 'date': timezone.now().isoformat(),768 "date": timezone.now().isoformat(),
634 'repo_revision': get_repo_revision(),769 "repo_revision": get_repo_revision(),
635 'counts': {770 "counts": {
636 'auth': Auth.objects.count(),771 "auth": Auth.objects.count(),
637 'storage': Storage.objects.count(),772 "storage": Storage.objects.count(),
638 'machine': Machine.objects.count(),773 "machine": Machine.objects.count(),
639 'source': Source.objects.count(),774 "source": Source.objects.count(),
640 'filter_set': FilterSet.objects.count(),775 "filter_set": FilterSet.objects.count(),
641 'backup_log': BackupLog.objects.count(),776 "backup_log": BackupLog.objects.count(),
642 },777 },
643 }778 }
644 return HttpResponse(json.dumps(out), content_type='application/json')779 return HttpResponse(json.dumps(out), content_type="application/json")
645780
646781
647@csrf_exempt782@csrf_exempt
648783
=== modified file 'turku_api/wsgi.py'
--- turku_api/wsgi.py 2015-07-30 22:41:42 +0000
+++ turku_api/wsgi.py 2020-06-21 23:58:30 +0000
@@ -25,9 +25,11 @@
2525
26import os26import os
27import sys27import sys
28
28BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))29BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
29sys.path.append(BASE_DIR)30sys.path.append(BASE_DIR)
30os.environ.setdefault("DJANGO_SETTINGS_MODULE", "turku_api.settings")31os.environ.setdefault("DJANGO_SETTINGS_MODULE", "turku_api.settings")
3132
32from django.core.wsgi import get_wsgi_application33from django.core.wsgi import get_wsgi_application # noqa: E402
34
33application = get_wsgi_application()35application = get_wsgi_application()

Subscribers

People subscribed via source and target branches

to all changes: