fixos 2.2.53__tar.gz → 2.2.54__tar.gz
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.
- {fixos-2.2.53 → fixos-2.2.54}/PKG-INFO +1 -1
- fixos-2.2.54/VERSION +1 -0
- fixos-2.2.54/fixos/cli/_cleanup_docker_volumes.py +221 -0
- {fixos-2.2.53 → fixos-2.2.54}/fixos/cli/cleanup_cmd.py +38 -2
- {fixos-2.2.53 → fixos-2.2.54}/fixos/config.py +1 -1
- {fixos-2.2.53 → fixos-2.2.54}/fixos/diagnostics/_storage_container_mixin.py +8 -4
- {fixos-2.2.53 → fixos-2.2.54}/fixos.egg-info/PKG-INFO +1 -1
- {fixos-2.2.53 → fixos-2.2.54}/fixos.egg-info/SOURCES.txt +2 -0
- {fixos-2.2.53 → fixos-2.2.54}/pyproject.toml +1 -1
- fixos-2.2.54/tests/unit/test_cleanup_docker_volumes.py +161 -0
- fixos-2.2.53/VERSION +0 -1
- {fixos-2.2.53 → fixos-2.2.54}/.env.example +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/CHANGELOG.md +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/LICENSE +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/MANIFEST.in +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/README.md +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/docker/README.md +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/docker/TEST_RESULTS.md +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/docker/TEST_RESULTS_V2.md +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/docker/alpine/Dockerfile +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/docker/arch/Dockerfile +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/docker/base/Dockerfile +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/docker/broken-audio/Dockerfile +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/docker/broken-full/Dockerfile +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/docker/broken-network/Dockerfile +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/docker/broken-thumbnails/Dockerfile +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/docker/debian/Dockerfile +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/docker/docker-compose.multi-system.yml +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/docker/docker-compose.yml +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/docker/fedora/Dockerfile +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/docker/test-multi-system.sh +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/docker/test-scenarios.sh +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/docker/ubuntu/Dockerfile +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/docker/validate-scenario.py +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/docs/examples/advanced_usage.py +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/docs/examples/quickstart.py +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/fixos/__init__.py +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/fixos/__main__.py +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/fixos/agent/__init__.py +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/fixos/agent/autonomous.py +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/fixos/agent/autonomous_session.py +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/fixos/agent/hitl.py +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/fixos/agent/hitl_session.py +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/fixos/agent/session_core.py +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/fixos/agent/session_handlers.py +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/fixos/agent/session_io.py +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/fixos/anonymizer.py +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/fixos/cli/__init__.py +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/fixos/cli/__main__.py +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/fixos/cli/_cleanup_flatpak.py +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/fixos/cli/_cleanup_home.py +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/fixos/cli/_cleanup_snap.py +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/fixos/cli/_cleanup_space.py +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/fixos/cli/_cleanup_system.py +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/fixos/cli/_cleanup_utils.py +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/fixos/cli/_cleanup_venvs.py +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/fixos/cli/ask_cmd.py +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/fixos/cli/config_cmd.py +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/fixos/cli/dashboard.py +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/fixos/cli/features_cmd.py +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/fixos/cli/fix_cmd.py +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/fixos/cli/history_cmd.py +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/fixos/cli/jetbrains_cmd.py +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/fixos/cli/main.py +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/fixos/cli/orchestrate_cmd.py +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/fixos/cli/output_formatter.py +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/fixos/cli/profile_cmd.py +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/fixos/cli/projects_cmd.py +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/fixos/cli/provider_cmd.py +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/fixos/cli/quick_cmd.py +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/fixos/cli/quickfix_cmd.py +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/fixos/cli/report_cmd.py +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/fixos/cli/rollback_cmd.py +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/fixos/cli/scan_cmd.py +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/fixos/cli/shared.py +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/fixos/cli/shell_cmd.py +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/fixos/cli/token_cmd.py +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/fixos/cli/watch_cmd.py +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/fixos/cli.py +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/fixos/config_interactive.py +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/fixos/constants.py +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/fixos/diagnostics/__init__.py +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/fixos/diagnostics/_flatpak_analysis_mixin.py +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/fixos/diagnostics/_flatpak_execution_mixin.py +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/fixos/diagnostics/_flatpak_recommendations_mixin.py +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/fixos/diagnostics/_storage_system_mixin.py +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/fixos/diagnostics/_storage_user_mixin.py +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/fixos/diagnostics/cache_discovery.py +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/fixos/diagnostics/checks/__init__.py +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/fixos/diagnostics/checks/_shared.py +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/fixos/diagnostics/checks/audio.py +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/fixos/diagnostics/checks/file_analysis.py +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/fixos/diagnostics/checks/hardware.py +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/fixos/diagnostics/checks/packages.py +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/fixos/diagnostics/checks/resources.py +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/fixos/diagnostics/checks/security.py +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/fixos/diagnostics/checks/storage_optimization.py +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/fixos/diagnostics/checks/system_core.py +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/fixos/diagnostics/checks/thumbnails.py +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/fixos/diagnostics/dev_project_analyzer.py +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/fixos/diagnostics/disk_analyzer.py +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/fixos/diagnostics/docker_network_cleanup.py +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/fixos/diagnostics/docker_startup_optimizer.py +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/fixos/diagnostics/flatpak_analyzer.py +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/fixos/diagnostics/jetbrains_ai.py +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/fixos/diagnostics/jetbrains_recovery.py +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/fixos/diagnostics/native_scan.py +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/fixos/diagnostics/orphaned_workloads.py +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/fixos/diagnostics/process_chains.py +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/fixos/diagnostics/project_scanner.py +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/fixos/diagnostics/quick_snapshot.py +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/fixos/diagnostics/service_cleanup.py +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/fixos/diagnostics/service_details.py +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/fixos/diagnostics/service_scanner.py +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/fixos/diagnostics/storage_analyzer.py +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/fixos/diagnostics/system_checks.py +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/fixos/diagnostics/utils.py +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/fixos/diagnostics/venv_cleanup.py +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/fixos/endpoint_refresh.py +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/fixos/features/__init__.py +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/fixos/features/auditor.py +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/fixos/features/catalog.py +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/fixos/features/data/packages.yaml +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/fixos/features/data/profiles/developer.yaml +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/fixos/features/data/profiles/office.yaml +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/fixos/features/data/profiles/sysadmin.yaml +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/fixos/features/installer.py +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/fixos/features/profiles.py +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/fixos/features/renderer.py +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/fixos/fixes/__init__.py +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/fixos/interactive/__init__.py +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/fixos/interactive/cleanup_planner.py +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/fixos/llm_shell.py +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/fixos/orchestrator/__init__.py +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/fixos/orchestrator/executor.py +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/fixos/orchestrator/graph.py +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/fixos/orchestrator/orchestrator.py +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/fixos/orchestrator/rollback.py +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/fixos/orphan_pins.py +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/fixos/platform_utils.py +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/fixos/plugins/__init__.py +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/fixos/plugins/base.py +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/fixos/plugins/builtin/__init__.py +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/fixos/plugins/builtin/audio.py +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/fixos/plugins/builtin/disk.py +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/fixos/plugins/builtin/hardware.py +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/fixos/plugins/builtin/resources.py +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/fixos/plugins/builtin/security.py +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/fixos/plugins/builtin/thumbnails.py +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/fixos/plugins/registry.py +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/fixos/profiles/__init__.py +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/fixos/profiles/desktop.yaml +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/fixos/profiles/developer.yaml +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/fixos/profiles/minimal.yaml +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/fixos/profiles/server.yaml +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/fixos/providers/__init__.py +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/fixos/providers/gemini_native.py +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/fixos/providers/llm.py +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/fixos/providers/llm_analyzer.py +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/fixos/providers/schemas.py +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/fixos/system_checks.py +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/fixos/utils/__init__.py +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/fixos/utils/anonymizer.py +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/fixos/utils/terminal.py +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/fixos/utils/timeout.py +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/fixos/utils/web_search.py +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/fixos/watch.py +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/fixos.egg-info/dependency_links.txt +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/fixos.egg-info/entry_points.txt +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/fixos.egg-info/requires.txt +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/fixos.egg-info/top_level.txt +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/pytest.ini +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/requirements-dev.txt +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/requirements.txt +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/scripts/pyqual-calibrate.py +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/setup.cfg +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/setup.py +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/tests/__init__.py +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/tests/conftest.py +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/tests/e2e/__init__.py +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/tests/e2e/test_anonymization_layers.py +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/tests/e2e/test_audio_broken.py +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/tests/e2e/test_cli.py +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/tests/e2e/test_diagnostics_integration.py +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/tests/e2e/test_executor.py +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/tests/e2e/test_multi_system.py +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/tests/e2e/test_network_broken.py +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/tests/e2e/test_thumbnails_broken.py +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/tests/test_fixos.py +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/tests/unit/__init__.py +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/tests/unit/test_anonymizer.py +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/tests/unit/test_ask_and_nl_group.py +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/tests/unit/test_cache_discovery.py +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/tests/unit/test_cleanup_cmd.py +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/tests/unit/test_cleanup_effectiveness.py +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/tests/unit/test_cleanup_planner.py +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/tests/unit/test_cleanup_retention.py +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/tests/unit/test_cleanup_safety.py +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/tests/unit/test_cleanup_space.py +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/tests/unit/test_cleanup_tmp.py +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/tests/unit/test_cleanup_utils.py +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/tests/unit/test_cli_main.py +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/tests/unit/test_core.py +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/tests/unit/test_dashboard.py +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/tests/unit/test_dev_project_analyzer.py +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/tests/unit/test_disk_analyzer.py +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/tests/unit/test_docker_network_cleanup.py +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/tests/unit/test_docker_startup_optimizer.py +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/tests/unit/test_endpoint_refresh.py +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/tests/unit/test_executor.py +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/tests/unit/test_gemini_native_client.py +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/tests/unit/test_jetbrains_cmd.py +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/tests/unit/test_jetbrains_recovery.py +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/tests/unit/test_llm_auth_stop.py +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/tests/unit/test_llm_provider.py +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/tests/unit/test_native_scan.py +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/tests/unit/test_ollama_endpoint.py +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/tests/unit/test_orchestrator.py +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/tests/unit/test_orphan_pins.py +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/tests/unit/test_orphaned_workloads.py +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/tests/unit/test_packaging_data.py +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/tests/unit/test_process_chains.py +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/tests/unit/test_project_scanner.py +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/tests/unit/test_projects_cmd.py +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/tests/unit/test_quick_cmd.py +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/tests/unit/test_quick_snapshot.py +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/tests/unit/test_resources_quick.py +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/tests/unit/test_security_quick.py +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/tests/unit/test_service_cleanup.py +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/tests/unit/test_service_scanner.py +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/tests/unit/test_shell_ux.py +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/tests/unit/test_storage_analyzer.py +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/tests/unit/test_system_checks_parallel.py +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/tests/unit/test_venv_cleanup.py +0 -0
- {fixos-2.2.53 → fixos-2.2.54}/tests/unit/test_version_parity.py +0 -0
fixos-2.2.54/VERSION
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
2.2.54
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
"""Selective removal of unused (dangling) Docker volumes.
|
|
2
|
+
|
|
3
|
+
Volumes hold real data, so nothing here is bulk: candidates are listed with
|
|
4
|
+
name, Compose project and age, the user picks them, and each one is checked
|
|
5
|
+
again right before ``docker volume rm``. ``--yes`` never selects volumes whose
|
|
6
|
+
name looks like stored data (databases, git mirrors, backups, ...).
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
import os
|
|
13
|
+
import re
|
|
14
|
+
import subprocess
|
|
15
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
16
|
+
from datetime import datetime, timezone
|
|
17
|
+
|
|
18
|
+
import click
|
|
19
|
+
|
|
20
|
+
from fixos.cli import _cleanup_space as space
|
|
21
|
+
from fixos.cli._cleanup_utils import _format_bytes
|
|
22
|
+
|
|
23
|
+
# Name tokens that usually mean irreplaceable data rather than a cache.
|
|
24
|
+
_DATA_TOKENS = re.compile(
|
|
25
|
+
r"(?:^|[_.-])(mirror|git|gitea|onedev|repo|repos|postgres|postgresql|pg|"
|
|
26
|
+
r"mysql|mariadb|mongo|mongodb|redis|db|database|backup|backups|minio|s3|"
|
|
27
|
+
r"vault|secrets?|keycloak|grafana|prometheus|influx|elastic|opensearch|"
|
|
28
|
+
r"nextcloud|uploads|media|webui|openwebui|open-webui|n8n|storage|certs|"
|
|
29
|
+
r"letsencrypt|wordpress|mattermost|synapse|chat-history)(?:$|[_.-])",
|
|
30
|
+
re.IGNORECASE,
|
|
31
|
+
)
|
|
32
|
+
_INSPECT_BATCH = 100
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _docker_lines(args: list[str], timeout: int = 30) -> list[str] | None:
|
|
36
|
+
try:
|
|
37
|
+
result = space._run_cmd(["docker", *args], timeout=timeout)
|
|
38
|
+
except (OSError, subprocess.TimeoutExpired):
|
|
39
|
+
return None
|
|
40
|
+
if result.returncode != 0:
|
|
41
|
+
return None
|
|
42
|
+
return [line for line in result.stdout.splitlines() if line.strip()]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _is_unused(name: str) -> bool:
|
|
46
|
+
"""Re-check right before removal: still dangling and no container (even stopped)."""
|
|
47
|
+
dangling = _docker_lines(["volume", "ls", "-q", "-f", "dangling=true", "-f", f"name=^{name}$"])
|
|
48
|
+
containers = _docker_lines(["ps", "-aq", "--filter", f"volume={name}"])
|
|
49
|
+
return dangling is not None and name in dangling and containers == []
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _age_days(created_at: str, now: datetime) -> float | None:
|
|
53
|
+
try:
|
|
54
|
+
created = datetime.fromisoformat(created_at.replace("Z", "+00:00"))
|
|
55
|
+
except ValueError:
|
|
56
|
+
return None
|
|
57
|
+
if created.tzinfo is None:
|
|
58
|
+
created = created.replace(tzinfo=timezone.utc)
|
|
59
|
+
return (now - created).total_seconds() / 86400
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
_SIZE_WORKERS = 8
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _du_batch(command: list[str], paths: list[str]) -> dict[str, int]:
|
|
66
|
+
sizes: dict[str, int] = {}
|
|
67
|
+
try:
|
|
68
|
+
result = space._run_cmd([*command, "-sbl0", "--", *paths], timeout=300)
|
|
69
|
+
except (OSError, subprocess.TimeoutExpired):
|
|
70
|
+
return sizes
|
|
71
|
+
for record in result.stdout.split("\0"):
|
|
72
|
+
size, sep, path = record.partition("\t")
|
|
73
|
+
if sep and size.isdigit():
|
|
74
|
+
sizes[path] = int(size)
|
|
75
|
+
return sizes
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _volume_sizes(mountpoints: list[str]) -> dict[str, int]:
|
|
79
|
+
"""Sizes via ``du`` in parallel batches; root-owned volumes use ``sudo -n``.
|
|
80
|
+
|
|
81
|
+
``sudo -n`` never prompts: without passwordless sudo the size stays unknown.
|
|
82
|
+
Unreadable mountpoints go straight to ``sudo -n``, so a partial size of an
|
|
83
|
+
unreadable directory is never reported.
|
|
84
|
+
"""
|
|
85
|
+
readable = [m for m in mountpoints if os.access(m, os.R_OK | os.X_OK)]
|
|
86
|
+
privileged = [m for m in mountpoints if m not in set(readable)]
|
|
87
|
+
jobs = [(["du"], readable[i::_SIZE_WORKERS]) for i in range(_SIZE_WORKERS)]
|
|
88
|
+
jobs += [(["sudo", "-n", "du"], privileged[i::_SIZE_WORKERS]) for i in range(_SIZE_WORKERS)]
|
|
89
|
+
jobs = [(command, paths) for command, paths in jobs if paths]
|
|
90
|
+
sizes: dict[str, int] = {}
|
|
91
|
+
if not jobs:
|
|
92
|
+
return sizes
|
|
93
|
+
with ThreadPoolExecutor(max_workers=min(_SIZE_WORKERS, len(jobs))) as pool:
|
|
94
|
+
for result in pool.map(lambda job: _du_batch(*job), jobs):
|
|
95
|
+
sizes.update(result)
|
|
96
|
+
return sizes
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _unused_volumes(
|
|
100
|
+
days: float | None, match: list[str], now: datetime | None = None
|
|
101
|
+
) -> list[dict] | None:
|
|
102
|
+
names = _docker_lines(["volume", "ls", "-q", "-f", "dangling=true"])
|
|
103
|
+
if names is None:
|
|
104
|
+
return None
|
|
105
|
+
now = now or datetime.now(timezone.utc)
|
|
106
|
+
records: list[dict] = []
|
|
107
|
+
for start in range(0, len(names), _INSPECT_BATCH):
|
|
108
|
+
raw = _docker_lines(["volume", "inspect", *names[start : start + _INSPECT_BATCH]], timeout=60)
|
|
109
|
+
if raw is None:
|
|
110
|
+
return None
|
|
111
|
+
records.extend(json.loads("\n".join(raw)))
|
|
112
|
+
candidates = []
|
|
113
|
+
for record in records:
|
|
114
|
+
name = record.get("Name", "")
|
|
115
|
+
if match and not any(token.lower() in name.lower() for token in match):
|
|
116
|
+
continue
|
|
117
|
+
age = _age_days(record.get("CreatedAt", ""), now)
|
|
118
|
+
if days is not None and (age is None or age < days):
|
|
119
|
+
continue
|
|
120
|
+
labels = record.get("Labels") or {}
|
|
121
|
+
project = labels.get("com.docker.compose.project") or "(bez projektu)"
|
|
122
|
+
protected = bool(_DATA_TOKENS.search(name))
|
|
123
|
+
candidates.append(
|
|
124
|
+
{
|
|
125
|
+
"name": name,
|
|
126
|
+
"description": name,
|
|
127
|
+
"project": project,
|
|
128
|
+
"age_days": age,
|
|
129
|
+
"mountpoint": record.get("Mountpoint", ""),
|
|
130
|
+
"protected": protected,
|
|
131
|
+
"size_bytes": 0,
|
|
132
|
+
"size_known": False,
|
|
133
|
+
}
|
|
134
|
+
)
|
|
135
|
+
sizes = _volume_sizes([c["mountpoint"] for c in candidates if c["mountpoint"]])
|
|
136
|
+
for candidate in candidates:
|
|
137
|
+
if candidate["mountpoint"] in sizes:
|
|
138
|
+
candidate["size_bytes"] = sizes[candidate["mountpoint"]]
|
|
139
|
+
candidate["size_known"] = True
|
|
140
|
+
age = candidate["age_days"]
|
|
141
|
+
size = _format_bytes(candidate["size_bytes"]) if candidate["size_known"] else "rozmiar nieznany"
|
|
142
|
+
candidate["note"] = (
|
|
143
|
+
f"{candidate['project']}, {age:.0f} dni" if age is not None else candidate["project"]
|
|
144
|
+
) + (" — wygląda na DANE, tylko ręczny wybór" if candidate["protected"] else "")
|
|
145
|
+
candidate["size_label"] = size
|
|
146
|
+
candidates.sort(key=lambda c: (c["size_known"], c["size_bytes"]), reverse=True)
|
|
147
|
+
return candidates
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _cleanup_docker_volumes(
|
|
151
|
+
json_output: bool,
|
|
152
|
+
dry_run: bool,
|
|
153
|
+
list_only: bool,
|
|
154
|
+
yes: bool,
|
|
155
|
+
days: float | None = None,
|
|
156
|
+
match: list[str] | None = None,
|
|
157
|
+
) -> None:
|
|
158
|
+
"""List unused Docker volumes and remove the selected ones."""
|
|
159
|
+
candidates = _unused_volumes(days, match or [])
|
|
160
|
+
if json_output:
|
|
161
|
+
space._echo_json(
|
|
162
|
+
{
|
|
163
|
+
"error": None if candidates is not None else "docker volume ls failed",
|
|
164
|
+
"candidates": [
|
|
165
|
+
{k: c[k] for k in ("name", "project", "age_days", "size_bytes", "size_known", "protected")}
|
|
166
|
+
for c in candidates or []
|
|
167
|
+
],
|
|
168
|
+
}
|
|
169
|
+
)
|
|
170
|
+
return
|
|
171
|
+
if candidates is None:
|
|
172
|
+
click.echo(click.style("Błąd: nie udało się odczytać wolumenów Dockera.", fg="red"))
|
|
173
|
+
return
|
|
174
|
+
if not candidates:
|
|
175
|
+
click.echo(click.style("Brak nieużywanych wolumenów Dockera dla podanych filtrów.", fg="green"))
|
|
176
|
+
return
|
|
177
|
+
known = sum(c["size_bytes"] for c in candidates if c["size_known"])
|
|
178
|
+
click.echo(
|
|
179
|
+
f"\n{click.style('DOCKER — nieużywane wolumeny (dane!):', fg='magenta', bold=True)} "
|
|
180
|
+
f"{len(candidates)}, znany rozmiar {_format_bytes(known)}"
|
|
181
|
+
)
|
|
182
|
+
for index, item in enumerate(candidates, 1):
|
|
183
|
+
color = "red" if item["protected"] else "yellow"
|
|
184
|
+
click.echo(
|
|
185
|
+
f" [{index:3d}] {click.style(item['name'], fg=color)}: {item['size_label']} — {item['note']}"
|
|
186
|
+
)
|
|
187
|
+
if list_only:
|
|
188
|
+
return
|
|
189
|
+
if yes:
|
|
190
|
+
selected = [i for i, c in enumerate(candidates) if not c["protected"]]
|
|
191
|
+
skipped = len(candidates) - len(selected)
|
|
192
|
+
if skipped:
|
|
193
|
+
click.echo(f" --yes pomija {skipped} wolumenów wyglądających na dane; wybierz je ręcznie.")
|
|
194
|
+
else:
|
|
195
|
+
selected = space._choose(candidates, "Numery wolumenów do usunięcia (np. 1,2, all)")
|
|
196
|
+
if not selected:
|
|
197
|
+
click.echo("Pominięto.")
|
|
198
|
+
return
|
|
199
|
+
if dry_run:
|
|
200
|
+
space._show_dry_run_commands([["docker", "volume", "rm", candidates[i]["name"]] for i in selected])
|
|
201
|
+
return
|
|
202
|
+
if not yes and not space._confirm_remove():
|
|
203
|
+
click.echo("Anulowano.")
|
|
204
|
+
return
|
|
205
|
+
freed = 0
|
|
206
|
+
for index in selected:
|
|
207
|
+
name = candidates[index]["name"]
|
|
208
|
+
if not _is_unused(name):
|
|
209
|
+
click.echo(f" ⏭️ {name}: jest już używany albo nie istnieje — pominięto")
|
|
210
|
+
continue
|
|
211
|
+
try:
|
|
212
|
+
result = space._run_cmd(["docker", "volume", "rm", name], timeout=120)
|
|
213
|
+
except (OSError, subprocess.TimeoutExpired) as exc:
|
|
214
|
+
click.echo(click.style(f" ❌ {name}: {exc}", fg="red"))
|
|
215
|
+
continue
|
|
216
|
+
if result.returncode == 0:
|
|
217
|
+
freed += candidates[index]["size_bytes"]
|
|
218
|
+
click.echo(click.style(f" ✅ {name}", fg="green"))
|
|
219
|
+
else:
|
|
220
|
+
click.echo(click.style(f" ❌ {name}: {result.stderr.strip()[:120]}", fg="red"))
|
|
221
|
+
click.echo(f"Usunięto wolumeny o znanym rozmiarze łącznie {_format_bytes(freed)}.")
|
|
@@ -14,6 +14,8 @@ import re
|
|
|
14
14
|
|
|
15
15
|
import click
|
|
16
16
|
|
|
17
|
+
from fixos.cli._cleanup_docker_volumes import _cleanup_docker_volumes
|
|
18
|
+
|
|
17
19
|
# Re-export public symbols used by fixos.cli (backward-compat)
|
|
18
20
|
from fixos.cli._cleanup_flatpak import _cleanup_flatpak_detailed
|
|
19
21
|
from fixos.cli._cleanup_space import (
|
|
@@ -1611,7 +1613,7 @@ def _execute_yes_cleanup(plan: dict, scanner, dry_run: bool = False) -> None:
|
|
|
1611
1613
|
"Wyczyść konkretną usługę "
|
|
1612
1614
|
"(docker, docker-all, docker-old, docker-containers, docker-networks, "
|
|
1613
1615
|
"docker-stale-services, orphaned-projects, ollama-old, snap-old, "
|
|
1614
|
-
"journal, user-cache, jetbrains, libvirt, gitive, docker-buildcache, tmp, venvs-old, "
|
|
1616
|
+
"journal, user-cache, jetbrains, libvirt, gitive, docker-buildcache, docker-volumes, tmp, venvs-old, "
|
|
1615
1617
|
"npm, ...)"
|
|
1616
1618
|
),
|
|
1617
1619
|
)
|
|
@@ -1719,6 +1721,23 @@ def _execute_yes_cleanup(plan: dict, scanner, dry_run: bool = False) -> None:
|
|
|
1719
1721
|
f"(domyślnie {DEFAULT_OLLAMA_OLD_UNUSED_DAYS}); pomija modele aktualnie uruchomione"
|
|
1720
1722
|
),
|
|
1721
1723
|
)
|
|
1724
|
+
@click.option(
|
|
1725
|
+
"--docker-volumes",
|
|
1726
|
+
"docker_volumes",
|
|
1727
|
+
is_flag=True,
|
|
1728
|
+
default=False,
|
|
1729
|
+
help=(
|
|
1730
|
+
"Wybierz i usuń nieużywane wolumeny Dockera (nazwa, projekt Compose, wiek, "
|
|
1731
|
+
"rozmiar przez du/sudo -n); filtry --days i --match; --yes pomija "
|
|
1732
|
+
"wolumeny wyglądające na dane"
|
|
1733
|
+
),
|
|
1734
|
+
)
|
|
1735
|
+
@click.option(
|
|
1736
|
+
"--match",
|
|
1737
|
+
"match",
|
|
1738
|
+
default=None,
|
|
1739
|
+
help="Dla --docker-volumes: fragmenty nazw rozdzielone przecinkami, np. ollama,huggingface",
|
|
1740
|
+
)
|
|
1722
1741
|
@click.option(
|
|
1723
1742
|
"--snap-old",
|
|
1724
1743
|
"snap_old",
|
|
@@ -1893,6 +1912,8 @@ def cleanup_services(
|
|
|
1893
1912
|
list_orphan_pins,
|
|
1894
1913
|
process_hours,
|
|
1895
1914
|
ollama_old,
|
|
1915
|
+
docker_volumes,
|
|
1916
|
+
match,
|
|
1896
1917
|
snap_old,
|
|
1897
1918
|
journal_cleanup,
|
|
1898
1919
|
user_cache,
|
|
@@ -1951,6 +1972,8 @@ def cleanup_services(
|
|
|
1951
1972
|
fixos cleanup --unpin-orphan-project /absolute/missing/project
|
|
1952
1973
|
# usuwa ochronę, ale nie zmienia Dockera ani procesów
|
|
1953
1974
|
fixos cleanup --ollama-old --days 90 --dry-run
|
|
1975
|
+
fixos cleanup --docker-volumes --list # nieużywane wolumeny: nazwa, projekt, wiek, rozmiar
|
|
1976
|
+
fixos cleanup --docker-volumes --match ollama,huggingface --days 90
|
|
1954
1977
|
# modele Ollama niezmieniane od 90+ dni
|
|
1955
1978
|
fixos cleanup -c ollama-old
|
|
1956
1979
|
fixos cleanup --snap-old # wyłączone rewizje snapów
|
|
@@ -2004,6 +2027,7 @@ def cleanup_services(
|
|
|
2004
2027
|
list_orphan_pins,
|
|
2005
2028
|
ollama_old,
|
|
2006
2029
|
snap_old,
|
|
2030
|
+
docker_volumes,
|
|
2007
2031
|
journal_cleanup,
|
|
2008
2032
|
user_cache,
|
|
2009
2033
|
jetbrains,
|
|
@@ -2053,6 +2077,7 @@ def cleanup_services(
|
|
|
2053
2077
|
ollama_old,
|
|
2054
2078
|
docker_buildcache,
|
|
2055
2079
|
snap_old,
|
|
2080
|
+
docker_volumes,
|
|
2056
2081
|
journal_cleanup,
|
|
2057
2082
|
user_cache,
|
|
2058
2083
|
jetbrains,
|
|
@@ -2106,6 +2131,17 @@ def cleanup_services(
|
|
|
2106
2131
|
"libvirt": (libvirt, _cleanup_libvirt),
|
|
2107
2132
|
"gitive": (gitive, _cleanup_gitive),
|
|
2108
2133
|
"docker-buildcache": (docker_buildcache, _cleanup_docker_buildcache),
|
|
2134
|
+
"docker-volumes": (
|
|
2135
|
+
docker_volumes,
|
|
2136
|
+
lambda j, d, lst, y: _cleanup_docker_volumes(
|
|
2137
|
+
j,
|
|
2138
|
+
d,
|
|
2139
|
+
lst,
|
|
2140
|
+
y,
|
|
2141
|
+
days=effective_days,
|
|
2142
|
+
match=[t.strip() for t in (match or "").split(",") if t.strip()],
|
|
2143
|
+
),
|
|
2144
|
+
),
|
|
2109
2145
|
"tmp": (
|
|
2110
2146
|
tmp_cleanup,
|
|
2111
2147
|
lambda j, d, lst, y: _cleanup_tmp(
|
|
@@ -2168,7 +2204,7 @@ def cleanup_services(
|
|
|
2168
2204
|
click.echo(
|
|
2169
2205
|
click.style(
|
|
2170
2206
|
"Akcje odzyskiwania miejsca (snap/journal/cache/JetBrains/"
|
|
2171
|
-
"libvirt/gitive/docker-buildcache/tmp/trash/thumbnails) uruchom jako osobną, pojedynczą akcję.",
|
|
2207
|
+
"libvirt/gitive/docker-buildcache/docker-volumes/tmp/trash/thumbnails) uruchom jako osobną, pojedynczą akcję.",
|
|
2172
2208
|
fg="red",
|
|
2173
2209
|
)
|
|
2174
2210
|
)
|
|
@@ -143,7 +143,7 @@ _OPENROUTER_MODEL_MIGRATIONS = {
|
|
|
143
143
|
}
|
|
144
144
|
|
|
145
145
|
|
|
146
|
-
DEFAULT_OLLAMA_HOST = "127.0.0.1:11434"
|
|
146
|
+
DEFAULT_OLLAMA_HOST = "127.0.0.1:11434" # nohardcode: allow NOHARDCODE-006 -- fallback when OLLAMA_HOST is unset or empty
|
|
147
147
|
|
|
148
148
|
|
|
149
149
|
def ollama_host_url() -> str:
|
|
@@ -3,15 +3,16 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import os
|
|
6
|
+
from typing import ClassVar
|
|
6
7
|
|
|
7
|
-
from fixos.diagnostics.storage_analyzer import StorageItem
|
|
8
8
|
from fixos.constants import MIN_DOCKER_DANGLING_MB, MIN_PODMAN_MB
|
|
9
|
+
from fixos.diagnostics.storage_analyzer import StorageItem
|
|
9
10
|
|
|
10
11
|
|
|
11
12
|
class _ContainerAnalyzerMixin:
|
|
12
13
|
"""Mixin providing container _analyze_* methods for StorageAnalyzer."""
|
|
13
14
|
|
|
14
|
-
_DOCKER_DF_SECTIONS = {
|
|
15
|
+
_DOCKER_DF_SECTIONS: ClassVar[dict[str, str]] = {
|
|
15
16
|
"Images space usage:": "images",
|
|
16
17
|
"Containers space usage:": "containers",
|
|
17
18
|
"Local Volumes space usage:": "volumes",
|
|
@@ -119,11 +120,14 @@ class _ContainerAnalyzerMixin:
|
|
|
119
120
|
size_bytes=total_docker,
|
|
120
121
|
category="containers",
|
|
121
122
|
risk="medium",
|
|
122
|
-
|
|
123
|
+
# Never a bulk volume prune: volumes are data and are
|
|
124
|
+
# chosen one by one with `fixos cleanup --docker-volumes`.
|
|
125
|
+
cleanup_command="docker builder prune --force --filter until=168h",
|
|
123
126
|
description=(
|
|
124
127
|
f"Docker łącznie: {StorageItem._format_size(total_docker)}. "
|
|
125
128
|
f"Obrazy: {total_images}, dangling: {dangling_images}. "
|
|
126
|
-
"
|
|
129
|
+
"Czyści cache buildów starszy niż 7 dni; wolumeny wybierz "
|
|
130
|
+
"ręcznie przez `fixos cleanup --docker-volumes`."
|
|
127
131
|
),
|
|
128
132
|
)
|
|
129
133
|
)
|
|
@@ -58,6 +58,7 @@ fixos/agent/session_handlers.py
|
|
|
58
58
|
fixos/agent/session_io.py
|
|
59
59
|
fixos/cli/__init__.py
|
|
60
60
|
fixos/cli/__main__.py
|
|
61
|
+
fixos/cli/_cleanup_docker_volumes.py
|
|
61
62
|
fixos/cli/_cleanup_flatpak.py
|
|
62
63
|
fixos/cli/_cleanup_home.py
|
|
63
64
|
fixos/cli/_cleanup_snap.py
|
|
@@ -187,6 +188,7 @@ tests/unit/test_anonymizer.py
|
|
|
187
188
|
tests/unit/test_ask_and_nl_group.py
|
|
188
189
|
tests/unit/test_cache_discovery.py
|
|
189
190
|
tests/unit/test_cleanup_cmd.py
|
|
191
|
+
tests/unit/test_cleanup_docker_volumes.py
|
|
190
192
|
tests/unit/test_cleanup_effectiveness.py
|
|
191
193
|
tests/unit/test_cleanup_planner.py
|
|
192
194
|
tests/unit/test_cleanup_retention.py
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
"""fixos cleanup --docker-volumes: listing, selection safety and re-check."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from datetime import datetime, timezone
|
|
7
|
+
from types import SimpleNamespace
|
|
8
|
+
|
|
9
|
+
import click
|
|
10
|
+
from click.testing import CliRunner
|
|
11
|
+
|
|
12
|
+
from fixos.cli import _cleanup_docker_volumes as volumes
|
|
13
|
+
from fixos.cli import _cleanup_space as space
|
|
14
|
+
from fixos.diagnostics.storage_analyzer import (
|
|
15
|
+
StorageAnalyzer, # loads the mixins in order
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
NOW = datetime(2026, 10, 13, tzinfo=timezone.utc)
|
|
19
|
+
INSPECT = [
|
|
20
|
+
{"Name": "llmass_huggingface-cache", "CreatedAt": "2025-10-06T10:00:00Z",
|
|
21
|
+
"Labels": {"com.docker.compose.project": "llmass"}, "Mountpoint": "/v/hf"},
|
|
22
|
+
{"Name": "ifuri-onedev_mirror-data", "CreatedAt": "2026-07-20T10:00:00Z",
|
|
23
|
+
"Labels": {"com.docker.compose.project": "ifuri-onedev"}, "Mountpoint": "/v/mirror"},
|
|
24
|
+
{"Name": "chat_ollama_data", "CreatedAt": "2026-10-10T10:00:00Z",
|
|
25
|
+
"Labels": None, "Mountpoint": "/v/chat"},
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class FakeDocker:
|
|
30
|
+
"""Answers the docker/du/sudo commands the module issues."""
|
|
31
|
+
|
|
32
|
+
def __init__(self, dangling=None, in_use=()):
|
|
33
|
+
self.dangling = list(dangling or [v["Name"] for v in INSPECT])
|
|
34
|
+
self.in_use = set(in_use)
|
|
35
|
+
self.calls: list[list[str]] = []
|
|
36
|
+
|
|
37
|
+
def __call__(self, argv, timeout=60):
|
|
38
|
+
self.calls.append(argv)
|
|
39
|
+
out, code = "", 0
|
|
40
|
+
if argv[:5] == ["docker", "volume", "ls", "-q", "-f"]:
|
|
41
|
+
names = self.dangling
|
|
42
|
+
if len(argv) > 6:
|
|
43
|
+
wanted = argv[-1].split("=", 1)[1].strip("^$")
|
|
44
|
+
names = [n for n in names if n == wanted]
|
|
45
|
+
out = "\n".join(names)
|
|
46
|
+
elif argv[:3] == ["docker", "volume", "inspect"]:
|
|
47
|
+
out = json.dumps([v for v in INSPECT if v["Name"] in argv[3:]])
|
|
48
|
+
elif argv[:3] == ["docker", "ps", "-aq"]:
|
|
49
|
+
name = argv[-1].split("=", 1)[1]
|
|
50
|
+
out = "c1" if name in self.in_use else ""
|
|
51
|
+
elif argv[:2] == ["du", "-sbl0"] or argv[:3] == ["sudo", "-n", "du"]:
|
|
52
|
+
sizes = {"/v/hf": 23_500_000_000, "/v/mirror": 7_400_000_000}
|
|
53
|
+
out = "".join(f"{sizes[p]}\t{p}\0" for p in argv if p in sizes)
|
|
54
|
+
elif argv[:3] == ["docker", "volume", "rm"]:
|
|
55
|
+
self.dangling.remove(argv[3])
|
|
56
|
+
return SimpleNamespace(returncode=code, stdout=out, stderr="")
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _invoke(fn, *args, input_text="", **kwargs):
|
|
60
|
+
@click.command()
|
|
61
|
+
def command():
|
|
62
|
+
fn(*args, **kwargs)
|
|
63
|
+
|
|
64
|
+
return CliRunner().invoke(command, input=input_text)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def test_lists_name_project_age_and_sorts_by_size(monkeypatch):
|
|
68
|
+
monkeypatch.setattr(space, "_run_cmd", FakeDocker())
|
|
69
|
+
|
|
70
|
+
found = volumes._unused_volumes(None, [], now=NOW)
|
|
71
|
+
|
|
72
|
+
assert [c["name"] for c in found] == [
|
|
73
|
+
"llmass_huggingface-cache", "ifuri-onedev_mirror-data", "chat_ollama_data",
|
|
74
|
+
]
|
|
75
|
+
assert found[0]["project"] == "llmass" and round(found[0]["age_days"]) == 372
|
|
76
|
+
assert found[2]["size_known"] is False
|
|
77
|
+
assert [c["protected"] for c in found] == [False, True, False]
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def test_match_and_days_filter(monkeypatch):
|
|
81
|
+
monkeypatch.setattr(space, "_run_cmd", FakeDocker())
|
|
82
|
+
|
|
83
|
+
found = volumes._unused_volumes(30, ["ollama", "huggingface"], now=NOW)
|
|
84
|
+
|
|
85
|
+
assert [c["name"] for c in found] == ["llmass_huggingface-cache"]
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def test_yes_never_removes_data_looking_volumes(monkeypatch):
|
|
89
|
+
docker = FakeDocker()
|
|
90
|
+
monkeypatch.setattr(space, "_run_cmd", docker)
|
|
91
|
+
|
|
92
|
+
result = _invoke(volumes._cleanup_docker_volumes, False, False, False, True)
|
|
93
|
+
|
|
94
|
+
removed = [c[3] for c in docker.calls if c[:3] == ["docker", "volume", "rm"]]
|
|
95
|
+
assert result.exit_code == 0
|
|
96
|
+
assert "ifuri-onedev_mirror-data" not in removed
|
|
97
|
+
assert set(removed) == {"llmass_huggingface-cache", "chat_ollama_data"}
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def test_volume_in_use_at_removal_time_is_skipped(monkeypatch):
|
|
101
|
+
docker = FakeDocker(in_use={"chat_ollama_data"})
|
|
102
|
+
monkeypatch.setattr(space, "_run_cmd", docker)
|
|
103
|
+
|
|
104
|
+
_invoke(volumes._cleanup_docker_volumes, False, False, False, False, input_text="3\ny\n")
|
|
105
|
+
|
|
106
|
+
assert not any(c[:3] == ["docker", "volume", "rm"] for c in docker.calls)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def test_dry_run_and_list_do_not_remove(monkeypatch):
|
|
110
|
+
docker = FakeDocker()
|
|
111
|
+
monkeypatch.setattr(space, "_run_cmd", docker)
|
|
112
|
+
|
|
113
|
+
_invoke(volumes._cleanup_docker_volumes, False, True, False, True)
|
|
114
|
+
_invoke(volumes._cleanup_docker_volumes, False, False, True, False)
|
|
115
|
+
|
|
116
|
+
assert not any(c[:3] == ["docker", "volume", "rm"] for c in docker.calls)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def test_full_analysis_never_prunes_volumes(monkeypatch):
|
|
120
|
+
from fixos.diagnostics._storage_container_mixin import _ContainerAnalyzerMixin
|
|
121
|
+
|
|
122
|
+
assert issubclass(StorageAnalyzer, _ContainerAnalyzerMixin)
|
|
123
|
+
|
|
124
|
+
class Analyzer(_ContainerAnalyzerMixin):
|
|
125
|
+
def __init__(self):
|
|
126
|
+
self.items = []
|
|
127
|
+
|
|
128
|
+
def _get_dir_size(self, path):
|
|
129
|
+
return 300 * 1024**3
|
|
130
|
+
|
|
131
|
+
analyzer = Analyzer()
|
|
132
|
+
analyzer._add_docker_total_item(10, 2)
|
|
133
|
+
|
|
134
|
+
command = analyzer.items[0].cleanup_command
|
|
135
|
+
assert "--volumes" not in command and "system prune" not in command
|
|
136
|
+
assert command == "docker builder prune --force --filter until=168h"
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def test_app_data_volumes_are_protected():
|
|
140
|
+
for name in ("mcp_openwebui-data", "proxy_open-webui-data", "n8n_storage", "mcp_git-repo-storage"):
|
|
141
|
+
assert volumes._DATA_TOKENS.search(name), name
|
|
142
|
+
for name in ("llmass_huggingface-cache", "chat_ollama_data", "if-uri_trivy-cache"):
|
|
143
|
+
assert not volumes._DATA_TOKENS.search(name), name
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def test_sizes_split_into_parallel_batches_without_unreadable_partial_sizes(monkeypatch):
|
|
147
|
+
calls = []
|
|
148
|
+
|
|
149
|
+
def fake_run(argv, timeout=60):
|
|
150
|
+
calls.append(argv)
|
|
151
|
+
paths = argv[argv.index("--") + 1 :]
|
|
152
|
+
return SimpleNamespace(returncode=0, stdout="".join(f"10\t{p}\0" for p in paths), stderr="")
|
|
153
|
+
|
|
154
|
+
monkeypatch.setattr(space, "_run_cmd", fake_run)
|
|
155
|
+
mountpoints = [f"/nonexistent/v{i}" for i in range(20)]
|
|
156
|
+
|
|
157
|
+
sizes = volumes._volume_sizes(mountpoints)
|
|
158
|
+
|
|
159
|
+
assert sizes == dict.fromkeys(mountpoints, 10)
|
|
160
|
+
assert len(calls) == volumes._SIZE_WORKERS
|
|
161
|
+
assert all(call[:3] == ["sudo", "-n", "du"] for call in calls)
|
fixos-2.2.53/VERSION
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
2.2.53
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|