simo 2.10.11__py3-none-any.whl → 2.11.2__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 (377) hide show
  1. simo/__pycache__/__init__.cpython-312.pyc +0 -0
  2. simo/__pycache__/asgi.cpython-312.pyc +0 -0
  3. simo/__pycache__/celeryc.cpython-312.pyc +0 -0
  4. simo/__pycache__/conf.cpython-312.pyc +0 -0
  5. simo/__pycache__/settings.cpython-312.pyc +0 -0
  6. simo/__pycache__/urls.cpython-312.pyc +0 -0
  7. simo/automation/__pycache__/__init__.cpython-312.pyc +0 -0
  8. simo/automation/__pycache__/app_widgets.cpython-312.pyc +0 -0
  9. simo/automation/__pycache__/controllers.cpython-312.pyc +0 -0
  10. simo/automation/__pycache__/forms.cpython-312.pyc +0 -0
  11. simo/automation/__pycache__/gateways.cpython-312.pyc +0 -0
  12. simo/automation/__pycache__/helpers.cpython-312.pyc +0 -0
  13. simo/automation/__pycache__/models.cpython-312.pyc +0 -0
  14. simo/automation/__pycache__/serializers.cpython-312.pyc +0 -0
  15. simo/automation/__pycache__/state.cpython-312.pyc +0 -0
  16. simo/automation/migrations/__pycache__/0001_initial.cpython-312.pyc +0 -0
  17. simo/automation/migrations/__pycache__/0002_update_helpers_in_scripts.cpython-312.pyc +0 -0
  18. simo/automation/migrations/__pycache__/__init__.cpython-312.pyc +0 -0
  19. simo/automation/templates/automations/__pycache__/auto_away.cpython-312.pyc +0 -0
  20. simo/automation/templates/automations/__pycache__/auto_state_script.cpython-312.pyc +0 -0
  21. simo/automation/templates/automations/__pycache__/phones_sleep_script.cpython-312.pyc +0 -0
  22. simo/backups/__pycache__/__init__.cpython-312.pyc +0 -0
  23. simo/backups/__pycache__/admin.cpython-312.pyc +0 -0
  24. simo/backups/__pycache__/dynamic_settings.cpython-312.pyc +0 -0
  25. simo/backups/__pycache__/models.cpython-312.pyc +0 -0
  26. simo/backups/__pycache__/tasks.cpython-312.pyc +0 -0
  27. simo/backups/migrations/__pycache__/0001_initial.cpython-312.pyc +0 -0
  28. simo/backups/migrations/__pycache__/0002_backuplog_backup_level_backup_size.cpython-312.pyc +0 -0
  29. simo/backups/migrations/__pycache__/0003_alter_backuplog_options_alter_backup_size.cpython-312.pyc +0 -0
  30. simo/backups/migrations/__pycache__/0004_alter_backup_options_alter_backuplog_options_and_more.cpython-312.pyc +0 -0
  31. simo/backups/migrations/__pycache__/__init__.cpython-312.pyc +0 -0
  32. simo/backups/rescue.img.xz +0 -0
  33. simo/backups/tasks.py +457 -16
  34. simo/core/__pycache__/__init__.cpython-312.pyc +0 -0
  35. simo/core/__pycache__/admin.cpython-312.pyc +0 -0
  36. simo/core/__pycache__/api.cpython-312.pyc +0 -0
  37. simo/core/__pycache__/api_auth.cpython-312.pyc +0 -0
  38. simo/core/__pycache__/api_meta.cpython-312.pyc +0 -0
  39. simo/core/__pycache__/app_widgets.cpython-312.pyc +0 -0
  40. simo/core/__pycache__/apps.cpython-312.pyc +0 -0
  41. simo/core/__pycache__/auto_urls.cpython-312.pyc +0 -0
  42. simo/core/__pycache__/autocomplete_views.cpython-312.pyc +0 -0
  43. simo/core/__pycache__/base_types.cpython-312.pyc +0 -0
  44. simo/core/__pycache__/context.cpython-312.pyc +0 -0
  45. simo/core/__pycache__/controllers.cpython-312.pyc +0 -0
  46. simo/core/__pycache__/dynamic_settings.cpython-312.pyc +0 -0
  47. simo/core/__pycache__/events.cpython-312.pyc +0 -0
  48. simo/core/__pycache__/filters.cpython-312.pyc +0 -0
  49. simo/core/__pycache__/form_fields.cpython-312.pyc +0 -0
  50. simo/core/__pycache__/forms.cpython-312.pyc +0 -0
  51. simo/core/__pycache__/gateways.cpython-312.pyc +0 -0
  52. simo/core/__pycache__/loggers.cpython-312.pyc +0 -0
  53. simo/core/__pycache__/managers.cpython-312.pyc +0 -0
  54. simo/core/__pycache__/middleware.cpython-312.pyc +0 -0
  55. simo/core/__pycache__/models.cpython-312.pyc +0 -0
  56. simo/core/__pycache__/permissions.cpython-312.pyc +0 -0
  57. simo/core/__pycache__/routing.cpython-312.pyc +0 -0
  58. simo/core/__pycache__/serializers.cpython-312.pyc +0 -0
  59. simo/core/__pycache__/signal_receivers.cpython-312.pyc +0 -0
  60. simo/core/__pycache__/socket_consumers.cpython-312.pyc +0 -0
  61. simo/core/__pycache__/storage.cpython-312.pyc +0 -0
  62. simo/core/__pycache__/tasks.cpython-312.pyc +0 -0
  63. simo/core/__pycache__/todos.cpython-312.pyc +0 -0
  64. simo/core/__pycache__/types.cpython-312.pyc +0 -0
  65. simo/core/__pycache__/views.cpython-312.pyc +0 -0
  66. simo/core/__pycache__/widgets.cpython-312.pyc +0 -0
  67. simo/core/controllers.py +6 -3
  68. simo/core/db_backend/__pycache__/__init__.cpython-312.pyc +0 -0
  69. simo/core/db_backend/__pycache__/base.cpython-312.pyc +0 -0
  70. simo/core/drf_braces/__pycache__/__init__.cpython-312.pyc +0 -0
  71. simo/core/drf_braces/__pycache__/mixins.cpython-312.pyc +0 -0
  72. simo/core/drf_braces/__pycache__/models.cpython-312.pyc +0 -0
  73. simo/core/drf_braces/__pycache__/parsers.cpython-312.pyc +0 -0
  74. simo/core/drf_braces/__pycache__/renderers.cpython-312.pyc +0 -0
  75. simo/core/drf_braces/__pycache__/utils.cpython-312.pyc +0 -0
  76. simo/core/drf_braces/fields/__pycache__/__init__.cpython-312.pyc +0 -0
  77. simo/core/drf_braces/fields/__pycache__/_fields.cpython-312.pyc +0 -0
  78. simo/core/drf_braces/fields/__pycache__/custom.cpython-312.pyc +0 -0
  79. simo/core/drf_braces/fields/__pycache__/mixins.cpython-312.pyc +0 -0
  80. simo/core/drf_braces/fields/__pycache__/modified.cpython-312.pyc +0 -0
  81. simo/core/drf_braces/forms/__pycache__/__init__.cpython-312.pyc +0 -0
  82. simo/core/drf_braces/forms/__pycache__/fields.cpython-312.pyc +0 -0
  83. simo/core/drf_braces/forms/__pycache__/serializer_form.cpython-312.pyc +0 -0
  84. simo/core/drf_braces/serializers/__pycache__/__init__.cpython-312.pyc +0 -0
  85. simo/core/drf_braces/serializers/__pycache__/enforce_validation_serializer.cpython-312.pyc +0 -0
  86. simo/core/drf_braces/serializers/__pycache__/form_serializer.cpython-312.pyc +0 -0
  87. simo/core/drf_braces/serializers/__pycache__/swapping.cpython-312.pyc +0 -0
  88. simo/core/drf_braces/tests/__pycache__/__init__.cpython-312.pyc +0 -0
  89. simo/core/drf_braces/tests/__pycache__/test_mixins.cpython-312.pyc +0 -0
  90. simo/core/drf_braces/tests/__pycache__/test_parsers.cpython-312.pyc +0 -0
  91. simo/core/drf_braces/tests/__pycache__/test_renderers.cpython-312.pyc +0 -0
  92. simo/core/drf_braces/tests/__pycache__/test_utils.cpython-312.pyc +0 -0
  93. simo/core/drf_braces/tests/fields/__pycache__/__init__.cpython-312.pyc +0 -0
  94. simo/core/drf_braces/tests/fields/__pycache__/test_custom.cpython-312.pyc +0 -0
  95. simo/core/drf_braces/tests/fields/__pycache__/test_fields.cpython-312.pyc +0 -0
  96. simo/core/drf_braces/tests/fields/__pycache__/test_mixins.cpython-312.pyc +0 -0
  97. simo/core/drf_braces/tests/fields/__pycache__/test_modified.cpython-312.pyc +0 -0
  98. simo/core/drf_braces/tests/forms/__pycache__/__init__.cpython-312.pyc +0 -0
  99. simo/core/drf_braces/tests/forms/__pycache__/test_fields.cpython-312.pyc +0 -0
  100. simo/core/drf_braces/tests/forms/__pycache__/test_serializer_form.cpython-312.pyc +0 -0
  101. simo/core/drf_braces/tests/serializers/__pycache__/__init__.cpython-312.pyc +0 -0
  102. simo/core/drf_braces/tests/serializers/__pycache__/test_enforce_validation_serializer.cpython-312.pyc +0 -0
  103. simo/core/drf_braces/tests/serializers/__pycache__/test_form_serializer.cpython-312.pyc +0 -0
  104. simo/core/drf_braces/tests/serializers/__pycache__/test_swapping.cpython-312.pyc +0 -0
  105. simo/core/management/__pycache__/__init__.cpython-312.pyc +0 -0
  106. simo/core/management/__pycache__/update.cpython-312.pyc +0 -0
  107. simo/core/management/_hub_template/hub/__pycache__/asgi.cpython-312.pyc +0 -0
  108. simo/core/management/_hub_template/hub/__pycache__/celeryc.cpython-312.pyc +0 -0
  109. simo/core/management/_hub_template/hub/__pycache__/manage.cpython-312.pyc +0 -0
  110. simo/core/management/_hub_template/hub/__pycache__/settings.cpython-312.pyc +0 -0
  111. simo/core/management/_hub_template/hub/__pycache__/urls.cpython-312.pyc +0 -0
  112. simo/core/management/_hub_template/hub/__pycache__/wsgi.cpython-312.pyc +0 -0
  113. simo/core/management/commands/__pycache__/__init__.cpython-312.pyc +0 -0
  114. simo/core/management/commands/__pycache__/gateways_manager.cpython-312.pyc +0 -0
  115. simo/core/management/commands/__pycache__/on_http_start.cpython-312.pyc +0 -0
  116. simo/core/management/commands/__pycache__/run_gateway.cpython-312.pyc +0 -0
  117. simo/core/migrations/__pycache__/0001_initial.cpython-312.pyc +0 -0
  118. simo/core/migrations/__pycache__/0002_load_icons.cpython-312.pyc +0 -0
  119. simo/core/migrations/__pycache__/0003_create_default_zones_and_categories.cpython-312.pyc +0 -0
  120. simo/core/migrations/__pycache__/0004_create_generic.cpython-312.pyc +0 -0
  121. simo/core/migrations/__pycache__/0005_component_subcomponents.cpython-312.pyc +0 -0
  122. simo/core/migrations/__pycache__/0006_alter_component_subcomponents.cpython-312.pyc +0 -0
  123. simo/core/migrations/__pycache__/0007_component_change_init_to.cpython-312.pyc +0 -0
  124. simo/core/migrations/__pycache__/0008_alter_component_change_init_to.cpython-312.pyc +0 -0
  125. simo/core/migrations/__pycache__/0009_auto_20220707_1404.cpython-312.pyc +0 -0
  126. simo/core/migrations/__pycache__/0010_historyaggregate.cpython-312.pyc +0 -0
  127. simo/core/migrations/__pycache__/0011_component_last_change.cpython-312.pyc +0 -0
  128. simo/core/migrations/__pycache__/0012_instance.cpython-312.pyc +0 -0
  129. simo/core/migrations/__pycache__/0013_auto_20231003_0754.cpython-312.pyc +0 -0
  130. simo/core/migrations/__pycache__/0014_zone_instance.cpython-312.pyc +0 -0
  131. simo/core/migrations/__pycache__/0015_auto_20231004_1113.cpython-312.pyc +0 -0
  132. simo/core/migrations/__pycache__/0016_auto_20231004_1113.cpython-312.pyc +0 -0
  133. simo/core/migrations/__pycache__/0017_auto_20231004_1313.cpython-312.pyc +0 -0
  134. simo/core/migrations/__pycache__/0018_auto_20231005_0622.cpython-312.pyc +0 -0
  135. simo/core/migrations/__pycache__/0019_alter_gateway_type.cpython-312.pyc +0 -0
  136. simo/core/migrations/__pycache__/0020_component_meta.cpython-312.pyc +0 -0
  137. simo/core/migrations/__pycache__/0021_auto_20231020_1041.cpython-312.pyc +0 -0
  138. simo/core/migrations/__pycache__/0022_auto_20231221_0735.cpython-312.pyc +0 -0
  139. simo/core/migrations/__pycache__/0023_auto_20231229_1352.cpython-312.pyc +0 -0
  140. simo/core/migrations/__pycache__/0024_alter_instance_device_report_history_days.cpython-312.pyc +0 -0
  141. simo/core/migrations/__pycache__/0025_auto_20240122_1321.cpython-312.pyc +0 -0
  142. simo/core/migrations/__pycache__/0026_category_instance.cpython-312.pyc +0 -0
  143. simo/core/migrations/__pycache__/0027_remove_component_tags.cpython-312.pyc +0 -0
  144. simo/core/migrations/__pycache__/0028_rename_subcomponents_component_slaves.cpython-312.pyc +0 -0
  145. simo/core/migrations/__pycache__/0029_auto_20240229_1331.cpython-312.pyc +0 -0
  146. simo/core/migrations/__pycache__/0030_alter_instance_timezone.cpython-312.pyc +0 -0
  147. simo/core/migrations/__pycache__/0031_auto_20240429_1231.cpython-312.pyc +0 -0
  148. simo/core/migrations/__pycache__/0032_auto_20240506_0834.cpython-312.pyc +0 -0
  149. simo/core/migrations/__pycache__/0033_auto_20240509_0821.cpython-312.pyc +0 -0
  150. simo/core/migrations/__pycache__/0034_component_error_msg.cpython-312.pyc +0 -0
  151. simo/core/migrations/__pycache__/0035_remove_instance_share_location.cpython-312.pyc +0 -0
  152. simo/core/migrations/__pycache__/0036_auto_20240521_0823.cpython-312.pyc +0 -0
  153. simo/core/migrations/__pycache__/0037_auto_20240606_1057.cpython-312.pyc +0 -0
  154. simo/core/migrations/__pycache__/0038_remove_instance_cover_image_and_more.cpython-312.pyc +0 -0
  155. simo/core/migrations/__pycache__/0039_instance_is_active_alter_instance_timezone.cpython-312.pyc +0 -0
  156. simo/core/migrations/__pycache__/0040_alter_instance_name.cpython-312.pyc +0 -0
  157. simo/core/migrations/__pycache__/0041_alter_instance_slug.cpython-312.pyc +0 -0
  158. simo/core/migrations/__pycache__/0042_alter_instance_timezone.cpython-312.pyc +0 -0
  159. simo/core/migrations/__pycache__/0043_alter_category_instance_alter_instance_timezone_and_more.cpython-312.pyc +0 -0
  160. simo/core/migrations/__pycache__/0044_alter_gateway_type.cpython-312.pyc +0 -0
  161. simo/core/migrations/__pycache__/0045_alter_instance_device_report_history_days_and_more.cpython-312.pyc +0 -0
  162. simo/core/migrations/__pycache__/0046_component_value_translation_alter_gateway_type.cpython-312.pyc +0 -0
  163. simo/core/migrations/__pycache__/0047_alter_component_value_translation.cpython-312.pyc +0 -0
  164. simo/core/migrations/__pycache__/0048_publicfile_privatefile.cpython-312.pyc +0 -0
  165. simo/core/migrations/__pycache__/0049_alter_gateway_type.cpython-312.pyc +0 -0
  166. simo/core/migrations/__pycache__/0050_componenthistory_alive.cpython-312.pyc +0 -0
  167. simo/core/migrations/__pycache__/__init__.cpython-312.pyc +0 -0
  168. simo/core/templates/core/__pycache__/value_translation.cpython-312.pyc +0 -0
  169. simo/core/templatetags/__pycache__/__init__.cpython-312.pyc +0 -0
  170. simo/core/templatetags/__pycache__/components_list.cpython-312.pyc +0 -0
  171. simo/core/utils/__pycache__/__init__.cpython-312.pyc +0 -0
  172. simo/core/utils/__pycache__/admin.cpython-312.pyc +0 -0
  173. simo/core/utils/__pycache__/api.cpython-312.pyc +0 -0
  174. simo/core/utils/__pycache__/cache.cpython-312.pyc +0 -0
  175. simo/core/utils/__pycache__/config_values.cpython-312.pyc +0 -0
  176. simo/core/utils/__pycache__/converters.cpython-312.pyc +0 -0
  177. simo/core/utils/__pycache__/easing.cpython-312.pyc +0 -0
  178. simo/core/utils/__pycache__/form_fields.cpython-312.pyc +0 -0
  179. simo/core/utils/__pycache__/form_widgets.cpython-312.pyc +0 -0
  180. simo/core/utils/__pycache__/formsets.cpython-312.pyc +0 -0
  181. simo/core/utils/__pycache__/helpers.cpython-312.pyc +0 -0
  182. simo/core/utils/__pycache__/json.cpython-312.pyc +0 -0
  183. simo/core/utils/__pycache__/logs.cpython-312.pyc +0 -0
  184. simo/core/utils/__pycache__/mixins.cpython-312.pyc +0 -0
  185. simo/core/utils/__pycache__/model_helpers.cpython-312.pyc +0 -0
  186. simo/core/utils/__pycache__/operations.cpython-312.pyc +0 -0
  187. simo/core/utils/__pycache__/relay.cpython-312.pyc +0 -0
  188. simo/core/utils/__pycache__/serialization.cpython-312.pyc +0 -0
  189. simo/core/utils/__pycache__/type_constants.cpython-312.pyc +0 -0
  190. simo/core/utils/__pycache__/validators.cpython-312.pyc +0 -0
  191. simo/fleet/__pycache__/__init__.cpython-312.pyc +0 -0
  192. simo/fleet/__pycache__/admin.cpython-312.pyc +0 -0
  193. simo/fleet/__pycache__/api.cpython-312.pyc +0 -0
  194. simo/fleet/__pycache__/apps.cpython-312.pyc +0 -0
  195. simo/fleet/__pycache__/auto_urls.cpython-312.pyc +0 -0
  196. simo/fleet/__pycache__/base_types.cpython-312.pyc +0 -0
  197. simo/fleet/__pycache__/ble.cpython-312.pyc +0 -0
  198. simo/fleet/__pycache__/controllers.cpython-312.pyc +0 -0
  199. simo/fleet/__pycache__/custom_dali_operations.cpython-312.pyc +0 -0
  200. simo/fleet/__pycache__/forms.cpython-312.pyc +0 -0
  201. simo/fleet/__pycache__/gateways.cpython-312.pyc +0 -0
  202. simo/fleet/__pycache__/managers.cpython-312.pyc +0 -0
  203. simo/fleet/__pycache__/models.cpython-312.pyc +0 -0
  204. simo/fleet/__pycache__/routing.cpython-312.pyc +0 -0
  205. simo/fleet/__pycache__/serializers.cpython-312.pyc +0 -0
  206. simo/fleet/__pycache__/socket_consumers.cpython-312.pyc +0 -0
  207. simo/fleet/__pycache__/tasks.cpython-312.pyc +0 -0
  208. simo/fleet/__pycache__/utils.cpython-312.pyc +0 -0
  209. simo/fleet/__pycache__/views.cpython-312.pyc +0 -0
  210. simo/fleet/controllers.py +65 -24
  211. simo/fleet/custom_dali_operations.py +14 -2
  212. simo/fleet/forms.py +2 -1
  213. simo/fleet/migrations/__pycache__/0001_initial.cpython-312.pyc +0 -0
  214. simo/fleet/migrations/__pycache__/0002_auto_20220422_0743.cpython-312.pyc +0 -0
  215. simo/fleet/migrations/__pycache__/0003_auto_20220422_0752.cpython-312.pyc +0 -0
  216. simo/fleet/migrations/__pycache__/0004_auto_20220422_0818.cpython-312.pyc +0 -0
  217. simo/fleet/migrations/__pycache__/0005_auto_20220428_0900.cpython-312.pyc +0 -0
  218. simo/fleet/migrations/__pycache__/0006_rename_mac_colonel_uid.cpython-312.pyc +0 -0
  219. simo/fleet/migrations/__pycache__/0007_colonel_socket_connected.cpython-312.pyc +0 -0
  220. simo/fleet/migrations/__pycache__/0008_i2cinterface.cpython-312.pyc +0 -0
  221. simo/fleet/migrations/__pycache__/0009_i2cinterface_name.cpython-312.pyc +0 -0
  222. simo/fleet/migrations/__pycache__/0010_auto_20220602_0746.cpython-312.pyc +0 -0
  223. simo/fleet/migrations/__pycache__/0011_i2cinterface_freq.cpython-312.pyc +0 -0
  224. simo/fleet/migrations/__pycache__/0012_colonel_logs_stream.cpython-312.pyc +0 -0
  225. simo/fleet/migrations/__pycache__/0013_alter_colonel_last_seen.cpython-312.pyc +0 -0
  226. simo/fleet/migrations/__pycache__/0014_auto_20220614_0659.cpython-312.pyc +0 -0
  227. simo/fleet/migrations/__pycache__/0015_auto_20220614_0754.cpython-312.pyc +0 -0
  228. simo/fleet/migrations/__pycache__/0016_auto_20220704_0840.cpython-312.pyc +0 -0
  229. simo/fleet/migrations/__pycache__/0017_alter_colonel_secret.cpython-312.pyc +0 -0
  230. simo/fleet/migrations/__pycache__/0018_colonel_instance.cpython-312.pyc +0 -0
  231. simo/fleet/migrations/__pycache__/0019_auto_20231006_0749.cpython-312.pyc +0 -0
  232. simo/fleet/migrations/__pycache__/0020_instanceoptions.cpython-312.pyc +0 -0
  233. simo/fleet/migrations/__pycache__/0021_auto_20231006_0819.cpython-312.pyc +0 -0
  234. simo/fleet/migrations/__pycache__/0022_remove_colonel_secret.cpython-312.pyc +0 -0
  235. simo/fleet/migrations/__pycache__/0023_colonel_is_authorized.cpython-312.pyc +0 -0
  236. simo/fleet/migrations/__pycache__/0024_colonel_pwm_frequency.cpython-312.pyc +0 -0
  237. simo/fleet/migrations/__pycache__/0025_auto_20240130_1334.cpython-312.pyc +0 -0
  238. simo/fleet/migrations/__pycache__/0026_rename_i2cinterface_scl_pin_and_more.cpython-312.pyc +0 -0
  239. simo/fleet/migrations/__pycache__/0027_auto_20240306_0802.cpython-312.pyc +0 -0
  240. simo/fleet/migrations/__pycache__/0028_remove_i2cinterface_scl_pin_no_and_more.cpython-312.pyc +0 -0
  241. simo/fleet/migrations/__pycache__/0029_alter_i2cinterface_scl_pin_and_more.cpython-312.pyc +0 -0
  242. simo/fleet/migrations/__pycache__/0030_colonelpin_label_alter_colonel_type.cpython-312.pyc +0 -0
  243. simo/fleet/migrations/__pycache__/0031_alter_colonel_type.cpython-312.pyc +0 -0
  244. simo/fleet/migrations/__pycache__/0032_auto_20240415_0736.cpython-312.pyc +0 -0
  245. simo/fleet/migrations/__pycache__/0033_auto_20240415_0736.cpython-312.pyc +0 -0
  246. simo/fleet/migrations/__pycache__/0034_auto_20240418_0735.cpython-312.pyc +0 -0
  247. simo/fleet/migrations/__pycache__/0035_auto_20240514_0855.cpython-312.pyc +0 -0
  248. simo/fleet/migrations/__pycache__/0036_auto_20240605_0702.cpython-312.pyc +0 -0
  249. simo/fleet/migrations/__pycache__/0037_alter_colonelpin_options_alter_colonelpin_no_and_more.cpython-312.pyc +0 -0
  250. simo/fleet/migrations/__pycache__/0038_alter_colonel_type.cpython-312.pyc +0 -0
  251. simo/fleet/migrations/__pycache__/0039_auto_20241016_1047.cpython-312.pyc +0 -0
  252. simo/fleet/migrations/__pycache__/0040_alter_colonel_pwm_frequency.cpython-312.pyc +0 -0
  253. simo/fleet/migrations/__pycache__/0041_alter_colonel_instance_and_more.cpython-312.pyc +0 -0
  254. simo/fleet/migrations/__pycache__/0042_auto_20241120_1028.cpython-312.pyc +0 -0
  255. simo/fleet/migrations/__pycache__/0043_auto_20241203_0930.cpython-312.pyc +0 -0
  256. simo/fleet/migrations/__pycache__/0044_auto_20241210_0707.cpython-312.pyc +0 -0
  257. simo/fleet/migrations/__pycache__/0045_alter_colonel_type_customdalidevice.cpython-312.pyc +0 -0
  258. simo/fleet/migrations/__pycache__/0046_delete_customdalidevice.cpython-312.pyc +0 -0
  259. simo/fleet/migrations/__pycache__/0047_customdalidevice.cpython-312.pyc +0 -0
  260. simo/fleet/migrations/__pycache__/0048_remove_customdalidevice_colonel_and_more.cpython-312.pyc +0 -0
  261. simo/fleet/migrations/__pycache__/0049_alter_customdalidevice_interface.cpython-312.pyc +0 -0
  262. simo/fleet/migrations/__pycache__/0050_customdalidevice_uid.cpython-312.pyc +0 -0
  263. simo/fleet/migrations/__pycache__/0051_customdalidevice_components.cpython-312.pyc +0 -0
  264. simo/fleet/migrations/__pycache__/0052_colonelpin_interface.cpython-312.pyc +0 -0
  265. simo/fleet/migrations/__pycache__/0053_auto_20250507_0713.cpython-312.pyc +0 -0
  266. simo/fleet/migrations/__pycache__/0054_auto_20250507_1256.cpython-312.pyc +0 -0
  267. simo/fleet/migrations/__pycache__/__init__.cpython-312.pyc +0 -0
  268. simo/generic/__pycache__/__init__.cpython-312.pyc +0 -0
  269. simo/generic/__pycache__/app_widgets.cpython-312.pyc +0 -0
  270. simo/generic/__pycache__/base_types.cpython-312.pyc +0 -0
  271. simo/generic/__pycache__/controllers.cpython-312.pyc +0 -0
  272. simo/generic/__pycache__/forms.cpython-312.pyc +0 -0
  273. simo/generic/__pycache__/gateways.cpython-312.pyc +0 -0
  274. simo/generic/__pycache__/models.cpython-312.pyc +0 -0
  275. simo/generic/__pycache__/routing.cpython-312.pyc +0 -0
  276. simo/generic/__pycache__/socket_consumers.cpython-312.pyc +0 -0
  277. simo/generic/__pycache__/tasks.cpython-312.pyc +0 -0
  278. simo/generic/migrations/__pycache__/0001_initial.cpython-312.pyc +0 -0
  279. simo/generic/migrations/__pycache__/0002_auto_20241126_0726.cpython-312.pyc +0 -0
  280. simo/generic/migrations/__pycache__/0003_auto_20250409_1404.cpython-312.pyc +0 -0
  281. simo/generic/migrations/__pycache__/__init__.cpython-312.pyc +0 -0
  282. simo/multimedia/__pycache__/__init__.cpython-312.pyc +0 -0
  283. simo/multimedia/__pycache__/admin.cpython-312.pyc +0 -0
  284. simo/multimedia/__pycache__/api.cpython-312.pyc +0 -0
  285. simo/multimedia/__pycache__/app_widgets.cpython-312.pyc +0 -0
  286. simo/multimedia/__pycache__/auto_urls.cpython-312.pyc +0 -0
  287. simo/multimedia/__pycache__/base_types.cpython-312.pyc +0 -0
  288. simo/multimedia/__pycache__/controllers.cpython-312.pyc +0 -0
  289. simo/multimedia/__pycache__/forms.cpython-312.pyc +0 -0
  290. simo/multimedia/__pycache__/models.cpython-312.pyc +0 -0
  291. simo/multimedia/__pycache__/serializers.cpython-312.pyc +0 -0
  292. simo/multimedia/__pycache__/views.cpython-312.pyc +0 -0
  293. simo/multimedia/migrations/__pycache__/0001_initial.cpython-312.pyc +0 -0
  294. simo/multimedia/migrations/__pycache__/0002_sound_length.cpython-312.pyc +0 -0
  295. simo/multimedia/migrations/__pycache__/0003_alter_sound_length.cpython-312.pyc +0 -0
  296. simo/multimedia/migrations/__pycache__/0004_auto_20231023_1055.cpython-312.pyc +0 -0
  297. simo/multimedia/migrations/__pycache__/0005_remove_sound_slug_sound_date_uploaded.cpython-312.pyc +0 -0
  298. simo/multimedia/migrations/__pycache__/0006_remove_sound_length_sound_duration.cpython-312.pyc +0 -0
  299. simo/multimedia/migrations/__pycache__/__init__.cpython-312.pyc +0 -0
  300. simo/notifications/__pycache__/__init__.cpython-312.pyc +0 -0
  301. simo/notifications/__pycache__/admin.cpython-312.pyc +0 -0
  302. simo/notifications/__pycache__/api.cpython-312.pyc +0 -0
  303. simo/notifications/__pycache__/models.cpython-312.pyc +0 -0
  304. simo/notifications/__pycache__/serializers.cpython-312.pyc +0 -0
  305. simo/notifications/__pycache__/utils.cpython-312.pyc +0 -0
  306. simo/notifications/migrations/__pycache__/0001_initial.cpython-312.pyc +0 -0
  307. simo/notifications/migrations/__pycache__/0002_notification_instance.cpython-312.pyc +0 -0
  308. simo/notifications/migrations/__pycache__/0003_alter_notification_instance.cpython-312.pyc +0 -0
  309. simo/notifications/migrations/__pycache__/__init__.cpython-312.pyc +0 -0
  310. simo/users/__pycache__/__init__.cpython-312.pyc +0 -0
  311. simo/users/__pycache__/admin.cpython-312.pyc +0 -0
  312. simo/users/__pycache__/api.cpython-312.pyc +0 -0
  313. simo/users/__pycache__/apps.cpython-312.pyc +0 -0
  314. simo/users/__pycache__/auth_backends.cpython-312.pyc +0 -0
  315. simo/users/__pycache__/auto_urls.cpython-312.pyc +0 -0
  316. simo/users/__pycache__/dynamic_settings.cpython-312.pyc +0 -0
  317. simo/users/__pycache__/managers.cpython-312.pyc +0 -0
  318. simo/users/__pycache__/middleware.cpython-312.pyc +0 -0
  319. simo/users/__pycache__/models.cpython-312.pyc +0 -0
  320. simo/users/__pycache__/permissions.cpython-312.pyc +0 -0
  321. simo/users/__pycache__/serializers.cpython-312.pyc +0 -0
  322. simo/users/__pycache__/sso_urls.cpython-312.pyc +0 -0
  323. simo/users/__pycache__/sso_views.cpython-312.pyc +0 -0
  324. simo/users/__pycache__/tasks.cpython-312.pyc +0 -0
  325. simo/users/__pycache__/utils.cpython-312.pyc +0 -0
  326. simo/users/__pycache__/views.cpython-312.pyc +0 -0
  327. simo/users/migrations/__pycache__/0001_initial.cpython-312.pyc +0 -0
  328. simo/users/migrations/__pycache__/0002_componentpermission.cpython-312.pyc +0 -0
  329. simo/users/migrations/__pycache__/0003_create_roles_and_system_user.cpython-312.pyc +0 -0
  330. simo/users/migrations/__pycache__/0004_user_secret_key.cpython-312.pyc +0 -0
  331. simo/users/migrations/__pycache__/0005_permissionsrole_instance.cpython-312.pyc +0 -0
  332. simo/users/migrations/__pycache__/0006_auto_20231003_0850.cpython-312.pyc +0 -0
  333. simo/users/migrations/__pycache__/0007_auto_20231003_1228.cpython-312.pyc +0 -0
  334. simo/users/migrations/__pycache__/0008_auto_20231003_1229.cpython-312.pyc +0 -0
  335. simo/users/migrations/__pycache__/0009_remove_user_role.cpython-312.pyc +0 -0
  336. simo/users/migrations/__pycache__/0010_auto_20231004_1313.cpython-312.pyc +0 -0
  337. simo/users/migrations/__pycache__/0011_auto_20231004_1313.cpython-312.pyc +0 -0
  338. simo/users/migrations/__pycache__/0012_alter_userinstancerole_unique_together.cpython-312.pyc +0 -0
  339. simo/users/migrations/__pycache__/0013_remove_user_roles.cpython-312.pyc +0 -0
  340. simo/users/migrations/__pycache__/0014_user_roles.cpython-312.pyc +0 -0
  341. simo/users/migrations/__pycache__/0015_remove_user_at_home.cpython-312.pyc +0 -0
  342. simo/users/migrations/__pycache__/0016_auto_20231005_1050.cpython-312.pyc +0 -0
  343. simo/users/migrations/__pycache__/0017_auto_20231221_0735.cpython-312.pyc +0 -0
  344. simo/users/migrations/__pycache__/0018_user_is_god.cpython-312.pyc +0 -0
  345. simo/users/migrations/__pycache__/0019_auto_20231221_1155.cpython-312.pyc +0 -0
  346. simo/users/migrations/__pycache__/0020_rename_is_god_user_is_master.cpython-312.pyc +0 -0
  347. simo/users/migrations/__pycache__/0021_alter_permissionsrole_instance.cpython-312.pyc +0 -0
  348. simo/users/migrations/__pycache__/0022_userdevicereportlog_instance.cpython-312.pyc +0 -0
  349. simo/users/migrations/__pycache__/0023_auto_20240105_0719.cpython-312.pyc +0 -0
  350. simo/users/migrations/__pycache__/0024_fingerprint.cpython-312.pyc +0 -0
  351. simo/users/migrations/__pycache__/0025_rename_name_fingerprint_type_and_more.cpython-312.pyc +0 -0
  352. simo/users/migrations/__pycache__/0026_fingerprint_name.cpython-312.pyc +0 -0
  353. simo/users/migrations/__pycache__/0027_permissionsrole_can_manage_components.cpython-312.pyc +0 -0
  354. simo/users/migrations/__pycache__/0028_auto_20240506_1146.cpython-312.pyc +0 -0
  355. simo/users/migrations/__pycache__/0029_alter_instanceuser_instance.cpython-312.pyc +0 -0
  356. simo/users/migrations/__pycache__/0030_userdevice_users.cpython-312.pyc +0 -0
  357. simo/users/migrations/__pycache__/0031_auto_20240923_1115.cpython-312.pyc +0 -0
  358. simo/users/migrations/__pycache__/0032_remove_userdevice_user_alter_userdevice_users.cpython-312.pyc +0 -0
  359. simo/users/migrations/__pycache__/0033_alter_user_ssh_key.cpython-312.pyc +0 -0
  360. simo/users/migrations/__pycache__/0034_instanceuser_last_seen_location_and_more.cpython-312.pyc +0 -0
  361. simo/users/migrations/__pycache__/0035_instanceuser_last_seen_speed_kmh_and_more.cpython-312.pyc +0 -0
  362. simo/users/migrations/__pycache__/0036_instanceuser_phone_on_charge_user_phone_on_charge.cpython-312.pyc +0 -0
  363. simo/users/migrations/__pycache__/0037_rename_last_seen_location_datetime_instanceuser_last_seen_and_more.cpython-312.pyc +0 -0
  364. simo/users/migrations/__pycache__/0038_userdevicereportlog_at_home_and_more.cpython-312.pyc +0 -0
  365. simo/users/migrations/__pycache__/0039_auto_20241117_1039.cpython-312.pyc +0 -0
  366. simo/users/migrations/__pycache__/0040_userdevicereportlog_location_smoothed_and_more.cpython-312.pyc +0 -0
  367. simo/users/migrations/__pycache__/0041_userdevicereportlog_speed_kmh_received.cpython-312.pyc +0 -0
  368. simo/users/migrations/__pycache__/0042_remove_userdevicereportlog_location_smoothed_and_more.cpython-312.pyc +0 -0
  369. simo/users/migrations/__pycache__/0043_userdevicereportlog_avg_speed_kmh.cpython-312.pyc +0 -0
  370. simo/users/migrations/__pycache__/0044_permissionsrole_is_person.cpython-312.pyc +0 -0
  371. simo/users/migrations/__pycache__/__init__.cpython-312.pyc +0 -0
  372. {simo-2.10.11.dist-info → simo-2.11.2.dist-info}/METADATA +1 -1
  373. {simo-2.10.11.dist-info → simo-2.11.2.dist-info}/RECORD +377 -376
  374. {simo-2.10.11.dist-info → simo-2.11.2.dist-info}/WHEEL +0 -0
  375. {simo-2.10.11.dist-info → simo-2.11.2.dist-info}/entry_points.txt +0 -0
  376. {simo-2.10.11.dist-info → simo-2.11.2.dist-info}/licenses/LICENSE.md +0 -0
  377. {simo-2.10.11.dist-info → simo-2.11.2.dist-info}/top_level.txt +0 -0
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
simo/backups/tasks.py CHANGED
@@ -140,26 +140,405 @@ def create_snap(lv_group, lv_name, snap_name=None, size=None, try_no=1):
140
140
 
