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.

Files changed (82) hide show
  1. simo/backups/__pycache__/admin.cpython-38.pyc +0 -0
  2. simo/backups/__pycache__/models.cpython-38.pyc +0 -0
  3. simo/backups/__pycache__/tasks.cpython-38.pyc +0 -0
  4. simo/backups/admin.py +10 -13
  5. simo/backups/migrations/0003_alter_backuplog_options_alter_backup_size.py +22 -0
  6. simo/backups/migrations/0004_alter_backup_options_alter_backuplog_options_and_more.py +29 -0
  7. simo/backups/migrations/__pycache__/0003_alter_backuplog_options_alter_backup_size.cpython-38.pyc +0 -0
  8. simo/backups/migrations/__pycache__/0004_alter_backup_options_alter_backuplog_options_and_more.cpython-38.pyc +0 -0
  9. simo/backups/models.py +1 -7
  10. simo/backups/tasks.py +221 -145
  11. simo/core/__pycache__/admin.cpython-38.pyc +0 -0
  12. simo/core/__pycache__/api.cpython-38.pyc +0 -0
  13. simo/core/__pycache__/app_widgets.cpython-38.pyc +0 -0
  14. simo/core/__pycache__/controllers.cpython-38.pyc +0 -0
  15. simo/core/__pycache__/events.cpython-38.pyc +0 -0
  16. simo/core/__pycache__/forms.cpython-38.pyc +0 -0
  17. simo/core/__pycache__/middleware.cpython-38.pyc +0 -0
  18. simo/core/__pycache__/models.cpython-38.pyc +0 -0
  19. simo/core/__pycache__/serializers.cpython-38.pyc +0 -0
  20. simo/core/__pycache__/tasks.cpython-38.pyc +0 -0
  21. simo/core/admin.py +4 -4
  22. simo/core/api.py +20 -4
  23. simo/core/app_widgets.py +5 -0
  24. simo/core/controllers.py +2 -2
  25. simo/core/events.py +2 -0
  26. simo/core/forms.py +2 -0
  27. simo/core/management/commands/gateways_manager.py +0 -3
  28. simo/core/middleware.py +7 -1
  29. simo/core/migrations/0042_alter_instance_timezone.py +18 -0
  30. simo/core/migrations/__pycache__/0042_alter_instance_timezone.cpython-38.pyc +0 -0
  31. simo/core/models.py +26 -6
  32. simo/core/serializers.py +17 -17
  33. simo/core/tasks.py +10 -7
  34. simo/fleet/__pycache__/controllers.cpython-38.pyc +0 -0
  35. simo/fleet/__pycache__/forms.cpython-38.pyc +0 -0
  36. simo/fleet/__pycache__/models.cpython-38.pyc +0 -0
  37. simo/fleet/controllers.py +86 -22
  38. simo/fleet/forms.py +224 -185
  39. simo/fleet/migrations/0038_alter_colonel_type.py +18 -0
  40. simo/fleet/migrations/0039_auto_20241016_1047.py +28 -0
  41. simo/fleet/migrations/0040_alter_colonel_pwm_frequency.py +18 -0
  42. simo/fleet/migrations/__pycache__/0038_alter_colonel_type.cpython-38.pyc +0 -0
  43. simo/fleet/migrations/__pycache__/0039_auto_20241016_1047.cpython-38.pyc +0 -0
  44. simo/fleet/migrations/__pycache__/0040_alter_colonel_pwm_frequency.cpython-38.pyc +0 -0
  45. simo/fleet/models.py +2 -2
  46. simo/generic/__pycache__/controllers.cpython-38.pyc +0 -0
  47. simo/generic/__pycache__/forms.cpython-38.pyc +0 -0
  48. simo/generic/__pycache__/gateways.cpython-38.pyc +0 -0
  49. simo/generic/__pycache__/models.cpython-38.pyc +0 -0
  50. simo/generic/controllers.py +41 -2
  51. simo/generic/forms.py +71 -7
  52. simo/generic/models.py +0 -1
  53. simo/generic/scripting/__init__.py +16 -0
  54. simo/generic/scripting/__pycache__/__init__.cpython-38.pyc +0 -0
  55. simo/generic/scripting/__pycache__/serializers.cpython-38.pyc +0 -0
  56. simo/generic/scripting/helpers.py +35 -0
  57. simo/generic/scripting/serializers.py +77 -0
  58. simo/generic/templates/admin/controller_widgets/weather_forecast.html +2 -2
  59. simo/notifications/__pycache__/models.cpython-38.pyc +0 -0
  60. simo/notifications/__pycache__/utils.cpython-38.pyc +0 -0
  61. simo/notifications/utils.py +30 -12
  62. simo/scripting.py +2 -2
  63. simo/users/__pycache__/api.cpython-38.pyc +0 -0
  64. simo/users/__pycache__/managers.cpython-38.pyc +0 -0
  65. simo/users/__pycache__/models.cpython-38.pyc +0 -0
  66. simo/users/__pycache__/serializers.cpython-38.pyc +0 -0
  67. simo/users/__pycache__/utils.cpython-38.pyc +0 -0
  68. simo/users/api.py +36 -7
  69. simo/users/managers.py +5 -1
  70. simo/users/migrations/0033_alter_user_ssh_key.py +18 -0
  71. simo/users/migrations/0034_instanceuser_last_seen_location_and_more.py +24 -0
  72. simo/users/migrations/__pycache__/0033_alter_user_ssh_key.cpython-38.pyc +0 -0
  73. simo/users/migrations/__pycache__/0034_instanceuser_last_seen_location_and_more.cpython-38.pyc +0 -0
  74. simo/users/models.py +37 -32
  75. simo/users/serializers.py +11 -8
  76. simo/users/utils.py +14 -3
  77. {simo-2.4.1.dist-info → simo-2.5.1.dist-info}/METADATA +1 -1
  78. {simo-2.4.1.dist-info → simo-2.5.1.dist-info}/RECORD +82 -59
  79. {simo-2.4.1.dist-info → simo-2.5.1.dist-info}/WHEEL +1 -1
  80. {simo-2.4.1.dist-info → simo-2.5.1.dist-info}/LICENSE.md +0 -0
  81. {simo-2.4.1.dist-info → simo-2.5.1.dist-info}/entry_points.txt +0 -0
  82. {simo-2.4.1.dist-info → simo-2.5.1.dist-info}/top_level.txt +0 -0
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', 'level', 'size_display'
11
- fields = 'datetime', 'device', 'level', 'size_display', 'filepath'
12
- readonly_fields = 'datetime', 'device', 'level', 'size_display', 'filepath'
13
- list_filter = 'datetime', 'mac', 'level'
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 size_display(self, obj):
27
- if obj.size > 1024 * 1024 * 1024 * 1024:
28
- return f'{round(obj.size / (1024 * 1024 * 1024), 2)} Gb'
29
- elif obj.size > 1024 * 1024 * 1024:
30
- return f'{round(obj.size / (1024 * 1024), 2)} Mb'
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/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, timezone
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, BackupLog
14
+ from simo.backups.models import Backup
14
15
 
