simo 2.10.11__py3-none-any.whl → 2.11.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of simo might be problematic. Click here for more details.
- simo/__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 +361 -17
- 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.1.dist-info}/METADATA +1 -1
- {simo-2.10.11.dist-info → simo-2.11.1.dist-info}/RECORD +377 -376
- {simo-2.10.11.dist-info → simo-2.11.1.dist-info}/WHEEL +0 -0
- {simo-2.10.11.dist-info → simo-2.11.1.dist-info}/entry_points.txt +0 -0
- {simo-2.10.11.dist-info → simo-2.11.1.dist-info}/licenses/LICENSE.md +0 -0
- {simo-2.10.11.dist-info → simo-2.11.1.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,311 @@ 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
|
+
|
|
151
199
|
for device in lsblk_data:
|
|
152
|
-
if not device
|
|
200
|
+
if not device.get("hotplug"):
|
|
153
201
|
continue
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
202
|
+
|
|
203
|
+
# Prefer partitions explicitly labelled "BACKUP".
|
|
204
|
+
for child in device.get("children", []):
|
|
205
|
+
if _has_backup_label(child):
|
|
206
|
+
return child
|
|
207
|
+
|
|
208
|
+
# Legacy fallback – whole-disk filesystems or partitions formatted as
|
|
209
|
+
# exFAT are still recognised in order to stay compatible with drives
|
|
210
|
+
# prepared by older software versions.
|
|
211
|
+
# NOTE: We intentionally keep this logic after the new BACKUP label
|
|
212
|
+
# check so that freshly provisioned media (ext4+label) wins.
|
|
213
|
+
|
|
214
|
+
# 1. Whole-disk (no partition table) exFAT volume.
|
|
215
|
+
if (device.get("fstype") or "").lower() == "exfat":
|
|
216
|
+
return device
|
|
217
|
+
|
|
218
|
+
# 2. Partitioned removable drive – look for any exFAT child.
|
|
219
|
+
for child in device.get("children", []):
|
|
220
|
+
if (child.get("fstype") or "").lower() == "exfat":
|
|
221
|
+
return child
|
|
222
|
+
|
|
223
|
+
# Nothing has been found.
|
|
224
|
+
return None
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def _find_blank_removable_device(lsblk_data):
|
|
228
|
+
"""Return the first removable block *device* that looks empty.
|
|
229
|
+
|
|
230
|
+
A device is considered *blank* when one of the following conditions is
|
|
231
|
+
met:
|
|
232
|
+
|
|
233
|
+
1. It has no children (partitions) **and** no recognised filesystem – the
|
|
234
|
+
original behaviour that covers brand-new, uninitialised drives.
|
|
235
|
+
2. It has no children (partitions) **and** an existing filesystem that is
|
|
236
|
+
effectively empty (e.g. a freshly formatted card).
|
|
237
|
+
|
|
238
|
+
Determining if a filesystem is *empty* is tricky without mounting it, but
|
|
239
|
+
for the purpose of automatically provisioning backup media we can use a
|
|
240
|
+
pragmatic heuristic: if the device is not mounted we temporarily mount it
|
|
241
|
+
read-only to a throw-away directory, inspect its contents and then unmount
|
|
242
|
+
it again. If it **is** already mounted we reuse the existing
|
|
243
|
+
mount-point. In both cases we treat the device as blank when the root of
|
|
244
|
+
the filesystem contains no entries other than implementation-specific
|
|
245
|
+
placeholders like the *lost+found* directory created by *mkfs.ext4*.
|
|
246
|
+
|
|
247
|
+
This relaxed definition allows the backup subsystem to reuse drives that
|
|
248
|
+
have been pre-formatted by the user but never actually used to store any
|
|
249
|
+
files.
|
|
250
|
+
"""
|
|
251
|
+
|
|
252
|
+
# --- Helper inner functions ------------------------------------------------
|
|
253
|
+
|
|
254
|
+
def _device_size_bytes(dev_name: str):
|
|
255
|
+
"""Return size of *dev_name* in bytes (or ``None`` on failure)."""
|
|
256
|
+
|
|
257
|
+
for cmd in (
|
|
258
|
+
f"blockdev --getsize64 /dev/{dev_name}",
|
|
259
|
+
f"lsblk -b -dn -o SIZE /dev/{dev_name}",
|
|
260
|
+
):
|
|
261
|
+
try:
|
|
262
|
+
out = subprocess.check_output(
|
|
263
|
+
cmd, shell=True, stderr=subprocess.DEVNULL
|
|
264
|
+
).strip()
|
|
265
|
+
return int(out)
|
|
266
|
+
except Exception:
|
|
267
|
+
continue
|
|
268
|
+
return None
|
|
269
|
+
|
|
270
|
+
def _fs_is_empty(entry: dict) -> bool:
|
|
271
|
+
"""Heuristic to decide if a filesystem represented by *entry* is empty."""
|
|
272
|
+
|
|
273
|
+
fstype = entry.get("fstype")
|
|
274
|
+
if not fstype:
|
|
275
|
+
# Unformatted – treat as empty.
|
|
276
|
+
return True
|
|
277
|
+
|
|
278
|
+
mountpoint = entry.get("mountpoint")
|
|
279
|
+
cleanup = False
|
|
280
|
+
|
|
281
|
+
if not mountpoint:
|
|
282
|
+
tmp_dir = f"/tmp/simo-bk-{uuid.uuid4().hex[:8]}"
|
|
283
|
+
try:
|
|
284
|
+
os.makedirs(tmp_dir, exist_ok=True)
|
|
285
|
+
res = subprocess.run(
|
|
286
|
+
f"mount -o ro /dev/{entry['name']} {tmp_dir}",
|
|
287
|
+
shell=True,
|
|
288
|
+
stderr=subprocess.PIPE,
|
|
289
|
+
)
|
|
290
|
+
if res.returncode:
|
|
291
|
+
shutil.rmtree(tmp_dir, ignore_errors=True)
|
|
292
|
+
print(f"Unable to mount {entry['name']} to inspect contents – skip")
|
|
293
|
+
return False
|
|
294
|
+
mountpoint = tmp_dir
|
|
295
|
+
cleanup = True
|
|
296
|
+
except Exception as exc:
|
|
297
|
+
shutil.rmtree(tmp_dir, ignore_errors=True)
|
|
298
|
+
print(f"Exception while mounting {entry['name']}: {exc}")
|
|
299
|
+
return False
|
|
300
|
+
|
|
301
|
+
try:
|
|
302
|
+
with os.scandir(mountpoint) as it:
|
|
303
|
+
entries = [e.name for e in it if not e.name.startswith('.')]
|
|
304
|
+
except Exception as exc:
|
|
305
|
+
print(f"Unable to read directory listing for {entry['name']}: {exc}")
|
|
306
|
+
if cleanup:
|
|
307
|
+
subprocess.run(f"umount {mountpoint}", shell=True)
|
|
308
|
+
shutil.rmtree(mountpoint, ignore_errors=True)
|
|
309
|
+
return False
|
|
310
|
+
|
|
311
|
+
if cleanup:
|
|
312
|
+
subprocess.run(f"umount {mountpoint}", shell=True)
|
|
313
|
+
shutil.rmtree(mountpoint, ignore_errors=True)
|
|
314
|
+
|
|
315
|
+
meaningful = [e for e in entries if e not in {"lost+found"}]
|
|
316
|
+
return not meaningful
|
|
317
|
+
|
|
318
|
+
# ---------------------------------------------------------------------------
|
|
319
|
+
|
|
320
|
+
_MIN_SIZE_BYTES = 32 * 1024 * 1024 * 1024 # 32 GiB
|
|
321
|
+
|
|
322
|
+
for device in lsblk_data:
|
|
323
|
+
if not device.get("hotplug"):
|
|
324
|
+
continue
|
|
325
|
+
|
|
326
|
+
size_bytes = _device_size_bytes(device["name"])
|
|
327
|
+
if size_bytes is None:
|
|
328
|
+
print(f"Could not obtain capacity of: {device['name']}")
|
|
329
|
+
continue
|
|
330
|
+
|
|
331
|
+
if size_bytes < _MIN_SIZE_BYTES:
|
|
332
|
+
print(f"Too small (<32 GiB): {device['name']}")
|
|
333
|
+
continue
|
|
334
|
+
|
|
335
|
+
children = device.get("children") or []
|
|
336
|
+
|
|
337
|
+
if not children:
|
|
338
|
+
# Whole-disk filesystem.
|
|
339
|
+
if _fs_is_empty(device):
|
|
340
|
+
return device
|
|
341
|
+
print(f"Whole-disk filesystem on {device['name']} is not empty – skip")
|
|
342
|
+
continue
|
|
343
|
+
|
|
344
|
+
if len(children) == 1:
|
|
345
|
+
child = children[0]
|
|
346
|
+
if _fs_is_empty(child):
|
|
347
|
+
return device
|
|
348
|
+
print(f"Single partition {child['name']} on {device['name']} is not empty – skip")
|
|
349
|
+
continue
|
|
350
|
+
|
|
351
|
+
print(f"More than one partition on {device['name']} – skip")
|
|
352
|
+
|
|
353
|
+
return None
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
def _ensure_rescue_image_written(blank_device_name: str):
|
|
357
|
+
"""Write *rescue.img* to the given **whole-disk** device.
|
|
358
|
+
|
|
359
|
+
The function is intentionally idempotent – if writing fails the caller can
|
|
360
|
+
attempt to call it again (e.g. the next time the periodic task runs).
|
|
361
|
+
|
|
362
|
+
It raises an exception on irrecoverable errors so that the caller can log
|
|
363
|
+
the failure.
|
|
364
|
+
"""
|
|
365
|
+
|
|
366
|
+
import tarfile, time
|
|
367
|
+
|
|
368
|
+
img_path = os.path.join(os.path.dirname(__file__), "rescue.img.xz")
|
|
369
|
+
|
|
370
|
+
# Write the image. We deliberately avoid using *python-dd* wrappers and
|
|
371
|
+
# rely on the time-tested `dd(1)` command.
|
|
372
|
+
dd_cmd = (
|
|
373
|
+
f"xzcat {img_path} | dd of=/dev/{blank_device_name} bs=4M conv=fsync"
|
|
374
|
+
)
|
|
375
|
+
res = subprocess.run(dd_cmd, shell=True, stderr=subprocess.PIPE)
|
|
376
|
+
if res.returncode:
|
|
377
|
+
raise RuntimeError(
|
|
378
|
+
f"Writing rescue image failed: {res.stderr.decode(errors='ignore')}"
|
|
379
|
+
)
|
|
380
|
+
|
|
381
|
+
# Make sure the kernel notices the new partition table.
|
|
382
|
+
subprocess.run(f"partprobe /dev/{blank_device_name}", shell=True)
|
|
383
|
+
|
|
384
|
+
# Give the device a moment to settle.
|
|
385
|
+
time.sleep(2)
|
|
386
|
+
|
|
387
|
+
# Enlarge the 3rd partition (BACKUP) to the rest of the disk and create /
|
|
388
|
+
# extend the exFAT filesystem. This is wrapped in a helper to keep the
|
|
389
|
+
# main flow readable.
|
|
390
|
+
_expand_backup_partition(blank_device_name)
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
def _expand_backup_partition(device_name: str):
|
|
394
|
+
"""Make partition 3 span leftover space and be ext4 labelled BACKUP.
|
|
395
|
+
|
|
396
|
+
Implementation is intentionally minimal and resilient:
|
|
397
|
+
– Use *sgdisk* only (no interactive prompts).
|
|
398
|
+
– Delete partition 3 (if present) and create a new one that fills all
|
|
399
|
+
remaining free space.
|
|
400
|
+
– Always create a fresh ext4 filesystem labelled BACKUP.
|
|
401
|
+
Because the rescue-image just flashed is empty, data loss is not a
|
|
402
|
+
concern and this deterministic route avoids edge-case errors.
|
|
403
|
+
"""
|
|
404
|
+
|
|
405
|
+
import time, shutil
|
|
406
|
+
|
|
407
|
+
def _dev_path(base: str) -> str:
|
|
408
|
+
"""Return /dev/<base>3 path handling devices that need 'p3'."""
|
|
409
|
+
direct = f"/dev/{base}3"
|
|
410
|
+
with_p = f"/dev/{base}p3"
|
|
411
|
+
return direct if os.path.exists(direct) else with_p
|
|
412
|
+
|
|
413
|
+
# 1. Ensure GPT headers cover the whole disk (harmless if already OK).
|
|
414
|
+
subprocess.run(f"sgdisk -e /dev/{device_name}", shell=True)
|
|
415
|
+
|
|
416
|
+
# 2. Drop existing partition 3 (ignore errors when it does not exist).
|
|
417
|
+
subprocess.run(f"sgdisk -d 3 /dev/{device_name}", shell=True)
|
|
418
|
+
|
|
419
|
+
# 3. Create new Linux filesystem partition occupying the rest of the disk.
|
|
420
|
+
create_cmd = f"sgdisk -n 3:0:0 -t 3:8300 -c 3:BACKUP /dev/{device_name}"
|
|
421
|
+
res = subprocess.run(create_cmd, shell=True, stderr=subprocess.PIPE)
|
|
422
|
+
if res.returncode:
|
|
423
|
+
raise RuntimeError(
|
|
424
|
+
"sgdisk failed to create BACKUP partition: " +
|
|
425
|
+
res.stderr.decode(errors="ignore")
|
|
426
|
+
)
|
|
427
|
+
|
|
428
|
+
# 4. Inform kernel and wait for udev.
|
|
429
|
+
subprocess.run(f"partprobe /dev/{device_name}", shell=True)
|
|
430
|
+
subprocess.run("udevadm settle", shell=True)
|
|
431
|
+
|
|
432
|
+
part_path = _dev_path(device_name)
|
|
433
|
+
for _ in range(5):
|
|
434
|
+
if os.path.exists(part_path):
|
|
435
|
+
break
|
|
436
|
+
time.sleep(1)
|
|
437
|
+
else:
|
|
438
|
+
raise RuntimeError("/dev node for new BACKUP partition did not appear")
|
|
439
|
+
|
|
440
|
+
# 5. Always create a fresh ext4 filesystem; wipe old signatures first.
|
|
441
|
+
subprocess.run(f"wipefs -a {part_path}", shell=True)
|
|
442
|
+
mkfs_cmd = f"mkfs.ext4 -F -L BACKUP {part_path}"
|
|
443
|
+
res = subprocess.run(mkfs_cmd, shell=True, stderr=subprocess.PIPE)
|
|
444
|
+
if res.returncode:
|
|
445
|
+
raise RuntimeError(
|
|
446
|
+
"mkfs.ext4 failed for BACKUP partition: " + res.stderr.decode(errors="ignore")
|
|
447
|
+
)
|
|
163
448
|
|
|
164
449
|
|
|
165
450
|
def get_partitions():
|
|
@@ -205,16 +490,39 @@ def get_partitions():
|
|
|
205
490
|
|
|
206
491
|
backup_device = get_backup_device(lsblk_data)
|
|
207
492
|
|
|
493
|
+
# If no suitable partition is available try to prepare one automatically.
|
|
494
|
+
if not backup_device:
|
|
495
|
+
blank_dev = _find_blank_removable_device(lsblk_data)
|
|
496
|
+
if blank_dev:
|
|
497
|
+
try:
|
|
498
|
+
_ensure_rescue_image_written(blank_dev["name"])
|
|
499
|
+
except Exception as exc:
|
|
500
|
+
BackupLog.objects.create(
|
|
501
|
+
level="error",
|
|
502
|
+
msg=(
|
|
503
|
+
"Can't prepare backup drive automatically.\n\n" +
|
|
504
|
+
str(exc)
|
|
505
|
+
),
|
|
506
|
+
)
|
|
507
|
+
else:
|
|
508
|
+
# Re-read block devices so that the freshly written partition
|
|
509
|
+
# table appears in *lsblk* output.
|
|
510
|
+
lsblk_data = json.loads(subprocess.check_output(
|
|
511
|
+
'lsblk --output NAME,HOTPLUG,MOUNTPOINT,FSTYPE,TYPE,LABEL,PARTLABEL --json',
|
|
512
|
+
shell=True
|
|
513
|
+
).decode())['blockdevices']
|
|
514
|
+
backup_device = get_backup_device(lsblk_data)
|
|
515
|
+
|
|
208
516
|
if not backup_device:
|
|
209
517
|
BackupLog.objects.create(
|
|
210
518
|
level='warning',
|
|
211
|
-
msg="Can't backup. No external
|
|
519
|
+
msg="Can't backup. No external BACKUP partition found and no blank removable device was available."
|
|
212
520
|
)
|
|
213
521
|
return
|
|
214
522
|
|
|
215
|
-
if
|
|
523
|
+
if backup_device.get('partlabel'):
|
|
216
524
|
sd_mountpoint = f"/media/{backup_device['partlabel']}"
|
|
217
|
-
elif
|
|
525
|
+
elif backup_device.get('label'):
|
|
218
526
|
sd_mountpoint = f"/media/{backup_device['label']}"
|
|
219
527
|
else:
|
|
220
528
|
sd_mountpoint = f"/media/{backup_device['name']}"
|
|
@@ -237,6 +545,8 @@ def get_partitions():
|
|
|
237
545
|
|
|
238
546
|
@celery_app.task
|
|
239
547
|
def perform_backup():
|
|
548
|
+
from simo.core.models import Instance
|
|
549
|
+
from simo.core.middleware import drop_current_instance
|
|
240
550
|
from simo.backups.models import BackupLog
|
|
241
551
|
try:
|
|
242
552
|
lv_group, lv_name, sd_mountpoint = get_partitions()
|
|
@@ -267,6 +577,12 @@ def perform_backup():
|
|
|
267
577
|
mac = str(hex(uuid.getnode()))
|
|
268
578
|
device_backups_path = f'{sd_mountpoint}/simo_backups/hub-{mac}'
|
|
269
579
|
|
|
580
|
+
drop_current_instance()
|
|
581
|
+
hub_meta = {
|
|
582
|
+
'instances': [inst.name for inst in Instance.objects.all()]
|
|
583
|
+
}
|
|
584
|
+
with open(os.path.join(device_backups_path, 'hub_meta.json'), 'w') as f:
|
|
585
|
+
f.write(json.dumps(hub_meta))
|
|
270
586
|
|
|
271
587
|
now = datetime.now()
|
|
272
588
|
month_folder = os.path.join(
|
|
@@ -288,6 +604,29 @@ def perform_backup():
|
|
|
288
604
|
f'borg init --encryption=none {month_folder}', shell=True
|
|
289
605
|
)
|
|
290
606
|
|
|
607
|
+
# ------------------------------------------------------------------
|
|
608
|
+
# Ensure that files stored on *separate* partitions – most importantly
|
|
609
|
+
# the /boot (kernel & initrd images) and /boot/efi (EFI System
|
|
610
|
+
# Partition) – are included in the snapshot. Otherwise the rescue
|
|
611
|
+
# procedure restores an empty /boot which leaves the system un-bootable
|
|
612
|
+
# once GRUB hands over control to the (missing) kernel.
|
|
613
|
+
#
|
|
614
|
+
# We temporarily bind-mount those paths into the read-only snapshot so
|
|
615
|
+
# that Borg treats them as regular directories residing on the same
|
|
616
|
+
# filesystem tree.
|
|
617
|
+
# ------------------------------------------------------------------
|
|
618
|
+
|
|
619
|
+
bind_mounts = []
|
|
620
|
+
for path in ("/boot", "/boot/efi"):
|
|
621
|
+
target = os.path.join(snap_mount_point, path.lstrip("/"))
|
|
622
|
+
# Create the mount-point inside the snapshot and bind-mount the live
|
|
623
|
+
# directory if it exists.
|
|
624
|
+
if os.path.ismount(path):
|
|
625
|
+
os.makedirs(target, exist_ok=True)
|
|
626
|
+
subprocess.run(["mount", "--bind", path, target], check=True)
|
|
627
|
+
bind_mounts.append(target)
|
|
628
|
+
|
|
629
|
+
# Directories that are safe to exclude – keep /boot out of this list!
|
|
291
630
|
exclude_dirs = (
|
|
292
631
|
'tmp', 'lost+found', 'proc', 'cdrom', 'dev', 'mnt', 'sys', 'run',
|
|
293
632
|
'var/tmp', 'var/cache', 'var/log', 'media',
|
|
@@ -326,6 +665,11 @@ def perform_backup():
|
|
|
326
665
|
stdout=subprocess.PIPE, stderr=subprocess.PIPE
|
|
327
666
|
)
|
|
328
667
|
|
|
668
|
+
# Unmount previously created bind-mounts (boot / boot/efi) *before*
|
|
669
|
+
# removing the snapshot so that no busy references remain.
|
|
670
|
+
for mnt in reversed(bind_mounts):
|
|
671
|
+
subprocess.run(["umount", mnt])
|
|
672
|
+
|
|
329
673
|
subprocess.run(["umount", snap_mount_point])
|
|
330
674
|
subprocess.run(
|
|
331
675
|
f"lvremove -f {lv_group}/{snap_name}", shell=True
|
|
@@ -421,4 +765,4 @@ def setup_periodic_tasks(sender, **kwargs):
|
|
|
421
765
|
sender.add_periodic_task(60 * 60, check_backups.s())
|
|
422
766
|
# perform auto backup every 12 hours
|
|
423
767
|
sender.add_periodic_task(60 * 60 * 12, perform_backup.s())
|
|
424
|
-
sender.add_periodic_task(60 * 60, clean_old_logs.s())
|
|
768
|
+
sender.add_periodic_task(60 * 60, clean_old_logs.s())
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
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/core/controllers.py
CHANGED
|
@@ -265,9 +265,7 @@ class ControllerBase(ABC):
|
|
|
265
265
|
except:
|
|
266
266
|
pass
|
|
267
267
|
|
|
268
|
-
|
|
269
|
-
self.component.gateway, self.component, set_val=value
|
|
270
|
-
).publish()
|
|
268
|
+
self._send_to_device(value)
|
|
271
269
|
if value != self.component.value:
|
|
272
270
|
self.component.value_previous = self.component.value
|
|
273
271
|
self.component.value = value
|
|
@@ -316,6 +314,11 @@ class ControllerBase(ABC):
|
|
|
316
314
|
).first()
|
|
317
315
|
self.component.save()
|
|
318
316
|
|
|
317
|
+
def _send_to_device(self, value):
|
|
318
|
+
GatewayObjectCommand(
|
|
319
|
+
self.component.gateway, self.component, set_val=value
|
|
320
|
+
).publish()
|
|
321
|
+
|
|
319
322
|
def _receive_from_device(
|
|
320
323
|
self, value, is_alive=True, battery_level=None, error_msg=None
|
|
321
324
|
):
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|