141
141
 
142
142
  def get_lvm_partition(lsblk_data):
143
+ """Return the *lsblk* entry describing the logical volume mounted as "/".
144
+
145
+ The original implementation returned prematurely when the first top-level
146
+ device contained any children – even if none of them matched the search
147
+ criteria. As a result the search stopped after inspecting just a single
148
+ branch of the device tree which broke setups where the root logical
149
+ volume was not located under the very first block device listed by
150
+ *lsblk* (e.g. when the machine had multiple drives).
151
+
152
+ The fixed version walks the whole tree depth-first and stops only after a
153
+ matching entry is found or the entire structure has been inspected.
154
+ """
155
+
143
156
  for device in lsblk_data:
144
- if device['type'] == 'lvm' and device['mountpoint'] == '/':
157
+ # Check the current node first.
158
+ if device.get('type') == 'lvm' and device.get('mountpoint') == '/':
145
159
  return device
146
- if 'children' in device:
147
- return get_lvm_partition(device['children'])
160
+
161
+ # Recursively search children (if any). The recursive call returns
162
+ # either the desired dictionary or *None* – propagate the first truthy
163
+ # value up the call stack so that the outermost caller gets the
164
+ # matching entry.
165
+ child_match = get_lvm_partition(device.get('children', [])) if device.get('children') else None
166
+ if child_match:
167
+ return child_match
168
+
169
+ # Nothing found on this branch.
170
+ return None
171
+
172
+
173
+ def _has_backup_label(dev: dict) -> bool:
174
+ """Return ``True`` when the given *lsblk* device description represents
175
+ the desired "backup" partition. The logic is kept in one place to make
176
+ future adjustments simpler.
177
+
178
+ The criteria as of now are:
179
+
180
+ The filesystem label (``label`` field) is exactly ``BACKUP`` – this is
181
+ how the pre-built *rescue.img* image names the 3rd partition that
182
+ will be used for storing backups.
183
+ """
184
+
185
+ label = (dev.get("label") or dev.get("partlabel") or "").upper()
186
+ if label == "BACKUP":
187
+ return True
188
+ return False
148
189
 
