remoteRF-server 1.7.20__tar.gz → 1.7.22__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.
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/PKG-INFO +1 -1
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/pyproject.toml +1 -1
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/remote_admin_shell.py +1 -1
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/server/acc_perms.py +12 -9
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/server/commands.py +9 -5
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/server/grpc_server.py +7 -1
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/server/reservation.py +12 -7
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/server/user_group_handler.py +1 -1
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/serverrf_cli.py +223 -7
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server.egg-info/PKG-INFO +1 -1
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/tests/test_client_sessions.py +57 -8
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/tests/test_commands.py +153 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/LICENSE +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/README.md +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/setup.cfg +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/__init__.py +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/common/__init__.py +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/common/grpc/__init__.py +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/common/grpc/deployment_capabilities.proto +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/common/grpc/deployment_capabilities_pb2.py +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/common/grpc/deployment_capabilities_pb2_grpc.py +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/common/grpc/federation.proto +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/common/grpc/federation_pb2.py +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/common/grpc/federation_pb2_grpc.py +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/common/grpc/grpc_host_pb2.py +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/common/grpc/grpc_host_pb2_grpc.py +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/common/grpc/grpc_pb2.py +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/common/grpc/grpc_pb2_grpc.py +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/common/grpc/v2_codec.py +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/common/idl/__init__.py +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/common/idl/device_schema.py +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/common/idl/pluto_mimo_schema.py +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/common/idl/pluto_schema.py +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/common/idl/schema.py +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/common/idl/v2.py +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/common/utils/__init__.py +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/common/utils/ansi_codes.py +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/common/utils/api_token.py +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/common/utils/db_connection.py +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/common/utils/db_location.py +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/common/utils/list_string.py +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/common/utils/panel.py +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/common/utils/process_arg.py +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/drivers/__init__.py +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/drivers/adalm_pluto/__init__.py +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/drivers/adalm_pluto/pluto_diag.py +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/drivers/adalm_pluto/pluto_mimo_schema.py +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/drivers/adalm_pluto/pluto_remote_server.py +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/drivers/adalm_pluto/pluto_schema.py +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/drivers/adalm_pluto/poe.py +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/drivers/bluetooth/__init__.py +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/drivers/bluetooth/bluetooth_schema.py +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/drivers/bluetooth_diag.py +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/drivers/gps/__init__.py +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/drivers/gps/gps_schema.py +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/drivers/gps_diag.py +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/drivers/hackrf/__init__.py +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/drivers/hackrf/hackrf_schema.py +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/drivers/krakensdr/__init__.py +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/drivers/krakensdr/krakensdr_schema.py +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/drivers/rfbeam_kmd7/__init__.py +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/drivers/rfbeam_kmd7/rfbeam_kmd7_schema.py +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/drivers/rtl_sdr/__init__.py +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/drivers/rtl_sdr/rtl_sdr_diag.py +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/drivers/rtl_sdr/rtl_sdr_schema.py +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/drivers/ti_awr6843/__init__.py +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/drivers/ti_awr6843/ti_awr6843_schema.py +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/drivers/ti_mmwave/__init__.py +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/drivers/ti_mmwave/ti_mmwave_schema.py +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/drivers/usrp/__init__.py +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/drivers/usrp/policy.py +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/drivers/usrp/profiles.py +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/drivers/usrp/uhd_manifest.py +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/drivers/usrp/usrp_diag.py +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/drivers/usrp/usrp_schema.py +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/drivers/usrp_phaser/__init__.py +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/drivers/usrp_phaser/usrp_phaser_schema.py +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/drivers/wifi/__init__.py +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/drivers/wifi/wifi_schema.py +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/host/__init__.py +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/host/host_auth_token.py +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/host/host_directory_store.py +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/host/host_tunnel_server.py +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/server/__init__.py +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/server/cert_provider.py +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/server/console.py +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/server/control.py +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/server/deployment/__init__.py +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/server/deployment/capabilities.py +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/server/device_manager.py +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/server/diag.py +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/server/direct_path.py +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/server/dynamic_v2.py +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/server/dynamic_v2_bridge.py +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/server/federation/__init__.py +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/server/federation/admin.py +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/server/federation/cli.py +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/server/federation/cluster.py +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/server/federation/contracts.py +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/server/federation/destination.py +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/server/federation/devices.py +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/server/federation/enrollment.py +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/server/federation/global_home.py +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/server/federation/home.py +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/server/federation/home_devices.py +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/server/federation/identity.py +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/server/federation/legacy_state.py +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/server/federation/links.py +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/server/federation/migrations.py +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/server/federation/peers.py +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/server/federation/permissions.py +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/server/federation/principals.py +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/server/federation/runtime.py +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/server/federation/trust.py +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/server/global_onboarding.py +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/server/host_direct.py +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/server/host_dynamic_v2.py +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/server/json_output.py +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/server/remote_admin.py +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/server/remote_admin_ssh.py +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/server/rpc_manager.py +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/server/schema_registry.py +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/server/startup_check.py +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/server/transport_counters.py +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/server/user_group_cli.py +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/server/v2_errors.py +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/tools/__init__.py +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/tools/gen_certs.py +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/tools/introspect_uhd.py +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server.egg-info/SOURCES.txt +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server.egg-info/dependency_links.txt +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server.egg-info/entry_points.txt +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server.egg-info/requires.txt +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server.egg-info/top_level.txt +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/rssi/__init__.py +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/rssi/cli.py +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/rssi/dongle.py +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/rssi/safety.py +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/rssi/scanner.py +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/tests/test_account_hardening.py +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/tests/test_account_names.py +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/tests/test_account_removal.py +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/tests/test_activity_log.py +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/tests/test_availability.py +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/tests/test_bind_host.py +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/tests/test_cert_provider.py +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/tests/test_cluster.py +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/tests/test_console.py +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/tests/test_contrib_drivers.py +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/tests/test_default_rtl_sdr_schema.py +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/tests/test_default_usrp_schema.py +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/tests/test_default_wifi_schema.py +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/tests/test_deployment_capabilities.py +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/tests/test_detached_device_rpc.py +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/tests/test_device_core_aliases.py +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/tests/test_device_handles.py +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/tests/test_device_init.py +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/tests/test_devices_add_wizard.py +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/tests/test_devices_list.py +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/tests/test_diag.py +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/tests/test_direct_fixed_port.py +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/tests/test_direct_path_server.py +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/tests/test_dynamic_v2.py +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/tests/test_federated_direct_path.py +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/tests/test_federated_dynamic_v2.py +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/tests/test_federation_edges.py +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/tests/test_first_run.py +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/tests/test_gate_d_contracts.py +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/tests/test_gate_e_federation.py +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/tests/test_gate_e_proto.py +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/tests/test_gate_f_enrollment.py +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/tests/test_gate_g1_logging.py +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/tests/test_gate_g_permissions.py +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/tests/test_gate_h_devices.py +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/tests/test_gate_i_direct_path.py +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/tests/test_gen_certs.py +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/tests/test_global_accounts_migration.py +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/tests/test_global_home.py +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/tests/test_global_onboarding.py +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/tests/test_global_v1_disposal.py +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/tests/test_hackrf_schema.py +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/tests/test_home_device_plane.py +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/tests/test_host_direct.py +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/tests/test_host_directory_store.py +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/tests/test_host_dynamic_v2.py +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/tests/test_idl_schema.py +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/tests/test_json_commands.py +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/tests/test_lab_logins.py +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/tests/test_native_drivers.py +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/tests/test_panel.py +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/tests/test_peer_transport.py +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/tests/test_pluto_compat.py +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/tests/test_pluto_poe.py +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/tests/test_process_arg.py +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/tests/test_proxy_lifecycle.py +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/tests/test_remote_admin.py +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/tests/test_reservation_cancel.py +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/tests/test_reservation_handoff.py +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/tests/test_reservation_policy_b.py +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/tests/test_reservation_timestamps.py +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/tests/test_rpc_manager_batch.py +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/tests/test_rpc_manager_calln.py +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/tests/test_rpc_manager_host_legacy.py +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/tests/test_rpc_manager_rx_n.py +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/tests/test_rtl_sdr_schema.py +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/tests/test_schema_registry.py +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/tests/test_soak_federation.py +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/tests/test_startup_check.py +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/tests/test_ti_mmwave_hardware.py +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/tests/test_ti_mmwave_schema.py +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/tests/test_transport_counters.py +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/tests/test_user_group_cli.py +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/tests/test_usrp_b205mini_uhd410_hardware.py +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/tests/test_usrp_client_server_hardware.py +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/tests/test_usrp_client_server_localhost.py +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/tests/test_usrp_n210_client_server_hardware.py +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/tests/test_usrp_preload.py +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/tests/test_usrp_profiles.py +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/tests/test_usrp_remoterf_rx_hardware.py +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/tests/test_usrp_uhd410_hardware.py +0 -0
- {remoterf_server-1.7.20 → remoterf_server-1.7.22}/tests/test_wipe.py +0 -0
|
@@ -75,7 +75,7 @@ TIERS = {
|
|
|
75
75
|
DENY_FLAGS = {"--secrets"}
|
|
76
76
|
# Destructive commands whose own confirmation -y answers (the step-up asked already).
|
|
77
77
|
TAKES_YES = {"devices wipe", "stats purge", "hosts wipe", "certs wipe", "users prune-proxies",
|
|
78
|
-
"hosts tokens", "certs reset"}
|
|
78
|
+
"hosts tokens", "certs reset", "groups remove"}
|
|
79
79
|
KEEP_BACKUPS = 10
|
|
80
80
|
PAIR_CODE = re.compile(r"[A-Z2-7]{4}-[A-Z2-7]{4}-[A-Z2-7]{4}-[A-Z2-7]{4}")
|
|
81
81
|
BUILTINS = ("help", "me", "revoke", "passwd", "log", "clear", "exit")
|
|
@@ -243,14 +243,16 @@ class perms_db:
|
|
|
243
243
|
)
|
|
244
244
|
|
|
245
245
|
@db_connection
|
|
246
|
-
def set_admin(self, username:str):
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
246
|
+
def set_admin(self, username:str, *, confirm: bool = True, quiet: bool = False):
|
|
247
|
+
if not quiet:
|
|
248
|
+
print(f"Setting {username} as an admin")
|
|
249
|
+
if confirm:
|
|
250
|
+
from .control import prompt
|
|
251
|
+
|
|
252
|
+
inpu = prompt(f"Are you sure you want to set {username} as an admin? \nThis gives them unrestricted access. (Y/N): ")
|
|
253
|
+
if inpu != 'Y' and inpu != 'y':
|
|
254
|
+
print(f"Admin set is canceled for user: {username}.")
|
|
255
|
+
return
|
|
254
256
|
|
|
255
257
|
|
|
256
258
|
# SQL command to insert a new user
|
|
@@ -260,7 +262,8 @@ class perms_db:
|
|
|
260
262
|
# Execute the SQL command
|
|
261
263
|
self.remove_user(username)
|
|
262
264
|
self.cursor.execute(insert_query, (username,))
|
|
263
|
-
|
|
265
|
+
if not quiet:
|
|
266
|
+
print("Admin added successfully")
|
|
264
267
|
|
|
265
268
|
except sqlite3.Error as e:
|
|
266
269
|
print("An error occurred:", e)
|
|
@@ -120,7 +120,7 @@ def _table() -> list[Command]:
|
|
|
120
120
|
Command(("devices wipe",), "Remove ALL devices [-y]", _shell("cmd_devices_wipe"), "Devices", after="reload_devices"),
|
|
121
121
|
# ---- users ----
|
|
122
122
|
Command(("users list",), "List accounts", _daemon("cmd_users_list"), "Users", where=D),
|
|
123
|
-
Command(("users manage",), "Change a user's role
|
|
123
|
+
Command(("users manage",), "Change a user's role: users manage <user> --role <user|power-user|admin> --yes [--json]; no arguments is interactive", _daemon("cmd_users_manage"), "Users", where=D, interactive=True),
|
|
124
124
|
Command(("users perms",), "Show permissions", _daemon("cmd_users_perms"), "Users", where=D),
|
|
125
125
|
Command(("users purge",), "Remove ALL accounts", _daemon("cmd_users_purge"), "Users", where=D),
|
|
126
126
|
Command(("users prune-proxies",), "Partner-deployment visitors whose home is gone: list them, -y removes [--stale DAYS]", _daemon("cmd_users_prune_proxies"), "Users", where=D),
|
|
@@ -131,15 +131,15 @@ def _table() -> list[Command]:
|
|
|
131
131
|
# ---- groups ----
|
|
132
132
|
Command(("groups list",), "List user groups", _shell("cmd_groups_list"), "Groups"),
|
|
133
133
|
Command(("groups create",), "Create a group: groups create <name> --devices <ids> [--max-reservations 3 --max-time 1800 --horizon 604800 --lifetime 0 --lan-only --public --share <clusters>] --yes [--json]; no name is interactive", _shell("cmd_groups_create"), "Groups"),
|
|
134
|
-
Command(("groups edit",), "Edit a
|
|
135
|
-
Command(("groups remove",), "Delete a
|
|
134
|
+
Command(("groups edit",), "Edit a group: groups edit <id|name> [--name N --devices 0,1 --max-reservations N --max-time SEC --lifetime SEC --horizon SEC --lan-only|--no-lan-only --share a,b|none] --yes [--json]; no arguments is interactive", _shell("cmd_groups_edit"), "Groups"),
|
|
135
|
+
Command(("groups remove",), "Delete a group: groups remove <id|name> --yes [--json]; no arguments is interactive", _shell("cmd_groups_remove"), "Groups"),
|
|
136
136
|
Command(("groups public",), "Share a group with anyone on RemoteRF Global, or stop: groups public <group> on|off", _federation("public"), "Groups"),
|
|
137
137
|
Command(("groups share",), "Where a group is shared, or change it: groups share <group> [<cluster> on|off]", _federation("share"), "Groups"),
|
|
138
138
|
Command(("groups csv",), "Export ALL user groups as CSV to Downloads", _shell("cmd_groups_csv"), "Groups"),
|
|
139
139
|
# ---- enrollment codes ----
|
|
140
140
|
Command(("codes list",), "List enrollment codes", _shell("cmd_codes_list"), "Enrollment codes"),
|
|
141
141
|
Command(("codes create",), "Create codes: codes create <group name|id|none> --count N [--length 10 --uses 1 --duration 3600] --yes [--json]; no group is interactive", _shell("cmd_codes_create"), "Enrollment codes"),
|
|
142
|
-
Command(("codes remove",), "Delete
|
|
142
|
+
Command(("codes remove",), "Delete codes: codes remove <code-or-invite> --yes [--json] | codes remove --group <id> --yes [--json]; no arguments is interactive", _shell("cmd_codes_remove"), "Enrollment codes"),
|
|
143
143
|
Command(("codes csv",), "Export ALL enrollment codes as CSV to Downloads", _shell("cmd_codes_csv"), "Enrollment codes"),
|
|
144
144
|
# ---- hosts ----
|
|
145
145
|
Command(("hosts status",), "Live host online/last_seen/devices", _daemon("cmd_hosts_status"), "Hosts", where=D),
|
|
@@ -353,7 +353,11 @@ def _run_in_daemon(command: Command, rest: list[str]) -> int:
|
|
|
353
353
|
if command.console_only and remote:
|
|
354
354
|
print(f"`{command.names[0]}` works only at the lab console.", file=sys.stderr)
|
|
355
355
|
return 1
|
|
356
|
-
|
|
356
|
+
users_manage_flags = command.names[0] == "users manage" and bool(rest)
|
|
357
|
+
if command.names[0] == "users manage" and not rest and not remote and not sys.stdin.isatty():
|
|
358
|
+
print("ERROR: non-interactive users manage requires a user, --role, and --yes", file=sys.stderr)
|
|
359
|
+
return 1
|
|
360
|
+
if command.console_only or (command.interactive and not remote and not users_manage_flags):
|
|
357
361
|
# Prompts need the console's terminal: type it there and attach. Never
|
|
358
362
|
# for a remote admin: that would hand them the live console, past the policy.
|
|
359
363
|
return console.run_in_console([*command.names[0].split(), *rest])
|
|
@@ -1371,7 +1371,13 @@ def cmd_clear(argv): clear()
|
|
|
1371
1371
|
def cmd_hosts_status(argv): _cmd_hosts_status(json_output="--json" in argv)
|
|
1372
1372
|
def cmd_hosts_wipe(argv): _cmd_hosts_wipe(yes=("-y" in argv or "--yes" in argv))
|
|
1373
1373
|
def cmd_users_list(argv): reservation_handler.print_all_accounts()
|
|
1374
|
-
def cmd_users_manage(argv):
|
|
1374
|
+
def cmd_users_manage(argv):
|
|
1375
|
+
if not argv:
|
|
1376
|
+
set_acc()
|
|
1377
|
+
return 0
|
|
1378
|
+
from remoteRF_server.serverrf_cli import _cmd_users_manage_noninteractive
|
|
1379
|
+
|
|
1380
|
+
return _cmd_users_manage_noninteractive(argv, perms)
|
|
1375
1381
|
def cmd_users_perms(argv): perms.print_perms()
|
|
1376
1382
|
def cmd_users_purge(argv): reservation_handler.remove_all_users()
|
|
1377
1383
|
def cmd_reservations_list(argv): reservation_handler.print_all_reservations(json_output="--json" in argv)
|
|
@@ -226,7 +226,8 @@ class ReservationHandler:
|
|
|
226
226
|
DROP TRIGGER IF EXISTS reserved_total_del;
|
|
227
227
|
CREATE TRIGGER reserved_total_ins AFTER INSERT ON Reservations
|
|
228
228
|
WHEN NEW.device_id < 1000000 BEGIN
|
|
229
|
-
UPDATE ReservedTotal SET seconds = seconds + (
|
|
229
|
+
UPDATE ReservedTotal SET seconds = seconds + MAX(0,
|
|
230
|
+
(julianday(NEW.end_time) - julianday(MAX(NEW.start_time, datetime('now', 'localtime')))) * 86400);
|
|
230
231
|
END;
|
|
231
232
|
CREATE TRIGGER reserved_total_del AFTER DELETE ON Reservations
|
|
232
233
|
WHEN OLD.device_id < 1000000 BEGIN
|
|
@@ -264,6 +265,8 @@ class ReservationHandler:
|
|
|
264
265
|
day = lambda r: f"'d' || ((CAST(strftime('%w', {r}.start_time, 'utc') AS INTEGER) + 6) % 7)" # noqa: E731
|
|
265
266
|
utc_date = lambda r: f"date({r}.start_time, 'utc')" # noqa: E731
|
|
266
267
|
length = lambda r: f"(julianday({r}.end_time) - julianday({r}.start_time)) * 86400" # noqa: E731
|
|
268
|
+
# A booking made after its start counts only from now on.
|
|
269
|
+
held = '''MAX(0, (julianday(NEW.end_time) - julianday(MAX(NEW.start_time, datetime('now', 'localtime')))) * 86400)'''
|
|
267
270
|
cancel = '''reservations = MAX(reservations - (OLD.start_time > datetime('now', 'localtime')), 0),
|
|
268
271
|
seconds = MAX(seconds - MAX(0,
|
|
269
272
|
(julianday(OLD.end_time) - julianday(MAX(OLD.start_time, datetime('now', 'localtime')))) * 86400), 0)'''
|
|
@@ -278,12 +281,13 @@ class ReservationHandler:
|
|
|
278
281
|
UNION ALL SELECT device_id, {day("r")}, {length("r")} FROM Reservations r)
|
|
279
282
|
WHERE device_id < 1000000 AND NOT EXISTS (SELECT 1 FROM ReservationBuckets)
|
|
280
283
|
GROUP BY device_id, bucket;
|
|
281
|
-
|
|
284
|
+
DROP TRIGGER IF EXISTS reservation_buckets_ins;
|
|
285
|
+
CREATE TRIGGER reservation_buckets_ins AFTER INSERT ON Reservations
|
|
282
286
|
WHEN NEW.device_id < 1000000 BEGIN
|
|
283
287
|
INSERT INTO ReservationBuckets VALUES
|
|
284
|
-
(NEW.device_id, 'all', 1, {
|
|
285
|
-
(NEW.device_id, {hour("NEW")}, 1, {
|
|
286
|
-
(NEW.device_id, {day("NEW")}, 1, {
|
|
288
|
+
(NEW.device_id, 'all', 1, {held}),
|
|
289
|
+
(NEW.device_id, {hour("NEW")}, 1, {held}),
|
|
290
|
+
(NEW.device_id, {day("NEW")}, 1, {held})
|
|
287
291
|
ON CONFLICT (device_id, bucket) DO UPDATE
|
|
288
292
|
SET reservations = reservations + 1, seconds = seconds + excluded.seconds;
|
|
289
293
|
END;
|
|
@@ -298,9 +302,10 @@ class ReservationHandler:
|
|
|
298
302
|
SELECT device_id, {utc_date("r")} AS day, COUNT(*), SUM({length("r")}) FROM Reservations r
|
|
299
303
|
WHERE device_id < 1000000 AND NOT EXISTS (SELECT 1 FROM ReservationDays)
|
|
300
304
|
GROUP BY device_id, day;
|
|
301
|
-
|
|
305
|
+
DROP TRIGGER IF EXISTS reservation_days_ins;
|
|
306
|
+
CREATE TRIGGER reservation_days_ins AFTER INSERT ON Reservations
|
|
302
307
|
WHEN NEW.device_id < 1000000 BEGIN
|
|
303
|
-
INSERT INTO ReservationDays VALUES (NEW.device_id, {utc_date("NEW")}, 1, {
|
|
308
|
+
INSERT INTO ReservationDays VALUES (NEW.device_id, {utc_date("NEW")}, 1, {held})
|
|
304
309
|
ON CONFLICT (device_id, day) DO UPDATE
|
|
305
310
|
SET reservations = reservations + 1, seconds = seconds + excluded.seconds;
|
|
306
311
|
END;
|
|
@@ -1449,11 +1449,15 @@ def _yes(argv: List[str]) -> bool:
|
|
|
1449
1449
|
|
|
1450
1450
|
|
|
1451
1451
|
def _parse_create_args(argv, *, value_flags, bool_flags):
|
|
1452
|
-
"""Small strict parser for
|
|
1452
|
+
"""Small strict parser for forced-command-safe flag forms."""
|
|
1453
1453
|
positional, values = [], {}
|
|
1454
1454
|
i = 0
|
|
1455
1455
|
while i < len(argv):
|
|
1456
1456
|
token = argv[i]
|
|
1457
|
+
if token == "-y" and "yes" in bool_flags:
|
|
1458
|
+
values["yes"] = True
|
|
1459
|
+
i += 1
|
|
1460
|
+
continue
|
|
1457
1461
|
if not token.startswith("--"):
|
|
1458
1462
|
positional.append(token)
|
|
1459
1463
|
i += 1
|
|
@@ -2204,15 +2208,141 @@ def cmd_groups_create(argv):
|
|
|
2204
2208
|
|
|
2205
2209
|
|
|
2206
2210
|
def cmd_groups_edit(argv):
|
|
2207
|
-
from remoteRF_server.server
|
|
2211
|
+
from remoteRF_server.server import user_group_cli as groups
|
|
2208
2212
|
|
|
2209
|
-
|
|
2213
|
+
if not argv:
|
|
2214
|
+
if sys.stdin.isatty():
|
|
2215
|
+
groups.edit_user_group_local(_prompt_session())
|
|
2216
|
+
return 0
|
|
2217
|
+
print("ERROR: non-interactive groups edit requires a group and change flags", file=sys.stderr)
|
|
2218
|
+
return 1
|
|
2219
|
+
if not _yes(argv):
|
|
2220
|
+
print("ERROR: non-interactive groups edit requires --yes", file=sys.stderr)
|
|
2221
|
+
return 1
|
|
2222
|
+
parsed, error = _parse_create_args(
|
|
2223
|
+
argv,
|
|
2224
|
+
value_flags={"name", "devices", "max-reservations", "max-time", "lifetime", "horizon", "share"},
|
|
2225
|
+
bool_flags={"yes", "json", "lan-only", "no-lan-only"},
|
|
2226
|
+
)
|
|
2227
|
+
if error:
|
|
2228
|
+
print(f"ERROR: {error}", file=sys.stderr)
|
|
2229
|
+
return 1
|
|
2230
|
+
pos, flags = parsed
|
|
2231
|
+
if len(pos) != 1:
|
|
2232
|
+
print("ERROR: groups edit requires exactly one group id or name", file=sys.stderr)
|
|
2233
|
+
return 1
|
|
2234
|
+
if "lan-only" in flags and "no-lan-only" in flags:
|
|
2235
|
+
print("ERROR: --lan-only and --no-lan-only are mutually exclusive", file=sys.stderr)
|
|
2236
|
+
return 1
|
|
2237
|
+
change_flags = set(flags) - {"yes", "json"}
|
|
2238
|
+
if not change_flags:
|
|
2239
|
+
print("ERROR: groups edit requires at least one change flag", file=sys.stderr)
|
|
2240
|
+
return 1
|
|
2241
|
+
|
|
2242
|
+
all_groups = groups.user_group_handler.get_all_user_groups(include_users=False) or []
|
|
2243
|
+
ref = pos[0].strip()
|
|
2244
|
+
selected = next((g for g in all_groups if str(g.get("group_id")) == ref), None)
|
|
2245
|
+
if selected is None:
|
|
2246
|
+
selected = next((g for g in all_groups if str(g.get("group_name")) == ref), None)
|
|
2247
|
+
if selected is None:
|
|
2248
|
+
print(f"ERROR: unknown group {ref!r}", file=sys.stderr)
|
|
2249
|
+
return 1
|
|
2250
|
+
group_id = int(selected["group_id"])
|
|
2251
|
+
|
|
2252
|
+
try:
|
|
2253
|
+
devices = _comma_ints(flags["devices"]) if "devices" in flags else None
|
|
2254
|
+
max_reservations = (_bounded_int(flags["max-reservations"], "--max-reservations", minimum=1)
|
|
2255
|
+
if "max-reservations" in flags else None)
|
|
2256
|
+
max_time = (_bounded_int(flags["max-time"], "--max-time", minimum=1)
|
|
2257
|
+
if "max-time" in flags else None)
|
|
2258
|
+
lifetime = (_bounded_int(flags["lifetime"], "--lifetime", minimum=0)
|
|
2259
|
+
if "lifetime" in flags else None)
|
|
2260
|
+
horizon = (_bounded_int(flags["horizon"], "--horizon", minimum=0)
|
|
2261
|
+
if "horizon" in flags else None)
|
|
2262
|
+
except ValueError as exc:
|
|
2263
|
+
print(f"ERROR: {exc}", file=sys.stderr)
|
|
2264
|
+
return 1
|
|
2265
|
+
name = flags.get("name")
|
|
2266
|
+
if name is not None and not str(name).strip():
|
|
2267
|
+
print("ERROR: --name must not be empty", file=sys.stderr)
|
|
2268
|
+
return 1
|
|
2269
|
+
lan_only = True if flags.get("lan-only") else False if flags.get("no-lan-only") else None
|
|
2270
|
+
|
|
2271
|
+
sharing = None
|
|
2272
|
+
if "share" in flags:
|
|
2273
|
+
from remoteRF_server.server.federation import cluster
|
|
2274
|
+
|
|
2275
|
+
sharing = groups._names(str(flags["share"]))
|
|
2276
|
+
try:
|
|
2277
|
+
cluster.share_group(str(group_id), sharing)
|
|
2278
|
+
except cluster.ClusterError as exc:
|
|
2279
|
+
print(f"ERROR: sharing unchanged: {exc}", file=sys.stderr)
|
|
2280
|
+
return 1
|
|
2281
|
+
|
|
2282
|
+
fields = (name, devices, max_time, max_reservations, lifetime, horizon, lan_only)
|
|
2283
|
+
if any(value is not None for value in fields):
|
|
2284
|
+
ok = groups.user_group_handler.edit_user_group(
|
|
2285
|
+
group_id=group_id,
|
|
2286
|
+
group_name=name,
|
|
2287
|
+
devices_whitelist=devices,
|
|
2288
|
+
max_reservation_time_sec=max_time,
|
|
2289
|
+
max_reservations=max_reservations,
|
|
2290
|
+
lifetime_sec=lifetime,
|
|
2291
|
+
lan_only=lan_only,
|
|
2292
|
+
max_horizon_sec=horizon,
|
|
2293
|
+
)
|
|
2294
|
+
if not ok:
|
|
2295
|
+
print(f"ERROR: failed to update group {ref!r}", file=sys.stderr)
|
|
2296
|
+
return 1
|
|
2297
|
+
|
|
2298
|
+
updated = next((g for g in groups.user_group_handler.get_all_user_groups(include_users=False) or []
|
|
2299
|
+
if int(g.get("group_id", -1)) == group_id), selected)
|
|
2300
|
+
if flags.get("json"):
|
|
2301
|
+
result = dict(updated)
|
|
2302
|
+
if sharing is not None:
|
|
2303
|
+
result["shared_with"] = sharing
|
|
2304
|
+
print(json.dumps(result, separators=(",", ":"), default=str))
|
|
2305
|
+
else:
|
|
2306
|
+
print(f"Updated group {updated.get('group_name', ref)} (id {group_id})")
|
|
2307
|
+
return 0
|
|
2210
2308
|
|
|
2211
2309
|
|
|
2212
2310
|
def cmd_groups_remove(argv):
|
|
2213
|
-
from remoteRF_server.server
|
|
2311
|
+
from remoteRF_server.server import user_group_cli as groups
|
|
2214
2312
|
|
|
2215
|
-
|
|
2313
|
+
if not argv:
|
|
2314
|
+
if sys.stdin.isatty():
|
|
2315
|
+
groups.remove_user_group_local(_prompt_session())
|
|
2316
|
+
return 0
|
|
2317
|
+
print("ERROR: non-interactive groups remove requires a group and --yes", file=sys.stderr)
|
|
2318
|
+
return 1
|
|
2319
|
+
if not _yes(argv):
|
|
2320
|
+
print("ERROR: non-interactive groups remove requires --yes", file=sys.stderr)
|
|
2321
|
+
return 1
|
|
2322
|
+
parsed, error = _parse_create_args(argv, value_flags=set(), bool_flags={"yes", "json"})
|
|
2323
|
+
if error:
|
|
2324
|
+
print(f"ERROR: {error}", file=sys.stderr)
|
|
2325
|
+
return 1
|
|
2326
|
+
pos, flags = parsed
|
|
2327
|
+
if len(pos) != 1:
|
|
2328
|
+
print("ERROR: groups remove requires exactly one group id or name", file=sys.stderr)
|
|
2329
|
+
return 1
|
|
2330
|
+
ref = pos[0].strip()
|
|
2331
|
+
all_groups = groups.user_group_handler.get_all_user_groups(include_users=False) or []
|
|
2332
|
+
selected = next((g for g in all_groups if str(g.get("group_id")) == ref), None)
|
|
2333
|
+
if selected is None:
|
|
2334
|
+
selected = next((g for g in all_groups if str(g.get("group_name")) == ref), None)
|
|
2335
|
+
if selected is None:
|
|
2336
|
+
print(f"ERROR: unknown group {ref!r}", file=sys.stderr)
|
|
2337
|
+
return 1
|
|
2338
|
+
if not groups.user_group_handler.delete_user_group(group_id=int(selected["group_id"])):
|
|
2339
|
+
print(f"ERROR: failed to delete group {ref!r}", file=sys.stderr)
|
|
2340
|
+
return 1
|
|
2341
|
+
if flags.get("json"):
|
|
2342
|
+
print(json.dumps(selected, separators=(",", ":"), default=str))
|
|
2343
|
+
else:
|
|
2344
|
+
print(f"Deleted group {selected['group_name']} (id {selected['group_id']})")
|
|
2345
|
+
return 0
|
|
2216
2346
|
|
|
2217
2347
|
|
|
2218
2348
|
def cmd_groups_csv(argv):
|
|
@@ -2305,9 +2435,48 @@ def cmd_codes_create(argv):
|
|
|
2305
2435
|
|
|
2306
2436
|
|
|
2307
2437
|
def cmd_codes_remove(argv):
|
|
2308
|
-
from remoteRF_server.server
|
|
2438
|
+
from remoteRF_server.server import user_group_cli as groups
|
|
2309
2439
|
|
|
2310
|
-
|
|
2440
|
+
if not argv:
|
|
2441
|
+
if sys.stdin.isatty():
|
|
2442
|
+
groups.remove_enrollment_code_local(_prompt_session())
|
|
2443
|
+
return 0
|
|
2444
|
+
print("ERROR: non-interactive codes remove requires a code or --group and --yes", file=sys.stderr)
|
|
2445
|
+
return 1
|
|
2446
|
+
if not _yes(argv):
|
|
2447
|
+
print("ERROR: non-interactive codes remove requires --yes", file=sys.stderr)
|
|
2448
|
+
return 1
|
|
2449
|
+
parsed, error = _parse_create_args(argv, value_flags={"group"}, bool_flags={"yes", "json"})
|
|
2450
|
+
if error:
|
|
2451
|
+
print(f"ERROR: {error}", file=sys.stderr)
|
|
2452
|
+
return 1
|
|
2453
|
+
pos, flags = parsed
|
|
2454
|
+
if (len(pos) != 1) == ("group" not in flags):
|
|
2455
|
+
print("ERROR: codes remove requires exactly one code/invite or --group <id>", file=sys.stderr)
|
|
2456
|
+
return 1
|
|
2457
|
+
rows = groups.user_group_handler.get_all_enrollment_codes() or []
|
|
2458
|
+
if "group" in flags:
|
|
2459
|
+
try:
|
|
2460
|
+
group_id = _bounded_int(flags["group"], "--group", minimum=0)
|
|
2461
|
+
except ValueError as exc:
|
|
2462
|
+
print(f"ERROR: {exc}", file=sys.stderr)
|
|
2463
|
+
return 1
|
|
2464
|
+
removed = [row for row in rows if int(row.get("group_id", -1)) == group_id]
|
|
2465
|
+
if not removed or not groups.user_group_handler.delete_enrollment_code_id(group_id=group_id):
|
|
2466
|
+
print(f"ERROR: no enrollment codes found for group {group_id}", file=sys.stderr)
|
|
2467
|
+
return 1
|
|
2468
|
+
result = {"group_id": group_id, "removed": removed}
|
|
2469
|
+
message = f"Deleted {len(removed)} enrollment code(s) for group {group_id}"
|
|
2470
|
+
else:
|
|
2471
|
+
code_name = pos[0].rpartition("/")[2].strip()
|
|
2472
|
+
selected = next((row for row in rows if row.get("code_name") == code_name), None)
|
|
2473
|
+
if selected is None or not groups.user_group_handler.delete_enrollment_code_name(code_name=code_name):
|
|
2474
|
+
print(f"ERROR: enrollment code {code_name!r} not found", file=sys.stderr)
|
|
2475
|
+
return 1
|
|
2476
|
+
result = selected
|
|
2477
|
+
message = f"Deleted enrollment code {code_name}"
|
|
2478
|
+
print(json.dumps(result, separators=(",", ":"), default=str) if flags.get("json") else message)
|
|
2479
|
+
return 0
|
|
2311
2480
|
|
|
2312
2481
|
|
|
2313
2482
|
def cmd_codes_csv(argv):
|
|
@@ -2316,6 +2485,53 @@ def cmd_codes_csv(argv):
|
|
|
2316
2485
|
export_enrollment_codes_csv_local()
|
|
2317
2486
|
|
|
2318
2487
|
|
|
2488
|
+
def _cmd_users_manage_noninteractive(argv, perms) -> int:
|
|
2489
|
+
"""Flag-only half of the daemon's users-manage command."""
|
|
2490
|
+
if "-y" not in argv and "--yes" not in argv:
|
|
2491
|
+
print("ERROR: non-interactive users manage requires --yes", file=sys.stderr)
|
|
2492
|
+
return 1
|
|
2493
|
+
parsed, error = _parse_create_args(
|
|
2494
|
+
argv, value_flags={"role"}, bool_flags={"yes", "json"},
|
|
2495
|
+
)
|
|
2496
|
+
if error:
|
|
2497
|
+
print(f"ERROR: {error}", file=sys.stderr)
|
|
2498
|
+
return 1
|
|
2499
|
+
positional, flags = parsed
|
|
2500
|
+
if len(positional) != 1 or "role" not in flags:
|
|
2501
|
+
print("ERROR: users manage requires exactly one user and --role <user|power-user|admin>", file=sys.stderr)
|
|
2502
|
+
return 1
|
|
2503
|
+
|
|
2504
|
+
username = positional[0]
|
|
2505
|
+
current = perms.get_perms(username)
|
|
2506
|
+
if not current:
|
|
2507
|
+
print(f"ERROR: user {username!r} does not exist", file=sys.stderr)
|
|
2508
|
+
return 1
|
|
2509
|
+
raw_role = str(flags["role"]).strip().lower().replace("_", "-")
|
|
2510
|
+
roles = {
|
|
2511
|
+
"u": "Normal User", "user": "Normal User", "normal-user": "Normal User", "normal user": "Normal User",
|
|
2512
|
+
"p": "Power User", "power": "Power User", "power-user": "Power User", "power user": "Power User",
|
|
2513
|
+
"a": "Admin", "admin": "Admin",
|
|
2514
|
+
}
|
|
2515
|
+
role = roles.get(raw_role)
|
|
2516
|
+
if role is None:
|
|
2517
|
+
print("ERROR: --role must be user, power-user, or admin", file=sys.stderr)
|
|
2518
|
+
return 1
|
|
2519
|
+
if role == "Normal User":
|
|
2520
|
+
perms.set_user(username)
|
|
2521
|
+
elif role == "Admin":
|
|
2522
|
+
perms.set_admin(username, confirm=False, quiet=True)
|
|
2523
|
+
elif current[0][0] != "Power User":
|
|
2524
|
+
print("ERROR: changing to power-user requires its device and quota details; use interactive users manage", file=sys.stderr)
|
|
2525
|
+
return 1
|
|
2526
|
+
|
|
2527
|
+
result = {"user": username, "role": role}
|
|
2528
|
+
if flags.get("json"):
|
|
2529
|
+
print(json.dumps(result, separators=(",", ":")))
|
|
2530
|
+
else:
|
|
2531
|
+
print(f"Set {username} role to {role}.")
|
|
2532
|
+
return 0
|
|
2533
|
+
|
|
2534
|
+
|
|
2319
2535
|
def cmd_hosts_token(argv: List[str]) -> int:
|
|
2320
2536
|
from .host.host_auth_token import create_host_token
|
|
2321
2537
|
|
|
@@ -113,14 +113,36 @@ class ReservedTotalTests(unittest.TestCase):
|
|
|
113
113
|
now = datetime.now()
|
|
114
114
|
add = "INSERT INTO Reservations (username, device_id, start_time, end_time, salt, hash) VALUES ('u', 1, ?, ?, '', '')"
|
|
115
115
|
total = lambda: db.execute("SELECT seconds FROM ReservedTotal").fetchone()[0]
|
|
116
|
-
db.execute(add, (now - timedelta(hours=3), now - timedelta(hours=2))) # 1 h, past
|
|
117
116
|
db.execute(add, (now + timedelta(hours=1), now + timedelta(hours=3))) # 2 h, future
|
|
117
|
+
db.execute(add, (now + timedelta(hours=4), now + timedelta(hours=5))) # 1 h, future
|
|
118
118
|
self.assertAlmostEqual(total(), 3 * 3600, delta=1)
|
|
119
|
+
# Let the 1 h one end; moving it back by UPDATE fires no trigger.
|
|
120
|
+
db.execute("UPDATE Reservations SET start_time = ?, end_time = ? WHERE start_time > ?",
|
|
121
|
+
(now - timedelta(hours=3), now - timedelta(hours=2), now + timedelta(hours=2)))
|
|
119
122
|
db.execute("DELETE FROM Reservations WHERE end_time < ?", (now,)) # purge: no change
|
|
120
123
|
self.assertAlmostEqual(total(), 3 * 3600, delta=1)
|
|
121
124
|
db.execute("DELETE FROM Reservations") # cancel the future one: unused 2 h refunded
|
|
122
125
|
self.assertAlmostEqual(total(), 3600, delta=2)
|
|
123
126
|
|
|
127
|
+
def test_a_booking_made_after_its_start_counts_only_from_then(self):
|
|
128
|
+
import sqlite3
|
|
129
|
+
from datetime import datetime, timedelta
|
|
130
|
+
from remoteRF_server.server import reservation
|
|
131
|
+
|
|
132
|
+
with tempfile.TemporaryDirectory() as tmp, \
|
|
133
|
+
mock.patch.object(reservation, "get_db_dir", return_value=Path(tmp)):
|
|
134
|
+
reservation.ReservationHandler()
|
|
135
|
+
db = sqlite3.connect(Path(tmp) / "reservations.db", isolation_level=None)
|
|
136
|
+
now = datetime.now()
|
|
137
|
+
add = "INSERT INTO Reservations (username, device_id, start_time, end_time, salt, hash) VALUES ('u', 1, ?, ?, '', '')"
|
|
138
|
+
total = lambda: db.execute("SELECT seconds FROM ReservedTotal").fetchone()[0]
|
|
139
|
+
db.execute(add, (now - timedelta(minutes=15), now + timedelta(minutes=15)))
|
|
140
|
+
self.assertAlmostEqual(total(), 15 * 60, delta=2)
|
|
141
|
+
db.execute("DELETE FROM Reservations") # cancelled right away: nothing held
|
|
142
|
+
self.assertAlmostEqual(total(), 0, delta=2)
|
|
143
|
+
db.execute(add, (now - timedelta(hours=2), now - timedelta(hours=1))) # already over
|
|
144
|
+
self.assertAlmostEqual(total(), 0, delta=2)
|
|
145
|
+
|
|
124
146
|
def test_a_partner_device_is_left_to_its_owner_even_with_the_old_triggers(self):
|
|
125
147
|
import sqlite3
|
|
126
148
|
from datetime import datetime, timedelta
|
|
@@ -255,15 +277,18 @@ class ReservationBucketsTests(unittest.TestCase):
|
|
|
255
277
|
|
|
256
278
|
self.handler()
|
|
257
279
|
db = self.connect()
|
|
258
|
-
self.add(db, 1, "
|
|
259
|
-
self.add(db, 1, "
|
|
260
|
-
self.add(db, ALIAS_BASE, "
|
|
280
|
+
self.add(db, 1, "2099-11-02 02:00:00", "2099-11-02 03:00:00") # Mon 02:00 here = Sun 20:30 UTC
|
|
281
|
+
self.add(db, 1, "2099-11-04 12:00:00", "2099-11-04 14:00:00") # Wed 12:00 here = Wed 06:30 UTC
|
|
282
|
+
self.add(db, ALIAS_BASE, "2099-11-02 02:00:00", "2099-11-02 03:00:00")
|
|
261
283
|
expected = {"all": (2, 3 * 3600), "h20": (1, 3600), "d6": (1, 3600), "h06": (1, 2 * 3600), "d2": (1, 2 * 3600)}
|
|
262
284
|
self.assertEqual(self.buckets(db, 1), expected)
|
|
263
285
|
self.assertEqual(self.buckets(db, ALIAS_BASE), {})
|
|
264
|
-
days = {"
|
|
286
|
+
days = {"2099-11-01": (1, 3600), "2099-11-04": (1, 2 * 3600)} # the first starts the UTC day before
|
|
265
287
|
self.assertEqual(self.days(db, 1), days)
|
|
266
288
|
self.assertEqual(self.days(db, ALIAS_BASE), {})
|
|
289
|
+
# Let them end; moving them back by UPDATE fires no trigger.
|
|
290
|
+
db.execute("UPDATE Reservations SET start_time = datetime(start_time, '-100 years'), "
|
|
291
|
+
"end_time = datetime(end_time, '-100 years')")
|
|
267
292
|
db.execute("DELETE FROM Reservations WHERE end_time < ?", (datetime.now(),)) # purge: no change
|
|
268
293
|
self.assertEqual(self.buckets(db, 1), expected)
|
|
269
294
|
self.assertEqual(self.days(db, 1), days)
|
|
@@ -281,10 +306,13 @@ class ReservationBucketsTests(unittest.TestCase):
|
|
|
281
306
|
self.assertEqual(values(2), [(1, 2 * 3600)] * 4)
|
|
282
307
|
db.execute("DELETE FROM Reservations WHERE device_id = 2") # cancel before start: all given back
|
|
283
308
|
self.assertEqual(values(2), [(0, 0)] * 4)
|
|
309
|
+
for n, seconds in values(3): # booked an hour after its start: only the rest counts
|
|
310
|
+
self.assertEqual(n, 1)
|
|
311
|
+
self.assertAlmostEqual(seconds, 3600, delta=2)
|
|
284
312
|
db.execute("DELETE FROM Reservations WHERE device_id = 3") # cancel after start: unused hour back
|
|
285
313
|
for n, seconds in values(3):
|
|
286
314
|
self.assertEqual(n, 1)
|
|
287
|
-
self.assertAlmostEqual(seconds,
|
|
315
|
+
self.assertAlmostEqual(seconds, 0, delta=2)
|
|
288
316
|
for table in ("ReservationBuckets", "ReservationDays"):
|
|
289
317
|
db.execute(f"UPDATE {table} SET reservations = 0, seconds = 0 WHERE device_id = 4")
|
|
290
318
|
db.execute("DELETE FROM Reservations WHERE device_id = 4")
|
|
@@ -317,9 +345,30 @@ class ReservationBucketsTests(unittest.TestCase):
|
|
|
317
345
|
time.tzset()
|
|
318
346
|
self.assertEqual(self.buckets(db, 1), expected)
|
|
319
347
|
self.assertEqual(self.days(db, 1), days)
|
|
320
|
-
self.add(db, 1, "
|
|
348
|
+
self.add(db, 1, "2099-11-02 02:20:00", "2099-11-02 02:50:00") # 20:50 UTC
|
|
321
349
|
self.assertEqual(self.buckets(db, 1)["h20"], (3, 7200))
|
|
322
|
-
self.assertEqual(self.days(db, 1), {"2026-11-01": (
|
|
350
|
+
self.assertEqual(self.days(db, 1), {"2026-11-01": (2, 5400), "2099-11-01": (1, 1800)})
|
|
351
|
+
|
|
352
|
+
def test_a_restart_replaces_the_old_insert_triggers(self):
|
|
353
|
+
from datetime import datetime, timedelta
|
|
354
|
+
|
|
355
|
+
self.handler()
|
|
356
|
+
db = self.connect()
|
|
357
|
+
# A lab from before: its insert triggers count the full length.
|
|
358
|
+
for name in ("reservation_buckets_ins", "reservation_days_ins"):
|
|
359
|
+
sql = db.execute("SELECT sql FROM sqlite_master WHERE name = ?", (name,)).fetchone()[0]
|
|
360
|
+
old = sql.replace("MAX(NEW.start_time, datetime('now', 'localtime'))", "NEW.start_time")
|
|
361
|
+
self.assertNotEqual(old, sql)
|
|
362
|
+
db.executescript(f"DROP TRIGGER {name}; {old};")
|
|
363
|
+
self.handler()
|
|
364
|
+
for name in ("reservation_buckets_ins", "reservation_days_ins", "reserved_total_ins"):
|
|
365
|
+
sql = db.execute("SELECT sql FROM sqlite_master WHERE name = ?", (name,)).fetchone()[0]
|
|
366
|
+
self.assertIn("MAX(NEW.start_time, datetime('now', 'localtime'))", sql)
|
|
367
|
+
now = datetime.now()
|
|
368
|
+
self.add(db, 5, now - timedelta(minutes=15), now + timedelta(minutes=15))
|
|
369
|
+
for n, seconds in [*self.buckets(db, 5).values(), *self.days(db, 5).values()]:
|
|
370
|
+
self.assertEqual(n, 1)
|
|
371
|
+
self.assertAlmostEqual(seconds, 15 * 60, delta=2)
|
|
323
372
|
|
|
324
373
|
def test_days_seed_into_an_existing_lab_that_already_has_buckets(self):
|
|
325
374
|
"""A lab upgrading from 1.7.14 has ReservationBuckets but no ReservationDays yet."""
|