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.
- simo/__pycache__/__init__.cpython-312.pyc +0 -0
- simo/__pycache__/asgi.cpython-312.pyc +0 -0
- simo/__pycache__/celeryc.cpython-312.pyc +0 -0
- simo/__pycache__/conf.cpython-312.pyc +0 -0
- simo/__pycache__/settings.cpython-312.pyc +0 -0
- simo/__pycache__/urls.cpython-312.pyc +0 -0
- simo/automation/__pycache__/__init__.cpython-312.pyc +0 -0
- simo/automation/__pycache__/app_widgets.cpython-312.pyc +0 -0
- simo/automation/__pycache__/controllers.cpython-312.pyc +0 -0
- simo/automation/__pycache__/forms.cpython-312.pyc +0 -0
- simo/automation/__pycache__/gateways.cpython-312.pyc +0 -0
- simo/automation/__pycache__/helpers.cpython-312.pyc +0 -0
- simo/automation/__pycache__/models.cpython-312.pyc +0 -0
- simo/automation/__pycache__/serializers.cpython-312.pyc +0 -0
- simo/automation/__pycache__/state.cpython-312.pyc +0 -0
- simo/automation/migrations/__pycache__/0001_initial.cpython-312.pyc +0 -0
- simo/automation/migrations/__pycache__/0002_update_helpers_in_scripts.cpython-312.pyc +0 -0
- simo/automation/migrations/__pycache__/__init__.cpython-312.pyc +0 -0
- simo/automation/templates/automations/__pycache__/auto_away.cpython-312.pyc +0 -0
- simo/automation/templates/automations/__pycache__/auto_state_script.cpython-312.pyc +0 -0
- simo/automation/templates/automations/__pycache__/phones_sleep_script.cpython-312.pyc +0 -0
- simo/backups/__pycache__/__init__.cpython-312.pyc +0 -0
- simo/backups/__pycache__/admin.cpython-312.pyc +0 -0
- simo/backups/__pycache__/dynamic_settings.cpython-312.pyc +0 -0
- simo/backups/__pycache__/models.cpython-312.pyc +0 -0
- simo/backups/__pycache__/tasks.cpython-312.pyc +0 -0
- simo/backups/migrations/__pycache__/0001_initial.cpython-312.pyc +0 -0
- simo/backups/migrations/__pycache__/0002_backuplog_backup_level_backup_size.cpython-312.pyc +0 -0
- simo/backups/migrations/__pycache__/0003_alter_backuplog_options_alter_backup_size.cpython-312.pyc +0 -0
- simo/backups/migrations/__pycache__/0004_alter_backup_options_alter_backuplog_options_and_more.cpython-312.pyc +0 -0
- simo/backups/migrations/__pycache__/__init__.cpython-312.pyc +0 -0
- simo/backups/rescue.img.xz +0 -0
- simo/backups/tasks.py +457 -16
- simo/core/__pycache__/__init__.cpython-312.pyc +0 -0
- simo/core/__pycache__/admin.cpython-312.pyc +0 -0
- simo/core/__pycache__/api.cpython-312.pyc +0 -0
- simo/core/__pycache__/api_auth.cpython-312.pyc +0 -0
- simo/core/__pycache__/api_meta.cpython-312.pyc +0 -0
- simo/core/__pycache__/app_widgets.cpython-312.pyc +0 -0
- simo/core/__pycache__/apps.cpython-312.pyc +0 -0
- simo/core/__pycache__/auto_urls.cpython-312.pyc +0 -0
- simo/core/__pycache__/autocomplete_views.cpython-312.pyc +0 -0
- simo/core/__pycache__/base_types.cpython-312.pyc +0 -0
- simo/core/__pycache__/context.cpython-312.pyc +0 -0
- simo/core/__pycache__/controllers.cpython-312.pyc +0 -0
- simo/core/__pycache__/dynamic_settings.cpython-312.pyc +0 -0
- simo/core/__pycache__/events.cpython-312.pyc +0 -0
- simo/core/__pycache__/filters.cpython-312.pyc +0 -0
- simo/core/__pycache__/form_fields.cpython-312.pyc +0 -0
- simo/core/__pycache__/forms.cpython-312.pyc +0 -0
- simo/core/__pycache__/gateways.cpython-312.pyc +0 -0
- simo/core/__pycache__/loggers.cpython-312.pyc +0 -0
- simo/core/__pycache__/managers.cpython-312.pyc +0 -0
- simo/core/__pycache__/middleware.cpython-312.pyc +0 -0
- simo/core/__pycache__/models.cpython-312.pyc +0 -0
- simo/core/__pycache__/permissions.cpython-312.pyc +0 -0
- simo/core/__pycache__/routing.cpython-312.pyc +0 -0
- simo/core/__pycache__/serializers.cpython-312.pyc +0 -0
- simo/core/__pycache__/signal_receivers.cpython-312.pyc +0 -0
- simo/core/__pycache__/socket_consumers.cpython-312.pyc +0 -0
- simo/core/__pycache__/storage.cpython-312.pyc +0 -0
- simo/core/__pycache__/tasks.cpython-312.pyc +0 -0
- simo/core/__pycache__/todos.cpython-312.pyc +0 -0
- simo/core/__pycache__/types.cpython-312.pyc +0 -0
- simo/core/__pycache__/views.cpython-312.pyc +0 -0
- simo/core/__pycache__/widgets.cpython-312.pyc +0 -0
- simo/core/controllers.py +6 -3
- simo/core/db_backend/__pycache__/__init__.cpython-312.pyc +0 -0
- simo/core/db_backend/__pycache__/base.cpython-312.pyc +0 -0
- simo/core/drf_braces/__pycache__/__init__.cpython-312.pyc +0 -0
- simo/core/drf_braces/__pycache__/mixins.cpython-312.pyc +0 -0
- simo/core/drf_braces/__pycache__/models.cpython-312.pyc +0 -0
- simo/core/drf_braces/__pycache__/parsers.cpython-312.pyc +0 -0
- simo/core/drf_braces/__pycache__/renderers.cpython-312.pyc +0 -0
- simo/core/drf_braces/__pycache__/utils.cpython-312.pyc +0 -0
- simo/core/drf_braces/fields/__pycache__/__init__.cpython-312.pyc +0 -0
- simo/core/drf_braces/fields/__pycache__/_fields.cpython-312.pyc +0 -0
- simo/core/drf_braces/fields/__pycache__/custom.cpython-312.pyc +0 -0
- simo/core/drf_braces/fields/__pycache__/mixins.cpython-312.pyc +0 -0
- simo/core/drf_braces/fields/__pycache__/modified.cpython-312.pyc +0 -0
- simo/core/drf_braces/forms/__pycache__/__init__.cpython-312.pyc +0 -0
- simo/core/drf_braces/forms/__pycache__/fields.cpython-312.pyc +0 -0
- simo/core/drf_braces/forms/__pycache__/serializer_form.cpython-312.pyc +0 -0
- simo/core/drf_braces/serializers/__pycache__/__init__.cpython-312.pyc +0 -0
- simo/core/drf_braces/serializers/__pycache__/enforce_validation_serializer.cpython-312.pyc +0 -0
- simo/core/drf_braces/serializers/__pycache__/form_serializer.cpython-312.pyc +0 -0
- simo/core/drf_braces/serializers/__pycache__/swapping.cpython-312.pyc +0 -0
- simo/core/drf_braces/tests/__pycache__/__init__.cpython-312.pyc +0 -0
- simo/core/drf_braces/tests/__pycache__/test_mixins.cpython-312.pyc +0 -0
- simo/core/drf_braces/tests/__pycache__/test_parsers.cpython-312.pyc +0 -0
- simo/core/drf_braces/tests/__pycache__/test_renderers.cpython-312.pyc +0 -0
- simo/core/drf_braces/tests/__pycache__/test_utils.cpython-312.pyc +0 -0
- simo/core/drf_braces/tests/fields/__pycache__/__init__.cpython-312.pyc +0 -0
- simo/core/drf_braces/tests/fields/__pycache__/test_custom.cpython-312.pyc +0 -0
- simo/core/drf_braces/tests/fields/__pycache__/test_fields.cpython-312.pyc +0 -0
- simo/core/drf_braces/tests/fields/__pycache__/test_mixins.cpython-312.pyc +0 -0
- simo/core/drf_braces/tests/fields/__pycache__/test_modified.cpython-312.pyc +0 -0
- simo/core/drf_braces/tests/forms/__pycache__/__init__.cpython-312.pyc +0 -0
- simo/core/drf_braces/tests/forms/__pycache__/test_fields.cpython-312.pyc +0 -0
- simo/core/drf_braces/tests/forms/__pycache__/test_serializer_form.cpython-312.pyc +0 -0
- simo/core/drf_braces/tests/serializers/__pycache__/__init__.cpython-312.pyc +0 -0
- simo/core/drf_braces/tests/serializers/__pycache__/test_enforce_validation_serializer.cpython-312.pyc +0 -0
- simo/core/drf_braces/tests/serializers/__pycache__/test_form_serializer.cpython-312.pyc +0 -0
- simo/core/drf_braces/tests/serializers/__pycache__/test_swapping.cpython-312.pyc +0 -0
- simo/core/management/__pycache__/__init__.cpython-312.pyc +0 -0
- simo/core/management/__pycache__/update.cpython-312.pyc +0 -0
- simo/core/management/_hub_template/hub/__pycache__/asgi.cpython-312.pyc +0 -0
- simo/core/management/_hub_template/hub/__pycache__/celeryc.cpython-312.pyc +0 -0
- simo/core/management/_hub_template/hub/__pycache__/manage.cpython-312.pyc +0 -0
- simo/core/management/_hub_template/hub/__pycache__/settings.cpython-312.pyc +0 -0
- simo/core/management/_hub_template/hub/__pycache__/urls.cpython-312.pyc +0 -0
- simo/core/management/_hub_template/hub/__pycache__/wsgi.cpython-312.pyc +0 -0
- simo/core/management/commands/__pycache__/__init__.cpython-312.pyc +0 -0
- simo/core/management/commands/__pycache__/gateways_manager.cpython-312.pyc +0 -0
- simo/core/management/commands/__pycache__/on_http_start.cpython-312.pyc +0 -0
- simo/core/management/commands/__pycache__/run_gateway.cpython-312.pyc +0 -0
- simo/core/migrations/__pycache__/0001_initial.cpython-312.pyc +0 -0
- simo/core/migrations/__pycache__/0002_load_icons.cpython-312.pyc +0 -0
- simo/core/migrations/__pycache__/0003_create_default_zones_and_categories.cpython-312.pyc +0 -0
- simo/core/migrations/__pycache__/0004_create_generic.cpython-312.pyc +0 -0
- simo/core/migrations/__pycache__/0005_component_subcomponents.cpython-312.pyc +0 -0
- simo/core/migrations/__pycache__/0006_alter_component_subcomponents.cpython-312.pyc +0 -0
- simo/core/migrations/__pycache__/0007_component_change_init_to.cpython-312.pyc +0 -0
- simo/core/migrations/__pycache__/0008_alter_component_change_init_to.cpython-312.pyc +0 -0
- simo/core/migrations/__pycache__/0009_auto_20220707_1404.cpython-312.pyc +0 -0
- simo/core/migrations/__pycache__/0010_historyaggregate.cpython-312.pyc +0 -0
- simo/core/migrations/__pycache__/0011_component_last_change.cpython-312.pyc +0 -0
- simo/core/migrations/__pycache__/0012_instance.cpython-312.pyc +0 -0
- simo/core/migrations/__pycache__/0013_auto_20231003_0754.cpython-312.pyc +0 -0
- simo/core/migrations/__pycache__/0014_zone_instance.cpython-312.pyc +0 -0
- simo/core/migrations/__pycache__/0015_auto_20231004_1113.cpython-312.pyc +0 -0
- simo/core/migrations/__pycache__/0016_auto_20231004_1113.cpython-312.pyc +0 -0
- simo/core/migrations/__pycache__/0017_auto_20231004_1313.cpython-312.pyc +0 -0
- simo/core/migrations/__pycache__/0018_auto_20231005_0622.cpython-312.pyc +0 -0
- simo/core/migrations/__pycache__/0019_alter_gateway_type.cpython-312.pyc +0 -0
- simo/core/migrations/__pycache__/0020_component_meta.cpython-312.pyc +0 -0
- simo/core/migrations/__pycache__/0021_auto_20231020_1041.cpython-312.pyc +0 -0
- simo/core/migrations/__pycache__/0022_auto_20231221_0735.cpython-312.pyc +0 -0
- simo/core/migrations/__pycache__/0023_auto_20231229_1352.cpython-312.pyc +0 -0
- simo/core/migrations/__pycache__/0024_alter_instance_device_report_history_days.cpython-312.pyc +0 -0
- simo/core/migrations/__pycache__/0025_auto_20240122_1321.cpython-312.pyc +0 -0
- simo/core/migrations/__pycache__/0026_category_instance.cpython-312.pyc +0 -0
- simo/core/migrations/__pycache__/0027_remove_component_tags.cpython-312.pyc +0 -0
- simo/core/migrations/__pycache__/0028_rename_subcomponents_component_slaves.cpython-312.pyc +0 -0
- simo/core/migrations/__pycache__/0029_auto_20240229_1331.cpython-312.pyc +0 -0
- simo/core/migrations/__pycache__/0030_alter_instance_timezone.cpython-312.pyc +0 -0
- simo/core/migrations/__pycache__/0031_auto_20240429_1231.cpython-312.pyc +0 -0
- simo/core/migrations/__pycache__/0032_auto_20240506_0834.cpython-312.pyc +0 -0
- simo/core/migrations/__pycache__/0033_auto_20240509_0821.cpython-312.pyc +0 -0
- simo/core/migrations/__pycache__/0034_component_error_msg.cpython-312.pyc +0 -0
- simo/core/migrations/__pycache__/0035_remove_instance_share_location.cpython-312.pyc +0 -0
- simo/core/migrations/__pycache__/0036_auto_20240521_0823.cpython-312.pyc +0 -0
- simo/core/migrations/__pycache__/0037_auto_20240606_1057.cpython-312.pyc +0 -0
- simo/core/migrations/__pycache__/0038_remove_instance_cover_image_and_more.cpython-312.pyc +0 -0
- simo/core/migrations/__pycache__/0039_instance_is_active_alter_instance_timezone.cpython-312.pyc +0 -0
- simo/core/migrations/__pycache__/0040_alter_instance_name.cpython-312.pyc +0 -0
- simo/core/migrations/__pycache__/0041_alter_instance_slug.cpython-312.pyc +0 -0
- simo/core/migrations/__pycache__/0042_alter_instance_timezone.cpython-312.pyc +0 -0
- simo/core/migrations/__pycache__/0043_alter_category_instance_alter_instance_timezone_and_more.cpython-312.pyc +0 -0
- simo/core/migrations/__pycache__/0044_alter_gateway_type.cpython-312.pyc +0 -0
- simo/core/migrations/__pycache__/0045_alter_instance_device_report_history_days_and_more.cpython-312.pyc +0 -0
- simo/core/migrations/__pycache__/0046_component_value_translation_alter_gateway_type.cpython-312.pyc +0 -0
- simo/core/migrations/__pycache__/0047_alter_component_value_translation.cpython-312.pyc +0 -0
- simo/core/migrations/__pycache__/0048_publicfile_privatefile.cpython-312.pyc +0 -0
- simo/core/migrations/__pycache__/0049_alter_gateway_type.cpython-312.pyc +0 -0
- simo/core/migrations/__pycache__/0050_componenthistory_alive.cpython-312.pyc +0 -0
- simo/core/migrations/__pycache__/__init__.cpython-312.pyc +0 -0
- simo/core/templates/core/__pycache__/value_translation.cpython-312.pyc +0 -0
- simo/core/templatetags/__pycache__/__init__.cpython-312.pyc +0 -0
- simo/core/templatetags/__pycache__/components_list.cpython-312.pyc +0 -0
- simo/core/utils/__pycache__/__init__.cpython-312.pyc +0 -0
- simo/core/utils/__pycache__/admin.cpython-312.pyc +0 -0
- simo/core/utils/__pycache__/api.cpython-312.pyc +0 -0
- simo/core/utils/__pycache__/cache.cpython-312.pyc +0 -0
- simo/core/utils/__pycache__/config_values.cpython-312.pyc +0 -0
- simo/core/utils/__pycache__/converters.cpython-312.pyc +0 -0
- simo/core/utils/__pycache__/easing.cpython-312.pyc +0 -0
- simo/core/utils/__pycache__/form_fields.cpython-312.pyc +0 -0
- simo/core/utils/__pycache__/form_widgets.cpython-312.pyc +0 -0
- simo/core/utils/__pycache__/formsets.cpython-312.pyc +0 -0
- simo/core/utils/__pycache__/helpers.cpython-312.pyc +0 -0
- simo/core/utils/__pycache__/json.cpython-312.pyc +0 -0
- simo/core/utils/__pycache__/logs.cpython-312.pyc +0 -0
- simo/core/utils/__pycache__/mixins.cpython-312.pyc +0 -0
- simo/core/utils/__pycache__/model_helpers.cpython-312.pyc +0 -0
- simo/core/utils/__pycache__/operations.cpython-312.pyc +0 -0
- simo/core/utils/__pycache__/relay.cpython-312.pyc +0 -0
- simo/core/utils/__pycache__/serialization.cpython-312.pyc +0 -0
- simo/core/utils/__pycache__/type_constants.cpython-312.pyc +0 -0
- simo/core/utils/__pycache__/validators.cpython-312.pyc +0 -0
- simo/fleet/__pycache__/__init__.cpython-312.pyc +0 -0
- simo/fleet/__pycache__/admin.cpython-312.pyc +0 -0
- simo/fleet/__pycache__/api.cpython-312.pyc +0 -0
- simo/fleet/__pycache__/apps.cpython-312.pyc +0 -0
- simo/fleet/__pycache__/auto_urls.cpython-312.pyc +0 -0
- simo/fleet/__pycache__/base_types.cpython-312.pyc +0 -0
- simo/fleet/__pycache__/ble.cpython-312.pyc +0 -0
- simo/fleet/__pycache__/controllers.cpython-312.pyc +0 -0
- simo/fleet/__pycache__/custom_dali_operations.cpython-312.pyc +0 -0
- simo/fleet/__pycache__/forms.cpython-312.pyc +0 -0
- simo/fleet/__pycache__/gateways.cpython-312.pyc +0 -0
- simo/fleet/__pycache__/managers.cpython-312.pyc +0 -0
- simo/fleet/__pycache__/models.cpython-312.pyc +0 -0
- simo/fleet/__pycache__/routing.cpython-312.pyc +0 -0
- simo/fleet/__pycache__/serializers.cpython-312.pyc +0 -0
- simo/fleet/__pycache__/socket_consumers.cpython-312.pyc +0 -0
- simo/fleet/__pycache__/tasks.cpython-312.pyc +0 -0
- simo/fleet/__pycache__/utils.cpython-312.pyc +0 -0
- simo/fleet/__pycache__/views.cpython-312.pyc +0 -0
- simo/fleet/controllers.py +65 -24
- simo/fleet/custom_dali_operations.py +14 -2
- simo/fleet/forms.py +2 -1
- simo/fleet/migrations/__pycache__/0001_initial.cpython-312.pyc +0 -0
- simo/fleet/migrations/__pycache__/0002_auto_20220422_0743.cpython-312.pyc +0 -0
- simo/fleet/migrations/__pycache__/0003_auto_20220422_0752.cpython-312.pyc +0 -0
- simo/fleet/migrations/__pycache__/0004_auto_20220422_0818.cpython-312.pyc +0 -0
- simo/fleet/migrations/__pycache__/0005_auto_20220428_0900.cpython-312.pyc +0 -0
- simo/fleet/migrations/__pycache__/0006_rename_mac_colonel_uid.cpython-312.pyc +0 -0
- simo/fleet/migrations/__pycache__/0007_colonel_socket_connected.cpython-312.pyc +0 -0
- simo/fleet/migrations/__pycache__/0008_i2cinterface.cpython-312.pyc +0 -0
- simo/fleet/migrations/__pycache__/0009_i2cinterface_name.cpython-312.pyc +0 -0
- simo/fleet/migrations/__pycache__/0010_auto_20220602_0746.cpython-312.pyc +0 -0
- simo/fleet/migrations/__pycache__/0011_i2cinterface_freq.cpython-312.pyc +0 -0
- simo/fleet/migrations/__pycache__/0012_colonel_logs_stream.cpython-312.pyc +0 -0
- simo/fleet/migrations/__pycache__/0013_alter_colonel_last_seen.cpython-312.pyc +0 -0
- simo/fleet/migrations/__pycache__/0014_auto_20220614_0659.cpython-312.pyc +0 -0
- simo/fleet/migrations/__pycache__/0015_auto_20220614_0754.cpython-312.pyc +0 -0
- simo/fleet/migrations/__pycache__/0016_auto_20220704_0840.cpython-312.pyc +0 -0
- simo/fleet/migrations/__pycache__/0017_alter_colonel_secret.cpython-312.pyc +0 -0
- simo/fleet/migrations/__pycache__/0018_colonel_instance.cpython-312.pyc +0 -0
- simo/fleet/migrations/__pycache__/0019_auto_20231006_0749.cpython-312.pyc +0 -0
- simo/fleet/migrations/__pycache__/0020_instanceoptions.cpython-312.pyc +0 -0
- simo/fleet/migrations/__pycache__/0021_auto_20231006_0819.cpython-312.pyc +0 -0
- simo/fleet/migrations/__pycache__/0022_remove_colonel_secret.cpython-312.pyc +0 -0
- simo/fleet/migrations/__pycache__/0023_colonel_is_authorized.cpython-312.pyc +0 -0
- simo/fleet/migrations/__pycache__/0024_colonel_pwm_frequency.cpython-312.pyc +0 -0
- simo/fleet/migrations/__pycache__/0025_auto_20240130_1334.cpython-312.pyc +0 -0
- simo/fleet/migrations/__pycache__/0026_rename_i2cinterface_scl_pin_and_more.cpython-312.pyc +0 -0
- simo/fleet/migrations/__pycache__/0027_auto_20240306_0802.cpython-312.pyc +0 -0
- simo/fleet/migrations/__pycache__/0028_remove_i2cinterface_scl_pin_no_and_more.cpython-312.pyc +0 -0
- simo/fleet/migrations/__pycache__/0029_alter_i2cinterface_scl_pin_and_more.cpython-312.pyc +0 -0
- simo/fleet/migrations/__pycache__/0030_colonelpin_label_alter_colonel_type.cpython-312.pyc +0 -0
- simo/fleet/migrations/__pycache__/0031_alter_colonel_type.cpython-312.pyc +0 -0
- simo/fleet/migrations/__pycache__/0032_auto_20240415_0736.cpython-312.pyc +0 -0
- simo/fleet/migrations/__pycache__/0033_auto_20240415_0736.cpython-312.pyc +0 -0
- simo/fleet/migrations/__pycache__/0034_auto_20240418_0735.cpython-312.pyc +0 -0
- simo/fleet/migrations/__pycache__/0035_auto_20240514_0855.cpython-312.pyc +0 -0
- simo/fleet/migrations/__pycache__/0036_auto_20240605_0702.cpython-312.pyc +0 -0
- simo/fleet/migrations/__pycache__/0037_alter_colonelpin_options_alter_colonelpin_no_and_more.cpython-312.pyc +0 -0
- simo/fleet/migrations/__pycache__/0038_alter_colonel_type.cpython-312.pyc +0 -0
- simo/fleet/migrations/__pycache__/0039_auto_20241016_1047.cpython-312.pyc +0 -0
- simo/fleet/migrations/__pycache__/0040_alter_colonel_pwm_frequency.cpython-312.pyc +0 -0
- simo/fleet/migrations/__pycache__/0041_alter_colonel_instance_and_more.cpython-312.pyc +0 -0
- simo/fleet/migrations/__pycache__/0042_auto_20241120_1028.cpython-312.pyc +0 -0
- simo/fleet/migrations/__pycache__/0043_auto_20241203_0930.cpython-312.pyc +0 -0
- simo/fleet/migrations/__pycache__/0044_auto_20241210_0707.cpython-312.pyc +0 -0
- simo/fleet/migrations/__pycache__/0045_alter_colonel_type_customdalidevice.cpython-312.pyc +0 -0
- simo/fleet/migrations/__pycache__/0046_delete_customdalidevice.cpython-312.pyc +0 -0
- simo/fleet/migrations/__pycache__/0047_customdalidevice.cpython-312.pyc +0 -0
- simo/fleet/migrations/__pycache__/0048_remove_customdalidevice_colonel_and_more.cpython-312.pyc +0 -0
- simo/fleet/migrations/__pycache__/0049_alter_customdalidevice_interface.cpython-312.pyc +0 -0
- simo/fleet/migrations/__pycache__/0050_customdalidevice_uid.cpython-312.pyc +0 -0
- simo/fleet/migrations/__pycache__/0051_customdalidevice_components.cpython-312.pyc +0 -0
- simo/fleet/migrations/__pycache__/0052_colonelpin_interface.cpython-312.pyc +0 -0
- simo/fleet/migrations/__pycache__/0053_auto_20250507_0713.cpython-312.pyc +0 -0
- simo/fleet/migrations/__pycache__/0054_auto_20250507_1256.cpython-312.pyc +0 -0
- simo/fleet/migrations/__pycache__/__init__.cpython-312.pyc +0 -0
- simo/generic/__pycache__/__init__.cpython-312.pyc +0 -0
- simo/generic/__pycache__/app_widgets.cpython-312.pyc +0 -0
- simo/generic/__pycache__/base_types.cpython-312.pyc +0 -0
- simo/generic/__pycache__/controllers.cpython-312.pyc +0 -0
- simo/generic/__pycache__/forms.cpython-312.pyc +0 -0
- simo/generic/__pycache__/gateways.cpython-312.pyc +0 -0
- simo/generic/__pycache__/models.cpython-312.pyc +0 -0
- simo/generic/__pycache__/routing.cpython-312.pyc +0 -0
- simo/generic/__pycache__/socket_consumers.cpython-312.pyc +0 -0
- simo/generic/__pycache__/tasks.cpython-312.pyc +0 -0
- simo/generic/migrations/__pycache__/0001_initial.cpython-312.pyc +0 -0
- simo/generic/migrations/__pycache__/0002_auto_20241126_0726.cpython-312.pyc +0 -0
- simo/generic/migrations/__pycache__/0003_auto_20250409_1404.cpython-312.pyc +0 -0
- simo/generic/migrations/__pycache__/__init__.cpython-312.pyc +0 -0
- simo/multimedia/__pycache__/__init__.cpython-312.pyc +0 -0
- simo/multimedia/__pycache__/admin.cpython-312.pyc +0 -0
- simo/multimedia/__pycache__/api.cpython-312.pyc +0 -0
- simo/multimedia/__pycache__/app_widgets.cpython-312.pyc +0 -0
- simo/multimedia/__pycache__/auto_urls.cpython-312.pyc +0 -0
- simo/multimedia/__pycache__/base_types.cpython-312.pyc +0 -0
- simo/multimedia/__pycache__/controllers.cpython-312.pyc +0 -0
- simo/multimedia/__pycache__/forms.cpython-312.pyc +0 -0
- simo/multimedia/__pycache__/models.cpython-312.pyc +0 -0
- simo/multimedia/__pycache__/serializers.cpython-312.pyc +0 -0
- simo/multimedia/__pycache__/views.cpython-312.pyc +0 -0
- simo/multimedia/migrations/__pycache__/0001_initial.cpython-312.pyc +0 -0
- simo/multimedia/migrations/__pycache__/0002_sound_length.cpython-312.pyc +0 -0
- simo/multimedia/migrations/__pycache__/0003_alter_sound_length.cpython-312.pyc +0 -0
- simo/multimedia/migrations/__pycache__/0004_auto_20231023_1055.cpython-312.pyc +0 -0
- simo/multimedia/migrations/__pycache__/0005_remove_sound_slug_sound_date_uploaded.cpython-312.pyc +0 -0
- simo/multimedia/migrations/__pycache__/0006_remove_sound_length_sound_duration.cpython-312.pyc +0 -0
- simo/multimedia/migrations/__pycache__/__init__.cpython-312.pyc +0 -0
- simo/notifications/__pycache__/__init__.cpython-312.pyc +0 -0
- simo/notifications/__pycache__/admin.cpython-312.pyc +0 -0
- simo/notifications/__pycache__/api.cpython-312.pyc +0 -0
- simo/notifications/__pycache__/models.cpython-312.pyc +0 -0
- simo/notifications/__pycache__/serializers.cpython-312.pyc +0 -0
- simo/notifications/__pycache__/utils.cpython-312.pyc +0 -0
- simo/notifications/migrations/__pycache__/0001_initial.cpython-312.pyc +0 -0
- simo/notifications/migrations/__pycache__/0002_notification_instance.cpython-312.pyc +0 -0
- simo/notifications/migrations/__pycache__/0003_alter_notification_instance.cpython-312.pyc +0 -0
- simo/notifications/migrations/__pycache__/__init__.cpython-312.pyc +0 -0
- simo/users/__pycache__/__init__.cpython-312.pyc +0 -0
- simo/users/__pycache__/admin.cpython-312.pyc +0 -0
- simo/users/__pycache__/api.cpython-312.pyc +0 -0
- simo/users/__pycache__/apps.cpython-312.pyc +0 -0
- simo/users/__pycache__/auth_backends.cpython-312.pyc +0 -0
- simo/users/__pycache__/auto_urls.cpython-312.pyc +0 -0
- simo/users/__pycache__/dynamic_settings.cpython-312.pyc +0 -0
- simo/users/__pycache__/managers.cpython-312.pyc +0 -0
- simo/users/__pycache__/middleware.cpython-312.pyc +0 -0
- simo/users/__pycache__/models.cpython-312.pyc +0 -0
- simo/users/__pycache__/permissions.cpython-312.pyc +0 -0
- simo/users/__pycache__/serializers.cpython-312.pyc +0 -0
- simo/users/__pycache__/sso_urls.cpython-312.pyc +0 -0
- simo/users/__pycache__/sso_views.cpython-312.pyc +0 -0
- simo/users/__pycache__/tasks.cpython-312.pyc +0 -0
- simo/users/__pycache__/utils.cpython-312.pyc +0 -0
- simo/users/__pycache__/views.cpython-312.pyc +0 -0
- simo/users/migrations/__pycache__/0001_initial.cpython-312.pyc +0 -0
- simo/users/migrations/__pycache__/0002_componentpermission.cpython-312.pyc +0 -0
- simo/users/migrations/__pycache__/0003_create_roles_and_system_user.cpython-312.pyc +0 -0
- simo/users/migrations/__pycache__/0004_user_secret_key.cpython-312.pyc +0 -0
- simo/users/migrations/__pycache__/0005_permissionsrole_instance.cpython-312.pyc +0 -0
- simo/users/migrations/__pycache__/0006_auto_20231003_0850.cpython-312.pyc +0 -0
- simo/users/migrations/__pycache__/0007_auto_20231003_1228.cpython-312.pyc +0 -0
- simo/users/migrations/__pycache__/0008_auto_20231003_1229.cpython-312.pyc +0 -0
- simo/users/migrations/__pycache__/0009_remove_user_role.cpython-312.pyc +0 -0
- simo/users/migrations/__pycache__/0010_auto_20231004_1313.cpython-312.pyc +0 -0
- simo/users/migrations/__pycache__/0011_auto_20231004_1313.cpython-312.pyc +0 -0
- simo/users/migrations/__pycache__/0012_alter_userinstancerole_unique_together.cpython-312.pyc +0 -0
- simo/users/migrations/__pycache__/0013_remove_user_roles.cpython-312.pyc +0 -0
- simo/users/migrations/__pycache__/0014_user_roles.cpython-312.pyc +0 -0
- simo/users/migrations/__pycache__/0015_remove_user_at_home.cpython-312.pyc +0 -0
- simo/users/migrations/__pycache__/0016_auto_20231005_1050.cpython-312.pyc +0 -0
- simo/users/migrations/__pycache__/0017_auto_20231221_0735.cpython-312.pyc +0 -0
- simo/users/migrations/__pycache__/0018_user_is_god.cpython-312.pyc +0 -0
- simo/users/migrations/__pycache__/0019_auto_20231221_1155.cpython-312.pyc +0 -0
- simo/users/migrations/__pycache__/0020_rename_is_god_user_is_master.cpython-312.pyc +0 -0
- simo/users/migrations/__pycache__/0021_alter_permissionsrole_instance.cpython-312.pyc +0 -0
- simo/users/migrations/__pycache__/0022_userdevicereportlog_instance.cpython-312.pyc +0 -0
- simo/users/migrations/__pycache__/0023_auto_20240105_0719.cpython-312.pyc +0 -0
- simo/users/migrations/__pycache__/0024_fingerprint.cpython-312.pyc +0 -0
- simo/users/migrations/__pycache__/0025_rename_name_fingerprint_type_and_more.cpython-312.pyc +0 -0
- simo/users/migrations/__pycache__/0026_fingerprint_name.cpython-312.pyc +0 -0
- simo/users/migrations/__pycache__/0027_permissionsrole_can_manage_components.cpython-312.pyc +0 -0
- simo/users/migrations/__pycache__/0028_auto_20240506_1146.cpython-312.pyc +0 -0
- simo/users/migrations/__pycache__/0029_alter_instanceuser_instance.cpython-312.pyc +0 -0
- simo/users/migrations/__pycache__/0030_userdevice_users.cpython-312.pyc +0 -0
- simo/users/migrations/__pycache__/0031_auto_20240923_1115.cpython-312.pyc +0 -0
- simo/users/migrations/__pycache__/0032_remove_userdevice_user_alter_userdevice_users.cpython-312.pyc +0 -0
- simo/users/migrations/__pycache__/0033_alter_user_ssh_key.cpython-312.pyc +0 -0
- simo/users/migrations/__pycache__/0034_instanceuser_last_seen_location_and_more.cpython-312.pyc +0 -0
- simo/users/migrations/__pycache__/0035_instanceuser_last_seen_speed_kmh_and_more.cpython-312.pyc +0 -0
- simo/users/migrations/__pycache__/0036_instanceuser_phone_on_charge_user_phone_on_charge.cpython-312.pyc +0 -0
- simo/users/migrations/__pycache__/0037_rename_last_seen_location_datetime_instanceuser_last_seen_and_more.cpython-312.pyc +0 -0
- simo/users/migrations/__pycache__/0038_userdevicereportlog_at_home_and_more.cpython-312.pyc +0 -0
- simo/users/migrations/__pycache__/0039_auto_20241117_1039.cpython-312.pyc +0 -0
- simo/users/migrations/__pycache__/0040_userdevicereportlog_location_smoothed_and_more.cpython-312.pyc +0 -0
- simo/users/migrations/__pycache__/0041_userdevicereportlog_speed_kmh_received.cpython-312.pyc +0 -0
- simo/users/migrations/__pycache__/0042_remove_userdevicereportlog_location_smoothed_and_more.cpython-312.pyc +0 -0
- simo/users/migrations/__pycache__/0043_userdevicereportlog_avg_speed_kmh.cpython-312.pyc +0 -0
- simo/users/migrations/__pycache__/0044_permissionsrole_is_person.cpython-312.pyc +0 -0
- simo/users/migrations/__pycache__/__init__.cpython-312.pyc +0 -0
- {simo-2.10.11.dist-info → simo-2.11.2.dist-info}/METADATA +1 -1
- {simo-2.10.11.dist-info → simo-2.11.2.dist-info}/RECORD +377 -376
- {simo-2.10.11.dist-info → simo-2.11.2.dist-info}/WHEEL +0 -0
- {simo-2.10.11.dist-info → simo-2.11.2.dist-info}/entry_points.txt +0 -0
- {simo-2.10.11.dist-info → simo-2.11.2.dist-info}/licenses/LICENSE.md +0 -0
- {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
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
simo/backups/migrations/__pycache__/0003_alter_backuplog_options_alter_backup_size.cpython-312.pyc
CHANGED
|
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
|
-
|
|
157
|
+
# Check the current node first.
|
|
158
|
+
if device.get('type') == 'lvm' and device.get('mountpoint') == '/':
|
|
145
159
|
return device
|
|
146
|
-
|
|
147
|
-
|
|
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
|
|
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
|
-
|
|
155
|
-
if
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
if
|
|
162
|
-
|
|
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
|
|
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
|
|
617
|
+
if backup_device.get('partlabel'):
|
|
216
618
|
sd_mountpoint = f"/media/{backup_device['partlabel']}"
|
|
217
|
-
elif
|
|
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
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|