149
190
 
150
191
  def get_backup_device(lsblk_data):
192
+ """Locate a removable partition that should be used to store backups.
193
+
194
+ Priority is given to a partition explicitly labelled ``BACKUP``. If such
195
+ a partition isn't found, the legacy rule – ‘any removable exFAT
196
+ partition’ – is used.
197
+ """
198
+
199
+ _MIN_SIZE_BYTES = 32 * 1024 * 1024 * 1024 # 32 GiB – keep in sync with
200
+ # _find_blank_removable_device.
201
+
202
+ def _device_size_bytes(dev_name: str):
203
+ """Return size of *dev_name* in bytes (or ``None`` on failure)."""
204
+
205
+ for cmd in (
206
+ f"blockdev --getsize64 /dev/{dev_name}",
207
+ f"lsblk -b -dn -o SIZE /dev/{dev_name}",
208
+ ):
209
+ try:
210
+ out = subprocess.check_output(
211
+ cmd, shell=True, stderr=subprocess.DEVNULL
212
+ ).strip()
213
+ return int(out)
214
+ except Exception:
215
+ continue
216
+ return None
217
+
218
+ # ------------------------------------------------------------------
219
+ # Helper: does the filesystem already contain legacy backups?
220
+ # ------------------------------------------------------------------
221
+
222
+ def _entry_has_simo_backups(entry: dict) -> bool:
223
+ """Return *True* when *entry* hosts legacy ``simo_backups`` folder.
224
+
225
+ The implementation borrows heavily from the _fs_is_empty() helper –
226
+ we temporarily mount the filesystem read-only when it is not mounted
227
+ yet, inspect the directory listing and clean everything up.
228
+ """
229
+
230
+ mountpoint = entry.get("mountpoint")
231
+ cleanup = False
232
+
233
+ if not mountpoint:
234
+ tmp_dir = f"/tmp/simo-bk-{uuid.uuid4().hex[:8]}"
235
+ try:
236
+ os.makedirs(tmp_dir, exist_ok=True)
237
+ res = subprocess.run(
238
+ f"mount -o ro /dev/{entry['name']} {tmp_dir}",
239
+ shell=True,
240
+ stderr=subprocess.PIPE,
241
+ )
242
+ if res.returncode:
243
+ shutil.rmtree(tmp_dir, ignore_errors=True)
244
+ return False
245
+ mountpoint = tmp_dir
246
+ cleanup = True
247
+ except Exception:
248
+ shutil.rmtree(tmp_dir, ignore_errors=True)
249
+ return False
250
+
251
+ has_backups = os.path.isdir(os.path.join(mountpoint, "simo_backups"))
252
+
253
+ if cleanup:
254
+ subprocess.run(f"umount {mountpoint}", shell=True)
255
+ shutil.rmtree(mountpoint, ignore_errors=True)
256
+
257
+ return has_backups
258
+
259
+ # ------------------------------------------------------------------
260
+ # Phase 1 – look for properly prepared BACKUP partition **>=32 GiB**.
261
+ # This is the preferred modern approach.
262
+ # ------------------------------------------------------------------
263
+
264
+ for device in lsblk_data:
265
+ if not device.get("hotplug"):
266
+ continue
267
+
268
+ # Capacity check – skip devices smaller than the required threshold.
269
+ size_bytes = _device_size_bytes(device["name"])
270
+ if size_bytes is None:
271
+ print(f"Could not obtain capacity of: {device['name']}")
272
+ continue
273
+
274
+ if size_bytes < _MIN_SIZE_BYTES:
275
+ continue
276
+
277
+ # Prefer partitions explicitly labelled "BACKUP".
278
+ for child in device.get("children", []):
279
+ if _has_backup_label(child):
280
+ return child
281
+
282
+ # Legacy fallback (modern capacity) – whole-disk or partitioned
283
+ # exFAT volumes are still acceptable for backward compatibility when
284
+ # they are large enough.
285
+
286
+ if (device.get("fstype") or "").lower() == "exfat":
287
+ return device
288
+
289
+ for child in device.get("children", []):
290
+ if (child.get("fstype") or "").lower() == "exfat":
291
+ return child
292
+
293
+
294
+ # ------------------------------------------------------------------
295
+ # Phase 2 – look for **existing** legacy backup drives.
296
+ # ------------------------------------------------------------------
297
+
298
+ if _find_blank_removable_device(lsblk_data):
299
+ # New empty disk is available, let's use it instead of trying to find
300
+ # legacy media
301
+ return None
302
+
303
+ for device in lsblk_data:
304
+ if not device.get("hotplug"):
305
+ continue
306
+
307
+ # Check the whole device first.
308
+ if device.get("mountpoint") or device.get("fstype"):
309
+ if _entry_has_simo_backups(device):
310
+ return device
311
+
312
+ # Check its partitions (if any).
313
+ for child in device.get("children", []):
314
+ if _entry_has_simo_backups(child):
315
+ return child
316
+
317
+ # Nothing has been found.
318
+ return None
319
+
320
+
321
+ def _find_blank_removable_device(lsblk_data):
322
+ """Return the first removable block *device* that looks empty.
323
+
324
+ A device is considered *blank* when one of the following conditions is
325
+ met:
326
+
327
+ 1. It has no children (partitions) **and** no recognised filesystem – the
328
+ original behaviour that covers brand-new, uninitialised drives.
329
+ 2. It has no children (partitions) **and** an existing filesystem that is
330
+ effectively empty (e.g. a freshly formatted card).
331
+
332
+ Determining if a filesystem is *empty* is tricky without mounting it, but
333
+ for the purpose of automatically provisioning backup media we can use a
334
+ pragmatic heuristic: if the device is not mounted we temporarily mount it
335
+ read-only to a throw-away directory, inspect its contents and then unmount
336
+ it again. If it **is** already mounted we reuse the existing
337
+ mount-point. In both cases we treat the device as blank when the root of
338
+ the filesystem contains no entries other than implementation-specific
339
+ placeholders like the *lost+found* directory created by *mkfs.ext4*.
340
+
341
+ This relaxed definition allows the backup subsystem to reuse drives that
342
+ have been pre-formatted by the user but never actually used to store any
343
+ files.
344
+ """
345
+
346
+ # --- Helper inner functions ------------------------------------------------
347
+
348
+ def _device_size_bytes(dev_name: str):
349
+ """Return size of *dev_name* in bytes (or ``None`` on failure)."""
350
+
351
+ for cmd in (
352
+ f"blockdev --getsize64 /dev/{dev_name}",
353
+ f"lsblk -b -dn -o SIZE /dev/{dev_name}",
354
+ ):
355
+ try:
356
+ out = subprocess.check_output(
357
+ cmd, shell=True, stderr=subprocess.DEVNULL
358
+ ).strip()
359
+ return int(out)
360
+ except Exception:
361
+ continue
362
+ return None
363
+
364
+ def _fs_is_empty(entry: dict) -> bool:
365
+ """Heuristic to decide if a filesystem represented by *entry* is empty."""
366
+
367
+ fstype = entry.get("fstype")
368
+ if not fstype:
369
+ # Unformatted – treat as empty.
370
+ return True
371
+
372
+ mountpoint = entry.get("mountpoint")
373
+ cleanup = False
374
+
375
+ if not mountpoint:
376
+ tmp_dir = f"/tmp/simo-bk-{uuid.uuid4().hex[:8]}"
377
+ try:
378
+ os.makedirs(tmp_dir, exist_ok=True)
379
+ res = subprocess.run(
380
+ f"mount -o ro /dev/{entry['name']} {tmp_dir}",
381
+ shell=True,
382
+ stderr=subprocess.PIPE,
383
+ )
384
+ if res.returncode:
385
+ shutil.rmtree(tmp_dir, ignore_errors=True)
386
+ print(f"Unable to mount {entry['name']} to inspect contents – skip")
387
+ return False
388
+ mountpoint = tmp_dir
389
+ cleanup = True
390
+ except Exception as exc:
391
+ shutil.rmtree(tmp_dir, ignore_errors=True)
392
+ print(f"Exception while mounting {entry['name']}: {exc}")
393
+ return False
394
+
395
+ try:
396
+ with os.scandir(mountpoint) as it:
397
+ entries = [e.name for e in it if not e.name.startswith('.')]
398
+ except Exception as exc:
399
+ print(f"Unable to read directory listing for {entry['name']}: {exc}")
400
+ if cleanup:
401
+ subprocess.run(f"umount {mountpoint}", shell=True)
402
+ shutil.rmtree(mountpoint, ignore_errors=True)
403
+ return False
404
+
405
+ if cleanup:
406
+ subprocess.run(f"umount {mountpoint}", shell=True)
407
+ shutil.rmtree(mountpoint, ignore_errors=True)
408
+
409
+ meaningful = [e for e in entries if e not in {"lost+found"}]
410
+ return not meaningful
411
+
412
+ # ---------------------------------------------------------------------------
413
+
414
+ _MIN_SIZE_BYTES = 32 * 1024 * 1024 * 1024 # 32 GiB
415
+
151
416
  for device in lsblk_data:
