simo 2.4.1__py3-none-any.whl → 2.5.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of simo might be problematic. Click here for more details.
- simo/backups/__pycache__/admin.cpython-38.pyc +0 -0
- simo/backups/__pycache__/models.cpython-38.pyc +0 -0
- simo/backups/__pycache__/tasks.cpython-38.pyc +0 -0
- simo/backups/admin.py +10 -13
- simo/backups/migrations/0003_alter_backuplog_options_alter_backup_size.py +22 -0
- simo/backups/migrations/0004_alter_backup_options_alter_backuplog_options_and_more.py +29 -0
- simo/backups/migrations/__pycache__/0003_alter_backuplog_options_alter_backup_size.cpython-38.pyc +0 -0
- simo/backups/migrations/__pycache__/0004_alter_backup_options_alter_backuplog_options_and_more.cpython-38.pyc +0 -0
- simo/backups/models.py +1 -7
- simo/backups/tasks.py +221 -145
- simo/core/__pycache__/admin.cpython-38.pyc +0 -0
- simo/core/__pycache__/api.cpython-38.pyc +0 -0
- simo/core/__pycache__/app_widgets.cpython-38.pyc +0 -0
- simo/core/__pycache__/controllers.cpython-38.pyc +0 -0
- simo/core/__pycache__/events.cpython-38.pyc +0 -0
- simo/core/__pycache__/forms.cpython-38.pyc +0 -0
- simo/core/__pycache__/middleware.cpython-38.pyc +0 -0
- simo/core/__pycache__/models.cpython-38.pyc +0 -0
- simo/core/__pycache__/serializers.cpython-38.pyc +0 -0
- simo/core/__pycache__/tasks.cpython-38.pyc +0 -0
- simo/core/admin.py +4 -4
- simo/core/api.py +20 -4
- simo/core/app_widgets.py +5 -0
- simo/core/controllers.py +2 -2
- simo/core/events.py +2 -0
- simo/core/forms.py +2 -0
- simo/core/management/commands/gateways_manager.py +0 -3
- simo/core/middleware.py +7 -1
- simo/core/migrations/0042_alter_instance_timezone.py +18 -0
- simo/core/migrations/__pycache__/0042_alter_instance_timezone.cpython-38.pyc +0 -0
- simo/core/models.py +26 -6
- simo/core/serializers.py +17 -17
- simo/core/tasks.py +10 -7
- simo/fleet/__pycache__/controllers.cpython-38.pyc +0 -0
- simo/fleet/__pycache__/forms.cpython-38.pyc +0 -0
- simo/fleet/__pycache__/models.cpython-38.pyc +0 -0
- simo/fleet/controllers.py +86 -22
- simo/fleet/forms.py +224 -185
- simo/fleet/migrations/0038_alter_colonel_type.py +18 -0
- simo/fleet/migrations/0039_auto_20241016_1047.py +28 -0
- simo/fleet/migrations/0040_alter_colonel_pwm_frequency.py +18 -0
- simo/fleet/migrations/__pycache__/0038_alter_colonel_type.cpython-38.pyc +0 -0
- simo/fleet/migrations/__pycache__/0039_auto_20241016_1047.cpython-38.pyc +0 -0
- simo/fleet/migrations/__pycache__/0040_alter_colonel_pwm_frequency.cpython-38.pyc +0 -0
- simo/fleet/models.py +2 -2
- simo/generic/__pycache__/controllers.cpython-38.pyc +0 -0
- simo/generic/__pycache__/forms.cpython-38.pyc +0 -0
- simo/generic/__pycache__/gateways.cpython-38.pyc +0 -0
- simo/generic/__pycache__/models.cpython-38.pyc +0 -0
- simo/generic/controllers.py +41 -2
- simo/generic/forms.py +71 -7
- simo/generic/models.py +0 -1
- simo/generic/scripting/__init__.py +16 -0
- simo/generic/scripting/__pycache__/__init__.cpython-38.pyc +0 -0
- simo/generic/scripting/__pycache__/serializers.cpython-38.pyc +0 -0
- simo/generic/scripting/helpers.py +35 -0
- simo/generic/scripting/serializers.py +77 -0
- simo/generic/templates/admin/controller_widgets/weather_forecast.html +2 -2
- simo/notifications/__pycache__/models.cpython-38.pyc +0 -0
- simo/notifications/__pycache__/utils.cpython-38.pyc +0 -0
- simo/notifications/utils.py +30 -12
- simo/scripting.py +2 -2
- simo/users/__pycache__/api.cpython-38.pyc +0 -0
- simo/users/__pycache__/managers.cpython-38.pyc +0 -0
- simo/users/__pycache__/models.cpython-38.pyc +0 -0
- simo/users/__pycache__/serializers.cpython-38.pyc +0 -0
- simo/users/__pycache__/utils.cpython-38.pyc +0 -0
- simo/users/api.py +36 -7
- simo/users/managers.py +5 -1
- simo/users/migrations/0033_alter_user_ssh_key.py +18 -0
- simo/users/migrations/0034_instanceuser_last_seen_location_and_more.py +24 -0
- simo/users/migrations/__pycache__/0033_alter_user_ssh_key.cpython-38.pyc +0 -0
- simo/users/migrations/__pycache__/0034_instanceuser_last_seen_location_and_more.cpython-38.pyc +0 -0
- simo/users/models.py +37 -32
- simo/users/serializers.py +11 -8
- simo/users/utils.py +14 -3
- {simo-2.4.1.dist-info → simo-2.5.1.dist-info}/METADATA +1 -1
- {simo-2.4.1.dist-info → simo-2.5.1.dist-info}/RECORD +82 -59
- {simo-2.4.1.dist-info → simo-2.5.1.dist-info}/WHEEL +1 -1
- {simo-2.4.1.dist-info → simo-2.5.1.dist-info}/LICENSE.md +0 -0
- {simo-2.4.1.dist-info → simo-2.5.1.dist-info}/entry_points.txt +0 -0
- {simo-2.4.1.dist-info → simo-2.5.1.dist-info}/top_level.txt +0 -0
|
Binary file
|
|
Binary file
|
|
Binary file
|
simo/backups/admin.py
CHANGED
|
@@ -7,10 +7,10 @@ from .models import Backup, BackupLog
|
|
|
7
7
|
|
|
8
8
|
@admin.register(Backup)
|
|
9
9
|
class BackupAdmin(DjangoObjectActions, admin.ModelAdmin):
|
|
10
|
-
list_display = 'datetime', 'device', '
|
|
11
|
-
fields = 'datetime', 'device', '
|
|
12
|
-
readonly_fields = 'datetime', 'device', '
|
|
13
|
-
list_filter = 'datetime', 'mac',
|
|
10
|
+
list_display = 'datetime', 'device', 'filepath'
|
|
11
|
+
fields = 'datetime', 'device', 'filepath'
|
|
12
|
+
readonly_fields = 'datetime', 'device', 'filepath'
|
|
13
|
+
list_filter = 'datetime', 'mac',
|
|
14
14
|
actions = 'restore',
|
|
15
15
|
changelist_actions = ('backup',)
|
|
16
16
|
|
|
@@ -23,15 +23,11 @@ class BackupAdmin(DjangoObjectActions, admin.ModelAdmin):
|
|
|
23
23
|
def has_delete_permission(self, request, obj=None):
|
|
24
24
|
return False
|
|
25
25
|
|
|
26
|
-
def
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
elif obj.size > 1024 * 1024:
|
|
32
|
-
return f'{round(obj.size / (1024 * 1024), 2)} Kb'
|
|
33
|
-
return f'{obj.size} bytes'
|
|
34
|
-
size_display.short_description = 'size'
|
|
26
|
+
def changelist_view(self, *args, **kwargs):
|
|
27
|
+
from .tasks import check_backups
|
|
28
|
+
check_backups()
|
|
29
|
+
return super().changelist_view(*args, **kwargs)
|
|
30
|
+
|
|
35
31
|
|
|
36
32
|
def restore(self, request, queryset):
|
|
37
33
|
if queryset.count() > 1:
|
|
@@ -69,6 +65,7 @@ class BackupAdmin(DjangoObjectActions, admin.ModelAdmin):
|
|
|
69
65
|
@admin.register(BackupLog)
|
|
70
66
|
class BackupLogAdmin(admin.ModelAdmin):
|
|
71
67
|
fields = 'datetime', 'level', 'msg'
|
|
68
|
+
list_display = fields
|
|
72
69
|
readonly_fields = fields
|
|
73
70
|
list_fields = fields
|
|
74
71
|
list_filter = 'datetime', 'level'
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Generated by Django 4.2.10 on 2024-10-09 09:16
|
|
2
|
+
|
|
3
|
+
from django.db import migrations, models
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Migration(migrations.Migration):
|
|
7
|
+
|
|
8
|
+
dependencies = [
|
|
9
|
+
('backups', '0002_backuplog_backup_level_backup_size'),
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
operations = [
|
|
13
|
+
migrations.AlterModelOptions(
|
|
14
|
+
name='backuplog',
|
|
15
|
+
options={'ordering': ('datetime',)},
|
|
16
|
+
),
|
|
17
|
+
migrations.AlterField(
|
|
18
|
+
model_name='backup',
|
|
19
|
+
name='size',
|
|
20
|
+
field=models.BigIntegerField(default=0),
|
|
21
|
+
),
|
|
22
|
+
]
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Generated by Django 4.2.10 on 2024-10-10 12:05
|
|
2
|
+
|
|
3
|
+
from django.db import migrations
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Migration(migrations.Migration):
|
|
7
|
+
|
|
8
|
+
dependencies = [
|
|
9
|
+
('backups', '0003_alter_backuplog_options_alter_backup_size'),
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
operations = [
|
|
13
|
+
migrations.AlterModelOptions(
|
|
14
|
+
name='backup',
|
|
15
|
+
options={},
|
|
16
|
+
),
|
|
17
|
+
migrations.AlterModelOptions(
|
|
18
|
+
name='backuplog',
|
|
19
|
+
options={},
|
|
20
|
+
),
|
|
21
|
+
migrations.RemoveField(
|
|
22
|
+
model_name='backup',
|
|
23
|
+
name='level',
|
|
24
|
+
),
|
|
25
|
+
migrations.RemoveField(
|
|
26
|
+
model_name='backup',
|
|
27
|
+
name='size',
|
|
28
|
+
),
|
|
29
|
+
]
|
simo/backups/migrations/__pycache__/0003_alter_backuplog_options_alter_backup_size.cpython-38.pyc
ADDED
|
Binary file
|
|
Binary file
|
simo/backups/models.py
CHANGED
|
@@ -6,12 +6,9 @@ class Backup(models.Model):
|
|
|
6
6
|
datetime = models.DateTimeField(db_index=True)
|
|
7
7
|
mac = models.CharField(max_length=100, db_index=True)
|
|
8
8
|
filepath = models.CharField(max_length=200)
|
|
9
|
-
level = models.IntegerField(default=0, db_index=True)
|
|
10
|
-
size = models.IntegerField(default=0)
|
|
11
9
|
|
|
12
10
|
class Meta:
|
|
13
11
|
unique_together = 'datetime', 'mac'
|
|
14
|
-
ordering = 'datetime',
|
|
15
12
|
|
|
16
13
|
@property
|
|
17
14
|
def device(self):
|
|
@@ -25,7 +22,4 @@ class BackupLog(models.Model):
|
|
|
25
22
|
level = models.CharField(default='info', choices=(
|
|
26
23
|
('info', "INFO"), ('warning', "WARNING"), ('error', "ERROR")
|
|
27
24
|
))
|
|
28
|
-
msg = models.TextField()
|
|
29
|
-
|
|
30
|
-
class Meta:
|
|
31
|
-
ordering = 'datetime',
|
|
25
|
+
msg = models.TextField()
|
simo/backups/tasks.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import os, subprocess, json, uuid, datetime, shutil
|
|
2
|
-
from datetime import datetime,
|
|
1
|
+
import os, subprocess, json, uuid, datetime, shutil, pytz
|
|
2
|
+
from datetime import datetime, timedelta
|
|
3
|
+
from django.utils import timezone
|
|
3
4
|
from celeryc import celery_app
|
|
4
5
|
from simo.conf import dynamic_settings
|
|
5
6
|
from simo.core.utils.helpers import get_random_string
|
|
@@ -10,15 +11,15 @@ def check_backups():
|
|
|
10
11
|
'''
|
|
11
12
|
syncs up backups on external medium to the database
|
|
12
13
|
'''
|
|
13
|
-
from simo.backups.models import Backup
|
|
14
|
+
from simo.backups.models import Backup
|
|
14
15
|
|
|
15
16
|
try:
|
|
16
|
-
lv_group, lv_name,
|
|
17
|
+
lv_group, lv_name, sd_mountpoint = get_partitions()
|
|
17
18
|
except:
|
|
18
19
|
return Backup.objects.all().delete()
|
|
19
20
|
|
|
20
21
|
|
|
21
|
-
backups_dir = os.path.join(
|
|
22
|
+
backups_dir = os.path.join(sd_mountpoint, 'simo_backups')
|
|
22
23
|
if not os.path.exists(backups_dir):
|
|
23
24
|
return Backup.objects.all().delete()
|
|
24
25
|
|
|
@@ -34,24 +35,25 @@ def check_backups():
|
|
|
34
35
|
year, month = int(year), int(month)
|
|
35
36
|
except:
|
|
36
37
|
continue
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
38
|
+
|
|
39
|
+
month_folder_path = os.path.join(hub_dir, month_folder)
|
|
40
|
+
res = subprocess.run(
|
|
41
|
+
f"borg list {month_folder_path} --json",
|
|
42
|
+
shell=True, stdout=subprocess.PIPE
|
|
43
|
+
)
|
|
44
|
+
try:
|
|
45
|
+
archives = json.loads(res.stdout.decode())['archives']
|
|
46
|
+
except Exception as e:
|
|
47
|
+
continue
|
|
48
|
+
|
|
49
|
+
for archive in archives:
|
|
50
|
+
make_datetime = datetime.fromisoformat(archive['start'])
|
|
51
|
+
make_datetime = make_datetime.replace(tzinfo=pytz.UTC)
|
|
52
|
+
filepath = f"{month_folder_path}::{archive['name']}"
|
|
53
|
+
|
|
48
54
|
obj, new = Backup.objects.update_or_create(
|
|
49
|
-
datetime=
|
|
50
|
-
|
|
51
|
-
tzinfo=timezone.utc
|
|
52
|
-
), mac=hub_mac, defaults={
|
|
53
|
-
'filepath': filepath, 'level': level,
|
|
54
|
-
'size': file_stats.st_size
|
|
55
|
+
datetime=make_datetime, mac=hub_mac, defaults={
|
|
56
|
+
'filepath': f"{month_folder_path}::{archive['name']}",
|
|
55
57
|
}
|
|
56
58
|
)
|
|
57
59
|
backups_mentioned.append(obj.id)
|
|
@@ -61,14 +63,80 @@ def check_backups():
|
|
|
61
63
|
dynamic_settings['backups__last_check'] = int(datetime.now().timestamp())
|
|
62
64
|
|
|
63
65
|
|
|
64
|
-
def
|
|
66
|
+
def clean_backup_snaps(lv_group, lv_name):
|
|
67
|
+
res = subprocess.run(
|
|
68
|
+
'lvs --report-format json', shell=True, stdout=subprocess.PIPE
|
|
69
|
+
)
|
|
70
|
+
lvs_data = json.loads(res.stdout.decode())
|
|
71
|
+
for volume in lvs_data['report'][0]['lv']:
|
|
72
|
+
if volume['vg_name'] != lv_group:
|
|
73
|
+
continue
|
|
74
|
+
if volume['origin'] != lv_name:
|
|
75
|
+
continue
|
|
76
|
+
if not volume['lv_name'].startswith(f"{lv_name}-bk-"):
|
|
77
|
+
continue
|
|
78
|
+
subprocess.run(
|
|
79
|
+
f"lvremove -f {lv_group}/{volume['lv_name']}", shell=True
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def create_snap(lv_group, lv_name, snap_name=None, size=None, try_no=1):
|
|
85
|
+
'''
|
|
86
|
+
:param lv_group:
|
|
87
|
+
:param lv_name:
|
|
88
|
+
:param snap_name: random snap name will be generated if not provided
|
|
89
|
+
:param size: Size in GB. If not provided, maximum available space in lvm will be used.
|
|
90
|
+
:return: snap_name
|
|
91
|
+
'''
|
|
92
|
+
if not snap_name:
|
|
93
|
+
snap_name = f"{lv_name}-bk-{get_random_string(5)}"
|
|
94
|
+
|
|
95
|
+
clean_backup_snaps(lv_group, lv_name)
|
|
96
|
+
|
|
97
|
+
res = subprocess.run(
|
|
98
|
+
'vgs --report-format json', shell=True, stdout=subprocess.PIPE
|
|
99
|
+
)
|
|
65
100
|
try:
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
shell=True
|
|
69
|
-
).decode()
|
|
101
|
+
vgs_data = json.loads(res.stdout.decode())
|
|
102
|
+
free_space = vgs_data['report'][0]['vg'][0]['vg_free']
|
|
70
103
|
except:
|
|
71
|
-
|
|
104
|
+
if try_no < 3:
|
|
105
|
+
clean_backup_snaps(lv_group, lv_name)
|
|
106
|
+
return create_snap(lv_group, lv_name, snap_name, size, try_no+1)
|
|
107
|
+
raise Exception("Unable to find free space on LVM!")
|
|
108
|
+
|
|
109
|
+
if not free_space.lower().endswith('g'):
|
|
110
|
+
if try_no < 3:
|
|
111
|
+
clean_backup_snaps(lv_group, lv_name)
|
|
112
|
+
return create_snap(lv_group, lv_name, snap_name, size, try_no+1)
|
|
113
|
+
raise Exception("Not enough free space on LVM!")
|
|
114
|
+
|
|
115
|
+
free_space = int(float(
|
|
116
|
+
vgs_data['report'][0]['vg'][0]['vg_free'].strip('g').strip('<')
|
|
117
|
+
))
|
|
118
|
+
|
|
119
|
+
if not size:
|
|
120
|
+
size = free_space
|
|
121
|
+
else:
|
|
122
|
+
if size > free_space:
|
|
123
|
+
if try_no < 3:
|
|
124
|
+
clean_backup_snaps(lv_group, lv_name)
|
|
125
|
+
return create_snap(lv_group, lv_name, snap_name, size, try_no + 1)
|
|
126
|
+
raise Exception(
|
|
127
|
+
f"There's only {free_space}G available on LVM, "
|
|
128
|
+
f"but you asked for {size}G"
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
res = subprocess.run(
|
|
132
|
+
f'lvcreate -s -n {snap_name} {lv_group}/{lv_name} -L {size}G',
|
|
133
|
+
shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE
|
|
134
|
+
)
|
|
135
|
+
if res.returncode:
|
|
136
|
+
raise Exception(res.stderr)
|
|
137
|
+
|
|
138
|
+
return snap_name
|
|
139
|
+
|
|
72
140
|
|
|
73
141
|
|
|
74
142
|
def get_lvm_partition(lsblk_data):
|
|
@@ -145,140 +213,131 @@ def get_partitions():
|
|
|
145
213
|
return
|
|
146
214
|
|
|
147
215
|
if lvm_partition.get('partlabel'):
|
|
148
|
-
|
|
216
|
+
sd_mountpoint = f"/media/{backup_device['partlabel']}"
|
|
149
217
|
elif lvm_partition.get('label'):
|
|
150
|
-
|
|
218
|
+
sd_mountpoint = f"/media/{backup_device['label']}"
|
|
151
219
|
else:
|
|
152
|
-
|
|
220
|
+
sd_mountpoint = f"/media/{backup_device['name']}"
|
|
153
221
|
|
|
154
|
-
if not os.path.exists(
|
|
155
|
-
os.makedirs(
|
|
222
|
+
if not os.path.exists(sd_mountpoint):
|
|
223
|
+
os.makedirs(sd_mountpoint)
|
|
156
224
|
|
|
157
|
-
if backup_device.get('mountpoint') !=
|
|
225
|
+
if backup_device.get('mountpoint') != sd_mountpoint:
|
|
158
226
|
|
|
159
227
|
if backup_device.get('mountpoint'):
|
|
160
228
|
subprocess.call(f"umount {backup_device['mountpoint']}", shell=True)
|
|
161
229
|
|
|
162
230
|
subprocess.call(
|
|
163
|
-
f'mount /dev/{backup_device["name"]} {
|
|
231
|
+
f'mount /dev/{backup_device["name"]} {sd_mountpoint}', shell=True,
|
|
164
232
|
stdout=subprocess.PIPE
|
|
165
233
|
)
|
|
166
234
|
|
|
167
|
-
return lv_group, lv_name,
|
|
235
|
+
return lv_group, lv_name, sd_mountpoint
|
|
168
236
|
|
|
169
237
|
|
|
170
238
|
@celery_app.task
|
|
171
239
|
def perform_backup():
|
|
172
240
|
from simo.backups.models import BackupLog
|
|
173
241
|
try:
|
|
174
|
-
lv_group, lv_name,
|
|
242
|
+
lv_group, lv_name, sd_mountpoint = get_partitions()
|
|
175
243
|
except:
|
|
176
244
|
return
|
|
177
245
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
try:
|
|
181
|
-
subprocess.check_output(
|
|
182
|
-
f'lvremove -f {lv_group}/{lv_name}-snap',
|
|
183
|
-
shell=True
|
|
184
|
-
)
|
|
185
|
-
except:
|
|
186
|
-
pass
|
|
187
|
-
output = create_snap(lv_group, lv_name)
|
|
246
|
+
snap_mount_point = '/var/backups/simo-main'
|
|
247
|
+
subprocess.run(f'umount {snap_mount_point}', shell=True)
|
|
188
248
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
249
|
+
try:
|
|
250
|
+
snap_name = create_snap(lv_group, lv_name)
|
|
251
|
+
except Exception as e:
|
|
252
|
+
print("Error creating temporary snap\n" + str(e))
|
|
253
|
+
BackupLog.objects.create(
|
|
254
|
+
level='error',
|
|
255
|
+
msg="Backup error. Unable to create temporary snap\n" + str(e)
|
|
256
|
+
)
|
|
192
257
|
return
|
|
193
258
|
|
|
259
|
+
shutil.rmtree(snap_mount_point, ignore_errors=True)
|
|
260
|
+
os.makedirs(snap_mount_point)
|
|
261
|
+
subprocess.run([
|
|
262
|
+
"mount",
|
|
263
|
+
f"/dev/mapper/{lv_group}-{snap_name.replace('-', '--')}",
|
|
264
|
+
snap_mount_point
|
|
265
|
+
])
|
|
266
|
+
|
|
194
267
|
mac = str(hex(uuid.getnode()))
|
|
195
|
-
device_backups_path = f'{
|
|
196
|
-
|
|
197
|
-
os.makedirs(device_backups_path)
|
|
268
|
+
device_backups_path = f'{sd_mountpoint}/simo_backups/hub-{mac}'
|
|
269
|
+
|
|
198
270
|
|
|
199
271
|
now = datetime.now()
|
|
200
|
-
level = now.day
|
|
201
272
|
month_folder = os.path.join(
|
|
202
273
|
device_backups_path, f'{now.year}-{now.month}'
|
|
203
274
|
)
|
|
204
275
|
if not os.path.exists(month_folder):
|
|
205
276
|
os.makedirs(month_folder)
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
level, date = filename.split('-')
|
|
216
|
-
level = int(level)
|
|
217
|
-
if level == 0:
|
|
218
|
-
level_0_exists = True
|
|
219
|
-
break
|
|
220
|
-
except:
|
|
221
|
-
continue
|
|
222
|
-
if not level_0_exists:
|
|
223
|
-
print("Level 0 does not exist! Backups must be started from 0!")
|
|
277
|
+
subprocess.run(
|
|
278
|
+
f'borg init --encryption=none {month_folder}', shell=True
|
|
279
|
+
)
|
|
280
|
+
else:
|
|
281
|
+
res = subprocess.run(
|
|
282
|
+
f'borg info --json {month_folder}',
|
|
283
|
+
shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE
|
|
284
|
+
)
|
|
285
|
+
if res.returncode:
|
|
224
286
|
shutil.rmtree(month_folder)
|
|
225
|
-
|
|
226
|
-
|
|
287
|
+
subprocess.run(
|
|
288
|
+
f'borg init --encryption=none {month_folder}', shell=True
|
|
289
|
+
)
|
|
227
290
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
291
|
+
exclude_dirs = (
|
|
292
|
+
'tmp', 'lost+found', 'proc', 'cdrom', 'dev', 'mnt', 'sys', 'run',
|
|
293
|
+
'var/tmp', 'var/cache', 'var/log', 'media',
|
|
294
|
+
)
|
|
295
|
+
backup_command = 'borg create --compression lz4'
|
|
296
|
+
for dir in exclude_dirs:
|
|
297
|
+
backup_command += f' --exclude={dir}'
|
|
233
298
|
|
|
234
|
-
estimated_size = int(subprocess.check_output(
|
|
235
|
-
f'dump -{level} -Squz9 -b 1024 {snap_mapper}',
|
|
236
|
-
shell=True
|
|
237
|
-
)) * 0.5
|
|
238
299
|
|
|
239
|
-
|
|
300
|
+
other_month_folders = []
|
|
240
301
|
for item in os.listdir(device_backups_path):
|
|
241
302
|
if not os.path.isdir(os.path.join(device_backups_path, item)):
|
|
242
303
|
continue
|
|
304
|
+
if os.path.join(device_backups_path, item) == month_folder:
|
|
305
|
+
continue
|
|
243
306
|
try:
|
|
244
307
|
year, month = item.split('-')
|
|
245
|
-
|
|
308
|
+
other_month_folders.append([
|
|
246
309
|
os.path.join(device_backups_path, item),
|
|
247
310
|
int(year) * 12 + int(month)
|
|
248
311
|
])
|
|
249
312
|
except:
|
|
250
313
|
continue
|
|
251
|
-
|
|
314
|
+
other_month_folders.sort(key=lambda v: v[1])
|
|
315
|
+
|
|
316
|
+
if other_month_folders:
|
|
317
|
+
# delete old backups to free up at least 20G of space
|
|
318
|
+
while shutil.disk_usage('/media/backup').free < 20 * 1024 * 1024 * 1024:
|
|
319
|
+
remove_folder = other_month_folders.pop()[0]
|
|
320
|
+
print(f"REMOVE: {remove_folder}")
|
|
321
|
+
shutil.rmtree(remove_folder)
|
|
322
|
+
|
|
323
|
+
backup_command += f' {month_folder}::{get_random_string()} .'
|
|
324
|
+
res = subprocess.run(
|
|
325
|
+
backup_command, shell=True, cwd=snap_mount_point,
|
|
326
|
+
stdout=subprocess.PIPE, stderr=subprocess.PIPE
|
|
327
|
+
)
|
|
252
328
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
shutil.rmtree(remove_folder)
|
|
329
|
+
subprocess.run(["umount", snap_mount_point])
|
|
330
|
+
subprocess.run(
|
|
331
|
+
f"lvremove -f {lv_group}/{snap_name}", shell=True
|
|
332
|
+
)
|
|
258
333
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
shell=True,
|
|
334
|
+
if res.returncode:
|
|
335
|
+
print("Backup error!")
|
|
336
|
+
BackupLog.objects.create(
|
|
337
|
+
level='error', msg="Backup error: \n" + res.stderr.decode()
|
|
264
338
|
)
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
try:
|
|
268
|
-
os.remove(backup_file)
|
|
269
|
-
BackupLog.objects.create(
|
|
270
|
-
level='error', msg="Can't backup. Dump failed."
|
|
271
|
-
)
|
|
272
|
-
print("Dump failed!")
|
|
273
|
-
except:
|
|
274
|
-
pass
|
|
275
|
-
|
|
276
|
-
subprocess.call(
|
|
277
|
-
f"lvremove -f {lv_group}/{lv_name}-snap", shell=True,
|
|
278
|
-
stdout=subprocess.PIPE
|
|
279
|
-
)
|
|
280
|
-
if success:
|
|
281
|
-
print("DONE!")
|
|
339
|
+
else:
|
|
340
|
+
print("Backup done!")
|
|
282
341
|
BackupLog.objects.create(
|
|
283
342
|
level='info', msg="Backup success!"
|
|
284
343
|
)
|
|
@@ -290,7 +349,7 @@ def restore_backup(backup_id):
|
|
|
290
349
|
backup = Backup.objects.get(id=backup_id)
|
|
291
350
|
|
|
292
351
|
try:
|
|
293
|
-
lv_group, lv_name,
|
|
352
|
+
lv_group, lv_name, sd_mountpoint = get_partitions()
|
|
294
353
|
except:
|
|
295
354
|
BackupLog.objects.create(
|
|
296
355
|
level='error',
|
|
@@ -298,51 +357,68 @@ def restore_backup(backup_id):
|
|
|
298
357
|
)
|
|
299
358
|
return
|
|
300
359
|
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
360
|
+
snap_mount_point = '/var/backups/simo-main'
|
|
361
|
+
subprocess.run(f'umount {snap_mount_point}', shell=True)
|
|
362
|
+
|
|
363
|
+
try:
|
|
364
|
+
snap_name = create_snap(lv_group, lv_name)
|
|
365
|
+
except Exception as e:
|
|
366
|
+
print("Error creating temporary snap\n" + str(e))
|
|
304
367
|
BackupLog.objects.create(
|
|
305
368
|
level='error',
|
|
306
|
-
msg="Can't restore.
|
|
369
|
+
msg="Can't restore. \n\n" + str(e)
|
|
307
370
|
)
|
|
308
371
|
return
|
|
309
372
|
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
373
|
+
shutil.rmtree(snap_mount_point, ignore_errors=True)
|
|
374
|
+
os.makedirs(snap_mount_point)
|
|
375
|
+
subprocess.run([
|
|
376
|
+
"mount",
|
|
377
|
+
f"/dev/mapper/{lv_group}-{snap_name.replace('-', '--')}",
|
|
378
|
+
snap_mount_point
|
|
379
|
+
])
|
|
380
|
+
|
|
381
|
+
# delete current contents of a snap
|
|
382
|
+
print("Delete original files and folders")
|
|
383
|
+
for f in os.listdir(snap_mount_point):
|
|
384
|
+
shutil.rmtree(os.path.join(snap_mount_point, f), ignore_errors=True)
|
|
385
|
+
|
|
386
|
+
print("Perform restoration")
|
|
387
|
+
res = subprocess.run(
|
|
388
|
+
f"borg extract {backup.filepath}", shell=True, cwd=snap_mount_point,
|
|
389
|
+
stderr=subprocess.PIPE
|
|
390
|
+
)
|
|
316
391
|
|
|
317
|
-
subprocess.
|
|
318
|
-
|
|
319
|
-
shell=True
|
|
392
|
+
subprocess.run(["umount", snap_mount_point])
|
|
393
|
+
subprocess.run(
|
|
394
|
+
f"lvremove -f {lv_group}/{snap_name}", shell=True
|
|
320
395
|
)
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
f"restore -C -v -b 1024 -f {backup.filepath} -D /var/backup/simo-main",
|
|
324
|
-
shell=True
|
|
325
|
-
)
|
|
326
|
-
subprocess.call(
|
|
327
|
-
f"lvconvert --mergesnapshot {lv_group}/{snap_name}",
|
|
328
|
-
shell=True
|
|
329
|
-
)
|
|
330
|
-
except Exception as e:
|
|
396
|
+
|
|
397
|
+
if res.returncode:
|
|
331
398
|
BackupLog.objects.create(
|
|
332
399
|
level='error',
|
|
333
|
-
msg="Can't restore. \n\n" +
|
|
400
|
+
msg="Can't restore. \n\n" + res.stderr.decode()
|
|
334
401
|
)
|
|
335
|
-
|
|
402
|
+
else:
|
|
403
|
+
print("Restore successful! Merge snapshot and reboot!")
|
|
336
404
|
subprocess.call(
|
|
337
|
-
f"
|
|
338
|
-
|
|
405
|
+
f"lvconvert --mergesnapshot {lv_group}/{snap_name}",
|
|
406
|
+
shell=True
|
|
339
407
|
)
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
408
|
+
subprocess.run('reboot', shell=True)
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
@celery_app.task
|
|
412
|
+
def clean_old_logs():
|
|
413
|
+
from .models import BackupLog
|
|
414
|
+
BackupLog.objects.filter(
|
|
415
|
+
datetime__lt=timezone.now() - timedelta(days=90)
|
|
416
|
+
)
|
|
343
417
|
|
|
344
418
|
|
|
345
419
|
@celery_app.on_after_finalize.connect
|
|
346
420
|
def setup_periodic_tasks(sender, **kwargs):
|
|
347
421
|
sender.add_periodic_task(60 * 60, check_backups.s())
|
|
348
|
-
|
|
422
|
+
# perform auto backup every 12 hours
|
|
423
|
+
sender.add_periodic_task(60 * 60 * 12, perform_backup.s())
|
|
424
|
+
sender.add_periodic_task(60 * 60, clean_old_logs.s())
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
simo/core/admin.py
CHANGED
|
@@ -232,11 +232,11 @@ class ComponentPermissionInline(admin.TabularInline):
|
|
|
232
232
|
return qs.filter(role__instance__in=request.user.instances)
|
|
233
233
|
|
|
234
234
|
|
|
235
|
-
|
|
236
|
-
|
|
235
|
+
def has_delete_permission(self, request, obj=None):
|
|
236
|
+
return False
|
|
237
237
|
|
|
238
|
-
|
|
239
|
-
|
|
238
|
+
def has_add_permission(self, request, obj=None):
|
|
239
|
+
return False
|
|
240
240
|
|
|
241
241
|
|
|
242
242
|
@admin.register(Component)
|