remoteRF-server 1.7.14__tar.gz → 1.7.16__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.14 → remoterf_server-1.7.16}/PKG-INFO +2 -2
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/pyproject.toml +2 -2
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/src/remoteRF_server/remote_admin_shell.py +1 -1
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/src/remoteRF_server/server/commands.py +1 -0
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/src/remoteRF_server/server/device_manager.py +30 -1
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/src/remoteRF_server/server/global_onboarding.py +60 -19
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/src/remoteRF_server/server/grpc_server.py +24 -1
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/src/remoteRF_server/server/reservation.py +23 -4
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/src/remoteRF_server.egg-info/PKG-INFO +2 -2
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/src/remoteRF_server.egg-info/requires.txt +1 -1
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/tests/test_client_sessions.py +35 -7
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/tests/test_global_onboarding.py +93 -6
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/tests/test_json_commands.py +83 -3
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/LICENSE +0 -0
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/README.md +0 -0
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/setup.cfg +0 -0
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/src/remoteRF_server/__init__.py +0 -0
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/src/remoteRF_server/common/__init__.py +0 -0
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/src/remoteRF_server/common/grpc/__init__.py +0 -0
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/src/remoteRF_server/common/grpc/deployment_capabilities.proto +0 -0
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/src/remoteRF_server/common/grpc/deployment_capabilities_pb2.py +0 -0
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/src/remoteRF_server/common/grpc/deployment_capabilities_pb2_grpc.py +0 -0
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/src/remoteRF_server/common/grpc/federation.proto +0 -0
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/src/remoteRF_server/common/grpc/federation_pb2.py +0 -0
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/src/remoteRF_server/common/grpc/federation_pb2_grpc.py +0 -0
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/src/remoteRF_server/common/grpc/grpc_host_pb2.py +0 -0
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/src/remoteRF_server/common/grpc/grpc_host_pb2_grpc.py +0 -0
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/src/remoteRF_server/common/grpc/grpc_pb2.py +0 -0
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/src/remoteRF_server/common/grpc/grpc_pb2_grpc.py +0 -0
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/src/remoteRF_server/common/grpc/v2_codec.py +0 -0
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/src/remoteRF_server/common/idl/__init__.py +0 -0
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/src/remoteRF_server/common/idl/device_schema.py +0 -0
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/src/remoteRF_server/common/idl/pluto_mimo_schema.py +0 -0
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/src/remoteRF_server/common/idl/pluto_schema.py +0 -0
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/src/remoteRF_server/common/idl/schema.py +0 -0
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/src/remoteRF_server/common/idl/v2.py +0 -0
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/src/remoteRF_server/common/utils/__init__.py +0 -0
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/src/remoteRF_server/common/utils/ansi_codes.py +0 -0
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/src/remoteRF_server/common/utils/api_token.py +0 -0
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/src/remoteRF_server/common/utils/db_connection.py +0 -0
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/src/remoteRF_server/common/utils/db_location.py +0 -0
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/src/remoteRF_server/common/utils/list_string.py +0 -0
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/src/remoteRF_server/common/utils/panel.py +0 -0
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/src/remoteRF_server/common/utils/process_arg.py +0 -0
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/src/remoteRF_server/drivers/__init__.py +0 -0
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/src/remoteRF_server/drivers/adalm_pluto/__init__.py +0 -0
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/src/remoteRF_server/drivers/adalm_pluto/pluto_diag.py +0 -0
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/src/remoteRF_server/drivers/adalm_pluto/pluto_mimo_schema.py +0 -0
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/src/remoteRF_server/drivers/adalm_pluto/pluto_remote_server.py +0 -0
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/src/remoteRF_server/drivers/adalm_pluto/pluto_schema.py +0 -0
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/src/remoteRF_server/drivers/adalm_pluto/poe.py +0 -0
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/src/remoteRF_server/drivers/bluetooth_diag.py +0 -0
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/src/remoteRF_server/drivers/gps_diag.py +0 -0
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/src/remoteRF_server/drivers/hackrf/__init__.py +0 -0
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/src/remoteRF_server/drivers/hackrf/hackrf_schema.py +0 -0
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/src/remoteRF_server/drivers/rtl_sdr/__init__.py +0 -0
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/src/remoteRF_server/drivers/rtl_sdr/rtl_sdr_diag.py +0 -0
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/src/remoteRF_server/drivers/rtl_sdr/rtl_sdr_schema.py +0 -0
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/src/remoteRF_server/drivers/ti_mmwave/__init__.py +0 -0
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/src/remoteRF_server/drivers/ti_mmwave/ti_mmwave_schema.py +0 -0
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/src/remoteRF_server/drivers/usrp/__init__.py +0 -0
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/src/remoteRF_server/drivers/usrp/policy.py +0 -0
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/src/remoteRF_server/drivers/usrp/profiles.py +0 -0
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/src/remoteRF_server/drivers/usrp/uhd_manifest.py +0 -0
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/src/remoteRF_server/drivers/usrp/usrp_diag.py +0 -0
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/src/remoteRF_server/drivers/usrp/usrp_schema.py +0 -0
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/src/remoteRF_server/drivers/usrp_phaser/__init__.py +0 -0
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/src/remoteRF_server/drivers/usrp_phaser/usrp_phaser_schema.py +0 -0
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/src/remoteRF_server/drivers/wifi/__init__.py +0 -0
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/src/remoteRF_server/drivers/wifi/wifi_schema.py +0 -0
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/src/remoteRF_server/host/__init__.py +0 -0
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/src/remoteRF_server/host/host_auth_token.py +0 -0
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/src/remoteRF_server/host/host_directory_store.py +0 -0
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/src/remoteRF_server/host/host_tunnel_server.py +0 -0
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/src/remoteRF_server/server/__init__.py +0 -0
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/src/remoteRF_server/server/acc_perms.py +0 -0
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/src/remoteRF_server/server/cert_provider.py +0 -0
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/src/remoteRF_server/server/console.py +0 -0
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/src/remoteRF_server/server/control.py +0 -0
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/src/remoteRF_server/server/deployment/__init__.py +0 -0
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/src/remoteRF_server/server/deployment/capabilities.py +0 -0
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/src/remoteRF_server/server/diag.py +0 -0
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/src/remoteRF_server/server/direct_path.py +0 -0
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/src/remoteRF_server/server/dynamic_v2.py +0 -0
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/src/remoteRF_server/server/dynamic_v2_bridge.py +0 -0
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/src/remoteRF_server/server/federation/__init__.py +0 -0
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/src/remoteRF_server/server/federation/admin.py +0 -0
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/src/remoteRF_server/server/federation/cli.py +0 -0
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/src/remoteRF_server/server/federation/cluster.py +0 -0
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/src/remoteRF_server/server/federation/contracts.py +0 -0
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/src/remoteRF_server/server/federation/destination.py +0 -0
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/src/remoteRF_server/server/federation/devices.py +0 -0
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/src/remoteRF_server/server/federation/enrollment.py +0 -0
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/src/remoteRF_server/server/federation/global_home.py +0 -0
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/src/remoteRF_server/server/federation/home.py +0 -0
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/src/remoteRF_server/server/federation/home_devices.py +0 -0
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/src/remoteRF_server/server/federation/identity.py +0 -0
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/src/remoteRF_server/server/federation/legacy_state.py +0 -0
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/src/remoteRF_server/server/federation/links.py +0 -0
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/src/remoteRF_server/server/federation/migrations.py +0 -0
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/src/remoteRF_server/server/federation/peers.py +0 -0
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/src/remoteRF_server/server/federation/permissions.py +0 -0
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/src/remoteRF_server/server/federation/principals.py +0 -0
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/src/remoteRF_server/server/federation/runtime.py +0 -0
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/src/remoteRF_server/server/federation/trust.py +0 -0
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/src/remoteRF_server/server/host_direct.py +0 -0
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/src/remoteRF_server/server/host_dynamic_v2.py +0 -0
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/src/remoteRF_server/server/json_output.py +0 -0
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/src/remoteRF_server/server/remote_admin.py +0 -0
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/src/remoteRF_server/server/remote_admin_ssh.py +0 -0
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/src/remoteRF_server/server/rpc_manager.py +0 -0
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/src/remoteRF_server/server/schema_registry.py +0 -0
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/src/remoteRF_server/server/startup_check.py +0 -0
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/src/remoteRF_server/server/user_group_cli.py +0 -0
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/src/remoteRF_server/server/user_group_handler.py +0 -0
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/src/remoteRF_server/server/v2_errors.py +0 -0
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/src/remoteRF_server/serverrf_cli.py +0 -0
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/src/remoteRF_server/tools/__init__.py +0 -0
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/src/remoteRF_server/tools/gen_certs.py +0 -0
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/src/remoteRF_server/tools/introspect_uhd.py +0 -0
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/src/remoteRF_server.egg-info/SOURCES.txt +0 -0
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/src/remoteRF_server.egg-info/dependency_links.txt +0 -0
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/src/remoteRF_server.egg-info/entry_points.txt +0 -0
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/src/remoteRF_server.egg-info/top_level.txt +0 -0
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/src/rssi/__init__.py +0 -0
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/src/rssi/cli.py +0 -0
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/src/rssi/dongle.py +0 -0
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/src/rssi/safety.py +0 -0
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/src/rssi/scanner.py +0 -0
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/tests/test_account_hardening.py +0 -0
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/tests/test_account_names.py +0 -0
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/tests/test_account_removal.py +0 -0
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/tests/test_activity_log.py +0 -0
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/tests/test_availability.py +0 -0
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/tests/test_bind_host.py +0 -0
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/tests/test_cert_provider.py +0 -0
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/tests/test_cluster.py +0 -0
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/tests/test_commands.py +0 -0
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/tests/test_console.py +0 -0
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/tests/test_contrib_drivers.py +0 -0
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/tests/test_default_rtl_sdr_schema.py +0 -0
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/tests/test_default_usrp_schema.py +0 -0
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/tests/test_default_wifi_schema.py +0 -0
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/tests/test_deployment_capabilities.py +0 -0
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/tests/test_detached_device_rpc.py +0 -0
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/tests/test_device_core_aliases.py +0 -0
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/tests/test_device_handles.py +0 -0
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/tests/test_device_init.py +0 -0
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/tests/test_devices_add_wizard.py +0 -0
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/tests/test_devices_list.py +0 -0
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/tests/test_diag.py +0 -0
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/tests/test_direct_fixed_port.py +0 -0
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/tests/test_direct_path_server.py +0 -0
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/tests/test_dynamic_v2.py +0 -0
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/tests/test_federated_direct_path.py +0 -0
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/tests/test_federated_dynamic_v2.py +0 -0
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/tests/test_federation_edges.py +0 -0
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/tests/test_first_run.py +0 -0
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/tests/test_gate_d_contracts.py +0 -0
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/tests/test_gate_e_federation.py +0 -0
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/tests/test_gate_e_proto.py +0 -0
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/tests/test_gate_f_enrollment.py +0 -0
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/tests/test_gate_g1_logging.py +0 -0
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/tests/test_gate_g_permissions.py +0 -0
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/tests/test_gate_h_devices.py +0 -0
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/tests/test_gate_i_direct_path.py +0 -0
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/tests/test_gen_certs.py +0 -0
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/tests/test_global_accounts_migration.py +0 -0
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/tests/test_global_home.py +0 -0
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/tests/test_global_v1_disposal.py +0 -0
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/tests/test_hackrf_schema.py +0 -0
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/tests/test_home_device_plane.py +0 -0
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/tests/test_host_direct.py +0 -0
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/tests/test_host_directory_store.py +0 -0
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/tests/test_host_dynamic_v2.py +0 -0
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/tests/test_idl_schema.py +0 -0
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/tests/test_lab_logins.py +0 -0
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/tests/test_panel.py +0 -0
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/tests/test_peer_transport.py +0 -0
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/tests/test_pluto_compat.py +0 -0
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/tests/test_pluto_poe.py +0 -0
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/tests/test_process_arg.py +0 -0
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/tests/test_proxy_lifecycle.py +0 -0
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/tests/test_remote_admin.py +0 -0
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/tests/test_reservation_cancel.py +0 -0
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/tests/test_reservation_handoff.py +0 -0
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/tests/test_reservation_policy_b.py +0 -0
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/tests/test_reservation_timestamps.py +0 -0
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/tests/test_rpc_manager_batch.py +0 -0
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/tests/test_rpc_manager_calln.py +0 -0
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/tests/test_rpc_manager_host_legacy.py +0 -0
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/tests/test_rpc_manager_rx_n.py +0 -0
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/tests/test_rtl_sdr_schema.py +0 -0
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/tests/test_schema_registry.py +0 -0
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/tests/test_soak_federation.py +0 -0
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/tests/test_startup_check.py +0 -0
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/tests/test_ti_mmwave_hardware.py +0 -0
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/tests/test_ti_mmwave_schema.py +0 -0
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/tests/test_user_group_cli.py +0 -0
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/tests/test_usrp_b205mini_uhd410_hardware.py +0 -0
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/tests/test_usrp_client_server_hardware.py +0 -0
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/tests/test_usrp_client_server_localhost.py +0 -0
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/tests/test_usrp_n210_client_server_hardware.py +0 -0
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/tests/test_usrp_preload.py +0 -0
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/tests/test_usrp_profiles.py +0 -0
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/tests/test_usrp_remoterf_rx_hardware.py +0 -0
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/tests/test_usrp_uhd410_hardware.py +0 -0
- {remoterf_server-1.7.14 → remoterf_server-1.7.16}/tests/test_wipe.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: remoteRF-server
|
|
3
|
-
Version: 1.7.
|
|
3
|
+
Version: 1.7.16
|
|
4
4
|
Summary: RemoteRF server-side control package
|
|
5
5
|
License: GPL-3.0-or-later
|
|
6
6
|
Classifier: Programming Language :: Python :: 3
|
|
@@ -10,7 +10,7 @@ Requires-Python: >=3.10
|
|
|
10
10
|
Description-Content-Type: text/markdown
|
|
11
11
|
License-File: LICENSE
|
|
12
12
|
Requires-Dist: remoterf-federation-core==0.22.0
|
|
13
|
-
Requires-Dist: remoterf-device-core==0.1.
|
|
13
|
+
Requires-Dist: remoterf-device-core==0.1.3
|
|
14
14
|
Requires-Dist: grpcio<2.0.0,>=1.81.1
|
|
15
15
|
Requires-Dist: protobuf<7.0.0,>=6.33.5
|
|
16
16
|
Requires-Dist: numpy
|
|
@@ -4,13 +4,13 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "remoteRF-server"
|
|
7
|
-
version = "1.7.
|
|
7
|
+
version = "1.7.16"
|
|
8
8
|
description = "RemoteRF server-side control package"
|
|
9
9
|
license = { text = "GPL-3.0-or-later" }
|
|
10
10
|
requires-python = ">=3.10"
|
|
11
11
|
dependencies = [
|
|
12
12
|
"remoterf-federation-core==0.22.0",
|
|
13
|
-
"remoterf-device-core==0.1.
|
|
13
|
+
"remoterf-device-core==0.1.3",
|
|
14
14
|
"grpcio>=1.81.1,<2.0.0",
|
|
15
15
|
"protobuf>=6.33.5,<7.0.0",
|
|
16
16
|
"numpy",
|
|
@@ -48,7 +48,7 @@ TIERS = {
|
|
|
48
48
|
"reservations list": READ, "groups list": READ, "codes list": READ, "hosts status": READ,
|
|
49
49
|
"hosts tokens": READ, "certs list": READ, "config list": READ, "global status": READ,
|
|
50
50
|
"cluster list": READ, "cluster info": READ, "users prune-proxies": READ,
|
|
51
|
-
"cluster codes list": READ, "schema browse": READ,
|
|
51
|
+
"cluster codes list": READ, "schema browse": READ, "schema served": READ,
|
|
52
52
|
# write
|
|
53
53
|
"devices add": WRITE, "devices remove": WRITE, "devices rename": WRITE, "groups create": WRITE,
|
|
54
54
|
"groups edit": WRITE, "groups public": WRITE, "groups share": WRITE, "codes create": WRITE,
|
|
@@ -162,6 +162,7 @@ def _table() -> list[Command]:
|
|
|
162
162
|
# ---- driver schemas approved on RemoteRF Global (server/schema_registry.py) ----
|
|
163
163
|
Command(("schema browse",), "Driver schemas approved on RemoteRF Global: schema browse [text]", _lazy(".schema_registry", "cmd_browse"), "Schemas"),
|
|
164
164
|
Command(("schema download",), "Save one to the drivers folder, hash checked: schema download <name|hash> [-y]", _lazy(".schema_registry", "cmd_download"), "Schemas"),
|
|
165
|
+
Command(("schema served",), "Which file serves a driver type, its hash, how each drivers file loaded: schema served <type> [--json]", _daemon("cmd_schema_served"), "Schemas", where=D),
|
|
165
166
|
# ---- clusters (sharing devices with other deployments) ----
|
|
166
167
|
Command(("cluster list",), "Clusters shared with other deployments", _federation("list"), "Clusters"),
|
|
167
168
|
Command(("cluster info",), "Every deployment, shared group and device in a cluster", _federation("info"), "Clusters"),
|
{remoterf_server-1.7.14 → remoterf_server-1.7.16}/src/remoteRF_server/server/device_manager.py
RENAMED
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
from __future__ import annotations
|
|
19
19
|
|
|
20
20
|
import importlib
|
|
21
|
+
import inspect
|
|
21
22
|
import logging
|
|
22
23
|
import os
|
|
23
24
|
import re
|
|
@@ -35,7 +36,8 @@ from contextlib import contextmanager
|
|
|
35
36
|
from remoterf_federation_core import is_device_handle
|
|
36
37
|
|
|
37
38
|
from ..common.utils import validate_token, get_remoterf_root, get_drivers_dir, device_name_key
|
|
38
|
-
from ..common.idl.schema import get_driver_class, load_drivers
|
|
39
|
+
from ..common.idl.schema import (OVERRIDE_PREFIX, get_driver_class, last_load_results, load_drivers,
|
|
40
|
+
override_stale)
|
|
39
41
|
from ..host import host_tunnel_server as hts
|
|
40
42
|
|
|
41
43
|
logger = logging.getLogger(__name__)
|
|
@@ -1384,6 +1386,33 @@ def acquire_device_by_gid(gid: int) -> Iterator[Tuple[int, object]]:
|
|
|
1384
1386
|
lock.release()
|
|
1385
1387
|
|
|
1386
1388
|
|
|
1389
|
+
def schema_served(device_type: str) -> Dict[str, Any]:
|
|
1390
|
+
"""`schema served <type>`: the file whose class serves a type now, its v1 hash, and how each drivers file loaded."""
|
|
1391
|
+
out: Dict[str, Any] = {"type": device_type, "hash": None, "module": None, "override": False,
|
|
1392
|
+
"source_sha256": None, "loads": last_load_results()}
|
|
1393
|
+
try:
|
|
1394
|
+
cls = get_driver_class(device_type)
|
|
1395
|
+
out["module"] = os.path.abspath(inspect.getfile(cls))
|
|
1396
|
+
except (KeyError, TypeError):
|
|
1397
|
+
return out
|
|
1398
|
+
out["override"] = Path(out["module"]).name.startswith(OVERRIDE_PREFIX)
|
|
1399
|
+
try:
|
|
1400
|
+
out["hash"] = cls().get_idl_hash()
|
|
1401
|
+
except Exception:
|
|
1402
|
+
pass
|
|
1403
|
+
try:
|
|
1404
|
+
out["source_sha256"] = _driver_sha256(Path(out["module"]))
|
|
1405
|
+
except OSError:
|
|
1406
|
+
pass
|
|
1407
|
+
return out
|
|
1408
|
+
|
|
1409
|
+
|
|
1410
|
+
def driver_overrides() -> List[Dict[str, Any]]:
|
|
1411
|
+
"""The @ops-rrf overrides in the drivers folder, stale ones included (they do not load)."""
|
|
1412
|
+
return [{"type": f.stem[len(OVERRIDE_PREFIX):], "file": f.name, "stale": override_stale(f)}
|
|
1413
|
+
for f in sorted(get_drivers_dir().glob(f"{OVERRIDE_PREFIX}*.py"))]
|
|
1414
|
+
|
|
1415
|
+
|
|
1387
1416
|
def device_inventory_snapshot() -> Dict[int, Dict[str, object]]:
|
|
1388
1417
|
"""Structured inventory for federated discovery.
|
|
1389
1418
|
|
{remoterf_server-1.7.14 → remoterf_server-1.7.16}/src/remoteRF_server/server/global_onboarding.py
RENAMED
|
@@ -64,7 +64,9 @@ deployments), what devices it has, how many reservations are active, how many
|
|
|
64
64
|
reservations have been made and the total time reserved so far (two numbers),
|
|
65
65
|
and your LAN address. For the devices you share on Global it also sends how
|
|
66
66
|
many reservations start in each hour of the day and on each day of the week
|
|
67
|
-
(UTC),
|
|
67
|
+
(UTC), the reservations and time reserved per device type, and for each
|
|
68
|
+
device shared on Global its name and its daily reservation counts and time
|
|
69
|
+
reserved over the last 31 days (counts only).
|
|
68
70
|
Nothing else leaves your server -- no credentials, no user or reservation
|
|
69
71
|
details -- and the connection only goes outward. Global publishes the network
|
|
70
72
|
totals, these breakdowns, and each deployment's name, version, devices, when
|
|
@@ -867,24 +869,41 @@ def _device_type(item: dict) -> str:
|
|
|
867
869
|
return str(item.get("device_type") or "unknown").lower()
|
|
868
870
|
|
|
869
871
|
|
|
872
|
+
def _host_devices() -> dict:
|
|
873
|
+
"""Device id -> DeviceInfo from the Host directory `serverrf status` lists."""
|
|
874
|
+
try:
|
|
875
|
+
from ..host import host_tunnel_server as hts
|
|
876
|
+
|
|
877
|
+
directory = hts.get_tunnel_registry().device_directory_cached(ttl_ms=0)
|
|
878
|
+
except Exception: # noqa: BLE001 - drivers are optional at report time
|
|
879
|
+
return {}
|
|
880
|
+
return {int(device_id): info for device_id, (_host_id, info, _active) in directory.items()
|
|
881
|
+
if str(device_id).isdigit()}
|
|
882
|
+
|
|
883
|
+
|
|
870
884
|
def _device_types() -> dict:
|
|
871
885
|
"""Device id -> type: devices.yml entries, then the Host directory `serverrf status` lists.
|
|
872
886
|
|
|
873
887
|
Types are bounded the way Global accepts them: a Host names its own kinds.
|
|
874
888
|
"""
|
|
875
889
|
types = {int(item["device_id"]): _device_type(item) for item in _registered_devices()}
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
directory = hts.get_tunnel_registry().device_directory_cached(ttl_ms=0)
|
|
880
|
-
except Exception: # noqa: BLE001 - drivers are optional at report time
|
|
881
|
-
directory = {}
|
|
882
|
-
for device_id, (_host_id, info, _active) in directory.items():
|
|
883
|
-
if str(device_id).isdigit():
|
|
884
|
-
types.setdefault(int(device_id), str(getattr(info, "kind", "") or "").strip().lower() or "unknown")
|
|
890
|
+
for device_id, info in _host_devices().items():
|
|
891
|
+
types.setdefault(device_id, str(getattr(info, "kind", "") or "").strip().lower() or "unknown")
|
|
885
892
|
return {gid: re.sub(r"[^a-z0-9_]", "_", kind)[:64] for gid, kind in types.items()}
|
|
886
893
|
|
|
887
894
|
|
|
895
|
+
def _device_names() -> dict:
|
|
896
|
+
"""Device id -> the name the devices list shows: devices.yml, then the Host's label.
|
|
897
|
+
|
|
898
|
+
Printable and at most 64 characters, the way Global accepts them.
|
|
899
|
+
"""
|
|
900
|
+
names = {int(item["device_id"]): item.get("name") for item in _registered_devices()}
|
|
901
|
+
for device_id, info in _host_devices().items():
|
|
902
|
+
names.setdefault(device_id, getattr(info, "label", ""))
|
|
903
|
+
return {gid: "".join(filter(str.isprintable, str(name or ""))).strip()[:64].rstrip() or f"device {gid}"
|
|
904
|
+
for gid, name in names.items()}
|
|
905
|
+
|
|
906
|
+
|
|
888
907
|
def device_summary() -> list:
|
|
889
908
|
"""One entry per registered device: its type and whether it is connected.
|
|
890
909
|
|
|
@@ -901,13 +920,15 @@ def device_summary() -> list:
|
|
|
901
920
|
|
|
902
921
|
def usage_summary() -> dict:
|
|
903
922
|
"""Reservations made on the devices shared on Global: by the UTC hour and
|
|
904
|
-
weekday they start,
|
|
923
|
+
weekday they start, reservations and seconds reserved per device type, and
|
|
924
|
+
per device by the UTC day they start over the last 31 days.
|
|
905
925
|
|
|
906
|
-
Counts only (ReservationBuckets); all zero when nothing is shared there.
|
|
926
|
+
Counts only (ReservationBuckets, ReservationDays); all zero when nothing is shared there.
|
|
907
927
|
"""
|
|
928
|
+
from datetime import datetime, timedelta, timezone
|
|
908
929
|
from .federation import cluster
|
|
909
930
|
|
|
910
|
-
usage = {"by_hour": [0] * 24, "by_weekday": [0] * 7, "by_type": {}}
|
|
931
|
+
usage = {"by_hour": [0] * 24, "by_weekday": [0] * 7, "by_type": {}, "by_device": []}
|
|
911
932
|
try:
|
|
912
933
|
shared = {int(d) for group in cluster.groups(cluster.GLOBAL) for d in group["devices"]}
|
|
913
934
|
except Exception: # noqa: BLE001 - not in the global cluster (yet), or its state is unreadable
|
|
@@ -927,6 +948,24 @@ def usage_summary() -> dict:
|
|
|
927
948
|
usage["by_hour" if bucket[0] == "h" else "by_weekday"][int(bucket[1:])] += n
|
|
928
949
|
busiest = sorted((kv for kv in by_type.items() if kv[1]["reservations"]), key=lambda kv: -kv[1]["reservations"])
|
|
929
950
|
usage["by_type"] = dict(busiest[:64]) # Global takes at most 64 types
|
|
951
|
+
|
|
952
|
+
# Global takes at most 128 devices and 1000 days in all: the oldest days go first.
|
|
953
|
+
today = datetime.now(timezone.utc).date()
|
|
954
|
+
by_device, entries = {}, 0
|
|
955
|
+
for device_id, day, n, seconds in _rows(
|
|
956
|
+
_db_dir() / "reservations.db",
|
|
957
|
+
"SELECT device_id, day, reservations, CAST(ROUND(seconds) AS INTEGER) FROM ReservationDays "
|
|
958
|
+
"WHERE day BETWEEN ? AND ? ORDER BY day DESC, device_id",
|
|
959
|
+
(str(today - timedelta(days=30)), str(today))):
|
|
960
|
+
if device_id not in shared or not (n or seconds) or (device_id not in by_device and len(by_device) == 128):
|
|
961
|
+
continue
|
|
962
|
+
if entries == 1000:
|
|
963
|
+
break
|
|
964
|
+
by_device.setdefault(device_id, {})[day] = [min(n, 2**31), min(seconds, 2**31)]
|
|
965
|
+
entries += 1
|
|
966
|
+
names = _device_names()
|
|
967
|
+
usage["by_device"] = [{"id": gid, "name": names.get(gid, f"device {gid}"), "type": types.get(gid, "unknown"),
|
|
968
|
+
"days": days} for gid, days in sorted(by_device.items())]
|
|
930
969
|
return usage
|
|
931
970
|
|
|
932
971
|
|
|
@@ -980,15 +1019,17 @@ def send_status() -> None:
|
|
|
980
1019
|
|
|
981
1020
|
payload = status_payload()
|
|
982
1021
|
# A Global older than a field refuses the note: drop the newest first
|
|
983
|
-
# (`usage`, then `reservations`), then
|
|
984
|
-
# was before each of them.
|
|
985
|
-
steps =
|
|
986
|
-
for drop in
|
|
1022
|
+
# (`usage.by_device`, `usage`, then `reservations`), then
|
|
1023
|
+
# `proxies`/`reserved_s`, as the note was before each of them.
|
|
1024
|
+
steps = [payload, dict(payload, usage={k: v for k, v in payload["usage"].items() if k != "by_device"})]
|
|
1025
|
+
for drop in (("usage",), ("usage", "reservations"), ("usage", "reservations", "proxies", "reserved_s")):
|
|
1026
|
+
steps.append({k: v for k, v in payload.items() if k not in drop})
|
|
1027
|
+
for status in steps:
|
|
987
1028
|
try:
|
|
988
|
-
call("POST", REPORT_PATH, {"kind": "status", "status":
|
|
1029
|
+
call("POST", REPORT_PATH, {"kind": "status", "status": status})
|
|
989
1030
|
break
|
|
990
1031
|
except GlobalError as exc:
|
|
991
|
-
if exc.code != "invalid_status" or
|
|
1032
|
+
if exc.code != "invalid_status" or status is steps[-1]:
|
|
992
1033
|
raise
|
|
993
1034
|
update(GLOBAL_LAST_REPORT_AT=datetime.now(timezone.utc).isoformat())
|
|
994
1035
|
|
|
@@ -55,7 +55,8 @@ from .host_dynamic_v2 import host_session_proxy
|
|
|
55
55
|
from .acc_perms import perms
|
|
56
56
|
from ..common.utils import *
|
|
57
57
|
from .device_manager import (close_local_devices, reload_devices, get_registered_devices,
|
|
58
|
-
get_all_devices_str, handle_report, device_inventory_snapshot
|
|
58
|
+
get_all_devices_str, handle_report, device_inventory_snapshot,
|
|
59
|
+
driver_overrides, schema_served)
|
|
59
60
|
from .user_group_cli import make_user_group_local, edit_user_group_local, list_user_groups_local, remove_user_group_local, make_enrollment_codes_local, remove_enrollment_code_local, list_enrollment_codes_local, export_user_groups_csv_local, export_enrollment_codes_csv_local
|
|
60
61
|
from .user_group_handler import start_usergroup_cleanup_loop
|
|
61
62
|
from .global_onboarding import start_agent
|
|
@@ -1205,6 +1206,7 @@ def _status_json() -> dict:
|
|
|
1205
1206
|
"devices_total": len(devices),
|
|
1206
1207
|
"devices_online": sum(bool(item["online"]) for item in devices.values()),
|
|
1207
1208
|
"reservations_active": int(active),
|
|
1209
|
+
"driver_overrides": driver_overrides(),
|
|
1208
1210
|
}
|
|
1209
1211
|
|
|
1210
1212
|
|
|
@@ -1457,6 +1459,27 @@ def cmd_stats_purge(argv):
|
|
|
1457
1459
|
def cmd_devices_list(argv): get_devices(json_output="--json" in argv)
|
|
1458
1460
|
|
|
1459
1461
|
|
|
1462
|
+
def cmd_schema_served(argv):
|
|
1463
|
+
words = [a for a in argv if a != "--json"]
|
|
1464
|
+
if len(words) != 1 or not re.fullmatch(r"[a-z0-9_]{1,64}", words[0]):
|
|
1465
|
+
print("Usage: schema served <type> [--json]")
|
|
1466
|
+
return 2
|
|
1467
|
+
value = schema_served(words[0])
|
|
1468
|
+
if "--json" in argv:
|
|
1469
|
+
from .json_output import print_json
|
|
1470
|
+
|
|
1471
|
+
print_json(value)
|
|
1472
|
+
return 0
|
|
1473
|
+
print(f"{value['type']}: {value['hash'] or 'no driver registered'}")
|
|
1474
|
+
if value["module"]:
|
|
1475
|
+
print(f" served by {value['module']}{' (rrfops override)' if value['override'] else ''}")
|
|
1476
|
+
print(f" file sha256 {value['source_sha256'] or '-'}")
|
|
1477
|
+
for load in value["loads"]:
|
|
1478
|
+
if not load["ok"]:
|
|
1479
|
+
print(f" {load['file']}: {load['error']}")
|
|
1480
|
+
return 0
|
|
1481
|
+
|
|
1482
|
+
|
|
1460
1483
|
def cmd_diag(argv):
|
|
1461
1484
|
from . import diag
|
|
1462
1485
|
|
|
@@ -258,9 +258,14 @@ class ReservationHandler:
|
|
|
258
258
|
# Reported to Global as `usage`, for the devices shared there. Seeded
|
|
259
259
|
# only into an empty table: the bucket names follow the process TZ, so a
|
|
260
260
|
# restart under another TZ must not count the current rows again.
|
|
261
|
+
# ReservationDays keeps them per UTC date of the start (`usage.by_device`).
|
|
261
262
|
hour = lambda r: f"'h' || strftime('%H', {r}.start_time, 'utc')" # noqa: E731
|
|
262
263
|
day = lambda r: f"'d' || ((CAST(strftime('%w', {r}.start_time, 'utc') AS INTEGER) + 6) % 7)" # noqa: E731
|
|
264
|
+
utc_date = lambda r: f"date({r}.start_time, 'utc')" # noqa: E731
|
|
263
265
|
length = lambda r: f"(julianday({r}.end_time) - julianday({r}.start_time)) * 86400" # noqa: E731
|
|
266
|
+
cancel = '''reservations = MAX(reservations - (OLD.start_time > datetime('now', 'localtime')), 0),
|
|
267
|
+
seconds = MAX(seconds - MAX(0,
|
|
268
|
+
(julianday(OLD.end_time) - julianday(MAX(OLD.start_time, datetime('now', 'localtime')))) * 86400), 0)'''
|
|
264
269
|
self.cursor.executescript(f'''
|
|
265
270
|
BEGIN IMMEDIATE;
|
|
266
271
|
CREATE TABLE IF NOT EXISTS ReservationBuckets (device_id INTEGER NOT NULL, bucket TEXT NOT NULL,
|
|
@@ -283,12 +288,26 @@ class ReservationHandler:
|
|
|
283
288
|
END;
|
|
284
289
|
CREATE TRIGGER IF NOT EXISTS reservation_buckets_del AFTER DELETE ON Reservations
|
|
285
290
|
WHEN OLD.device_id < 1000000 BEGIN
|
|
286
|
-
UPDATE ReservationBuckets SET
|
|
287
|
-
reservations = MAX(reservations - (OLD.start_time > datetime('now', 'localtime')), 0),
|
|
288
|
-
seconds = MAX(seconds - MAX(0,
|
|
289
|
-
(julianday(OLD.end_time) - julianday(MAX(OLD.start_time, datetime('now', 'localtime')))) * 86400), 0)
|
|
291
|
+
UPDATE ReservationBuckets SET {cancel}
|
|
290
292
|
WHERE device_id = OLD.device_id AND bucket IN ('all', {hour("OLD")}, {day("OLD")});
|
|
291
293
|
END;
|
|
294
|
+
CREATE TABLE IF NOT EXISTS ReservationDays (device_id INTEGER NOT NULL, day TEXT NOT NULL,
|
|
295
|
+
reservations INTEGER NOT NULL, seconds REAL NOT NULL, PRIMARY KEY (device_id, day));
|
|
296
|
+
INSERT INTO ReservationDays
|
|
297
|
+
SELECT device_id, {utc_date("r")} AS day, COUNT(*), SUM({length("r")}) FROM Reservations r
|
|
298
|
+
WHERE device_id < 1000000 AND NOT EXISTS (SELECT 1 FROM ReservationDays)
|
|
299
|
+
GROUP BY device_id, day;
|
|
300
|
+
CREATE TRIGGER IF NOT EXISTS reservation_days_ins AFTER INSERT ON Reservations
|
|
301
|
+
WHEN NEW.device_id < 1000000 BEGIN
|
|
302
|
+
INSERT INTO ReservationDays VALUES (NEW.device_id, {utc_date("NEW")}, 1, {length("NEW")})
|
|
303
|
+
ON CONFLICT (device_id, day) DO UPDATE
|
|
304
|
+
SET reservations = reservations + 1, seconds = seconds + excluded.seconds;
|
|
305
|
+
END;
|
|
306
|
+
CREATE TRIGGER IF NOT EXISTS reservation_days_del AFTER DELETE ON Reservations
|
|
307
|
+
WHEN OLD.device_id < 1000000 BEGIN
|
|
308
|
+
UPDATE ReservationDays SET {cancel}
|
|
309
|
+
WHERE device_id = OLD.device_id AND day = {utc_date("OLD")};
|
|
310
|
+
END;
|
|
292
311
|
COMMIT;
|
|
293
312
|
''')
|
|
294
313
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: remoteRF-server
|
|
3
|
-
Version: 1.7.
|
|
3
|
+
Version: 1.7.16
|
|
4
4
|
Summary: RemoteRF server-side control package
|
|
5
5
|
License: GPL-3.0-or-later
|
|
6
6
|
Classifier: Programming Language :: Python :: 3
|
|
@@ -10,7 +10,7 @@ Requires-Python: >=3.10
|
|
|
10
10
|
Description-Content-Type: text/markdown
|
|
11
11
|
License-File: LICENSE
|
|
12
12
|
Requires-Dist: remoterf-federation-core==0.22.0
|
|
13
|
-
Requires-Dist: remoterf-device-core==0.1.
|
|
13
|
+
Requires-Dist: remoterf-device-core==0.1.3
|
|
14
14
|
Requires-Dist: grpcio<2.0.0,>=1.81.1
|
|
15
15
|
Requires-Dist: protobuf<7.0.0,>=6.33.5
|
|
16
16
|
Requires-Dist: numpy
|
|
@@ -207,7 +207,7 @@ class ReservationCountTests(unittest.TestCase):
|
|
|
207
207
|
|
|
208
208
|
|
|
209
209
|
class ReservationBucketsTests(unittest.TestCase):
|
|
210
|
-
"""Per device, by UTC hour and
|
|
210
|
+
"""Per device, by UTC hour, weekday and date of the start: counted like the totals above."""
|
|
211
211
|
|
|
212
212
|
def setUp(self):
|
|
213
213
|
import time
|
|
@@ -244,6 +244,11 @@ class ReservationBucketsTests(unittest.TestCase):
|
|
|
244
244
|
rows = db.execute("SELECT bucket, reservations, seconds FROM ReservationBuckets WHERE device_id = ?", (device_id,))
|
|
245
245
|
return {bucket: (n, round(seconds)) for bucket, n, seconds in rows}
|
|
246
246
|
|
|
247
|
+
@staticmethod
|
|
248
|
+
def days(db, device_id):
|
|
249
|
+
rows = db.execute("SELECT day, reservations, seconds FROM ReservationDays WHERE device_id = ?", (device_id,))
|
|
250
|
+
return {day: (n, round(seconds)) for day, n, seconds in rows}
|
|
251
|
+
|
|
247
252
|
def test_buckets_by_utc_hour_and_weekday_survive_purge_and_skip_partner_devices(self):
|
|
248
253
|
from datetime import datetime
|
|
249
254
|
from remoteRF_server.server.federation.home_devices import ALIAS_BASE
|
|
@@ -256,8 +261,12 @@ class ReservationBucketsTests(unittest.TestCase):
|
|
|
256
261
|
expected = {"all": (2, 3 * 3600), "h20": (1, 3600), "d6": (1, 3600), "h06": (1, 2 * 3600), "d2": (1, 2 * 3600)}
|
|
257
262
|
self.assertEqual(self.buckets(db, 1), expected)
|
|
258
263
|
self.assertEqual(self.buckets(db, ALIAS_BASE), {})
|
|
264
|
+
days = {"2026-11-01": (1, 3600), "2026-11-04": (1, 2 * 3600)} # the first starts the UTC day before
|
|
265
|
+
self.assertEqual(self.days(db, 1), days)
|
|
266
|
+
self.assertEqual(self.days(db, ALIAS_BASE), {})
|
|
259
267
|
db.execute("DELETE FROM Reservations WHERE end_time < ?", (datetime.now(),)) # purge: no change
|
|
260
268
|
self.assertEqual(self.buckets(db, 1), expected)
|
|
269
|
+
self.assertEqual(self.days(db, 1), days)
|
|
261
270
|
|
|
262
271
|
def test_a_cancel_gives_back_like_the_totals_and_never_below_zero(self):
|
|
263
272
|
from datetime import datetime, timedelta
|
|
@@ -268,17 +277,18 @@ class ReservationBucketsTests(unittest.TestCase):
|
|
|
268
277
|
self.add(db, 2, now + timedelta(hours=1), now + timedelta(hours=3))
|
|
269
278
|
self.add(db, 3, now - timedelta(hours=1), now + timedelta(hours=1))
|
|
270
279
|
self.add(db, 4, now + timedelta(hours=1), now + timedelta(hours=2))
|
|
271
|
-
values = lambda
|
|
272
|
-
self.assertEqual(values(2), [(1, 2 * 3600)] *
|
|
280
|
+
values = lambda gid: sorted([*self.buckets(db, gid).values(), *self.days(db, gid).values()]) # noqa: E731
|
|
281
|
+
self.assertEqual(values(2), [(1, 2 * 3600)] * 4)
|
|
273
282
|
db.execute("DELETE FROM Reservations WHERE device_id = 2") # cancel before start: all given back
|
|
274
|
-
self.assertEqual(values(2), [(0, 0)] *
|
|
283
|
+
self.assertEqual(values(2), [(0, 0)] * 4)
|
|
275
284
|
db.execute("DELETE FROM Reservations WHERE device_id = 3") # cancel after start: unused hour back
|
|
276
285
|
for n, seconds in values(3):
|
|
277
286
|
self.assertEqual(n, 1)
|
|
278
287
|
self.assertAlmostEqual(seconds, 3600, delta=2)
|
|
279
|
-
|
|
288
|
+
for table in ("ReservationBuckets", "ReservationDays"):
|
|
289
|
+
db.execute(f"UPDATE {table} SET reservations = 0, seconds = 0 WHERE device_id = 4")
|
|
280
290
|
db.execute("DELETE FROM Reservations WHERE device_id = 4")
|
|
281
|
-
self.assertEqual(values(4), [(0, 0)] *
|
|
291
|
+
self.assertEqual(values(4), [(0, 0)] * 4) # never negative
|
|
282
292
|
|
|
283
293
|
def test_buckets_seed_from_old_rows_once(self):
|
|
284
294
|
import time
|
|
@@ -292,15 +302,33 @@ class ReservationBucketsTests(unittest.TestCase):
|
|
|
292
302
|
self.add(db, 1, "2026-11-02 02:15:00", "2026-11-02 02:45:00")
|
|
293
303
|
self.add(db, ALIAS_BASE, "2026-11-02 02:00:00", "2026-11-02 03:00:00")
|
|
294
304
|
expected = {"all": (2, 5400), "h20": (2, 5400), "d6": (2, 5400)}
|
|
305
|
+
days = {"2026-11-01": (2, 5400)}
|
|
295
306
|
self.handler()
|
|
296
307
|
self.assertEqual(self.buckets(db, 1), expected)
|
|
297
308
|
self.assertEqual(self.buckets(db, ALIAS_BASE), {})
|
|
309
|
+
self.assertEqual(self.days(db, 1), days)
|
|
310
|
+
self.assertEqual(self.days(db, ALIAS_BASE), {})
|
|
298
311
|
self.handler() # a restart does not seed again
|
|
299
312
|
self.assertEqual(self.buckets(db, 1), expected)
|
|
313
|
+
self.assertEqual(self.days(db, 1), days)
|
|
300
314
|
with mock.patch.dict("os.environ", {"TZ": "UTC0"}):
|
|
301
315
|
time.tzset()
|
|
302
|
-
self.handler() # nor one under another TZ, where the rows map to h02/d0
|
|
316
|
+
self.handler() # nor one under another TZ, where the rows map to h02/d0/2026-11-02
|
|
303
317
|
time.tzset()
|
|
304
318
|
self.assertEqual(self.buckets(db, 1), expected)
|
|
319
|
+
self.assertEqual(self.days(db, 1), days)
|
|
305
320
|
self.add(db, 1, "2026-11-02 02:20:00", "2026-11-02 02:50:00") # 20:50 UTC
|
|
306
321
|
self.assertEqual(self.buckets(db, 1)["h20"], (3, 7200))
|
|
322
|
+
self.assertEqual(self.days(db, 1), {"2026-11-01": (3, 7200)})
|
|
323
|
+
|
|
324
|
+
def test_days_seed_into_an_existing_lab_that_already_has_buckets(self):
|
|
325
|
+
"""A lab upgrading from 1.7.14 has ReservationBuckets but no ReservationDays yet."""
|
|
326
|
+
self.handler()
|
|
327
|
+
db = self.connect()
|
|
328
|
+
db.executescript("DROP TRIGGER reservation_days_ins; DROP TRIGGER reservation_days_del; "
|
|
329
|
+
"DROP TABLE ReservationDays;")
|
|
330
|
+
self.add(db, 1, "2026-11-04 12:00:00", "2026-11-04 13:00:00")
|
|
331
|
+
buckets = self.buckets(db, 1)
|
|
332
|
+
self.handler()
|
|
333
|
+
self.assertEqual(self.days(db, 1), {"2026-11-04": (1, 3600)})
|
|
334
|
+
self.assertEqual(self.buckets(db, 1), buckets)
|
|
@@ -509,7 +509,8 @@ class StatusReportTests(TunnelTests):
|
|
|
509
509
|
self.assertEqual(payload["proxies"], 1)
|
|
510
510
|
self.assertEqual(payload["active_reservations"], 1)
|
|
511
511
|
self.assertEqual(payload["reservations"], 5)
|
|
512
|
-
self.assertEqual(payload["usage"],
|
|
512
|
+
self.assertEqual(payload["usage"], # not on global
|
|
513
|
+
{"by_hour": [0] * 24, "by_weekday": [0] * 7, "by_type": {}, "by_device": []})
|
|
513
514
|
self.assertEqual(
|
|
514
515
|
sorted(entry["type"] for entry in payload["devices"]),
|
|
515
516
|
["adalm_pluto", "usrp_2901"],
|
|
@@ -528,7 +529,8 @@ class StatusReportTests(TunnelTests):
|
|
|
528
529
|
text = " ".join(g.CONSENT_TEXT.split())
|
|
529
530
|
for item in ("name, version", "devices", "joined", "last online", "how many reservations",
|
|
530
531
|
"time reserved, at https://{domain}/stats.json", "devices you share on Global",
|
|
531
|
-
"each hour of the day", "each day of the week", "per device type", "these breakdowns"
|
|
532
|
+
"each hour of the day", "each day of the week", "per device type", "these breakdowns",
|
|
533
|
+
"for each device shared on Global its name and its daily reservation counts and time reserved"):
|
|
532
534
|
self.assertIn(item, text)
|
|
533
535
|
|
|
534
536
|
def test_status_payload_survives_missing_state(self):
|
|
@@ -539,7 +541,7 @@ class StatusReportTests(TunnelTests):
|
|
|
539
541
|
self.assertEqual(payload["proxies"], 0)
|
|
540
542
|
self.assertEqual(payload["reservations"], 0)
|
|
541
543
|
self.assertEqual(payload["devices"], [])
|
|
542
|
-
self.assertEqual(payload["usage"], {"by_hour": [0] * 24, "by_weekday": [0] * 7, "by_type": {}})
|
|
544
|
+
self.assertEqual(payload["usage"], {"by_hour": [0] * 24, "by_weekday": [0] * 7, "by_type": {}, "by_device": []})
|
|
543
545
|
self.assertIsNone(payload["cert_sha256"])
|
|
544
546
|
|
|
545
547
|
def test_usage_counts_only_the_devices_shared_on_global(self):
|
|
@@ -568,7 +570,76 @@ class StatusReportTests(TunnelTests):
|
|
|
568
570
|
"unknown": {"reservations": 1, "seconds": 60}})
|
|
569
571
|
|
|
570
572
|
with mock.patch.object(cluster, "groups", side_effect=cluster.ClusterError("no cluster named global")):
|
|
571
|
-
self.assertEqual(g.usage_summary(),
|
|
573
|
+
self.assertEqual(g.usage_summary(),
|
|
574
|
+
{"by_hour": [0] * 24, "by_weekday": [0] * 7, "by_type": {}, "by_device": []})
|
|
575
|
+
|
|
576
|
+
def days_table(self, rows):
|
|
577
|
+
import sqlite3
|
|
578
|
+
|
|
579
|
+
with sqlite3.connect(g.cfg_dir() / "db" / "reservations.db") as conn:
|
|
580
|
+
conn.execute("CREATE TABLE ReservationDays (device_id INTEGER, day TEXT, reservations INTEGER, "
|
|
581
|
+
"seconds REAL, PRIMARY KEY (device_id, day))")
|
|
582
|
+
conn.executemany("INSERT INTO ReservationDays VALUES (?,?,?,?)", rows)
|
|
583
|
+
|
|
584
|
+
def test_by_device_reports_the_last_31_days_of_shared_devices_by_name(self):
|
|
585
|
+
from datetime import datetime, timedelta, timezone
|
|
586
|
+
from remoteRF_server.server.federation import cluster
|
|
587
|
+
|
|
588
|
+
self.seed()
|
|
589
|
+
(g.cfg_dir() / "devices.yml").write_text(
|
|
590
|
+
"devices:\n"
|
|
591
|
+
" - {device_id: 0, device_type: adalm_pluto, name: Bench Pluto}\n"
|
|
592
|
+
" - {device_id: 1, device_type: usrp_2901, name: Not shared}\n"
|
|
593
|
+
" - {device_id: 2, device_type: adalm_pluto, name: \"\\e[31mRed\\nPluto \"}\n"
|
|
594
|
+
" - {device_id: 3, device_type: adalm_pluto, name: \"\\t\"}\n"
|
|
595
|
+
f" - {{device_id: 4, device_type: adalm_pluto, name: {'x' * 80}}}\n",
|
|
596
|
+
encoding="utf-8",
|
|
597
|
+
)
|
|
598
|
+
self.use_host_registry([("host-a", 200, "adalm_pluto_mimo_ykush")])
|
|
599
|
+
today = datetime.now(timezone.utc).date()
|
|
600
|
+
day = lambda n: str(today - timedelta(days=n)) # noqa: E731
|
|
601
|
+
self.days_table([
|
|
602
|
+
(0, day(0), 2, 5400.4), (0, day(30), 1, 60.0), (0, day(31), 9, 999.0), # day 31 is out of the window
|
|
603
|
+
(0, str(today + timedelta(days=1)), 1, 60.0), # not started yet
|
|
604
|
+
(0, day(3), 0, 0.2), # every reservation cancelled
|
|
605
|
+
(1, day(0), 4, 400.0), # not shared on Global
|
|
606
|
+
(2, day(1), 1, 30.0), (3, day(1), 0, 30.0), (4, day(1), 1, 0.0),
|
|
607
|
+
(5, day(2), 1, 60.0), # gone from devices.yml
|
|
608
|
+
(6, day(31), 1, 60.0), # nothing left in the window
|
|
609
|
+
(200, day(5), 3, 180.0),
|
|
610
|
+
])
|
|
611
|
+
shared = [{"devices": [0, 2, 3, 4, 5, 6]}, {"devices": [200]}]
|
|
612
|
+
with mock.patch.object(cluster, "groups", return_value=shared):
|
|
613
|
+
by_device = g.usage_summary()["by_device"]
|
|
614
|
+
self.assertEqual(by_device, [
|
|
615
|
+
{"id": 0, "name": "Bench Pluto", "type": "adalm_pluto", "days": {day(0): [2, 5400], day(30): [1, 60]}},
|
|
616
|
+
{"id": 2, "name": "[31mRedPluto", "type": "adalm_pluto", "days": {day(1): [1, 30]}},
|
|
617
|
+
{"id": 3, "name": "device 3", "type": "adalm_pluto", "days": {day(1): [0, 30]}},
|
|
618
|
+
{"id": 4, "name": "x" * 64, "type": "adalm_pluto", "days": {day(1): [1, 0]}},
|
|
619
|
+
{"id": 5, "name": "device 5", "type": "unknown", "days": {day(2): [1, 60]}},
|
|
620
|
+
{"id": 200, "name": "Pluto 200", "type": "adalm_pluto_mimo_ykush", "days": {day(5): [3, 180]}},
|
|
621
|
+
])
|
|
622
|
+
|
|
623
|
+
# A rename shows on the next note.
|
|
624
|
+
path = g.cfg_dir() / "devices.yml"
|
|
625
|
+
path.write_text(path.read_text(encoding="utf-8").replace("Bench Pluto", "Bench A"), encoding="utf-8")
|
|
626
|
+
with mock.patch.object(cluster, "groups", return_value=shared):
|
|
627
|
+
self.assertEqual(g.usage_summary()["by_device"][0]["name"], "Bench A")
|
|
628
|
+
|
|
629
|
+
def test_by_device_drops_the_oldest_days_first_to_fit_globals_caps(self):
|
|
630
|
+
from datetime import datetime, timedelta, timezone
|
|
631
|
+
from remoteRF_server.server.federation import cluster
|
|
632
|
+
|
|
633
|
+
self.seed()
|
|
634
|
+
today = datetime.now(timezone.utc).date()
|
|
635
|
+
self.days_table([(gid, str(today - timedelta(days=n)), 1, 60.0) for gid in range(130) for n in range(10)])
|
|
636
|
+
with mock.patch.object(cluster, "groups", return_value=[{"devices": list(range(130))}]):
|
|
637
|
+
by_device = g.usage_summary()["by_device"]
|
|
638
|
+
self.assertEqual([entry["id"] for entry in by_device], list(range(128)))
|
|
639
|
+
days = [d for entry in by_device for d in entry["days"]]
|
|
640
|
+
self.assertEqual(len(days), 1000)
|
|
641
|
+
self.assertEqual(min(days), str(today - timedelta(days=7))) # days 8 and 9 dropped, day 7 cut short
|
|
642
|
+
self.assertEqual(sum(d == min(days) for d in days), 1000 - 7 * 128)
|
|
572
643
|
|
|
573
644
|
def use_host_registry(self, persisted=(), online_hosts=()):
|
|
574
645
|
"""The Host directory as the server loads it at boot, with online_hosts connected."""
|
|
@@ -647,6 +718,7 @@ class StatusReportTests(TunnelTests):
|
|
|
647
718
|
with mock.patch.object(g, "call", side_effect=old_global):
|
|
648
719
|
g.send_status()
|
|
649
720
|
self.assertEqual(sent, [
|
|
721
|
+
set(g.STATUS_FIELDS),
|
|
650
722
|
set(g.STATUS_FIELDS),
|
|
651
723
|
set(g.STATUS_FIELDS) - {"usage"},
|
|
652
724
|
set(g.STATUS_FIELDS) - {"usage", "reservations"},
|
|
@@ -665,6 +737,7 @@ class StatusReportTests(TunnelTests):
|
|
|
665
737
|
with mock.patch.object(g, "call", side_effect=global_with_reserved_s):
|
|
666
738
|
g.send_status()
|
|
667
739
|
self.assertEqual(sent, [
|
|
740
|
+
set(g.STATUS_FIELDS),
|
|
668
741
|
set(g.STATUS_FIELDS),
|
|
669
742
|
set(g.STATUS_FIELDS) - {"usage"},
|
|
670
743
|
set(g.STATUS_FIELDS) - {"usage", "reservations"},
|
|
@@ -681,7 +754,21 @@ class StatusReportTests(TunnelTests):
|
|
|
681
754
|
|
|
682
755
|
with mock.patch.object(g, "call", side_effect=global_with_reservations):
|
|
683
756
|
g.send_status()
|
|
684
|
-
self.assertEqual(sent, [set(g.STATUS_FIELDS), set(g.STATUS_FIELDS) - {"usage"}])
|
|
757
|
+
self.assertEqual(sent, [set(g.STATUS_FIELDS), set(g.STATUS_FIELDS), set(g.STATUS_FIELDS) - {"usage"}])
|
|
758
|
+
|
|
759
|
+
def test_status_drops_only_by_device_for_a_global_that_predates_it(self):
|
|
760
|
+
self.activate()
|
|
761
|
+
sent = []
|
|
762
|
+
|
|
763
|
+
def global_with_usage(method, path, body):
|
|
764
|
+
sent.append(body["status"])
|
|
765
|
+
if "by_device" in body["status"]["usage"]:
|
|
766
|
+
raise g.GlobalError("invalid_status")
|
|
767
|
+
|
|
768
|
+
with mock.patch.object(g, "call", side_effect=global_with_usage):
|
|
769
|
+
g.send_status()
|
|
770
|
+
self.assertEqual([set(status) for status in sent], [set(g.STATUS_FIELDS)] * 2)
|
|
771
|
+
self.assertEqual(sent[1]["usage"], {"by_hour": [0] * 24, "by_weekday": [0] * 7, "by_type": {}})
|
|
685
772
|
|
|
686
773
|
def test_status_gives_up_when_even_the_oldest_note_is_refused(self):
|
|
687
774
|
self.activate()
|
|
@@ -689,7 +776,7 @@ class StatusReportTests(TunnelTests):
|
|
|
689
776
|
with mock.patch.object(g, "call", side_effect=g.GlobalError("invalid_status")) as call:
|
|
690
777
|
with self.assertRaises(g.GlobalError):
|
|
691
778
|
g.send_status()
|
|
692
|
-
self.assertEqual(call.call_count,
|
|
779
|
+
self.assertEqual(call.call_count, 5)
|
|
693
780
|
|
|
694
781
|
def test_reports_only_flow_once_active(self):
|
|
695
782
|
self.join()
|