152
- if not device['hotplug']:
417
+ if not device.get("hotplug"):
418
+ continue
419
+
420
+ size_bytes = _device_size_bytes(device["name"])
421
+ if size_bytes is None:
422
+ print(f"Could not obtain capacity of: {device['name']}")
153
423
  continue
154
- target_device = None
155
- if device.get('fstype') == 'exfat':
156
- target_device = device
157
- elif device.get('children'):
158
- for child in device.get('children'):
159
- if child.get('fstype') == 'exfat':
160
- target_device = child
161
- if target_device:
162
- return target_device
424
+
425
+ if size_bytes < _MIN_SIZE_BYTES:
426
+ print(f"Too small (<32 GiB): {device['name']}")
427
+ continue
428
+
429
+ children = device.get("children") or []
430
+
431
+ if not children:
432
+ # Whole-disk filesystem.
433
+ if _fs_is_empty(device):
434
+ return device
435
+ print(f"Whole-disk filesystem on {device['name']} is not empty – skip")
436
+ continue
437
+
438
+ if len(children) == 1:
439
+ child = children[0]
440
+ if _fs_is_empty(child):
441
+ return device
442
+ print(f"Single partition {child['name']} on {device['name']} is not empty – skip")
443
+ continue
444
+
445
+ print(f"More than one partition on {device['name']} – skip")
446
+
447
+ return None
448
+
449
+
450
+ def _ensure_rescue_image_written(blank_device_name: str):
451
+ """Write *rescue.img* to the given **whole-disk** device.
452
+
453
+ The function is intentionally idempotent – if writing fails the caller can
454
+ attempt to call it again (e.g. the next time the periodic task runs).
455
+
456
+ It raises an exception on irrecoverable errors so that the caller can log
457
+ the failure.
458
+ """
459
+
460
+ import tarfile, time
461
+
462
+ img_path = os.path.join(os.path.dirname(__file__), "rescue.img.xz")
463
+
464
+ # Write the image. We deliberately avoid using *python-dd* wrappers and
465
+ # rely on the time-tested `dd(1)` command.
466
+ dd_cmd = (
467
+ f"xzcat {img_path} | dd of=/dev/{blank_device_name} bs=4M conv=fsync"
468
+ )
469
+ res = subprocess.run(dd_cmd, shell=True, stderr=subprocess.PIPE)
470
+ if res.returncode:
471
+ raise RuntimeError(
472
+ f"Writing rescue image failed: {res.stderr.decode(errors='ignore')}"
473
+ )
474
+
475
+ # Make sure the kernel notices the new partition table.
476
+ subprocess.run(f"partprobe /dev/{blank_device_name}", shell=True)
477
+
478
+ # Give the device a moment to settle.
479
+ time.sleep(2)
480
+
481
+ # Enlarge the 3rd partition (BACKUP) to the rest of the disk and create /
482
+ # extend the exFAT filesystem. This is wrapped in a helper to keep the
483
+ # main flow readable.
484
+ _expand_backup_partition(blank_device_name)
485
+
486
+
487
+ def _expand_backup_partition(device_name: str):
488
+ """Make partition 3 span leftover space and be ext4 labelled BACKUP.
489
+
490
+ Implementation is intentionally minimal and resilient:
491
+ – Use *sgdisk* only (no interactive prompts).
492
+ – Delete partition 3 (if present) and create a new one that fills all
493
+ remaining free space.
494
+ – Always create a fresh ext4 filesystem labelled BACKUP.
495
+ Because the rescue-image just flashed is empty, data loss is not a
496
+ concern and this deterministic route avoids edge-case errors.
497
+ """
498
+
499
+ import time, shutil
500
+
501
+ def _dev_path(base: str) -> str:
502
+ """Return /dev/<base>3 path handling devices that need 'p3'."""
503
+ direct = f"/dev/{base}3"
504
+ with_p = f"/dev/{base}p3"
505
+ return direct if os.path.exists(direct) else with_p
506
+
507
+ # 1. Ensure GPT headers cover the whole disk (harmless if already OK).
508
+ subprocess.run(f"sgdisk -e /dev/{device_name}", shell=True)
509
+
510
+ # 2. Drop existing partition 3 (ignore errors when it does not exist).
511
+ subprocess.run(f"sgdisk -d 3 /dev/{device_name}", shell=True)
512
+
513
+ # 3. Create new Linux filesystem partition occupying the rest of the disk.
514
+ create_cmd = f"sgdisk -n 3:0:0 -t 3:8300 -c 3:BACKUP /dev/{device_name}"
515
+ res = subprocess.run(create_cmd, shell=True, stderr=subprocess.PIPE)
516
+ if res.returncode:
517
+ raise RuntimeError(
518
+ "sgdisk failed to create BACKUP partition: " +
519
+ res.stderr.decode(errors="ignore")
520
+ )
521
+
522
+ # 4. Inform kernel and wait for udev.
523
+ subprocess.run(f"partprobe /dev/{device_name}", shell=True)
524
+ subprocess.run("udevadm settle", shell=True)
525
+
526
+ part_path = _dev_path(device_name)
527
+ for _ in range(5):
528
+ if os.path.exists(part_path):
529
+ break
530
+ time.sleep(1)
531
+ else:
532
+ raise RuntimeError("/dev node for new BACKUP partition did not appear")
533
+
534
+ # 5. Always create a fresh ext4 filesystem; wipe old signatures first.
535
+ subprocess.run(f"wipefs -a {part_path}", shell=True)
536
+ mkfs_cmd = f"mkfs.ext4 -F -L BACKUP {part_path}"
537
+ res = subprocess.run(mkfs_cmd, shell=True, stderr=subprocess.PIPE)
538
+ if res.returncode:
539
+ raise RuntimeError(
540
+ "mkfs.ext4 failed for BACKUP partition: " + res.stderr.decode(errors="ignore")
541
+ )
163
542
 