15
16
  try:
16
- lv_group, lv_name, mountpoint = get_partitions()
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(mountpoint, 'simo_backups')
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
- for filename in os.listdir(os.path.join(hub_dir, month_folder)):
38
- try:
39
- day, time, level, back = filename.split('.')
40
- hour, minute, second = time.split('-')
41
- day, hour, minute, second, level = \
42
- int(day), int(hour), int(minute), int(second), int(level)
43
- except:
44
- continue
45
-
46
- filepath = os.path.join(hub_dir, month_folder, filename)
47
- file_stats = os.stat(filepath)
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=datetime(
50
- year, month, day, hour, minute, second,
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 create_snap(lv_group, lv_name):
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
- return subprocess.check_output(
67
- f'lvcreate -s -n {lv_name}-snap {lv_group}/{lv_name} -L 5G',
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
- return
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
- mountpoint = f"/media/{backup_device['partlabel']}"
216
+ sd_mountpoint = f"/media/{backup_device['partlabel']}"
149
217
  elif lvm_partition.get('label'):
150
- mountpoint = f"/media/{backup_device['label']}"
218
+ sd_mountpoint = f"/media/{backup_device['label']}"
151
219
  else:
152
- mountpoint = f"/media/{backup_device['name']}"
220
+ sd_mountpoint = f"/media/{backup_device['name']}"
153
221
 
154
- if not os.path.exists(mountpoint):
155
- os.makedirs(mountpoint)
222
+ if not os.path.exists(sd_mountpoint):
223
+ os.makedirs(sd_mountpoint)
156
224
 
157
- if backup_device.get('mountpoint') != 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"]} {mountpoint}', shell=True,
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, mountpoint
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, mountpoint = get_partitions()
242
+ lv_group, lv_name, sd_mountpoint = get_partitions()
175
243
  except:
176
244
  return
177
245
 
178
- output = create_snap(lv_group, lv_name)
179
- if not output or f'Logical volume "{lv_name}-snap" created' not in output:
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
- if f'Logical volume "{lv_name}-snap" created' not in output:
190
- print(output)
191
- print(f"Unable to create {lv_name}-snap.")
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'{mountpoint}/simo_backups/hub-{mac}'
196
- if not os.path.exists(device_backups_path):
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
- level = 0
207
-
208
- if level != 0:
209
- # check if level 0 exists
210
- level_0_exists = False
211
- for filename in os.listdir(month_folder):
212
- if '-' not in filename:
213
- continue
214
- try:
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
- os.makedirs(month_folder)
226
- level = 0
287
+ subprocess.run(
288
+ f'borg init --encryption=none {month_folder}', shell=True
289
+ )
227
290
 
228
- time_mark = now.strftime("%H-%M-%S")
229
- backup_file = f"{month_folder}/{now.day}.{time_mark}.{level}.back"
230
- snap_mapper = f"/dev/mapper/{lv_group}-{lv_name.replace('-', '--')}--snap"
231
- label = f"simo {now.strftime('%Y-%m-%d')}"
232
- dumpdates_file = os.path.join(month_folder, 'dumpdates')
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
- folders = []
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
- folders.append([
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
- folders.sort(key=lambda v: v[1])
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
- # delete old backups to free up space required for this backup to take place
254
- while shutil.disk_usage('/media/backup').free < estimated_size:
255
- remove_folder = folders.pop()[0]
256
- print(f"REMOVE: {remove_folder}")
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
- success = False
260
- try:
261
- subprocess.check_call(
262
- f'dump -{level} -quz9 -b 1024 -L "{label}" -D {dumpdates_file} -f {backup_file} {snap_mapper}',
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
- success = True
266
- except:
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, mpt = get_partitions()
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
- snap_name = f'{lv_name}-{get_random_string(5)}'
302
- output = create_snap(lv_group, snap_name)
303
- if not output or f'Logical volume "{lv_name}-snap" created' not in output:
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. Can't create LVM snapshot\n\n" + output
369
+ msg="Can't restore. \n\n" + str(e)
307
370
  )
308
371
  return
309
372
 
310
- if not os.path.exists('/var/backups/simo-main'):
311
- os.makedirs('/var/backups/simo-main')
312
-
313
- subprocess.call('umount /var/backup/simo-main', shell=True)
314
-
315
- subprocess.call('rm -rf /var/backup/simo-main/*', shell=True)
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.call(
318
- f"mount /dev/mapper/{lv_group}-{snap_name.replace('-', '--')} /var/backup/simo-main",
319
- shell=True, stdout=subprocess.PIPE
392
+ subprocess.run(["umount", snap_mount_point])
393
+ subprocess.run(
394
+ f"lvremove -f {lv_group}/{snap_name}", shell=True
320
395
  )
321
- try:
322
- subprocess.call(
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" + str(e)
400
+ msg="Can't restore. \n\n" + res.stderr.decode()
334
401
  )
335
- subprocess.call('umount /var/backup/simo-main', shell=True)
402
+ else:
403
+ print("Restore successful! Merge snapshot and reboot!")
336
404
  subprocess.call(
337
- f"lvremove -f {lv_group}/{snap_name}", shell=True,
338
- stdout=subprocess.PIPE
405
+ f"lvconvert --mergesnapshot {lv_group}/{snap_name}",
406
+ shell=True
339
407
  )
340
- else:
341
- print("All good! REBOOT!")
342
- subprocess.call('reboot')
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
- sender.add_periodic_task(60 * 60 * 8, perform_backup.s()) # perform auto backup every 8 hours
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
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
- # def has_delete_permission(self, request, obj=None):
236
- # return False
235
+ def has_delete_permission(self, request, obj=None):
236
+ return False
237
237
 
238
- # def has_add_permission(self, request, obj=None):
239
- # return False
238
+ def has_add_permission(self, request, obj=None):
239
+ return False
240
240
 
241
241
 
242
242
  @admin.register(Component)