164
543
 
165
544
  def get_partitions():
@@ -205,16 +584,39 @@ def get_partitions():
205
584
 
206
585
  backup_device = get_backup_device(lsblk_data)
207
586
 
587
+ # If no suitable partition is available try to prepare one automatically.
588
+ if not backup_device:
589
+ blank_dev = _find_blank_removable_device(lsblk_data)
590
+ if blank_dev:
591
+ try:
592
+ _ensure_rescue_image_written(blank_dev["name"])
593
+ except Exception as exc:
594
+ BackupLog.objects.create(
595
+ level="error",
596
+ msg=(
597
+ "Can't prepare backup drive automatically.\n\n" +
598
+ str(exc)
599
+ ),
600
+ )
601
+ else:
602
+ # Re-read block devices so that the freshly written partition
603
+ # table appears in *lsblk* output.
604
+ lsblk_data = json.loads(subprocess.check_output(
605
+ 'lsblk --output NAME,HOTPLUG,MOUNTPOINT,FSTYPE,TYPE,LABEL,PARTLABEL --json',
606
+ shell=True
607
+ ).decode())['blockdevices']
608
+ backup_device = get_backup_device(lsblk_data)
609
+
208
610
  if not backup_device:
209
611
  BackupLog.objects.create(
210
612
  level='warning',
211
- msg="Can't backup. No external exFAT backup device on this machine."
613
+ msg="Can't backup. No external BACKUP partition found and no blank removable device was available."
212
614
  )
213
615
  return
214
616
 
215
- if lvm_partition.get('partlabel'):
617
+ if backup_device.get('partlabel'):
216
618
  sd_mountpoint = f"/media/{backup_device['partlabel']}"
217
- elif lvm_partition.get('label'):
619
+ elif backup_device.get('label'):
218
620
  sd_mountpoint = f"/media/{backup_device['label']}"
219
621
  else:
220
622
  sd_mountpoint = f"/media/{backup_device['name']}"
@@ -237,6 +639,8 @@ def get_partitions():
237
639
 
238
640
  @celery_app.task
239
641
  def perform_backup():
642
+ from simo.core.models import Instance
643
+ from simo.core.middleware import drop_current_instance
240
644
  from simo.backups.models import BackupLog
241
645
  try:
242
646
  lv_group, lv_name, sd_mountpoint = get_partitions()
@@ -267,6 +671,15 @@ def perform_backup():
267
671
  mac = str(hex(uuid.getnode()))
268
672
  device_backups_path = f'{sd_mountpoint}/simo_backups/hub-{mac}'
269
673
 
674
+ if not os.path.exists(device_backups_path):
675
+ os.makedirs(device_backups_path)
676
+
677
+ drop_current_instance()
678
+ hub_meta = {
679
+ 'instances': [inst.name for inst in Instance.objects.all()]
680
+ }
681
+ with open(os.path.join(device_backups_path, 'hub_meta.json'), 'w') as f:
682
+ f.write(json.dumps(hub_meta))
270
683
 
271
684
  now = datetime.now()
272
685
  month_folder = os.path.join(
@@ -288,6 +701,29 @@ def perform_backup():
288
701
  f'borg init --encryption=none {month_folder}', shell=True
289
702
  )
290
703
 
704
+ # ------------------------------------------------------------------
705
+ # Ensure that files stored on *separate* partitions – most importantly
706
+ # the /boot (kernel & initrd images) and /boot/efi (EFI System
707
+ # Partition) – are included in the snapshot. Otherwise the rescue
708
+ # procedure restores an empty /boot which leaves the system un-bootable
709
+ # once GRUB hands over control to the (missing) kernel.
710
+ #
711
+ # We temporarily bind-mount those paths into the read-only snapshot so
712
+ # that Borg treats them as regular directories residing on the same
713
+ # filesystem tree.
714
+ # ------------------------------------------------------------------
715
+
716
+ bind_mounts = []
717
+ for path in ("/boot", "/boot/efi"):
718
+ target = os.path.join(snap_mount_point, path.lstrip("/"))
719
+ # Create the mount-point inside the snapshot and bind-mount the live
720
+ # directory if it exists.
721
+ if os.path.ismount(path):
722
+ os.makedirs(target, exist_ok=True)
723
+ subprocess.run(["mount", "--bind", path, target], check=True)
724
+ bind_mounts.append(target)
725
+
726
+ # Directories that are safe to exclude – keep /boot out of this list!
291
727
  exclude_dirs = (
292
728
  'tmp', 'lost+found', 'proc', 'cdrom', 'dev', 'mnt', 'sys', 'run',
293
729
  'var/tmp', 'var/cache', 'var/log', 'media',
@@ -326,6 +762,11 @@ def perform_backup():
326
762
  stdout=subprocess.PIPE, stderr=subprocess.PIPE
327
763
  )
328
764
 
765
+ # Unmount previously created bind-mounts (boot / boot/efi) *before*
766
+ # removing the snapshot so that no busy references remain.
767
+ for mnt in reversed(bind_mounts):
768
+ subprocess.run(["umount", mnt])
769
+
329
770
  subprocess.run(["umount", snap_mount_point])
330
771
  subprocess.run(
331
772
  f"lvremove -f {lv_group}/{snap_name}", shell=True
Binary file
Binary file
Binary file
Binary file